Files
ai-kefu-jc/docs/superpowers/plans/2026-08-18-ai-role-boundary.md
T
2026-09-29 14:15:12 +08:00

527 lines
32 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.
# 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 日志行做)