Files
ai-kefu-jc/docs/2026-08-18-contact-driven-lead-email-changelog.md
2026-09-29 14:15:12 +08:00

11 KiB
Raw Permalink Blame History

改动文档:联系方式驱动的询盘邮件(批次 1)

  • 日期:2026-08-17 ~ 2026-08-18
  • 分支:chany_dev_dev(基于 ee03e8d,全部改动在工作区,尚未提交)
  • 状态:77 项测试全绿(基线 17 项),本地真实 GLM + SMTP 冒烟通过,用户实测验收通过
  • 实施档案:superpowers/plans/2026-08-17-contact-driven-lead-email.md(决策 D1-D13、12 个任务的全过程记录)

1. 背景与解决的问题

# 线上问题 根因(代码实证)
1 大量无联系方式的询盘邮件推送至销售邮箱 widget/embedded/standalone 来源的会话结束条件没有联系方式守卫(仅 app 来源有);GLM 评估不遵守"无联系方式不给过"的 prompt 规则
2 有意向客户没留联系方式,无法触达 一判定 interested 就结束会话并锁死客户(ended 后输入框禁用),连补联系方式的机会都没有
3 客户已给的联系方式被 AI 丢失/重复索要 extractor 某轮返回空联系方式时浅合并整层覆盖(数据丢失);上下文提醒不含"禁止再索要"指令

修复后的一句话:联系方式是发询盘邮件的唯一条件——管道确定性检测到即发;没有就永不发、永不结束会话;已给的绝不丢失、绝不重复索要。


2. 核心架构

客户每条消息
  ├─ AI 回复(system.md 话术:价值交换要号 → 先聊后引 WhatsApp 兜底)
  ├─ 统一联系方式管道(app/services/contact_pipeline.py,纯函数)
  │    源1: extractor 提取(多语言 LLM)
  │    源2: 高置信正则(邻接绑定:裸数字一律不收,防价格/年份/车架号误捕)
  │    源3: 独立裸号(整条消息仅 ≥9 位数字 = 最显式自述)
  │    → key 归一化 → 深合并(新值覆盖) → 最新显式自述覆盖 → 最近提及裁决
  │    → 格式校验分级:confirmed(可触发)/ suspect(仅用于提醒)
  ├─ 邮件触发器(ConversationManager 内聚,路由层零邮件逻辑)
  │    confirmed 首次出现 → [新询盘·标签];24h 跨会话同值 → [线索回访]轻提醒
  │    联系方式变化 → [询盘·更正N](每会话≤3封);达上限后锁邮件不锁快照
  ├─ 定向提醒:confirmed→"绝对禁止再索要";suspect→"礼貌确认一次"(值经消毒防提示词注入)
  ├─ 评估前置:<3 条客户消息跳过 GLM 评估(省 1/3 调用),意向仅作邮件标签
  ├─ 每轮评估落库:interested+无联系方式 = 卡单记录(admin 可筛人工兜底)
  └─ 会话永不自动结束(决策 A)——关闭留给后续超时清理

3. 文件清单

新增(5 个文件,1010 行)

文件 行数 职责
app/services/contact_pipeline.py 200 统一联系方式管道:normalize_contacts(中英文 key 别名归一)、merge_contacts(深合并,堵浅合并抹数据)、split_validate(confirmed/suspect 分级;微信 ≥4 字符)、scan_user_messages(高置信正则扫描,全量客户消息历史)、latest_stated_contacts(最新显式自述,唯一可覆盖 extractor 回传的来源)、standalone_number(独立裸号 ≥9 位)、resolve_by_recency(最近提及裁决:杀 extractor 历史振荡、压幻觉值)、canon(去重规范形)
tests/test_contact_pipeline.py 253 管道 34 项单测(含误报反例:价格/年份/里程/VIN/订单号)
tests/test_lead_email_trigger.py 417 触发器/结束守卫/提醒/更正链 25 项集成测试(FakeAI 桩,含用户实测 bug 的完整复刻用例)
tests/test_email_rendering.py 66 邮件渲染 3 项(标签、HTML 转义、内部 key 过滤、RFC2047 扁平化)
scripts/baseline_stats.py 74 上线前后对比基线统计(只读单次脚本)

修改(9 个文件,+323 / −329)

文件 改动摘要
app/services/conversation.py 核心重构。新增:管道接线(深合并修复、正则辅源、覆盖、裁决)、_maybe_send_lead_email 触发器、_get_or_init_evaluation/_recently_emailed_canons 辅助、评估前置、每轮评估落库(卡单标记)、定向提醒与短消息增强的过滤/消毒。删除:自动结束逻辑与 _finalize(决策 A)、process_message 死代码(83 行)、_has_real_contact、旧 ended&&interested→发邮件 路径的全部依赖
app/services/email_service.py 全量重写。全字段 html.escape(D12 防注入);主题意向标签 [新询盘·意向强/待确认/无意向/未评估] + 空白折叠加固;中文标签联系方式列;新增 send_returning_lead_email([线索回访])与 send_correction_email(contact_info, count)([询盘·更正N]);"邮件为触发时快照"提示
app/api/routes/chat.py 拆除 _try_send_email 整个函数、email 相关导入与 ended 触发块;WS 连接生命周期保持 HEAD 语义(ended 后关闭连接)
app/api/routes/chat_rest.py 拆除 lazy-import 触发块;docstring 更新;App 响应契约字段不变(ended 恒为 false,见 §8 知会事项)
app/models/database.py Evaluation 表新增 correction_count INT DEFAULT 0
app/main.py _migrate 新增 PRAGMA 幂等迁移(实测二次运行 no-op)
app/prompts/system.md ①价值交换固定模板(只承诺"完整报价单",禁具体价格/折扣/时限);②先聊后引(聊天内最多要两次,才推 WhatsApp 按钮);③拿到联系方式后确认一句继续解答,不急着收尾;④系统提醒配合(已提供→禁再要;疑似→礼貌确认一次)
app/prompts/extractor.md 客户最后提供的号码覆盖旧值;单独数字串也是号码
tests/test_chat_rest_message.py 删除 2 个钉死旧触发路径的测试(被 manager 层新测试取代),保留 reply/404/429

4. 行为对照

场景 改前 改后
widget 无联系方式 + GLM 误判 interested 结束会话锁死客户 + 发无联系方式邮件 零邮件;会话永不结束,AI 按话术继续争取
第 1 条消息就带联系方式 等判定结束才发 立即发 [新询盘·未评估]
联系方式被 extractor 空轮抹掉 数据丢失 深合并保留
AI 重复索要已给的联系方式 常发生 定向提醒禁止;suspect 值礼貌确认一次
换号(说/不说"更正") 依赖 extractor(实测常丢) 最新显式自述 / 独立裸号 / 最近提及,确定性翻新
同号镜像两个 key(手机+WhatsApp) 更正后新旧混排 同步翻新,邮件/快照均为最新
extractor 在历史号码间振荡 每次翻转烧一封更正邮件 最近提及裁决熄灭(实测复刻用例锁死)
同号码 24h 内再开新会话 每会话一封全量 一行 [线索回访] 轻提醒
更正次数 —(新能力) 每会话 ≤3 封 [更正N];达上限锁邮件不锁快照(admin 永远最新)
会话结束 contact+interested+≥3轮即结束锁死 永不自动结束(决策 A)
GLM 评估调用 每条消息 <3 条客户消息跳过
邮件 HTML 客户可控文本不转义 全转义 + 主题行加固

5. 关键决策记录(产品拍板存档)

决策 内容
D1 联系方式 = 发邮件唯一条件(不看消息数、不看意向评级)
D2 意向降级为邮件标签;评估跳过时标签为 [未评估](与客户端事件的 pending 语义分离)
D4 同会话全量邮件 1 封 + 更正邮件 ≤3 封;锁邮件不锁快照
D5 24h 跨会话同值去重 → 轻提醒
D6 confirmed→禁止再索要 / suspect→礼貌确认(含提示词注入消毒)
D7 评估前置(<3 轮跳过)
D8 interested+无联系方式 → 卡单记录(admin 人工兜底数据基础)
D9 旧路由层触发路径整体拆除(与新触发器同批部署,杜绝双发窗口)
决策 A(2026-08-18) 会话永不自动结束(邮件解耦后自动结束只剩伤害:锁死没问完的客户)
更正上限(2026-08-18) 1 → 3 次(防轰炸与漏最终号的折中)

6. 实测驱动的修复链(2026-08-18 用户实测发现的 4 个 bug)

用户反馈 定位 修复(任务号)
"wrong number, use X" 不触发更正 extractor 回传旧值 + 扫描器首中/填空无法翻新 最新显式自述覆盖(Task 12)
更正邮件里手机/WhatsApp 新旧混排 extractor 镜像同号进两个 key,只翻新命中 key 同号同步(12b)→ 升级为最近提及裁决(12c)
更正配额被烧光、末封邮件显示旧号 extractor 在历史号码间振荡,每次翻转触发虚假更正 最近提及裁决(12c)
直接发裸号不触发更正 裸号唯一捕获源是概率性的 extractor 独立裸号确定性捕获,≥9 位下限排除价格/年份(12d)

7. 部署注意事项 ⚠️

  1. 整体上线,禁止部署中间态:新触发器与旧路径拆除必须同一批次(D9,中间态存在双发邮件窗口)。
  2. 迁移自动执行:启动时 _migrate 自动为旧库追加 correction_count 列,无需手工 SQL。
  3. 跑生产基线:部署机上执行 PYTHONIOENCODING=utf-8 python scripts/baseline_stats.py,覆盖 docs/superpowers/plans/2026-08-17-baseline.txt(当前为开发库占位数据)。
  4. SMTP 必须配置:首触邮件即时发送;未配置时每条消息都会重试并记日志。
  5. 上线后观测(对照基线):①新邮件 100% 带联系方式;②全量邮件量 vs 基线上界;③[更正]/[回访] 占比;④admin 卡单数量。

8. 已知限制与残余风险(台账摘录)

项 状态
App 端 ended 恒为 false(决策 A) App 若有"对话结束"闭环 UI 需调整;widget/独立页零影响
会话只生不死(无超时清理) 数据卫生问题,功能无害;超时清理排健康轨,上线后尽早做
第 1 轮 extractor 幻觉出从未提及的号码 → 首封邮件混排 双低概率条件叠加;whatsapp 侧仍真实可联;出现再做 12e
中文无冒号给号句式("我手机号是X")无确定性捕获 extractor 兜底;实测命中率待观察,低则补中文语境模式
号码变体击穿 24h 去重 / 撤回已给联系方式 / 客户离开页面后无法找回 best-effort / 接受 / 物理死局(火力已前置到"人还在"时)
ADMIN_PASSWORD 仍为默认 admin123 H1 暂缓(用户决策),强烈建议尽快在 .env 修改
发送成功后落库失败的毫秒级窗口 at-least-once 有意选择(漏线索比重复邮件贵)

9. 未包含(后续批次,见实施档案 §6)

  • 批次 2:widget 会话持久化(localStorage+48h 过期,解决刷新丢会话/重复线索)、前端历史渲染(独立页 loadHistory 空函数)、WS 限流 + 消息长度上限
  • 批次 3:admin"缺联系方式线索"筛选(基于卡单记录)、UI 留资卡片
  • 健康轨:删 Redis、SQLite 备份+WAL、GLM 超时重试、admin 分页修复、国家映射(加拿大笔误)、超时清理会话、删除无调用方的 AIService.chat、ADMIN_PASSWORD