9d2e2b224d
Co-Authored-By: Claude Code <noreply@anthropic.com>
7.8 KiB
7.8 KiB
设计:号码区号补全(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 本每轮跑,零额外调用 |
| 话术生成与确认句必出 | 提醒注入 + 既有墙骨架(白名单放行/缓冲替换——四轮生产实测过的机制) | GLM 对反惯性指令 0/8 服从的血泪教训,确认回显必须墙保 |
3. 判定树(cc_completion.classify(number, ip_iso))
号码
├─ 以 +/00 开头 ──────────────────► HAS_CC(现有流程直走,不动)
├─ 以 IP 国码开头且去码后剩余合法 ──► HAS_CC_NAKED → 静默规范化(无对话动作)
├─ phonenumbers region=IP国 解析合法 ─► NEED_CC,建议=IP 国(高置信)
└─ 均不匹配(VPN/罕见格式)─────────► NEED_CC,建议=naive(去前导0+IP码,回显靠客户纠错)
IP 无效/未知 → NEED_CC 无建议国 → 问"方便发带区号完整号吗"(1 轮为限)
4. 状态机(每 canon 组独立,存 raw_info["_cc"] 结构化 dict)
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 相同)共享一个确认流,确认一次; 不同号各自独立状态——换号(canon 变化)即重置该组状态机重走判定树 (结构性解决"改号残留":旧补全永不跟随新号)
- attempts 只被 canon 变化重置;国名重述不重置(防"无限换国名")
- 回显确认轮(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 建议,不无限纠缠)
- 内部状态卫生(三通道封堵,上游对照吸收):
- extractor 入口
$existing_info过滤_前缀键(我们已有,复核) - extractor 出口:消费
cc_reply后即剥离(瞬态分类非事实,不残留) - extractor 出口:防御性丢弃任何回传的
_前缀键(防 LLM 回显_cc覆盖 Python 管理的状态机——上游发现的洞)
- extractor 入口
- 存量迁移:升级部署时存量
_cc_asked=True且号码裸的会话 → 映射为_cc{confirmed:false, source:fallback}(已问过,不再追问,直通邮箱轻补)
5. 邮件/推送时序(需求方定案:首封即发补全号)
| 时点 | 发什么 | 标注 |
|---|---|---|
| 首封(裸号 confirmed 当轮,零延迟) | 判定树同步算出的 e164 补全号(ip_proposed/naive/prefix_normalized) | confirmed=false 时尾注 "(区号按 IP 推测,未经客户确认)";no_ip 时裸号 + "(缺国家区号)" |
| 客户确认(号码未变) | 不补发(首封已可拨;后续渲染自动去标注) | 状态升级透明 |
| 客户纠正/换号 | 现有改号更正通道(canon 变化自然触发) | 现有机制零新逻辑 |
- 渲染点:email_service(正文联系方式行 + 更正/回头客路径,签名加参取
_cc); lead_push 的 contact 给干净 e164(契约要求"带国际区号且有效"),标注走 profile 派生(出站时由_cc计算,不落正常键——零 LLM 泄漏面) profile_out先消费_cc生成补全 → 再过滤_键(顺序钉死)- 面板同步:cc-pending 期间
ev.contact_info带未确认标注(防"邮件带 补全、后台显裸号"的分裂) - 邮箱轻补串行:号码确认流落定(confirmed 或 attempts 耗尽)后才注入
6. IP 入口与基础修复
/session路由注入Request,XFF 第一段合并进conv.client_info(WS 路由 已有同款;chat_rest.py:107 只喂限流修不到数据流)ip_service新增resolve_region_code(ip) -> ISO(iptocc 直透); 修既有 bug:"AU": "加拿大"重复键(应为 CA)
7. 依赖与测试
requirements.txt+phonenumberslite(~3MB、零传递依赖、离线、Apache 2.0)- 测试(离线确定性 + 集成):
- 判定树矩阵(号码形态 × IP 国,含静默规范化分支)
- 状态机全转移(确认/否定/国名/换号重置/2 轮无回应/国名重述不重置/ 同 canon 组共享)
- 三通道卫生(入口过滤/出口剥离 cc_reply/丢弃
_回显) - 邮件时序(首封带补全+标注、确认后不补发、改号走更正、no_ip 兜底)
- 墙(白名单精确放行:同号放/异号拦;确认句缓冲替换)
- 存量迁移;推送 contact 纯 e164;
NAME_TO_REGION映射 - 真实 LLM 验收(cc_reply 分类质量、回显确认全流程、误判对照)
- 既有测试迁移:D1 断言"confirmed 即发"若 fixture 用本地格式号 → 更新为 "confirmed 且判定树已同步跑"(行为等价,渲染值变化)
8. 与上游设计的对照结论
吸收:判定树静默规范化分支、_cc 结构化+canon 重置、extractor 答语分类、
出口卫生三封堵、同 canon 组共享、面板同步闸。
不采纳:上游"待确认暂缓发信"(违背零阻塞——以首封补全号+标注替代,兼得
及时性与可拨性);上游墙豁免参数(我们已有四轮实测的墙骨架+白名单)。
9. 风险
| 风险 | 等级 | 缓解 |
|---|---|---|
| IP 误判(VPN)致首封补全号错 | 中 | 标注"未经客户确认"透明 + 客户否定走重算/改号更正双通道 |
| naive 补全结构错 | 低 | 回显确认天然暴露;confirmed=false 标注兜底 |
| cc_reply 分类错误 | 低 | 最坏=多问一轮或保持未确认标注;attempts 上限防纠缠 |
| NAME_TO_REGION 覆盖不足 | 低 | 映射失败=未捕获,attempts 递增走兜底 |
| 墙白名单滥用扩大 | 低 | 逐段精确比对 + 评审断言锁死 |