235 lines
14 KiB
Markdown
235 lines
14 KiB
Markdown
|
|
# 我方联系方式外发 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` 单提交即回滚 |
|