Files
ai-kefu-jc/docs/superpowers/plans/2026-08-20-company-contact-handoff.md
T
2026-09-29 14:15:12 +08:00

235 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 我方联系方式外发 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` 单提交即回滚 |