4130e2e3f5
Co-Authored-By: Claude Code <noreply@anthropic.com>
177 lines
13 KiB
Markdown
177 lines
13 KiB
Markdown
# 设计:号码区号补全(IP 推测 + 回显确认)— 终版 v2
|
||
|
||
- 日期:2026-10-08(v2 融合版:两轮独立审查 + 上游同日设计对照吸收 + 需求方三处优化)
|
||
- 前置:2026-10-08-dual-contact-and-country-code-design.md 及墙层增补——本设计
|
||
**替换**其中"请客户重发完整号"路径
|
||
- 对照:上游汽车项目同日设计 `D:\refactor\ai-kefu\docs\superpowers\specs\
|
||
2026-10-08-whatsapp-cc-completion-design.md`(吸收其 6 点,见 §8)
|
||
|
||
## 1. 目标
|
||
|
||
客户号码缺国家区号时:按 IP 推测 → 补全为 E.164 → **回显给客户确认**(顺带
|
||
暴露抄错的数字)→ 首封线索邮件即带补全号(零延迟、可拨、标注透明)。杜绝
|
||
发出拨不通的本地格式号码,同时不阻塞线索交付。
|
||
|
||
## 2. 架构(确定性优先,LLM 只做它擅长的)
|
||
|
||
| 职责 | 归属 | 理由 |
|
||
|---|---|---|
|
||
| 判定(有无码/建议国)、E.164 补全、状态机、邮件渲染 | **Python 确定性**(新模块 `cc_completion` + phonenumberslite) | 状态不可能被幻觉;可离线确定性测试;邮件触发不押在 LLM |
|
||
| 客户答语理解(肯定/否定/国名) | **extractor LLM**(schema 加 `cc_reply` 可选字段,单轮无状态分类) | "yeah that's right"/"naur"/"我在迪拜"正则极脆弱,恰是 LLM 强项;extractor 本每轮跑,零额外调用。**双门控**:extractor.md 声明"仅当客户在回应号码确认时填写 cc_reply";Python 侧仅在 `_cc` 等待态(未 confirmed 且 attempts 未耗尽)时消费——防止对无关问题的 yes 被误当确认;**漏填兜底**(extractor 未识别出客户在答确认)→ 等同无回应,attempts 递增走 fallback |
|
||
| 话术生成与确认句必出 | 提醒注入 + **既有墙骨架**(白名单放行/缓冲替换——四轮生产实测过的机制) | GLM 对反惯性指令 0/8 服从的血泪教训,确认回显必须墙保 |
|
||
|
||
## 3. 判定树(`cc_completion.classify(number, ip_iso)`)
|
||
|
||
```
|
||
号码
|
||
├─ 以 +/00 开头 ──────────────────► HAS_CC(现有流程直走,不动)
|
||
├─ 以 IP 国码开头且去码后剩余合法 ──► HAS_CC_NAKED → 静默规范化(无对话
|
||
│ 动作;此分支无确认防线,依赖剩余段对该国 is_valid 严格校验——碰撞需
|
||
│ "以X码开头+剩余恰为X国合法号"多重巧合,错误率极低,拨错有客户反馈兜底)
|
||
├─ phonenumbers region=IP国 解析合法 ─► NEED_CC,建议=IP 国(高置信)
|
||
└─ 均不匹配(VPN/罕见格式)─────────► NEED_CC,建议=naive(去前导0+IP码,回显靠客户纠错)
|
||
IP 无效/未知 → NEED_CC 无建议国 → 问"方便发带区号完整号吗"(1 轮为限)
|
||
```
|
||
|
||
## 4. 状态机(每 canon 组独立,存 `raw_info["_cc"]` 结构化 dict)
|
||
|
||
```python
|
||
raw_info["_cc"] = {
|
||
"whatsapp": {"e164": "+254712345678", "intl": "+254 712 345 678",
|
||
"confirmed": false, "attempts": 0,
|
||
"source": "ip_proposed", # ip_proposed|naive|ip_confirm|
|
||
# prefix_normalized|customer_stated|no_ip|fallback
|
||
"canon": "0712345678"}}
|
||
```
|
||
|
||
- **canon 组**:phone 与 whatsapp 同号(canon 相同)共享一个确认流,确认一次
|
||
(attempts 以组为计数主体);**不同号时只有 whatsapp 组走确认流**(主联系
|
||
渠道,对话永远只确认一个号),phone 组一律 `ip_proposed` 静默补全+标注、
|
||
不确认(第五轮自查简化)——**换号(canon 变化)即重置该组状态机重走判定树**
|
||
(结构性解决"改号残留":旧补全永不跟随新号)
|
||
- **source 语义钉死**:`prefix_normalized` → **confirmed=true**(码是客户自己
|
||
写的,首封**无**"未经确认"标注);`customer_stated`=客户报国名重算后确认;
|
||
attempts 耗尽落定时 source 改写为 `fallback`(标注保留"未经客户确认");
|
||
标注按 `(confirmed, source)` 分派而非仅 confirmed
|
||
- attempts 只被 canon 变化重置;国名重述不重置(防"无限换国名");no_ip 路径
|
||
上限 1 轮(attempts 上限口径与邮箱轻补解锁条件=「confirmed=true 或
|
||
attempts 耗尽或 source∈{prefix_normalized,fallback}」统一)
|
||
- **实现调和记录(Task 2 审查)**:no_ip **不在**终态 source 集内——新鲜 no_ip
|
||
态 cc_due=True 问一次、attempts≥1 落定(spec 原字面把 no_ip 列入落定集会
|
||
导致"永远不问",与本节"问 1 轮"自相矛盾,实现按正确语义调和)
|
||
- **迟到信息决策(Task 2 审查;Task 3 实现调和)**:落定态收到迟到 **country
|
||
(客户主动报国)→ 受控升级口:重算 e164 并保留信息(Task 6 出站用重算号),但
|
||
attempts 已过 cap → fallback 落定、不再回显**(原"再回显一轮"与 cap 语义冲突,
|
||
实现按保守调和);**no_ip 耗尽态排除在报国口外**(静默终态);迟到 **affirm
|
||
不升级**;混合答语(affirm+country 同填)走 affirm 分支 country 忽略
|
||
(Task 2 transit 既有 tie-break)。Task 6 渲染需防御 **fallback-无-e164**
|
||
组合(迁移分支可产出,有机状态机不产生)
|
||
- **回显确认轮**(NEED_CC 且有建议国):AI 注入逐字句 "…reach you at
|
||
{intl} — is that right?"(中英双语);**墙白名单**放行该精确号码
|
||
(`no_contact_value(allow_number=...)`,逐匹配段 digits_only 比对,只放
|
||
系统注入的那一个号);**确认句墙**(缓冲机制)保证必出
|
||
- 答语(下一轮 extractor 的 `cc_reply`):
|
||
- `affirm=true` → `confirmed=true, source=ip_confirm` → 落定
|
||
- `affirm=false, country=X` → `NAME_TO_REGION[X]`(中英映射表)转 ISO 重算
|
||
e164 → 再回显(attempts+1)
|
||
- `affirm=false` 无 country → 请重发完整号(新号到达=canon 变化自然重置)
|
||
- 无回应/不可解析 → attempts 随轮递增,**每渠道上限 2 轮** → 落定
|
||
`confirmed=false`(保持现有 e164 建议,不无限纠缠)
|
||
- **内部状态卫生(三通道封堵,上游对照吸收)**:
|
||
1. extractor 入口 `$existing_info` 过滤 `_` 前缀键(我们已有,复核)
|
||
2. **extractor 出口:消费 `cc_reply` 后即剥离**(瞬态分类非事实,不残留)
|
||
3. **extractor 出口:防御性丢弃任何回传的 `_` 前缀键**(防 LLM 回显 `_cc`
|
||
覆盖 Python 管理的状态机——上游发现的洞)
|
||
- **存量迁移**:升级部署时存量 `_cc_asked=True` 且号码裸的会话 → 映射为
|
||
`_cc{confirmed:false, source:fallback}`(已问过,不再追问,直通邮箱轻补)
|
||
|
||
## 4b. 提示词层改写(终审补——不改则回显机制与红线自相矛盾)
|
||
|
||
v2 核心是让 AI **回显补全号**,而现行提示词全线教的是"不回显+请重发"——
|
||
不改提示词,GLM 会拒写号、确认句墙轮轮整段替换(重演锚点竞争教训):
|
||
|
||
1. **system.md 五处禁令例外句**(:59/:175/:223/:229/引导策略节)从"号码缺国家
|
||
代码时可请客户重发完整号一次"改为"号码缺国家代码时,按系统提醒**回显补全
|
||
后的完整号码**请客户确认(仅系统提供的号码,禁止自编)"
|
||
2. **好差示例**(裸号组)改为回显确认形态;新增差例:"AI 自己拼号码/回显客户
|
||
裸号原文"——只允许回显系统注入的补全号
|
||
3. **conversation.py CC 轮让位句**("回复中绝不写出号码数字")改为"回复中
|
||
绝不写出号码数字——**唯一例外是系统提供的补全号原文**(逐字)"
|
||
4. **确认句墙检测词钉死**:缓冲裁决标准 = **系统注入的 e164(digits_only
|
||
等值)在场**(辅锚 "is that right"/"对吗"),替换现有 "country code" 检测
|
||
5. **测试迁移补项**:tests/test_system_prompt_dual_contact.py 全部断言按新
|
||
语义更新(五处例外句/好差示例/模板句同源)
|
||
6. **拆除/改造清单**(206beae..cbf234d 刚上线机制的处置):`_CC_ASK_EN/_ZH`
|
||
追问句 → 改为回显确认模板(`_CC_ECHO_*`,与 reminder/墙三方同源);
|
||
`_CC_WALL_*` 检测词替换(见 4);`_cc_asked` 计数 → 存量映射(§4)+
|
||
新代码不再写入;`_contact_completion_note` 的 cc 分支重写(判定树挂接)
|
||
|
||
## 5. 邮件/推送时序(需求方定案:首封即发补全号)
|
||
|
||
| 时点 | 发什么 | 标注 |
|
||
|---|---|---|
|
||
| 首封(裸号 confirmed 当轮,**零延迟**) | 判定树同步算出的 **e164 补全号**(ip_proposed/naive/prefix_normalized) | `confirmed=false` 时尾注 **"(区号按 IP 推测,未经客户确认)"**;`no_ip` 时裸号 + **"(缺国家区号)"** |
|
||
| 客户确认(号码未变) | **不补发**(首封已可拨;后续渲染自动去标注) | 状态升级透明 |
|
||
| 客户纠正/换号 | 现有**改号更正通道**(canon 变化自然触发) | 现有机制零新逻辑 |
|
||
|
||
- 渲染点:email_service(正文联系方式行 + 更正/回头客路径,签名加参取 `_cc`;
|
||
**邮件主题行的号码也用 e164、不加标注**——主题拥挤);
|
||
lead_push 的 contact 给干净 e164(契约要求"带国际区号且有效"),标注走
|
||
profile 派生(出站时由 `_cc` 计算,不落正常键——零 LLM 泄漏面)
|
||
- `profile_out` 先消费 `_cc` 生成补全 → 再过滤 `_` 键(顺序钉死)
|
||
- **面板同步**:cc-pending 期间 `ev.contact_info` 带未确认标注(防"邮件带
|
||
补全、后台显裸号"的分裂)
|
||
- 邮箱轻补串行:号码确认流落定(confirmed 或 attempts 耗尽)后才注入
|
||
|
||
## 6. 管道接入点与 IP 入口(终审钉死)
|
||
|
||
- **两个钩子**(锚点错了"首封即发补全号"会静默失效;第五轮自查优化——
|
||
判定树挪轮前,全链同轮完成,客户少等一轮):
|
||
1. **轮前**:即时扫描发现裸号后**同步跑判定树**(纯计算毫秒级)建 `_cc`
|
||
→ 本轮 reminder 即注入回显确认 → **同轮回显**;与一次性计数写回同事务落库
|
||
2. **轮末**:消费 `cc_reply` + 状态更新(确认落定/重算/换号重置)——在
|
||
`_maybe_send_lead_email` **之前**(conversation.py 约 :500-526 区间),
|
||
首封补全号与推送派生依赖已建好的 `_cc`
|
||
- `/session` 路由注入 `Request`,XFF 第一段合并进 `conv.client_info`
|
||
(**服务端 XFF 优先于客户端自报 ip**——WS 现状是客户端覆盖服务端、可伪造,
|
||
一并修正);chat_rest.py:107 只喂限流修不到数据流
|
||
- `ip_service` 新增 `resolve_region_code(ip) -> ISO`(iptocc 直透);
|
||
**修既有 bug**:`"AU": "加拿大"` 重复键(应为 CA)
|
||
- 面板同步载体:`ev.contact_info` 值尾注(canon() 纯数字提取,尾注不影响
|
||
24h 去重/更正判定)
|
||
|
||
## 7. 依赖与测试
|
||
|
||
- `requirements.txt` + `phonenumberslite`(~3MB、零传递依赖、离线、Apache 2.0)
|
||
- 测试(离线确定性 + 集成):
|
||
1. 判定树矩阵(号码形态 × IP 国,含静默规范化分支)
|
||
2. 状态机全转移(确认/否定/国名/换号重置/2 轮无回应/国名重述不重置/
|
||
同 canon 组共享)
|
||
3. 三通道卫生(入口过滤/出口剥离 cc_reply/丢弃 `_` 回显)
|
||
4. 邮件时序(首封带补全+标注、确认后不补发、改号走更正、no_ip 兜底)
|
||
5. 墙(白名单精确放行:同号放/异号拦;确认句缓冲替换)
|
||
6. 存量迁移;推送 contact 纯 e164;`NAME_TO_REGION` 映射
|
||
7. 提示词静态断言(4b-5):五处例外句新语义/回显好差示例/让位句例外/
|
||
_CC_ECHO 三方同源
|
||
8. 真实 LLM 验收(cc_reply 分类质量与门控、回显确认全流程、误判对照——
|
||
无关问题的 yes 不得触发确认)
|
||
- 既有测试迁移:D1 断言"confirmed 即发"若 fixture 用本地格式号 → 更新为
|
||
"confirmed 且判定树已同步跑"(行为等价,渲染值变化)
|
||
|
||
## 8. 与上游设计的对照结论
|
||
|
||
吸收:判定树静默规范化分支、`_cc` 结构化+canon 重置、extractor 答语分类、
|
||
出口卫生三封堵、同 canon 组共享、面板同步闸。
|
||
不采纳:上游"待确认暂缓发信"(违背零阻塞——以首封补全号+标注替代,兼得
|
||
及时性与可拨性);上游墙豁免参数(我们已有四轮实测的墙骨架+白名单)。
|
||
|
||
## 9. 风险
|
||
|
||
| 风险 | 等级 | 缓解 |
|
||
|---|---|---|
|
||
| IP 误判(VPN)致首封补全号错 | 中 | 标注"未经客户确认"透明 + 客户否定走重算/改号更正双通道 |
|
||
| naive 补全结构错 | 低 | 回显确认天然暴露;confirmed=false 标注兜底 |
|
||
| cc_reply 分类错误 | 低 | 最坏=多问一轮或保持未确认标注;attempts 上限防纠缠 |
|
||
| NAME_TO_REGION 覆盖不足 | 低 | 映射失败=未捕获,attempts 递增走兜底 |
|
||
| 墙白名单滥用扩大 | 低 | 逐段精确比对 + 评审断言锁死 |
|