# 我方联系方式外发 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 话术与红线 - [ ] "引流兜底"节后新增: ```markdown ### 客户索要我方联系方式(最高意向信号,立即响应) 客户问"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 模块级): ```python 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`): ```python # === 前移段(早退之前,与 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) ``` ```python # === 体内注入段(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 追加: ```python 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): 1. web 独立页问 "how can I contact you" → 按钮指路,**回复零号码零邮箱** 2. 问 "你们有微信吗" → 引导回 WhatsApp 按钮 3. 双重请求 "怎么联系你们?多少钱" → 指按钮 + 不报价 4. 连续索要 ×3 → 互留句只出现一次 5. REST 模拟 App 会话(source=app)索要 → 反向留资话术 6. 正常问车(续航/里程/年份)→ 数字正常回答,零误杀 ### 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` 单提交即回滚 |