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

32 KiB
Raw Blame History

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: 新增"职能边界"章节(放在身份介绍之后、对话流程之前——最高优先级位置):
## 职能边界(最高优先级,仅语言规则除外)

你的准则只有一条:**需求了解 + 留资**——介绍车、挖意向、收联系方式。
你不是销售:不做任何承诺(价格/时间/库存)、不说绝对化的话、不代表公司做决定。
涉及价格、时效和实际行动的事,一律由客户经理承接——你的任务是把需求和联系方式收好,交出去。

### 三类请求的处理
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: 写失败测试(追加)
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(若未有)与模块级触发词表:

# 职能边界触发词(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 恰是首条即问价的最高频入口;且提醒按触发类型路由,预算路径不被一刀切带偏):

    @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)

新增模块级预算触发:

# 预算路径路由(评审#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: 追加
- 客户询问价格、优惠、运费或试驾/交付安排,应视为意向积极信号(有真实购买兴趣)
  • 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: 写脚本
"""职能边界破防率观测(只读): 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):

"""职能边界输出过滤(方案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 日志行做)