Files
group_fqcd_jr/docs/customer-service-routing-legacy-keywords.md
T
张胜宇 5d0becb67d 客服 Agent 重构收口:五出口决策链 + 知识库档位隔离 + 前端入参边界(答辩演示版本)
一、客服 Agent 智能增强(正面回应"不智能、动不动就转人工")
- 决策链由 2 个出口扩到 5 个:E1 澄清 / E2 计算型 / E3 知识直返 / E4 证据约束生成 / E5 分级回退
- 转人工从"默认动作"降为最后一档 E5c,只保留 4 类白名单:
  P0 反诈 / P1 账户与个人数据 / P2 写操作与争议 / 用户明确要求人工
- 46 条金标实测(修复前 → 修复后):
  转人工率 43.5% → 10.9%;出口准确率 45.7% → 100%;事实正确率 69.6% → 100%
  禁忌违反 1 → 0;档位越权 / 无出处数字 / 误拒 四项零容忍全 0
- 安全不变量 INV-1~INV-5;零容忍规则未删,改的是挂载点
  (输出侧字面黑名单 → 检索层档位隔离 + 判定层合规词表 + 输出守护)

二、知识库:档位单点化与物理隔离
- 新增 app/core/knowledge_tier.py 作为档位规则唯一落点(G-03),
  knowledge_contracts.py 原定义块改为显式再导出(X as X,非副本)
- 档位过滤由 bool 默认值(fail-open)改为 tiers 必填集合(缺参即 TypeError)
- Milvus 侧四集合按 visibility 分区键物理隔离;双 schema 收敛为一套
- 新增 app/core/actor.py:访客三元组与匿名判定的唯一构造/判定点(G-01/G-01b)
- 新增 app/core/fund_fee_rules.py:费率计算纯函数

三、前端入参边界对齐(本轮 W11 新修,4 处"校验宽于存储")
- message 加 max_length=8000(与浮窗 widget.js 的 maxlength 一致)
- session_id 加 1—64;idempotency_key 上限 128 → 64(对齐列宽 String(64))
- feedback_type 加 max_length=32(对齐列宽 String(32))
- 8 条路径参数补 min_length=1 + max_length=64 + 字符集正则
  ({session_id} / {run_id} / {handover_id})
- 改前超限值会落到 MySQL 才失败(500);改后一律 422 AGENT_INPUT_INVALID + 字段级定位
- 新增 tests/unit/api/test_frontend_boundaries.py(33 例),含"端点表 ↔ OpenAPI 全量对照"

四、投顾模块整体清除(D4.4 / D4.5)
- 删除投顾相关 controller / schema / model / repository / service 及门户页面
- tools/portal_api_check.py 同步作废 AD003/AD005/AD011/A047 四条用例与 advisor_t 登录
  (端点与账号均已不存在,此前稳定报 3 条假红)

五、验证(提交前实测)
- pytest -q:1856 passed / 2 skipped / 0 failed
- ruff check app tools tests:19(= 基线);mypy app:2(= 基线)
- 前端接口契约体检 portal_api_check.py:38 项,通过 34,失败 0,跳过 4
- 全链路冒烟 e2e_smoke_test.py --read-only:31/31
- HTTP 全链路探针 http_probe.py:11/11 succeeded
- 跨文档一致性 _consistency.py:GATE PASS
- 真机边界复验 12 条:12/12 符合预期

六、纪律与文档
- 可改文件白名单 A-09(docs/46)与底座会签申请单 A-10(docs/47,组 1—组 4 全部受理)
- 零 DDL:未新增/修改任何表结构,89 张业务表与基线一致
- 证据留痕:docs/evidence/**(含 46 条金标 score、快照、清除与重建记录)
- 未提交(刻意排除,见提交说明):仓库内 客服agent/ 与 开发文档/ 是 2026-09-16 前的
  过期副本(Todolist 440 行 vs 权威 D2.1 1167 行),权威正本在仓库外;
  _chunks_report.txt 是 tools/build_knowledge_chunks.py 生成的本地产物
2026-09-20 14:33:30 +08:00

206 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.
# 一期客服确定性路由:关键词表留痕与迁移对照
> **日期**:2026-09-16
> **触发**:客服模块重构前代码清理(依据《客服与投顾模块重构前代码清理建议-2026-09-15.md》§二 A2/A3/B1/B2)
> **性质**:**留痕文档**。一期路由已删除,但其词表是这些拦截能力的**唯一存放处**,
> 故原样抄录于此,并给出逐条迁移去向。**不要只依赖 git 历史追溯**——本轮出现过
> "死代码里的能力被当成已实现"的误判,词表必须留在可检索的文档里。
>
> ## ⚠️ 2026-09-16 后续更新(形态A 模块级清除,必读)
>
> 上文 §一 写的「保留未删」**已被后续的客服模块整体删除推翻**:
>
> - `app/service/agent/customer_service_routing.py`(含 `chitchat_streak()` 与 `_CHITCHAT_*`)
> —— **整个文件已删除**,`chitchat_streak()` 不复存在。
> - 先期把 8 条提示词注入词迁移进去的 `app/core/customer_service_rules.py`
> —— **也随模块一并删除**。
>
> **后果(重建时必须补齐)**:本文件 §二 的词表现在是这些拦截能力**在本仓库中的唯一副本**。
> 以下能力当前**在生效路径上不存在**:
> 1. 提示词注入拦截(8 条词)—— 红队 RT-009/010 仍会失败;
> 2. 凭据披露拦截(`_SECURITY_DISCLOSURE_PHRASES`);
> 3. 安全关键词路由(`_SECURITY_KEYWORDS`)与零容忍词(`ZERO_TOLERANCE_WORDS`)。
>
> `app/service/agent_run_application_service.py` 的 `build_outbox_metadata()` 也不再覆写
> `chitchat_streak` / `clarification_round` / `session_context`(`AgentRequest` 上的字段仍在,
> 但已无写入方)。**重建客服 Agent 时按 §三 的对照表逐条恢复,并保留本文件作为验收基线。**
---
> ## ✅ 2026-09-18 更新(`C-06` 双向验证)
>
> 上面「提示词注入拦截在生效路径上不存在、红队 `RT-009`/`010` 仍会失败」的警告**已过期**:
> `app/core/customer_service_rules.py` 的 `PROMPT_INJECTION_KEYWORDS` 8 条已在 **P0 之后、
> 合规拦截之前**生效,红队 `RT-009`/`RT-010` 在 `C-06` 双向验证中**由失败转为通过**
> (证据 `docs/evidence/20260918-t2i-c06-bidirectional.json`)。
> §二 的词表仍是**迁移对照基线**,不要据此认为当前实现仍有缺口。
---
## 一、删除的是什么
| 项 | 位置(删除前) | 判定 |
|---|---|---|
| `CustomerServiceIntentRouter.classify()` | `app/service/agent/customer_service_routing.py` L40-65 | **全仓无调用方**。全仓 `.classify(` 仅命中 `app/service/agent/base.py:205` 的 `IntentClassifier.classify`(另一个类,无关) |
| `CustomerServiceRoute` | 同文件 L7-12 | 只在 `classify()` 内部被构造 10 次,无外部消费方 |
| 9 组关键词常量 | 同文件 L16-37 | 仅被 `classify()` 使用(`_CHITCHAT_*` 除外,见下) |
**关键事实(决定了本次删除的风险等级)**:`classify()` 无生产调用方,因此
**删除本身不产生任何行为变更**。它的真实代价不是"回归",而是"这些词表原本只存在于
一个不生效的位置"——例如提示词注入拦截的 8 条词**从未在任何生效路径上运行过**
(红队 RT-009/010 因此必然失败)。所以本次处置是**先迁移、后删除**,而不是只删除。
**保留未删**:`_CHITCHAT_MESSAGES`、`_CHITCHAT_PHRASES`(`chitchat_streak()` 依赖,
而 `chitchat_streak()` 被 `app/service/agent_run_application_service.py:57` 用于
组装 outbox 元数据的 `chitchat_streak` 字段)。二期路由 `app/core/customer_service_rules.route_message()`
(`customer_service.py:215` 调用)与 `IntentClassifier` 均已接管其余能力。
---
## 二、一期词表原样留痕(删除前的完整内容)
```python
_SECURITY_KEYWORDS = ("验证码", "密码泄露", "被盗", "诈骗", "非本人交易")
# 明确的凭据披露和提示词注入必须在知识检索前拦截,避免把攻击内容当作普通 FAQ。
_SECURITY_DISCLOSURE_PHRASES = ("密码是", "密码为", "我的密码", "验证码是", "验证码为")
_PROMPT_INJECTION_KEYWORDS = (
"忽略之前", "忽略所有规则", "系统提示词", "开发者消息", "泄露提示词", "越过限制",
"不要遵守规则", "显示内部指令",
)
_COMPLIANCE_KEYWORDS = ("推荐", "收益最高", "稳赚", "保本", "帮我买", "替我交易")
_ACCOUNT_KEYWORDS = ("持仓", "收益", "订单", "定投", "银行卡", "风险测评", "投诉进度")
_HUMAN_TRANSFER_KEYWORDS = ("转人工", "人工客服", "投诉", "赔偿", "法律", "纠纷")
_POLICY_KEYWORDS = (
"申购", "赎回", "到账", "费率", "手续费", "确认份额", "交易日", "分红", "规则", "政策"
)
_PRODUCT_KEYWORDS = (
"产品", "基金代码", "基金经理", "份额类别", "a类", "c类", "净值", "风险等级"
)
_REFERENCE_PHRASES = ("这个", "那个", "它的", "刚才", "上面", "前面", "这只", "那只")
# ↓ 以下两组已保留在原文件(chitchat_streak 在用),此处仅作对照
_CHITCHAT_MESSAGES = frozenset({
"你好", "您好", "嗨", "哈喽", "在吗", "谢谢", "谢谢你", "再见", "拜拜",
"你是谁", "你叫什么", "你今天开心吗",
})
_CHITCHAT_PHRASES = ("今天天气", "讲个笑话", "你几岁", "你开心吗", "你忙吗")
```
### 原 `classify()` 的判定顺序(供追溯语义用)
```
security(_SECURITY_KEYWORDS ∪ _SECURITY_DISCLOSURE_PHRASES) → intent=security_notice
→ _PROMPT_INJECTION_KEYWORDS → intent=compliance_refusal
→ _COMPLIANCE_KEYWORDS → intent=compliance_refusal
→ _ACCOUNT_KEYWORDS → intent=account_entry
→ _HUMAN_TRANSFER_KEYWORDS → intent=human_transfer
→ 闲聊 → intent=chitchat
→ _POLICY_KEYWORDS → knowledge_intents=(policy_explain,)
→ _PRODUCT_KEYWORDS → knowledge_intents=(product_inquiry,)
→ 兜底 → knowledge_intents=(faq,)
```
---
## 三、迁移对照表(逐条)
### 3.1 已迁入二期(本次完成)
| 一期条目 | 原属词表 | 二期落点 | 说明 |
|---|---|---|---|
| `非本人交易` | `_SECURITY_KEYWORDS` | `P0_KEYWORDS` | 二期 P0 原先只有「不是我本人操作」,该说法会漏到知识检索 |
| `替我交易` | `_COMPLIANCE_KEYWORDS` | `P2_KEYWORDS` | 不被 `P2_PATTERNS` 覆盖(句式白名单无「交易」二字),须用字面词补 |
| `纠纷` | `_HUMAN_TRANSFER_KEYWORDS` | `P2_KEYWORDS` | 投诉赔偿类的常见同义说法 |
| 8 条注入词(全部) | `_PROMPT_INJECTION_KEYWORDS` | **新增** `PROMPT_INJECTION_KEYWORDS` + `PROMPT_INJECTION_PRIORITY` + `PROMPT_INJECTION_REPLY` | 落地于 `route_message()`,位置在 **P0 之后、合规拦截之前**(混合输入先拿反诈话术) |
### 3.2 已被二期覆盖,无需迁移
| 一期条目 | 二期覆盖方式 |
|---|---|
| `验证码`、`密码泄露`、`被盗`、`诈骗`、`转账给`、`安全账户` 等 | 已在 `P0_KEYWORDS` |
| `密码是` / `密码为` / `我的密码` / `验证码是` / `验证码为` | 全部含「密码」或「验证码」子串,被 `P0_KEYWORDS` 覆盖 |
| `稳赚`、`保本` | 已在 `ZERO_TOLERANCE_WORDS`(输入侧) |
| `帮我买` | 已在 `P2_KEYWORDS` |
| `转人工`、`人工客服`、`投诉`、`赔偿`、`法律` | 已在 `P2_KEYWORDS` |
| `投诉进度` | 已在 `P1_KEYWORDS` |
| `风险测评` | `P1_KEYWORDS` 有「风险测评结果」(限定式) |
### 3.3 有意不迁(含理由)
| 一期条目 | 处置 | 理由 |
|---|---|---|
| `推荐` | **不迁**,交 Todolist **C-09**(访客侧推介边界) | C-09 的 DoD 明确把「推介请求句式」的**输入侧**识别限定在**访客**,且要求**客户侧行为不变**。现在若把它加进全局输入拦截,会改变客户侧行为,与 C-09 ③ 的回归要求正面冲突 |
| `收益最高` | **不迁**,交 Todolist **C-03 / C-05** | ① 它属「收益承诺 / 比较类」而非安全类;② 加入 `ZERO_TOLERANCE_WORDS` 会破坏 `tests/unit/core/test_customer_service_rules.py:87-92` 的 DB 镜像断言(那 11 条与库内 `agent_negative_word` 被断死相等);③ MVP 已定**输出侧**只拦收益比较与稀缺性,C-05(数据层)是设计指定的落点 |
| `持仓` / `收益` / `订单` / `定投` / `银行卡` 等**裸词**账户问法 | **不迁**,交 Todolist **C-03 ③** | 二期 P1 刻意用「我的持仓」这类**限定式**,避免把公开问题(「什么是持仓」)误判成本人账户查询。补裸词需配套设计,属 C-03 范围 |
| `_POLICY_KEYWORDS`、`_PRODUCT_KEYWORDS` | **不迁** | 其作用是产出 `knowledge_intents=(policy_explain/product_inquiry)`。二期意图由底座 `IntentClassifier` 产出,`INTENT_POLICY`/`INTENT_PRODUCT` 已在 `customer_service.py:44-45` 独立定义,不再需要关键词路由 |
| `_REFERENCE_PHRASES` | **不迁** | 原用于标记 `requires_context`;二期无此机制(会话上下文由 `customer_service_session_memory_service` 承担) |
---
## 四、本次改动清单
| 文件 | 动作 |
|---|---|
| `app/service/agent/customer_service_routing.py` | 删除 `classify()`、`CustomerServiceRoute`、9 组词表;保留 `chitchat_streak()` 与 `_CHITCHAT_*`(87 行 → 约 45 行) |
| `app/service/agent/customer_service_agent.py` | **整文件删除**(5 行向后兼容桩,生产零引用) |
| `app/core/customer_service_rules.py` | 新增 `PROMPT_INJECTION_KEYWORDS` / `PROMPT_INJECTION_PRIORITY` / `PROMPT_INJECTION_REPLY` + 注入拦截分支;`P0_KEYWORDS` 补 `非本人交易`;`P2_KEYWORDS` 补 `替我交易`、`纠纷` |
| `tests/unit/service/test_agent_governance.py` | import 从 `customer_service_agent` 改为 `implementations.customer_service` |
| `tests/unit/core/test_customer_service_rules.py` | 新话术纳入 `REPLY_CONSTANTS`(过"自绊检查"+ 带官方联系方式两条守卫);新增注入拦截 / 混合优先级 / 迁移词回归共 4 组用例 |
| 本文件 | 新增(留痕) |
---
## 五、验证方式与本次限制
设计上应执行 `docs/14` §13 的统一验收命令集。**本次的实际限制**:
- 本机 Managed Python 3.13.14 **未安装项目依赖**(pytest / fastapi / sqlalchemy / pydantic / redis / pymilvus 等全部 `ModuleNotFoundError`),仓库内**无 venv** → `pytest`、`ruff`、`mypy` **无法运行**。
- 因此本次实际执行的验证为:① 全量 `py_compile` 语法校验;② 全仓静态引用扫描(确认无悬空 import)。
- **遗留动作**:补齐依赖后必须补跑 `pytest tests/unit/core/test_customer_service_rules.py`、
`pytest tests/unit/service/test_agent_governance.py` 与全量 `ruff check`。
---
## 六、2026-09-18 重建记录(词表最终落点)
客服 Agent 本体已于 2026-09-18 重建(`T2`)。**本文件 §二 的词表已按 §三 对照表逐条恢复**,最终落点如下:
| 词表 | 重建后的落点 |
|---|---|
| `_SECURITY_KEYWORDS` | `app/core/customer_service_rules.py` 的 `P0_KEYWORDS` |
| `_SECURITY_DISCLOSURE_PHRASES` | 同文件的 `SECURITY_DISCLOSURE_PHRASES` |
| `_PROMPT_INJECTION_KEYWORDS`(8 条) | 同文件的 `PROMPT_INJECTION_KEYWORDS` + `PROMPT_INJECTION_PRIORITY` + `PROMPT_INJECTION_REPLY`,位置在 **P0 之后、合规拦截之前** |
| `_COMPLIANCE_KEYWORDS`(`稳赚` / `保本` 部分) | 同文件的 `ZERO_TOLERANCE_WORDS` |
| `_HUMAN_TRANSFER_KEYWORDS` | 同文件的 `P2_EXPLICIT_HUMAN_KEYWORDS` / `P2_WRITE_DISPUTE_KEYWORDS` |
| `_CHITCHAT_MESSAGES` / `_CHITCHAT_PHRASES` | 同文件的 `CHITCHAT_MESSAGES` / `CHITCHAT_PHRASES`(原在 `customer_service_routing.py`,该文件已删,故**收口进规则模块**);消费方是 `agent_run_application_service.build_outbox_metadata` 的 `chitchat_streak` 覆写 |
| `_POLICY_KEYWORDS` / `_PRODUCT_KEYWORDS` / `_ACCOUNT_KEYWORDS` / `_REFERENCE_PHRASES` | 按 §3.3「有意不迁」维持不迁 |
⚠️ **本文件 §二 与 §三 的对照关系不再需要**(词表已落到生效路径);保留它们是为了记录「能力曾经只存在于一个不生效的位置」这段事实。
---
## 七、2026-09-19 `C-07` 收口:残留取证、留痕完整性与门禁补跑
> `C-07`(删除一期确定性路由 + 兼容桩)的删除动作已由 **2026-09-16「形态A 模块级清除」代删**;本步做的是**取证与收口**(前置 `S-3` 要求的 `C-06` 已于 2026-09-18 通过)。
| 收口项 | 实测 |
|---|---|
| 文件残留 | `app/service/agent/customer_service_routing.py`、`app/service/agent/customer_service_agent.py`(5 行兼容桩)**均不存在** |
| 符号残留 | `CustomerServiceIntentRouter` **0 命中**、`CustomerServiceRoute` **0 命中**;`.classify(` 6 命中**全部**是底座 `IntentClassifier`(`base.py:205` 与两处测试) |
| 词表残留 | `_PROMPT_INJECTION_KEYWORDS` / `_SECURITY_DISCLOSURE_PHRASES` 的命中**全部是注释里的留痕引用**,无任何定义 |
| import 指向 | `bootstrap.py:26` 与 `test_customer_service_agent.py:16` → `app.service.agent.implementations.customer_service`;`chitchat_streak` 的消费方 `agent_run_application_service.py:14` → `app.core.customer_service_rules` |
| **留痕完整性(程序化)** | 用 `ast` 取出两版历史文件(`e9b3d27` 2026-09-10 / `ef098e6` 2026-09-11)的全部常量,逐字面比对本文档 §二:**缺失 = 0**(原版 7 组;后版 10 组,含新增注入 8 条、凭据披露 5 条、指代 8 条) |
| **补正一处** | §二「原 `classify()` 的判定顺序」块用了 5 个 `intent` 名,缺第 6 个 **`public_knowledge`**(一期 `CustomerServiceRoute` 的公开知识直答取值;此名另见于 `D4.6` 基线文档的「预期路由」列)。对应关系:`public_knowledge` ≡ 二期的 `knowledge_intents=(policy_explain / product_inquiry / faq)`,由底座 `IntentClassifier` 产出 |
| 门禁(补跑 §五 遗留) | pytest **2 failed / 1577 passed / 2 skipped**(失败集 = `T0` 基线同两项,与本步无关)、ruff **22**、mypy **3**、`compileall app` 通过 |
- 证据:`docs/evidence/20260919-t2j-c07-legacy-routing-removal.json`