Co-Authored-By: Claude Code <noreply@anthropic.com>
14 KiB
我方联系方式外发 v2:按钮指路 + 零号码红线(Company Contact Handoff)
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 客户索要我方联系方式时:web 客户 → 引导点击窗口顶部的 WhatsApp/邮箱按钮;App 客户(无按钮)→ 反向留资由客户经理主动联系。AI 在任何情况下不口头输出号码或邮箱——与站点动态配置的按钮值天然零冲突,防幻觉墙简化为无值比对的"零号码墙"。
Architecture: 话术(按钮指路 + source 分流)+ 提醒(触发词前移注入 + 来源标注)+ 零号码墙(AI 输出出现 ≥10 位数字串或邮箱即拦,仅客户回显豁免)。无新配置、无前端改动、无 schema。
Tech Stack: system.md / evaluator.md / conversation.py / boundary_filter.py / pytest
1. 背景(v1 → v2 的决策记录)
- v1 设计(.env 全局真值注入 prompt + 真值比对墙 + 毒值过滤)历经四轮评审收敛
- v1 被推翻的关键事实(用户澄清):前端 WhatsApp/邮箱按钮是按站点动态配置的(data-* 属性)——不同嵌入站点可配不同号码。后端单值注入必然与站点按钮冲突,且真值墙会误拦站点自配的正确号码
- v2 决策(用户拍板):AI 永不口头报号/邮箱,只做两件事——web 指按钮(按钮配什么号都和 AI 无关,零冲突)、App 反向留资(客户经理主动联系)。v1 的配置管道、真值比对、尾缀匹配、毒值三入口过滤全部不再需要
- v1 四轮评审中仍适用的发现全部继承:提醒前移防早退吞(R3-1)、独立叠加(#3)、词表含"你的"(R3-3)、互留条件排除 suspect(R4-4)、节奏豁免补索要场景(R4-2)、未提供渠道引导(R4-3)、冒烟路径与收尾任务(R5-2/R5-3)
2. 设计
D1:零号码红线(v2 核心)
AI 回复中永远不出现:
- 任何 ≥10 位连续数字串(含带空格/横线/括号的分隔形态)
- 任何邮箱地址
引导一律通过:按钮指路(web,按钮由站点动态配置、AI 不需要知道值)或反向留资(App,请客户留下联系方式由客户经理主动联系)。
误杀安全性(v1 已验证沿用):年份(4 位)、里程(逗号/空格断开 <10 位)、VIN(字母混合无长数字串)、续航(600km)、电池(85kWh)均天然不触发;AI 正常聊车内容零误杀。
D2:source 分流(App 无按钮的承接)
后端知道会话来源(conv.source),注入提醒层:
| 来源 | 客户索要联系方式时的话术 |
|---|---|
| widget / embedded / standalone(web,页面顶部有 WhatsApp/Email 按钮) | "Just tap the WhatsApp button at the top of this chat — our manager replies fast!"(中文同理) |
| app(无按钮) | 反向留资:"Drop your WhatsApp and our manager will reach out to you directly with the full quote!" |
D3:互留(已拍板:加)
- web:给完按钮指路后轻问一句 "Drop yours too and our manager will send you the full quote!"——每会话最多一次;confirmed 或 suspect 时不加(suspect 时 D6 要求礼貌确认而非再索要)
- App:反向留资本身就是主话术,不再叠加互留
D4:未提供渠道(继承 R4-3)
客户索要微信/Telegram/LINE/ins 等未配置渠道 → 引导到按钮("We mainly use WhatsApp — tap the button at the top, fastest way to reach us"),绝不提供/编造任何渠道账号。
D5:零号码墙(v1 真值墙的简化形态)
句子门控管道并列新增一类违规,无值比对:
AI 输出句中出现 ≥10 位数字串 或 邮箱
├─ 该串出现在近期客户消息中(客户号的礼貌回显)→ 放行
└─ 否则 → 拦截该句,替换句按 source 分流:
web → "Just tap the WhatsApp button at the top — our manager replies fast!"
App → "Drop your WhatsApp and our manager will reach out directly!"
- 无 allowed 集合、无国家码尾缀、无公司值依赖——墙不认识任何"正确号码",它只认识"AI 不该有号码"这一条
- 客户回显豁免保留(便宜且防误杀礼貌确认:"Got it, 07012345678")
- 替换句为通用句式(v1 R5-1 教训:不写死渠道名与值的绑定)
- 墙的附带收益(R3-4 同理):任意场景幻觉出的热线号/邮箱一律被拦
D6:与现有体系交互(沿用 v1 核实 + 增补)
| 体系 | 结论 |
|---|---|
| 价格墙 | 并列两类违规、同一拦截管道,互不干扰 |
| 留资管道 | AI 不再输出号码 → v1 的"公司号被回显当成客户号"(R4-1)主通路消失;残余=客户从按钮链接复制号码回显(罕见),记录接受 |
| 动态配置按钮 | 零冲突(AI 不报值,站点配什么都一致) |
| D6 禁止再索要 / suspect 确认 | 互留条件已排除;App 反向留资在已留资时由 D6 提醒压制 |
| evaluator | 索要我方联系方式 = 最强意向信号 |
| 前端/配置/schema | 全部零改动 |
3. 文件结构
| 文件 | 动作 | 职责 |
|---|---|---|
app/prompts/system.md |
修改 | 索要话术节(web/App 双话术)+ 零号码红线 + 未提供渠道 + 节奏豁免补索要 |
app/services/conversation.py |
修改 | CONTACT_ASK_TRIGGER_RE + 提醒前移/独立/互留条件/source 标注 + 墙接线(传 source 选替换句) |
app/services/boundary_filter.py |
修改 | no_contact_value(sentence, user_history) 零号码判定 + 按来源的替换句 |
app/prompts/evaluator.md |
修改 | 最强意向信号 |
tests/test_boundary_filter.py、tests/test_lead_email_trigger.py |
修改 | 墙/提醒单测与集成 |
4. 任务分解
Task 15.1: system.md 话术与红线
- "引流兜底"节后新增:
### 客户索要我方联系方式(最高意向信号,立即响应)
客户问"how can I contact you / 你们的WhatsApp多少"时:
- 系统提醒会告知当前会话来源。**网页会话**(有页面按钮)→ 指引点击:
"Just tap the WhatsApp button at the top of this chat — our manager replies fast!"
(邮箱同理:"Tap the Email button at the top.")
- **App 会话**(没有页面按钮)→ 反向留资:
"Drop your WhatsApp and our manager will reach out to you directly with the full quote!"
- 中文版同理("直接点窗口顶部的 WhatsApp 按钮就行,客户经理回复很快~")
- 【互留】网页会话给完指路后可轻问一句:"Drop yours too and our manager will send you
the full quote!"——每会话最多一次;客户已留过或疑似留过则不加
- 客户索要未提供的渠道(微信/Telegram/LINE/ins)→ "We mainly use WhatsApp — tap the
button at the top, fastest way to reach us.",绝不提供或编造任何渠道账号
- 红线区追加:
- 回复中不能出现任何电话号码或邮箱地址——客户自己的号码也不回显(确认收到即可);引导联系一律用页面按钮(网页)或请客户留联系方式(App) - 节奏豁免补一类:"客户主动问价、问时效、提实质请求或索要我方联系方式时…"
- 验证加载:
python -c "from app.services.ai_service import SYSTEM_PROMPT as p; assert '索要我方联系方式' in p and '不能出现任何电话号码' in p; print('ok')"
Task 15.2: 提醒层
- 触发词表(conversation.py 模块级):
CONTACT_ASK_TRIGGER_RE = re.compile(
r"(your|our)\s+(whatsapp|number|phone|email|contact)|"
r"(contact|reach|call|email)\s+(you|us)|"
r"你(们)?的?\s?(whatsapp|号码|电话|微信|邮箱|联系方式)|"
r"(怎么|如何)联系|给我.{0,4}(号码|电话|联系方式)",
re.IGNORECASE,
)
_build_context_reminder接线(签名加可选参数source: str = "",调用处传conv.source.value):
# === 前移段(早退之前,与 boundary_note 并列) ===
contact_note = ""
if CONTACT_ASK_TRIGGER_RE.search(latest_msg or ""):
if source == "app":
contact_note = ("⚠️ 客户正在索要我方联系方式(最高意向信号)。当前为 App 会话,"
"没有页面按钮——请引导客户留下他的联系方式,客户经理会主动联系:"
"绝对禁止输出任何号码或邮箱。")
else:
contact_note = ("⚠️ 客户正在索要我方联系方式(最高意向信号)。引导点击窗口顶部的 "
"WhatsApp/Email 按钮即可——绝对禁止口头报出任何号码或邮箱。")
if not raw_info:
return "\n".join(x for x in (boundary_note, contact_note) if x)
# === 体内注入段(confirmed/suspect 判定后) ===
if contact_note:
note = contact_note
if source != "app" and not confirmed and not suspect:
note += "客户尚未留过联系方式,可顺势轻问一句(每会话最多一次)。"
parts.append(note)
- 调用处 stream 流程加索要率日志:
logger.info("Contact-ask reminder fired (source=%s)", conv.source.value) - 调用处传 source(找到
_build_context_reminder(...)唯一调用点补参数)
Task 15.3: 零号码墙(boundary_filter.py + conversation.py 接线)
- boundary_filter.py 追加:
LONG_NUMBER_RE = re.compile(r"\+?\d[\d\s\-().]{9,17}\d") # ≥10位连续数字(允许分隔形态)
ANY_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
NO_CONTACT_WEB = ("Just tap the WhatsApp button at the top of this chat — "
"our manager replies fast!")
NO_CONTACT_APP = ("Drop your WhatsApp and our manager will reach out to you directly "
"with the full quote!")
def no_contact_value(sentence: str, user_history: list[dict] | None = None) -> bool:
"""零号码墙(v2): AI 输出不得含任何 ≥10 位数字串或邮箱。
唯一豁免: 该串出现在近期客户消息中(客户自己号码的礼貌回显)。
无值比对、无公司值依赖——年份/里程/VIN/续航天然安全(已验证)。"""
for m in LONG_NUMBER_RE.finditer(sentence or ""):
c = digits_only(m.group(0))
if user_history and any(c in digits_only(msg.get("content") or "")
for msg in user_history if msg.get("role") == "user"):
continue # 客户回显豁免
return True
for m in ANY_EMAIL_RE.finditer(sentence or ""):
if user_history and any(m.group(0).lower() in (msg.get("content") or "").lower()
for msg in user_history if msg.get("role") == "user"):
continue
return True
return False
(digits_only 从 contact_pipeline import。)
- conversation.py 句子门控接线:价格违规判定之后并列
no_contact_value(sentence, history_for_ai);命中走同一拦截-掐断-替换路径,替换句按conv.source.value == "app"选 NO_CONTACT_APP / NO_CONTACT_WEB;日志行No-contact wall replaced(对齐观测模式)。 - 注意:现有集成测试的 FakeAI/MixedAI/DirtyAI 流式回复均不含 ≥10 位数字或邮箱("ok reply"/"Nice pick!"/"$25k")——确认零破坏。
Task 15.4: evaluator.md
- 意向积极信号行追加:
客户主动索要我方联系方式(WhatsApp/邮箱/电话),视为最强意向信号之一
Task 15.5: 测试与冒烟
- 单测(test_boundary_filter.py):任意号码拦截("call us at +86 138 0013 8000")、任意邮箱拦截、客户回显豁免("Got it, 07012345678" + history 含之)、白名单安全("2024 model / 45,000 km / VIN LFV3A23C8K3123456 / 85kWh")、替换句无号码自检(NO_CONTACT_* 不含 ≥10 位数字/邮箱)
- 单测(test_lead_email_trigger.py):索要触发提醒("how can I contact you"/"你们的whatsapp多少"/"你的微信多少");首条消息空画像也有提醒;app source 分流话术(含"主动联系"字样);已留资/suspect 无互留句;普通消息不触发;与价格提醒叠加共存
- 集成(test_lead_email_trigger.py):
NumberAI(流式回复含 "+86 138 0013 8000")→ 客户端收到的是按钮指路句且落库一致;app source 会话同样违规 → 收到留资句 - 全量回归:预计 106 → ~120,全绿
- 真机冒烟(重启服务,真实 GLM):
- web 独立页问 "how can I contact you" → 按钮指路,回复零号码零邮箱
- 问 "你们有微信吗" → 引导回 WhatsApp 按钮
- 双重请求 "怎么联系你们?多少钱" → 指按钮 + 不报价
- 连续索要 ×3 → 互留句只出现一次
- REST 模拟 App 会话(source=app)索要 → 反向留资话术
- 正常问车(续航/里程/年份)→ 数字正常回答,零误杀
Task 15.6: 收尾(R5-3 惯例)
- 全量回归最终确认
- changelog 文档(docs/2026-08-20-company-contact-handoff-changelog.md)
- 单提交;重启本地服务交付冒烟
5. 决策点(全部已定)
| # | 决策 | 状态 |
|---|---|---|
| 1 | 互留一句 | 已定:加,每会话一次,confirmed/suspect 排除 |
| 2 | 引导方式 | 已定(2026-08-20 v2):按钮指路 + 零号码红线——溶解动态配置难题,App 由反向留资承接 |
| 3 | 前端同步 | 随 v2 消失——AI 不报值,站点按钮自由配置 |
6. 风险与残余
| 风险/残余 | 处置 |
|---|---|
| GLM 被客户磨狠了仍报号 | 零号码墙确定性拦截(无值比对,比 v1 更严) |
| 客户坚持要"号码本身" | web:点按钮即加号(wa.me 直达);App:反向留资后客户经理主动联系——接受 |
| 客户从按钮链接复制号码回显 → 被留资管道误捕获 | AI 从不报号后主通路消失;残余极罕见,出现再补毒值 belt(记录) |
| 纯字母渠道 ID(微信号)编造 | 零号码墙拦不住字母 ID——prompt"绝不提供未配置渠道"兜底(继承 R4-3 残余) |
| 提醒/话术被价格提醒叠加时指令过载 | 两提醒内容兼容(指按钮+不报价);冒烟 #3 验证 |
| 回滚 | 纯 prompt+提醒+墙扩展,git revert 单提交即回滚 |