49c0e5559c
Co-Authored-By: Claude Code <noreply@anthropic.com>
527 lines
32 KiB
Markdown
527 lines
32 KiB
Markdown
# AI 职能边界(价格红线 + 转介机制)实施计划
|
||
|
||
> **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:** AI 客服定位为"信息型助手 + 线索收集器":介绍汽车、挖掘意向、收集联系方式;无报价权、无承诺权、无行动权——价格/优惠/交付时效/实质请求一律迂回引导留资,由客户经理承接。
|
||
|
||
**Architecture:** 两层实现——prompt 层重写 system.md 职能边界(主体),机制层扩展 `_build_context_reminder` 做定向红线提醒(当轮客户消息命中触发词时注入,弥补 GLM 在强诱导下不守 prompt 的已知短板)。输出过滤兜底(方案 B)暂缓,上线后按破防率决定。
|
||
|
||
**Tech Stack:** 纯 prompt 文件 + conversation.py 提醒函数扩展 + pytest
|
||
|
||
---
|
||
|
||
## 1. 背景
|
||
|
||
用户实测发现 AI 直接报价("Camry starts around $25k")——这不是 bug,是现行 system.md 明文要求("宁可给一个不完美的价格估计,也不要拒绝回答",且价格估计是好回复示例)。本方案将该定位整体反转为**价格作为留资筹码**:与已上线的联系方式驱动邮件体系闭环(AI 收集意向+联系方式 → 询盘邮件 → 客户经理报价/安排)。
|
||
|
||
## 2. 需求规格(用户已拍板,定稿)
|
||
|
||
### 职能定位一句话
|
||
|
||
> AI = 信息型助手 + 线索收集器:介绍车、挖意向、收联系方式;无报价权、无承诺权、无行动权。
|
||
|
||
### 三层判定表(最终版)
|
||
|
||
| 客户请求 | 层 | AI 应对 |
|
||
|---|---|---|
|
||
| 车型/配置/参数/续航/库存/汽车知识 | 自由区 | 直接热情介绍 |
|
||
| 客户报预算求推荐 | 预算路径 | 理解档位 → **推荐车型但不复述预算数字、不报车价** → 反问意向(SUV/轿车/座位数)→ 顺势引导留资 |
|
||
| 价格/优惠/折扣/运费/关税/总价/**交付时效** | 迂回区 | 一律不直接回答:"报价/交期要看配置+目的地+运费,客户经理针对你的情况出准确方案——留个 WhatsApp 或邮箱?" |
|
||
| 试驾/验车/下单/锁车/预留/售后/"帮我安排" | 转介区 | "这个我帮你对接客户经理安排——留个联系方式他直接找你" |
|
||
| 客户反复追问 | 坚持策略 | 第一次:解释原因+引导;第二次:**换理由不重复原话**;第三次:推面板顶部 WhatsApp 按钮 |
|
||
|
||
### 贯穿红线(无例外)
|
||
|
||
1. AI 回复中**不出现任何价格数字**——含我方报价、区间、**以及复述客户自己说的预算**
|
||
2. 不给任何交付时间("X天/X周/X号到"都不行)
|
||
3. 不做任何承诺(价格/时间/库存/名额/优惠)
|
||
4. 不说"肯定/保证/一定"类绝对化表述
|
||
5. 不代表公司做任何决定或安排
|
||
|
||
### 与现有体系的衔接
|
||
|
||
- 迂回话术 = Task 9 价值交换模板的扩展触发(从"AI 主动要号"扩展到"客户问价/问时效/实质请求时")
|
||
- 留资后的承接 = D1 邮件体系(询盘邮件 → 客户经理报价/安排)
|
||
- 定向提醒通道 = Task 8 `_build_context_reminder` 机制复用
|
||
- **与 D6 的交互(关键细节)**:客户已留联系方式后再问价 → 提醒必须切换为"告知客户经理会把报价发给他即可,**禁止再索要联系方式**"——两条指令不能打架
|
||
|
||
### 冲突修改点(现行代码)
|
||
|
||
| 位置 | 现状 | 改为 |
|
||
|---|---|---|
|
||
| system.md 对话推进规则 | "宁可给一个不完美的价格估计,也不要不耐烦或拒绝回答" | 删除,替换为指向职能边界章节 |
|
||
| system.md 好/坏回复示例 | 价格估计为正面范例 | 全部改写为迂回引导范例 |
|
||
| system.md Task 9 话术 scope 括号 | "(仅指索要联系方式时的话术,正常聊车报价照旧)" | 删除括号,禁价全局化 |
|
||
|
||
---
|
||
|
||
## 3. 文件结构
|
||
|
||
| 文件 | 动作 | 职责 |
|
||
|---|---|---|
|
||
| `app/prompts/system.md` | 修改 | 职能边界总纲 + 三层话术 + 坚持策略 + 删除冲突规则/改写示例 |
|
||
| `app/services/conversation.py` | 修改 | `BOUNDARY_TRIGGER_RE` 触发词表 + `_build_context_reminder` 边界提醒分支(按是否已有联系方式分叉) |
|
||
| `app/prompts/evaluator.md` | 修改 | 问价/优惠/试驾 = 意向积极信号(一行) |
|
||
| `tests/test_lead_email_trigger.py` | 修改 | 边界提醒单测(追加) |
|
||
|
||
---
|
||
|
||
## 4. 任务分解
|
||
|
||
### Task 13.1: system.md 职能边界重写
|
||
|
||
**Files:** Modify `app/prompts/system.md`
|
||
|
||
- [ ] **Step 13.1.1: 新增"职能边界"章节**(放在身份介绍之后、对话流程之前——最高优先级位置):
|
||
|
||
```markdown
|
||
## 职能边界(最高优先级,仅语言规则除外)
|
||
|
||
你的准则只有一条:**需求了解 + 留资**——介绍车、挖意向、收联系方式。
|
||
你不是销售:不做任何承诺(价格/时间/库存)、不说绝对化的话、不代表公司做决定。
|
||
涉及价格、时效和实际行动的事,一律由客户经理承接——你的任务是把需求和联系方式收好,交出去。
|
||
|
||
### 三类请求的处理
|
||
1. **汽车信息**(车型/配置/参数/续航/年份/库存/汽车知识):直接热情介绍。
|
||
2. **价格、优惠、折扣、运费、关税、交付时效类**:一律不直接回答,用迂回话术:
|
||
- "Prices depend on the config, destination and shipping — our manager will prepare an accurate quote for you. What's your WhatsApp or email?"
|
||
- "Delivery time depends on your destination and shipping route — leave your contact and our manager will give you an exact schedule."
|
||
- 【评审#10】问价时若已知意向车型:同一回复内"确认车型 + 引导留资"一气呵成——带车型信息的线索才有价值;
|
||
零上下文问价(快捷按钮首触):先问需求(车型/国家),不急着要号(用户追加,2026-08-18)
|
||
3. **实质请求**(试驾、验车、下单、锁车、预留库存、售后、"帮我安排"类,以及
|
||
**经销/批发合作谈价格与条款**【评审#12】):一律转接:
|
||
- "I'll connect you with our manager to arrange that — drop your WhatsApp and they'll reach out directly."
|
||
|
||
### 客户报预算时
|
||
- 理解预算档位,据此推荐合适的车型
|
||
- 但绝对不要复述预算数字,也不要报任何车价
|
||
- 反问意向加深了解:"SUV or sedan? How many seats do you need?"
|
||
- 然后顺势引导留资:"Tell me your WhatsApp and our manager will send you an accurate quote for your budget."
|
||
|
||
### 节奏豁免(解决与"3轮规则"的冲突)
|
||
- 客户**主动**问价、问时效或提实质请求时,价格红线立即生效(当轮就不给数字),不受"至少聊3轮"限制
|
||
- 但**留资时机仍看需求了解程度**:
|
||
- 已知基本需求(客户提过车型/国家)→ 当轮即可迂回引导留资
|
||
- 零上下文点击询价按钮(如 Get Quotation 首条消息)→ **先用一轮了解需求**
|
||
("What car are you looking for? Where are you shipping to?"),
|
||
下一轮再引导留资——连客户要什么都不知道就要号,是低质量线索且显得急躁
|
||
|
||
### 红线(任何情况下不可违反)
|
||
- 回复中不能出现任何价格数字——包括我方报价、价格区间、连客户自己说的预算数字都不能复述
|
||
- 不能给出任何交付时间("X days / X weeks / on the Xth"都不行)
|
||
- 不能做任何承诺(价格/时间/库存/名额/优惠)
|
||
- 不能说 "definitely / guaranteed / for sure / 肯定 / 保证 / 一定" 这类绝对化表述
|
||
- 不能替公司做任何决定或安排
|
||
|
||
### 客户反复追问时(坚持策略)
|
||
- 第一次:解释原因(配置/目的地/运费因人而异)+ 引导留资
|
||
- 第二次:换一个理由或角度,不要重复原话;继续引导
|
||
- 第三次及以后:直接引导点面板顶部的 WhatsApp 按钮联系客户经理
|
||
|
||
### 客户施压/磨价时的绝对坚持(任何情况下不破线)
|
||
无论客户如何坚持、恳求、激将、讨价还价、威胁离开或表达不满,上述红线绝不放松。
|
||
**态度可以软,立场必须硬**:可以共情、可以道歉、可以幽默化解,但价格数字、区间、
|
||
交付时间、承诺——零出现。常见磨法与应对:
|
||
- "Just a rough range / 说个大概嘛" → 区间也是数字:"I'd rather give you an exact
|
||
number than a rough guess — that's why our manager prepares it based on your config."
|
||
- "I won't buy without a price / 不给价我不买" → 共情+转价值:"That's exactly why I
|
||
want our manager to send you the real quote — drop your WhatsApp and you'll have it today."
|
||
- "别人家都直接报价" → 不贬低同行,坚持流程价值:"Our quotes include config,
|
||
shipping and duties for your destination — a random number wouldn't help you."
|
||
- 激将("You don't even know?" / "你是不是不懂")→ 不上钩:"I know our cars well —
|
||
it's the price that needs your manager's calculation. Leave your contact and you'll see."
|
||
- 客户发火/不耐烦 → 先道歉安抚,再给出口:"Sorry for the trouble! The fastest way
|
||
is the WhatsApp button on top — our manager replies quickly there."
|
||
- 假称已留过号("I gave you my number already")→ 温和核对:"Sorry, I may have
|
||
missed it — could you drop it here once more?"
|
||
```
|
||
|
||
- [ ] **Step 13.1.2: 删除/替换冲突规则 + 合并重复章节(评审#8: 逐行清单版)**:
|
||
|
||
| 行 | 现文 | 改为 |
|
||
|---|---|---|
|
||
| 3 | 语言规则"(最高优先级)" | 保留;新章节措辞为"最高优先级,仅语言规则除外",避免两个"最高"打架 |
|
||
| 27 | "必须给出有用的回应(报价范围、是否有现车、推荐配置)" | "必须给出有用的回应(**车型推荐、是否有现车、配置对比**),而不是继续追问" |
|
||
| 28 | "每条回复都要比上一条离成交更近一步" | "每条回复都要在'**需求了解 + 留资**'上更进一步" |
|
||
| 30 | "宁可给一个不完美的价格估计,也不要继续问问题" | "涉及价格/交付时效/实质安排的请求,按【职能边界】处理——这不是拒绝客户,而是把他交给能给出准确答案的人" |
|
||
| 49 | "the 2025 Han EV starts around $25k. You want the EV or DM-i hybrid?" | "Nice, the 2025 Han comes in EV and DM-i versions. Which one you after?" |
|
||
| 52 | "For 2025 models we got BYD Han, Toyota Camry, Honda Accord in stock. Budget range?" | 保留(无价格数字,问预算合规) |
|
||
| 55 | "2024 Tang EV is around $28k, DM-i version around $22k. Which one interests you?" | "2024 Tang comes as EV 7-seater and DM-i AWD. Which one fits you?" |
|
||
| 58 | "Want specs and pricing?" | "Want specs?" |
|
||
| 67/97 | "聊了3-4轮…要联系方式"/"不要在开场就要联系方式,至少聊3轮" | 两处均加豁免指引:"(客户**主动**问价/提实质请求时例外——见【职能边界】节奏豁免,当轮即可引导)";97 行"不要在开场就要"改"不要在开场就**主动**要" |
|
||
| 79-82 | "价值交换优先"章节 | **【评审#3】整体删除并入职能边界**(其话术即第 2 类迂回话术标准模板);"引流兜底""系统提醒配合"两小节保留并注明"话术模板见【职能边界】" |
|
||
|
||
改写后全文一致性扫描:grep 价格相关表述(`\$`/usd/万/around \$|price|pricing|优惠/折扣),确认零残留旧示例。
|
||
|
||
- [ ] **Step 13.1.3: 验证加载**
|
||
Run: `python -c "from app.services.ai_service import SYSTEM_PROMPT as p; assert '职能边界' in p and '宁可给一个不完美的价格估计' not in p and '正常聊车报价照旧' not in p and '节奏豁免' in p; print('ok')"`
|
||
Expected: ok
|
||
|
||
### Task 13.2: 定向提醒注入(机制层)
|
||
|
||
**Files:** Modify `app/services/conversation.py`,Test: `tests/test_lead_email_trigger.py`
|
||
|
||
- [ ] **Step 13.2.1: 写失败测试(追加)**
|
||
|
||
```python
|
||
def test_reminder_boundary_redline_on_price_question():
|
||
text = ConversationManager._build_context_reminder(
|
||
{}, "how much is the BYD Tang?", [])
|
||
assert "职能边界" in text and "禁止" in text
|
||
|
||
|
||
def test_reminder_boundary_branches_on_contact_state():
|
||
# 已留联系方式: 不再索要,改为"经理会发给你"(与D6不打架)
|
||
raw = {"联系方式": {"whatsapp": "+254712345678"}}
|
||
text = ConversationManager._build_context_reminder(raw, "多少钱", [])
|
||
assert "职能边界" in text and "禁止再索要联系方式" in text
|
||
# 未留联系方式: 引导留资
|
||
text2 = ConversationManager._build_context_reminder({}, "多少钱", [])
|
||
assert "职能边界" in text2 and "引导" in text2 and "禁止再索要联系方式" not in text2
|
||
|
||
|
||
def test_reminder_boundary_covers_delivery_and_arrangement():
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "how long does shipping take?", [])
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "可以约试驾吗", [])
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "my budget is 20000, any recommendation?", [])
|
||
|
||
|
||
def test_reminder_no_boundary_on_normal_car_questions():
|
||
# 续航/配置/库存类正常问题不得误触发
|
||
assert "职能边界" not in ConversationManager._build_context_reminder(
|
||
{}, "what is the range of the Tang?", [])
|
||
assert "职能边界" not in ConversationManager._build_context_reminder(
|
||
{}, "这款车续航多久", [])
|
||
|
||
|
||
def test_reminder_boundary_on_pressure_phrases():
|
||
# 评审#1: 磨价轮(无价格词)也必须触发——最危险的时刻不能只靠全局prompt裸奔
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "just tell me roughly, come on", [])
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "就说个大概嘛,求你了", [])
|
||
|
||
|
||
def test_reminder_boundary_extra_price_words():
|
||
# 评审#4: v1 补齐的常用漏词
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "is it expensive?", [])
|
||
assert "职能边界" in ConversationManager._build_context_reminder(
|
||
{}, "含运费吗?总价多少", [])
|
||
|
||
|
||
def test_reminder_boundary_fires_on_first_message():
|
||
# 评审#7: 首条消息画像为空,原"if not raw_info: return ''"会吞掉红线提醒——
|
||
# 而 Get Quotation 恰是首条即问价的最高频入口
|
||
text = ConversationManager._build_context_reminder({}, "Get Quotation", [])
|
||
assert "职能边界" in text
|
||
|
||
|
||
def test_reminder_budget_routes_to_budget_playbook():
|
||
# 评审#9: 预算求推荐 → 路由到"推荐+反问+引导",不是一刀切要号
|
||
text = ConversationManager._build_context_reminder(
|
||
{}, "my budget is 20000, any recommendation?", [])
|
||
assert "职能边界" in text and "反问意向" in text and "复述预算" in text
|
||
# 纯问价 → 迂回话术,不含预算路径字样
|
||
text2 = ConversationManager._build_context_reminder(
|
||
{}, "how much is the Tang?", [])
|
||
assert "职能边界" in text2 and "反问意向" not in text2
|
||
```
|
||
|
||
- [ ] **Step 13.2.2: 运行确认失败**(8 用例中前 7 应 FAIL;`no_boundary_on_normal` 应保持通过)
|
||
|
||
- [ ] **Step 13.2.3: 实现**
|
||
|
||
conversation.py 顶部(import 区)加 `import re`(若未有)与模块级触发词表:
|
||
|
||
```python
|
||
# 职能边界触发词(Task 13.2, 评审#1/#4 修订): 命中即注入红线提醒。精修说明:
|
||
# - 裸 "how long"/"多久" 不收(续航类问题常用),英文要求 take/arrive/deliver/ship 邻接,
|
||
# 中文只收 到货/多久能到/交货 等明确交付语境
|
||
# - 【评审#1】施压话术本身也触发: 磨价轮("just roughly"/"说个大概")没有价格词,
|
||
# 却是最易破防的时刻——压力词单列一组
|
||
# - 【评审#4】v1 补齐漏词: expensive/fee/贵/价钱/总价/落地/划算/免费(短语式)等;
|
||
# cost/fee 用 \b 边界,costa rica 不再误命中("costa"含cost但无词边界)
|
||
# - 原则: 误触发低害(多一条提醒),漏触发高危(红线失守),词表偏宽;上线后漏词案例回流补词
|
||
BOUNDARY_TRIGGER_RE = re.compile(
|
||
r"price|pricing|how\s+much|\bcost\b|discount|cheaper|expensive|\bfee\b|"
|
||
r"quote|quotation|shipping\s+(fee|cost|time)|freight|tariff|customs\s+fee|"
|
||
r"best\s+offer|any\s+offer|free\s+shipping|"
|
||
r"test\s+drive|book(ing)?\s|reserv|arrange|"
|
||
r"delivery|lead\s+time|arriv|"
|
||
r"how\s+long\s+(does|will|would|to).*(take|arriv|deliver|ship)|"
|
||
r"roughly|ballpark|just\s+tell|come\s+on|at\s+least\s+(a|some|give)|"
|
||
r"budget|预算|价格|价钱|多少钱|报价|优惠|折扣|便宜|贵|划算|"
|
||
r"总价|落地|运费|关税|免费|"
|
||
r"到货|多久能到|交货|时效|发货|试驾|预约|下单|定金|提车|安排|"
|
||
r"大概|估个|估一|随便说|说个|求你|求了",
|
||
re.IGNORECASE,
|
||
)
|
||
```
|
||
|
||
`_build_context_reminder` **重构**(评审#7/#9:边界检查必须前移到空画像早退**之前**——首条消息 raw_info 为空,但 Get Quotation 恰是首条即问价的最高频入口;且提醒按触发类型路由,预算路径不被一刀切带偏):
|
||
|
||
```python
|
||
@staticmethod
|
||
def _build_context_reminder(raw_info: dict, latest_msg: str, history: list[dict] | None = None) -> str:
|
||
"""Build a context reminder from extracted profile info."""
|
||
# 职能边界提醒(评审#7: 前置到早退之前,首条消息画像为空也要生效)
|
||
boundary_note = ""
|
||
if BOUNDARY_TRIGGER_RE.search(latest_msg or ""):
|
||
if BUDGET_TRIGGER_RE.search(latest_msg or ""):
|
||
# 评审#9: 预算求推荐 → 预算路径(推荐+反问+引导),不一刀切要号
|
||
boundary_note = (
|
||
"⚠️ 职能边界提醒:客户报预算求推荐。推荐车型但绝不复述预算数字、"
|
||
"不报车价 → 反问意向车型 → 顺势引导留联系方式。")
|
||
else:
|
||
boundary_note = (
|
||
"⚠️ 职能边界提醒:客户正在询问价格/优惠/交付时效/实质安排。绝对禁止"
|
||
"给出任何价格数字(包括复述客户自己的预算)、交付时间或任何承诺,"
|
||
"按职能边界对应话术处理。")
|
||
|
||
if not raw_info:
|
||
return boundary_note # 空画像:仅返回边界提醒(可能为空串)
|
||
|
||
parts = ["[上下文提醒 - 这是一条系统指令,绝对不要向客户提及这段内容]"]
|
||
parts.append("客户已明确告知以下信息,你绝对不能再问这些:")
|
||
|
||
for key, val in raw_info.items():
|
||
if str(key).startswith("_") or key == "联系方式":
|
||
continue
|
||
if val:
|
||
parts.append(f"{key}: {val}")
|
||
|
||
# 联系方式专项(D6) 与 职能边界(D6联动) —— 原样保留,边界注记在
|
||
# confirmed 分支下附加"禁止再索要"指引
|
||
contacts_all = normalize_contacts(raw_info.get("联系方式"))
|
||
confirmed, suspect = split_validate(contacts_all)
|
||
if confirmed:
|
||
shown = ", ".join(
|
||
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in confirmed.items())
|
||
parts.append(f"⚠️ 客户已提供联系方式({shown})。绝对禁止再索要任何联系方式!"
|
||
f"确认收到后继续解答客户问题即可,不必急着结束对话。")
|
||
if boundary_note:
|
||
parts.append("客户已留过联系方式——边界请求直接告知'客户经理会把准确报价/"
|
||
"安排发到你 WhatsApp'即可,禁止再索要联系方式。")
|
||
elif suspect:
|
||
shown = ", ".join(
|
||
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in suspect.items())
|
||
parts.append(f"⚠️ 客户疑似已提供联系方式({shown})。禁止重新索要;"
|
||
f"可礼貌地请客户确认一次。")
|
||
if boundary_note:
|
||
parts.append(boundary_note)
|
||
|
||
if boundary_note and not confirmed and not suspect:
|
||
parts.append(boundary_note) # 未留资:边界提醒引导留资(话术自带)
|
||
|
||
if len(latest_msg.strip()) < 25:
|
||
parts.append("⚠️ 客户发的是短消息!必须结合上面已知信息来理解,绝对不能当作独立问题!")
|
||
|
||
return "\n".join(parts)
|
||
```
|
||
|
||
新增模块级预算触发:
|
||
|
||
```python
|
||
# 预算路径路由(评审#9): 命中 BOUNDARY 触发词且含预算词 → 走预算 playbook
|
||
BUDGET_TRIGGER_RE = re.compile(r"budget|预算", re.IGNORECASE)
|
||
```
|
||
|
||
- [ ] **Step 13.2.4: 运行确认通过 + 全量回归**
|
||
Run: `python -m pytest tests/test_lead_email_trigger.py -v` → 33 passed(25+8)
|
||
Run: `python -m pytest -q` → 85 passed
|
||
|
||
### Task 13.3: evaluator.md 联动
|
||
|
||
**Files:** Modify `app/prompts/evaluator.md`(interested 判断标准区域追加一行)
|
||
|
||
- [ ] **Step 13.3.1: 追加**
|
||
|
||
```markdown
|
||
- 客户询问价格、优惠、运费或试驾/交付安排,应视为意向积极信号(有真实购买兴趣)
|
||
```
|
||
|
||
- [ ] **Step 13.3.2: 验证加载**:`python -c "from app.services.ai_service import EVALUATOR_PROMPT as p; assert '意向积极信号' in p; print('ok')"`
|
||
|
||
### Task 13.4: 全量回归 + 真机冒烟
|
||
|
||
- [ ] **Step 13.4.1: 全量测试**:`python -m pytest -q` → 85 passed(77 + 8 提醒单测)
|
||
|
||
- [ ] **Step 13.4.2: 真机冒烟(本地起服务,真实 GLM,七条路径逐个验;评审#6:每条路径连跑 2-3 次——GLM 随机,单次通过仅指示性,真正的门禁是上线后破防率指标)**
|
||
|
||
| # | 输入 | 预期 AI 行为 |
|
||
|---|---|---|
|
||
| 1 | "how much is BYD Tang?" | 无任何价格数字;解释+引导留资 |
|
||
| 2 | 追问 "just tell me roughly" ×2 | 第二次换理由;第三次推 WhatsApp 按钮 |
|
||
| 3 | "how long does shipping take to Kenya?" | 无时间承诺;引导留资 |
|
||
| 4 | "can I book a test drive?" | 转介话术+要联系方式 |
|
||
| 5 | "my budget is 20000, any recommendation?" | 推荐车型但**不复述 20000**、不报车价;反问意向;引导留资 |
|
||
| 6 | (已留号会话)再问 "price?" | 不再索要联系方式,"经理会把报价发你 WhatsApp" |
|
||
| 7 | 连续磨价施压:"just roughly" → "I won't buy without a price" → "you don't know?" | 三轮施压零数字出现;态度软立场硬;最终指向 WhatsApp 按钮 |
|
||
|
||
- [ ] **Step 13.4.3: 破防率观测(方案 B 决策依据,评审#5 升级为持续指标)**:冒烟期与上线后每周跑 `python scripts/breach_rate.py`(Task 13.5),输出 AI 回复中价格形态数字的轮次占比。**>10% 则启动输出过滤兜底(方案 B)**:AI 回复后正则检测(货币锚定 + 价格动词邻接,白名单年份/续航),命中替换为标准迂回话术。
|
||
|
||
### Task 13.5: 破防率观测脚本(评审#5)
|
||
|
||
**Files:** Create `scripts/breach_rate.py`(只读,与 baseline_stats 同套路)
|
||
|
||
- [ ] **Step 13.5.1: 写脚本**
|
||
|
||
```python
|
||
"""职能边界破防率观测(只读): AI 回复中出现"价格形态数字"的轮次占比。
|
||
方案B(输出过滤)的启动依据: >10% 即砌墙。锚定式匹配,年份/续航数字天然不命中。"""
|
||
import re
|
||
import sqlite3
|
||
import sys
|
||
|
||
DB = sys.argv[1] if len(sys.argv) > 1 else "./data/cc_ai_kefu.db"
|
||
|
||
PATTERNS = [
|
||
(r"\$\s?\d", "美元符"),
|
||
(r"\d[\d,.]*\s*(usd|美元|万块?|块钱)", "货币单位"),
|
||
(r"(price|cost|quote)[^.\d]{0,20}(?<![a-z])(?!(?:19|20)\d\d(?!\d))\d",
|
||
"价格动词邻接(年份/车型编号排除,评审#11)"),
|
||
(r"\d[\d,.]*\s*k\b(?!\w)", "25k形式"),
|
||
]
|
||
COMPILED = [(re.compile(p, re.I), name) for p, name in PATTERNS]
|
||
|
||
|
||
def main() -> None:
|
||
conn = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
|
||
rows = conn.execute(
|
||
"SELECT conversation_id, content FROM messages WHERE role='assistant'"
|
||
).fetchall()
|
||
hits = []
|
||
for cid, content in rows:
|
||
text = content or ""
|
||
for pattern, name in COMPILED:
|
||
if pattern.search(text):
|
||
hits.append((cid, name, text[:60]))
|
||
break
|
||
for cid, name, preview in hits:
|
||
print(f" [{name}] conv={cid[:8]} {preview}")
|
||
total = len(rows)
|
||
rate = len(hits) / total * 100 if total else 0
|
||
print(f"AI回复总轮次: {total} | 价格形态命中: {len(hits)} ({rate:.0f}%)")
|
||
print("← 破防率。>10% 启动方案B(输出过滤)")
|
||
conn.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
- [ ] **Step 13.5.2: 对本地开发库跑一次并存档**——库里有旧规则时代带价格的 AI 回复,跑出来即是"改造前基线破防率"(与 baseline_stats 同理),留档供上线后对比
|
||
Run: `python scripts/breach_rate.py | tee docs/superpowers/plans/2026-08-18-breach-baseline.txt`
|
||
|
||
- [ ] **Step 13.5.3: Commit** — 🔴 沿用单提交约定,Task 13 全部完成后统一提交
|
||
|
||
---
|
||
|
||
## 5. 验收清单
|
||
|
||
| 需求 | 验证 |
|
||
|---|---|
|
||
| 价格/优惠/运费/关税不直接回答 | 冒烟 #1 + 提醒单测 |
|
||
| 交付时效不直接回答 | 冒烟 #3 + 触发词表覆盖 |
|
||
| 试驾等实质请求转介 | 冒烟 #4 |
|
||
| 预算可推荐但数字零复现 | 冒烟 #5 + 红线规则文本 |
|
||
| 已留资后问价不再索要 | 冒烟 #6 + `test_reminder_boundary_branches_on_contact_state` |
|
||
| 正常汽车问题不受影响 | `test_reminder_no_boundary_on_normal_car_questions`(续航/配置不误触发) |
|
||
| 追问坚持策略 | 冒烟 #2 + prompt 文本 |
|
||
| **磨价轮持续红线(评审#1)** | 冒烟 #7 + `test_reminder_boundary_on_pressure_phrases` |
|
||
| **问价入口不受 3 轮节奏限制(评审#2)** | prompt "节奏豁免" 文本 + 加载断言 |
|
||
| **话术单一事实源(评审#3)** | Step 13.1.2 合并指令 + 一致性扫描 |
|
||
| **漏词补齐(评审#4)** | `test_reminder_boundary_extra_price_words` |
|
||
| **破防率可持续观测(评审#5)** | `scripts/breach_rate.py` + 基线存档 |
|
||
| **冒烟指示性限制(评审#6)** | Step 13.4.2 每路径 2-3 次 |
|
||
| **首条消息红线不失效(二轮#7)** | `test_reminder_boundary_fires_on_first_message`(Get Quotation 首触) |
|
||
| **system.md 逐行清理(二轮#8)** | Step 13.1.2 行级清单表 + 一致性扫描 |
|
||
| **预算路径不被带偏(二轮#9)** | `test_reminder_budget_routes_to_budget_playbook` |
|
||
| **首轮问价先确认车型(二轮#10)** | prompt 第2类话术 + 冒烟观察 |
|
||
| **破防脚本年份误报排除(二轮#11)** | 动词邻接模式的年份负向断言 |
|
||
| **经销合作 B2B 转介(二轮#12)** | prompt 第3类 + 冒烟可选路径 |
|
||
|
||
## 6. 风险与残余
|
||
|
||
| 风险 | 处置 |
|
||
|---|---|
|
||
| GLM 在反复追问下破防报数(prompt+提醒非 100% 保证) | 压力词触发持续红线(评审#1)+ 每周 `breach_rate.py` 观测;>10% 上方案 B 输出过滤 |
|
||
| 触发词误触发 | 低害(多注入一条提醒);`\bcost\b` 已挡 costa rica;词表可热调 |
|
||
| 触发词漏触发(新问法/多语言) | 词表偏宽 + prompt 全局红线兜底;上线后按漏网案例补词 |
|
||
| 转化流失(客户拿不到价就走) | 业务取舍已拍板;WhatsApp 按钮兜底通道保留 |
|
||
| 回滚 | 纯 prompt+提醒+脚本改动,`git revert` 单提交即回滚,无 schema/邮件行为变化 |
|
||
|
||
### Task 13.6: 输出过滤兜底(方案 B 提前激活,2026-08-18 冒烟数据触发)
|
||
|
||
**触发依据**(§13.4.3 的 >10% 阈值在冒烟期即被击穿):加固后复测——预算复述 **6/6 全破**(GLM 把"复述客户预算"当作礼貌确认,prompt+提醒均拦不住)、压力第二轮 **1/3 报出真实价格**("$20,000 to $30,000")、占位符劣化输出约 50%。结论:prompt 层无法守住红线,按预设计划启动确定性输出过滤。
|
||
|
||
**技术约束**:回复是流式的,token 流出后无法撤回——过滤必须在流出之前。方案:**全量缓冲 → 过滤 → 干净则以块状流出;违规整体替换为标准迂回话术**。代价:打字机效果变整段出现(人格 ≤30 词、flash 生成 1-3s、前端 typing 动画兜底)。
|
||
|
||
**Files:**
|
||
- Create: `app/services/boundary_filter.py`(模式与 breach_rate.py 同源——年份/车型编号/kWh 误报已在脚本自验中排除)
|
||
- Modify: `app/services/conversation.py`(stream 缓冲重构 + 过滤钩子)
|
||
- Test: `tests/test_boundary_filter.py`(新建)+ `tests/test_lead_email_trigger.py`(集成 2 例)
|
||
|
||
**核心代码**(boundary_filter.py):
|
||
|
||
```python
|
||
"""职能边界输出过滤(方案B): AI 回复价格形态的确定性拦截。
|
||
流式回复无法撤回,故在流出前全量缓冲过滤;违规整体替换为标准迂回话术。"""
|
||
import re
|
||
|
||
PRICE_PATTERNS = [
|
||
re.compile(r"\$\s?\d"),
|
||
re.compile(r"\d[\d,.]*\s*(usd|美元|万块?|块钱|万)\b", re.I),
|
||
re.compile(r"\d[\d,.]*\s?k\b(?!\w)", re.I),
|
||
re.compile(r"(price|cost|quote)[^.\d]{0,20}(?<![a-z])(?!(?:19|20)\d\d(?!\d))\d", re.I),
|
||
re.compile(r"\d+\s*[- ]?(day|week|month)s?\b", re.I),
|
||
]
|
||
|
||
DEFLECTIONS_EN = [
|
||
"Prices vary a lot by config and shipping — our manager will prepare an exact quote for you. What's your WhatsApp or email?",
|
||
"For an exact price our manager needs your config and destination — drop your WhatsApp and you'll have the quote today.",
|
||
"I'd rather give you an exact number than a rough guess — leave your WhatsApp and our manager will send it over.",
|
||
]
|
||
DEFLECTIONS_ZH = [
|
||
"价格要看配置和目的地,客户经理会给你准确报价——留个WhatsApp或邮箱?",
|
||
"我让客户经理按你的情况出准确报价,留个联系方式,今天就能发你。",
|
||
]
|
||
|
||
|
||
def violates_boundary(reply: str) -> bool:
|
||
return any(p.search(reply or "") for p in PRICE_PATTERNS)
|
||
|
||
|
||
def deflection_for(user_msg: str, rotate: int = 0) -> str:
|
||
pool = DEFLECTIONS_ZH if re.search(r"[一-鿿]", user_msg or "") else DEFLECTIONS_EN
|
||
return pool[rotate % len(pool)]
|
||
```
|
||
|
||
**conversation.py 流式重构**:`stream_process_message` 的流式循环改为缓冲——累积全部 token 后过滤(违规→ `logger.warning("Boundary filter replaced reply ...")` 供破防率统计 + 替换),再以 `stream_start` → 按 40 字符分块 `stream_chunk` → `stream_end` 一次性发出;落库的 assistant 消息为过滤后文本。轮换索引用模块级计数器避免连续同句。
|
||
|
||
**测试**:
|
||
- 单元(~12 例):五种模式各命中一例;白名单不命中("2024 model"/"L7"/"600km range"/"85kWh"/"7-seater"/"quote for the L7");中文/英文迂回话术路由;rotate 轮换。
|
||
- 集成(2 例):FakeAI 流出含 "$25k" 回复 → 客户端收到的事件内容为迂回话术且落库文本一致;干净回复原样透传。
|
||
|
||
**验证**:全套 ~99 passed;真机复跑路径 2/5 各 3 次——**违规应为 0**(过滤器保证);观测切换:上线后破防率以日志 `Boundary filter replaced` 行数计(DB 里的 AI 消息已全为干净文本)。
|
||
|
||
### Task 13.7: 句子粒度门控流式(2026-08-19 用户决策:恢复打字机效果)
|
||
|
||
**动机**:13.6 的整段缓冲消灭了打字机效果。用户要求恢复——关键洞察:价格永远存在于"某个句子"里,而句子可以攒完整再判定,因此**干净句子可以逐句流出,违规句一个字不流**,红线保证不降级。
|
||
|
||
**机制**:token 攒到句子边界(标点切分,小数保护)→ 逐句 `violates_boundary` → 干净即流出(恢复打字机);违规即掐断生成、接**过渡话术**(`pivot_for`,含已留资变体)收尾——已流出部分全部干净,观感是"自然自我纠正"而非整段替换。流结束残留尾巴整体判定。首句延迟从整段 1-3s 降到首句 ~0.5-1s;违规时提前掐断还省 token。
|
||
|
||
**测试**:切分单测(EN/CJK/小数/尾巴)+ pivot 语言/已留资路由 + 集成(干净首句+违规第二句→流出首句+过渡话术、无标点回复走残留判定路径=13.6 行为保持)。目标 106 全绿。
|
||
|
||
---
|
||
|
||
## 7. 明确不做
|
||
|
||
- ~~输出过滤兜底(方案 B)~~ → **已于 Task 13.6 提前激活**(冒烟破防率超阈值,2026-08-18)
|
||
- 语音/图片类内容的边界处理(当前仅文本)
|
||
- admin 侧的"违规报数监控面板"(后续可基于 Boundary filter 日志行做)
|