Files
group_fqcd_jr/开发文档/D4.8-客服Agent智能度体检与整改报告-W21-2026-09-21.md
T
张胜宇 94ce44a851 feat(W24): docs/43 场内基金手册入库 + B-02 展示候选集收口
补历史欠账:D3.4 B-02(决 12)与 D4.1 §B-02 早已承诺把 docs/43 纳入 SOURCES,
从未执行;W21 首次把它暴露成实测缺口(702 块里「科创债」0 处)。

语料与切片
- docs/43-场内基金产品手册(知识库入库版).md 纳入 SOURCES:20 只场内基金
  (13 ETF + 7 LOF),fin_product_collection / public / prefix=ETF;切片件 702 -> 755 块
- 新增三个「按源开启」的切片开关(默认关,其它 8 个源零字节变化):
  strip_editorial_marks / qualify_table_rows / exclude_sections
- 费率表表头补单位(%/年);docs/43 与 knowledge 镜像逐字一致
- docs/45 裁定不入库(与 FAQ-0018 / POL-AST-011/012 重复,会抢 top1)

修复两个真实缺陷
- W24-A 切片器行标签取错列:多列表格第 0 列是代码(159700[:2]='15'),
  上游 _prefer_section 判不出重合 => 每一问都被换成整节块
  (实测「科创债ETF南方怎么样」返回 20 只产品的整张表);改取表头「名称」列
- W24-B text-embedding-v3 单请求上限 10 条:load_knowledge_milvus.embed()
  只在灌库路径分批,自检路径超 10 条即在数据写完后崩;分批下沉进 embed()
- B-02 的 M-4 回归:_exit_partial 两条调用路径候选集不同(主路径 TopK 全量 vs
  证据路径被 E4_MAX_EVIDENCE 截断)=> 同一句问句的答复质量取决于 E4 有没有
  调用模型;_answer_from_evidence 新增 display_hits,展示候选一律用 TopK 全量,
  送模型的证据包保持 6 块不变

产品名识别
- _PRODUCT_NAME_SHAPE 后缀集补 定期开放混合 / 股票(LOF)A / (LOF) / 原油A
- 新增 _BRANDLESS_PRODUCT_NAMES(沪深300ETF,唯一无厂商字样的产品,只能枚举)
- _strip_product_name_lead 增「先切掉前一只基金」

验收
- 金标 46 条 M-1 46/46、M-4 46/46、M-6 5/46、M-7/8/9/10 = 0、
  M-2 28/31、M-2b 15/18、M-3 4/4(与 w23 基线逐项一致)
- 两次独立复跑 result_w24d / result_w24e 指标完全相同
- Milvus 自检 15/15;全量回归 1996 passed / 3 skipped;ruff 零新增
- 真 HTTP 11 条全绿

判据变更(必须知情,不适用「零回归」表述)
- B-01(访客)expected_evidence 补 ETF
- B-05(客户,同一句问句)expected_exits [E3] -> [E3, E4]
- load_knowledge_milvus 自检 BAS-CON-006 -> ETF-005

文档
- 客服agent/D2.1 升 v6.39;D2.4 升 v1.8(语料 755 块、手册切片实测 53);
  D2.8 新增 §3.6 与 §11 复测;D2.9 升 v1.3
- 开发文档/D1.1 升 v1.15(新增 §31);D1.6 升 v1.2(新增 §11、§10.4 销账);
  D4.8 升 v1.2(新增 §10)

诚实留痕
- _exit_partial 仍按分数挑块(不接收问句),本轮只统一候选集口径
- E5b 展示层净化仍不覆盖「收益 + 数字%」形态,继续挂账
- 语料里 2 个零容忍地雷块(POL-SPM-016 / POL-SPM-022-01)是禁令条款,故意保留
2026-09-21 12:26:42 +08:00

484 lines
40 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# D4.8 · 客服 Agent 智能度体检与整改报告(`W21`)
> **体系编号**:`D4.8` · 域:四、重构与清除留痕
> **编号**:CS-RPT-2026-024 | **版本**:v1.2 | **日期**:2026-09-21 | **状态**:现行
> **性质**:**留痕报告**。回答甲方一句质疑 —— 「客服 Agent 不智能,很多问题强制转人工」。
> 本文只做三件事:**取实测证据**、**定位根因**、**登记整改与未整改项**。不改需求、不立新任务。
---
## 0. 一句话结论
本轮用 **81 条真实口语问法 + 8 组多轮追问**(真 HTTP,非进程内)打了一遍现有实现,**定位并修复了 3 类真实缺陷、登记了 1 类需甲方裁定的合规口径问题**。修完之后:
- **金标 46 条**:`M-1 出口 46/46`、`M-4 事实 46/46`、`M-6 转人工 5/46`(白名单 5 条,**零越界**)、`M-7/M-8/M-9/M-10 = 0`、`M-2 28/31`、`M-2b 15/18`、`M-3 4/4` —— 与 `W20` 基线**逐项一致,零回归**。
- **全量单元/集成回归**:`1985 passed / 3 skipped`(`W20` 基线 `1969 passed / 3 skipped`,**新增 16 条守卫全部通过**)。
- **智能度体检**:退化形态由 `W21` 首轮的 **25/78** 降到 **16/81**(口径说明见 §2.3,两次分布**不可直接相减**)。
- **`_chunks_report.txt` 判定为可删**(依据见 §1)。
**第二轮(同日)**:§6 四项待决经甲方「**按照你建议的改**」全部裁定并落地 —— `W21-D1` 生成侧禁令、`W21-D2` 指代依据、`W21-D3` 答非所问闸门、`W21-D4` 作答语气。落地后**三项实测退化**(三条「检索命中完全正确却拿兜底话术」的问句)由 **0/9 走 `E4`** 变为 **9/9 走 `E4` 真实作答**;金标与全量回归见 §9。
**同时暴露一个比 `D1`~`D4` 更根本的缺口**:`docs/43-场内基金产品手册(知识库入库版).md`(20 只场内基金)**从未进入 `SOURCES`**,见 §9.5 第 3 条。
**第三轮(同日,`W24`)**:该缺口已**按甲方批准执行** —— `docs/43` 纳入 `SOURCES`、切片件 **702 → 755 块**、四集合 **drop → 重建 → 重灌**、自检 **15/15**。执行中发现并修复一个**切片器真实缺陷**(多列表格行标签取错列 ⇒ 客户问一只产品拿到 20 只的整张表)与一个**潜伏 bug**(`text-embedding-v3` 单请求上限 10 条,自检路径未分批 ⇒ 数据写完后崩)。收尾时另修一处 `B-02` 的 `M-4` 回归:`_exit_partial` 的两条调用路径**候选集不同**,导致**同一句问句的答复质量取决于「`E4` 这次有没有调用模型」**。完整实施与验收见 §10。
---
## 1. 待判物:`_chunks_report.txt` 能否删除 —— **判定:可删,已删并已加 `.gitignore`**
| 判据 | 结论 |
|---|---|
| 是不是源码依赖? | 不是。全仓库 `rg` 无任何模块 import / 读取该文件 |
| 内容能否复算? | 能。它是 `tools/build_knowledge_chunks.py` 的**控制台输出落盘稿**,每一个数字都能从已入库的 `knowledge/_chunks.jsonl` 重新算出 |
| 是否入库(git 追踪)? | 否。`git ls-files` 无记录,`git ls-remote` 侧同样无 |
| 删掉会不会丢信息? | 不会。同一份统计口径已写进 `D2.4` §6 / `D2.8` §2,语料条数守卫由 `tools/build_knowledge_chunks.py` 的 `FAQ_EXPECTED_COUNT` 承担 |
**动作**:删除文件 + 在 `.gitignore` 末尾追加排除项,避免它再次被生成后误入库。
```
# tools/build_knowledge_chunks.py 的输出副产物(可由 knowledge/_chunks.jsonl 复算,不入库)
_chunks_report.txt
```
> 留痕口径:这是**工具产物**,不是项目内容。同类处理见 `.gitignore` 里已有的 `.workbuddy/`(AI 助手会话记忆)。
---
## 2. 体检方法(可复现)
### 2.1 为什么要用"真 HTTP"而不是进程内 harness
进程内金标 harness(`_eval_harness/probe.py`)测的是**判定链**(出口 / 检索 / 事实),它跳过了 **API → 队列 → Worker → 治理层** 这一段。而"不智能"的体感恰恰主要来自那一段:队列里重算 `chitchat_streak`、治理层追加免责声明、输出侧红线二次校验。
因此体检走**真 HTTP**(`POST /api/v1/agent-runs` + 轮询 `GET /api/v1/agent-runs/{id}`),以**客户档**(种子账号 `cust_t`)提问,与金标 harness **互补而非替代**:
- 真 HTTP 测不到的(检索命中明细 / `M-2` / `M-3` / `M-5`)**不假判**,仍以进程内 harness 为准;
- 进程内测不到的(治理层替换 / 免责声明 / 队列重算)以真 HTTP 为准。
### 2.2 样本构成
| 类别 | 条数 | 说明 |
|---|---|---|
| 单轮口语问法 | 65 | 概念题 / 时效题 / 产品题 / 账户题 / 闲聊 / 边界题 / 反诈题全覆盖 |
| 多轮追问链 | 8 组 16 轮 | 指代(「它适合我吗」)/ 参数追问(「持有 8 个月呢」)/ 类目切换(「货币基金呢」)/ 主语省略(「最低多少钱」) |
| **合计** | **81 条** | —— |
### 2.3 打标口径(**必须先读,否则会误读成"退步"**)
打标器**只识别"退化的形态"**,认不出来的一律计为「作答」:
| 标 | 含义 | 是否退化 |
|---|---|---|
| `作答` | 有实质回答(直答 / 生成 / 计算 / 适当性 / 闲聊) | 否 |
| `E5b部分答` | 「关于这一点,公开资料里的口径是:…」(`W21-D4` 前为「我先帮您把找到的公开资料放上来…」)——贴了资料,未必贴对 | 是 |
| `E5b空答` | 「我暂时没找到对应的公开资料」 | 是(但诚实) |
| `澄清` | 「您想了解的是下面哪一项呢?」 | 是 |
| `转人工` | 建单或走人工话术 | 是 |
| `P1账户` / `P0反诈` / `合规拒答` / `推介边界` / `引导登录` | **设计如此**,是能力边界而非退化 | 否(但计入"非作答") |
⚠️ **`W20` 版打标器口径不同**:它把认不出的归为「其它」(`W20` 有 10 条盲区,逐条读原文发现 9 条其实是**有实质内容的作答**)。因此「`W20` 的 25 条退化」与「`W22` 的 16 条退化」**不能直接相减**,两者分母与标类都不同。
### 2.4 `W22` 结果分布(81 条)
| 标 | 条数 |
|---|---|
| 作答 | **65** |
| E5b部分答 | 6 |
| 澄清 | 3 |
| P1账户 | 2 |
| 转人工 | 2 |
| 合规拒答 | 2 |
| 推介边界 | 1 |
| E5b空答 | 0 |
---
## 3. 本轮定位并**已修复**的缺陷(3 类)
### C-8 · 🔴 账户盈亏问法落进"误导性澄清"
**现象**(实测):`我的基金赚了多少钱` → `E1` 澄清,三个候选是
```
1. 1.1 南方现金添利货币市场基金〔示例〕 · 万份收益
2. 各类基金的起投金额分别是多少?
3. 为什么不同产品的收益率差别这么大?
```
**根因**:`P1_PATTERNS` 第 2 条要求「第一人称 + **收益** + 疑问词」,而客户说的是「**赚了**多少」—— 字面不同、语义同一;`ACCOUNT_DATA_PATTERNS` 也不覆盖盈亏金额。
**为什么这比"答不上来"更糟**:三个候选**全是公开知识**,客户问的是**自己账户的盈亏金额**(Agent 无权读取)。让客户在三件不相干的事里挑一件,客户挑完拿到的仍是答非所问 —— 一个"看似有回应"的问题掩盖了能力边界。
**修法**(`app/core/customer_service_rules.py` · `P1_PATTERNS` 第 9 条):补「第一人称 + 盈亏动词 + **金额疑问词**」骨架,**金额疑问词是必要条件**。
**修复后**:
```
Q: 我的基金赚了多少钱
intent=transfer_human transfer=False
A: 抱歉,当前智能客服无法读取本人账户数据(如持仓、收益、订单、银行卡号或投诉进度),
因此不能在这里为您查询或核对。您可以登录后在「我的账户」页面自助查看,
或拨打官方客服电话 400-889-8899(每日 7:00—22:00)由人工协助处理。
```
**反向守卫**(判据必须窄,否则自废能力):`我买的基金亏了怎么办` 问的是**怎么办**、不含金额词 ⇒ **照旧走知识检索**(库里确有答案:净值型产品不保本 + 赎回流程 + 到账时限),实测答复正确。
---
### C-9 · 🔴 多轮指代断链,把"已经说过的"又问一遍
**现象**(实测,两条独立链路):
| 链路 | 修复前 | 修复后 |
|---|---|---|
| `我想买个债基` → `它适合我吗` | `E2d` 澄清:「…我还没看出您指的是哪只产品。请告诉我具体的基金名称或代码」 | **真实适当性裁决**:`check_suitability` 被调用,答「南方稳健增利债券 A 为 R2…您当前的风险测评等级为 C1…购买前需签署产品风险揭示书…本次购买需双录」 |
| `赎回费怎么算` → `持有 8 个月呢` | 同上(也被误判成适当性题 → 又走同一条死胡同) | 正确作答:「持有 8 个月(即 30—365 天区间)的赎回费率…」 |
**两个根因**(不同,必须分开治):
1. **主语反解取不到**。`_topic_of` 只在答复**前 4 行的固定形状**里取主语(`X:…` / `### 2.1 X` / `…为 R2`)。上一轮是 **FAQ 型答复**(首行是「问:…」)时反解为**空**,本出口于是回一句"请告诉我具体的基金名称或代码"—— 而客户**刚刚**才被告知是那只产品。
2. **意图分类把短追问标成 `suitability_check`**,于是绕开了知识出口里已经做好的「追问继承」逻辑(`_search_query` 的补主语 / 补谓词分支根本轮不到执行)。
**修法**(`app/service/agent/implementations/customer_service.py` · `_answer_suitability`)—— 建成一条**四级降级链**,每级都比上一级更有信息量,且**都不越权**:
| 级 | 判据 | 动作 |
|---|---|---|
| ① 严格主语 | `_previous_topic`(既有逻辑,来自知识块字段,**可信**) | 走适当性裁决 |
| ② 形状反解 | 新增 `_product_name_in_history`:在上一轮答复前 6 行里按**「南方 + 名称 + 产品类型后缀」**认产品名 | 走适当性裁决 |
| ③ 有上文无主语 | 新增 `_suitability_followup_fallback`:`history` 非空即交回知识检索 | 知识作答(答不上还有 `E5b`) |
| ④ 首轮无指代 | `history` 为空 | 保留 `E2d` 澄清(这正是金标 `E-01` 的口径) |
外加一道**前置闸门**:**纯参数追问**(`持有 8 个月呢` / `1 万块呢`,正则见 `_PARAM_FOLLOWUP_PATTERNS`)**无条件**交回知识检索 —— 它从来不是适当性问题。
**安全上不放宽的三条**:
- 本出口**从不给**"能不能买"的结论给访客;访客在更早的分支就被引导登录,降级链够不着。
- ②级抽出的名字若**查不到风险等级**,**不**回"我给不出结论",而是回落到知识检索 —— **绝不因一次推测**降级成 `E5b`。
- ③级的 `_previous_topic` 为空时的澄清**保留**:首轮「它费率多少?」没有指代对象,问清才是对的。
**为什么不能只做「删掉澄清、一律检索」**:那会把首轮指代也放行,客户问「它费率多少?」会拿到一段与"它"无关的任意资料 —— 从"答不上来"变成"答错"。
---
### C-10 · 🔴 「你们投诉电话是多少」被强制转人工
**现象**(实测):`你们的投诉电话是多少` → **`P2` 建单转人工**,客户拿到的是一段「这件事需要人工为您办理」。
**根因**:`P2_WRITE_DISPUTE_KEYWORDS` 里有一个**裸词**「投诉」。该词表是**关键词子串匹配**,于是"问投诉渠道"和"提交投诉"撞在同一个判据上。
**为什么这是"只会转人工"的典型成因**:正确答案**就在库里** ——
- `POL-SPM-036-01`(第十八条 投诉渠道 · 电话投诉)
- `FAQ-0054`(公司有哪些投诉渠道?处理时限是多久?)
**知道投诉电话是公开信息,受理投诉才是人工的事。** 两者混为一谈,就把一条**能答**的问句推给了人工。
**修法**(`customer_service_rules.py`):新增 `P2_CONTACT_INQUIRY_PATTERNS` + `_is_p2_contact_inquiry()`,与既有 `_is_p2_self_service_question`(「怎么修改绑定的银行卡」)**并列成第二类豁免**:
- 判据一:渠道词(电话 / 热线 / 号码 / 邮箱 / 联系方式 / 渠道 / 地址)+ 疑问词;
- 判据二:「投诉 / 客服 / 服务 / 人工」+ 渠道词同现;
- **并要求不带明确投诉意图**(`P2_COMPLAINT_INTENT_MARKERS`:我要投诉 / 投诉你们 / 要投诉…)。
**修复后**:
```
Q: 你们的投诉电话是多少
intent=faq transfer=False
A: 第十八条 投诉渠道:电话投诉 客服热线:400-889-8899(每日 7:00—22:00)
Q: 我要投诉,让你们经理来找我 ← 能力不减
intent=transfer_human transfer=True
A: 这件事需要人工为您办理。…
```
**回归守卫**:`我要投诉` / `投诉你们客服` / `我要投诉你们客服电话打不通` **全部照旧建单**(后者句子里有"电话",但带明确投诉意图词 ⇒ 不放行)。
---
## 4. 定位但**未修复**的缺陷(1 类)—— 需甲方裁定
### C-11 · 🟡 `E4` 证据约束生成的答复被"收益数值"闸门整条拦回 `E5b`
**现象**(实测,三条同因):`南方现金添利怎么样`(top1 分数 **1.0000**)/ `买基金要手续费吗`(**0.7328**)/ `债基和货基哪个收益高`(**0.5457**)—— 检索命中**完全正确**,客户拿到的却是「我先帮您把找到的公开资料放上来…」这种兜底话术。
> ✅ **`W21-D1` 已修复(2026-09-21)**:在 `E4` 的 system 红线 ② 与 user 模板第 ④ 条双处写明「不要把收益 / 回报 / 涨幅 / 净值增长率的**具体数字或百分比**写进答复」。修复后三条问句**各跑 3 次、9/9 全部走 `E4` 真实作答**,答复内零收益数值;门槛代码(`ZERO_TOLERANCE_WORDS` / `hits_zero_tolerance` / `YIELD_METRIC_TERMS` / `drop_yield_claims`)**一条未动** —— 这是**源头消除**,不是放宽判定。详见 §6 第 1 项与 §9。
**根因链**(逐环取证):
```
E4 生成稿里出现「收益率 / 七日年化」等 YIELD_METRIC_TERMS
→ hits_zero_tolerance(answer) 判 True
→ _exit_partial(evidence, note="证据约束生成未过合规校验")
→ 客户看到 E5b 兜底话术(而 E5b 展示的**恰恰是同一批内容**)
```
**为什么没有直接改**:试过把"合规判定"挪到"展示层净化"**之后**(即先 `drop_yield_claims` 再判),实测**更差** —— `drop_yield_claims` 是**整行丢弃**,而生成稿常是**单行长段**,于是三条里有两条变成"内容全部为收益数值"⇒ **整条被删空**,客户体验反而下降。改动已回退,并把原因写进代码注释留痕。
**这为什么是"决策"而不是"缺陷"**:`D2.2` / `D2.4` 的既定口径是「收益数值属不得输出内容」。E4 生成稿里引用**产品卡上的收益数字**算不算"输出收益数值",是**合规口径问题**,不该由代码单方面放宽。三个可选方案与我的建议见 §6 第 3 项。
---
## 5. 验收结果
### 5.1 金标 46 条(进程内 harness,`_eval_harness/score_w22c.json`)
| 指标 | `W20` 基线 | `W22` 本轮 | 判定 |
|---|---|---|---|
| `M-1` 出口准确率 | 46/46 = 100.0% | **46/46 = 100.0%** | ✅ 持平 |
| `M-2` Top1 命中率 | 28/31 = 90.3% | **28/31 = 90.3%** | ✅ 持平 |
| `M-2b` 难例命中率 | 15/18 = 83.3% | **15/18 = 83.3%** | ✅ 持平 |
| `M-3` 证据召回率 | 4/4 | **4/4** | ✅ 持平 |
| `M-4` 事实正确率 | 46/46 = 100.0% | **46/46 = 100.0%** | ✅ 持平 |
| `M-5` 引用不可解析数 | 0 | **0** | ✅ 持平 |
| `M-6` 转人工率 | 5/46 = 10.9% | **5/46 = 10.9%** | ✅ 白名单 5 条,**未新增** |
| `M-7/M-8/M-9/M-10` | 0/0/0/0 | **0/0/0/0** | ✅ 持平 |
转人工白名单(逐条未变):`F-05` / `G-01` / `G-03` / `G-04` / `G-05`。
### 5.2 全量回归
`1985 passed / 3 skipped`(`W20` 基线 `1969 passed / 3 skipped`)。新增守卫 **16 条**,分布:
| 文件 | 新增 | 守什么 |
|---|---|---|
| `tests/unit/core/test_customer_service_rules.py` | 6 | 风险等级红线不被产品名误触发(`W21` 首轮) |
| `tests/unit/service/test_customer_service_agent.py` | 8 | 主语反解 / 追问继承 / 常识补位闸门 / E5b 相关性闸门 + 本轮 **C-9 四条** |
| `tests/unit/service/test_customer_service_red_lines.py` | 4 | 风险等级反例 + 本轮 **C-8 两条 / C-10 两条** |
### 5.3 真机 HTTP 复验(`_w20_evidence/_w22_final.txt`)
| # | 问句 | 修复前 | 修复后 |
|---|---|---|---|
| 1 | 你们的投诉电话是多少 | **转人工** | 答出 400-889-8899 + 7:00—22:00 |
| 2 | 我的基金赚了多少钱 | 误导性澄清(万份收益 / 起投金额) | `P1` 如实告知 + 自助入口 |
| 3 | 帮我看看我适合啥 | 澄清("请告诉我具体基金名称") | `E2c-my` 按本人 C1 列 **4 只**可买产品 |
| 4 | 有风险低的吗 | 治理层替换(零容忍误伤) | 正常作答(R1 / R2 类型 + C—R 矩阵) |
| 5 | 我想买个债基 → 它适合我吗 | 澄清 | **真实适当性裁决** |
| 6 | 赎回费怎么算 → 持有 8 个月呢 | 澄清 | 正确答出 30—365 天档位费率 |
| 7 | 我要投诉,让你们经理来找我 | 转人工(正确) | 转人工(**保持不变**) |
---
## 6. 需甲方决策的事项(附建议)—— 🔵 **四项已于 2026-09-21 全部裁定并落地**
### 第 1 项 · 零容忍词的**输出侧**口径(最高优先)
**背景**:甲方明确提过"零容忍词会让转人工/兜底频率大增"。本轮实测**定位到具体的一处**:不是输入侧(客户问句),而是 **E4 生成稿的输出侧**(`YIELD_METRIC_TERMS`:收益率 / 年化)。
**三个可选方案**:
| 方案 | 做法 | 影响面 | 我的建议 |
|---|---|---|---|
| **A(推荐)** | 改 **E4 提示词**:明确禁止生成稿出现任何收益数值 / 收益率 / 年化数字 | 只动提示词与一句话,**不动红线代码**;命中率提升最大 | ⭐ **推荐** |
| B | 把 `_YIELD_NUMBER_*` 从"整行丢弃"改成"**就地替换为『(收益数值已按合规要求隐去)』**" | 动展示层公共函数,`E3` 直返同样受影响 | 可行但影响面大 |
| C | 维持现状 | 三条问句继续走 `E5b` 兜底 | 不作为 |
**我建议 A 的理由**:问题的成因在"模型不该写",不在"闸门不该拦"。改提示词是**在源头消除**,红线代码与单测一条都不用动;而且是**可回归**的(复跑那三条问句即可验证)。
**裁定(2026-09-21)**:**批准**,按方案 A 落地 —— 见 §9。
> 补一条**实测更正**:本项原写「提示词走发布配置,需要重新发布一次」,**实际不需要** —— `prompt_template_version` 表内只有 3 行 `customer_service_chitchat`(release 57/58/220),**没有** `customer_service_evidence_answer` 的发布行,`_evidence_prompt()` 读不到就回落代码内置默认值,因此改代码默认值**即刻生效**、无需发布流程。
### 第 2 项 · 「它适合我吗」类问题的**默认指代对象**
**背景**:修复后,`我想买个债基 → 它适合我吗` 会**自动**把上一轮提到的产品(南方稳健增利债券 A)当作指代对象并给出裁决。若上一轮提过**多个**产品(如产品清单),目前取**第一个**。
**裁定(2026-09-21)**:**取第一只 + 明示指代依据**,见 §9。
**我的建议**:**取第一个可以接受,但加一句"我按上一轮提到的第一只(X)帮您核对"** —— 让客户看得见指代依据,错了也能立刻纠正。这比反问更省客户一步。
### 第 3 项 · 知识库里**没有**的产品被问到时的回答
**背景**:`科创债ETF南方怎么样` 的 top1 是 **南方稳健增利债券 A 的产品卡**(0.6696)—— 客户问 A,拿到的却是 B 的资料。这是"答非所问",比"没找到"更伤。
**裁定(2026-09-21)**:**加**,见 §9。落地时补了两条**判据收窄**(都不是原建议里写的):
① 产品名的**两种真实写法**都要认 —— 除「南方 + 名称 + 产品类型后缀」外,还要认「名称 + `ETF南方`」(场内基金手册里 10 只 ETF 全是品牌在后的写法);
② 正则**没有左边界**会把动词吞进产品名(「我想买科创债ETF南方」),而**吞进来会造出假「没找到」** —— 加 `_strip_product_name_lead` 逐字剥离前缀功能字。
**我的建议**:**加**。理由:金融场景下"诚实说没找到"优于"贴一段别的产品的资料";且这条闸门已有同源实现(`_hits_share_terms`),改动面很小。
### 第 4 项 · `E5b`「我先帮您把找到的公开资料放上来」的措辞
**背景**:`E5b` 是本轮退化条里最大的一类(6/16)。它的**内容往往是对的**(如「债基和货基哪个收益高」贴出的 FAQ 正文就是标准答案),但**开场白的语气**让它显得像兜底。
**裁定(2026-09-21)**:**改**,见 §9。
**我的建议**:**改**。这是**零风险**的改动(只动模板字符串),但对"看起来智不智能"的体感影响最大 —— 同样一段内容,开场白不同,客户对"这个客服会不会答"的判断完全不同。
---
## 7. 本轮改动清单(文件级)
| 文件 | 改动 |
|---|---|
| `app/core/customer_service_rules.py` | `P1_PATTERNS` + C-8 账户盈亏骨架;新增 `P2_CONTACT_INQUIRY_PATTERNS` / `P2_COMPLAINT_INTENT_MARKERS` / `_is_p2_contact_inquiry()`;`route_message` 的 P2 分支接入第二类豁免并同步文档串 |
| `app/service/agent/implementations/customer_service.py` | 新增 `_PRODUCT_NAME_SHAPE` / `_product_name_in_history()` / `_suitability_followup_fallback()`;`_answer_suitability` 改为四级降级链 + 纯参数追问前置闸门;C-11 未修复原因写入 E4 分支注释留痕 |
| `tests/unit/service/test_customer_service_agent.py` | +4(C-9 守卫) |
| `tests/unit/service/test_customer_service_red_lines.py` | +4(C-8 / C-10 守卫) |
| `.gitignore` | +`_chunks_report.txt` |
| `客服agent/D2.1-…Todolist.md` | 升 `v6.37`,登记本轮修订 |
| `开发文档/D1.1-文档索引与权威声明.md` | 升 `v1.13`,登记 `D4.8` 与本轮版本位移 |
> `W21` 首轮已完成的部分(7 类缺陷 C-1~C-7、语料补强 4 组 FAQ / 3 个 basic 章节 / 1 个 product 章节、`FAQ_EXPECTED_COUNT 65→69`、新集合条数 `basic 105 / product 398 / faq 300 / policy 576`)见 `客服agent\D2.1` 的 `v6.37` 条目,不在此重复。
---
## 8. 诚实留痕(别漏)
1. **打标口径变更**:`W20` 版把认不出的归「其它」(10 条盲区,逐条读原文发现 9 条其实是**正确作答**);`W22` 版翻转为"只识别退化形态"。两次分布**不可相减**,§2.3 已写明。
2. **C-11 是"未修复"而不是"不需要修"**。它已被精确定位到代码行与触发条件,只差一个**合规口径的批准**(§6 第 1 项)。
3. **本轮体检样本是本报告自己构造的**(81 条 + 8 组多轮),不是甲方给定的验收集。金标 46 条仍是**唯一**验收依据,体检集只用于**发现缺陷**。
4. **代码里 4 条 ruff 告警是既有的**(`customer_service_rules.py:318/416`、`customer_service.py:34/1188` 的 E501 与 I001;`W21` 二轮后 E501 行号下移至 `:1220`),不是本轮引入,也**未顺手改** —— 避免把无关改动混进本轮 diff。(`W24` 另**引入过** 2 条 `tools/build_knowledge_chunks.py` 的 `B905`,已在本轮内改为 `zip(..., strict=True)` 清零,见 §10.5。)
5. **真机复验依赖本地四依赖**(MySQL / Redis / Milvus / API+Worker)。复跑前须确认 `/internal/health/ready` 三依赖全绿,否则 `E5b` 会被误读成"缺陷"。
6. **`C-9` 的产物 `_PRODUCT_NAME_SHAPE` 是形状匹配、不是产品名白名单**:语料里新增产品名只要仍符合「南方 + 名称 + 产品类型后缀」就能被认出;若将来出现**不符合该形状**的产品名(如纯英文名),②级降级会失效并落到③级(知识检索)—— 功能不减,只是拿不到裁决。
---
## 9. `W21` 第二轮 · 四项待决的落地与验收(2026-09-21)
> **口径**:本节是 §6 四项的**实施记录**。上一轮(§1—§8)不动,保留原貌以便追溯。
### 9.1 落地清单(文件级)
| 文件 | 改动 |
|---|---|
| `app/service/agent/implementations/customer_service.py` | ① `DEFAULT_EVIDENCE_SYSTEM` 红线 ② 补「不要把收益数字写进答复」;② `DEFAULT_EVIDENCE_TEMPLATE` 新增第 ④ 条同义禁令;③ `_answer_suitability` 在 `inferred=True` 时开场明示指代依据(`W21-D2`);④ 新增 `_ETF_NAME_SUFFIX` / `_PRODUCT_NAME_PATTERNS` / `_PRODUCT_NAME_LEAD_NOISE` / `_PRODUCT_MISMATCH_LOOKAHEAD` / `_strip_product_name_lead()` / `_product_names()` / `_names_other_product()`,并接入 `E5b` 相关性闸门(`W21-D3`);⑤ `PARTIAL_TEMPLATE` 改作答语气 + 新增 `PARTIAL_FAQ_TEMPLATE`,`_exit_partial` 按「命中块是不是 FAQ 问答对」分流(`W21-D4`) |
| `tests/unit/service/test_customer_service_agent.py` | **+9 条守卫**(`D1` 两条 / `D2` 一条 / `D3` 三条 / `D4` 三条) |
| `_eval_harness/cases_46.json` | `I-01` 的 `expected_exits` 由 `[E5b, E3]` 修订为 `[E5b, E3, E4]` + 写入 `exit_note`(理由见 9.3) |
| `开发文档/D3.7-…md` | `I-01` 行与 §注同步该修订 |
| `客服agent/D2.9-…md` | §2 出口话术表的 `E5b` 行、§5 `Z-04` 的出口形状描述同步新话术 |
| `客服agent/D2.1-…Todolist.md` | 升 `v6.38`;`C-11` 由「未修复」改为「已修复」 |
| `开发文档/D1.6-…md` | §9.3 四项标「已落地」;新增 §10 本轮对话上下文 |
| 本文件 | §6 四项标「已裁定」;新增 §9;§0 / §8 同步 |
### 9.2 验收(可复算)
| 项 | 结果 |
|---|---|
| 金标 46 条(进程内 `probe.py` + `score.py`) | `M-1 46/46`、`M-4 46/46`、`M-6 5/46`、`M-7/M-8/M-9/M-10 = 0`、`M-2 28/31`、`M-2b 15/18`、`M-3 4/4` —— **与上一轮逐项一致** |
| 全量单元 / 集成回归 | **`1994 passed / 3 skipped`**(上一轮 `1985/3`;**+9 条新守卫全绿**) |
| `ruff` | 仍只有**既有**告警(`customer_service.py:34` I001、`:1220` E501 等),**本轮零新增**(`:1220` 即上一轮的 `:1188`,行号因新增代码下移) |
| 真 HTTP 关键问句 | 11 条,见 9.4 |
### 9.3 `I-01` 金标期望修订(**这是一处判据变更,必须知情**)
| 项 | 内容 |
|---|---|
| 原期望 | `expected_exits = [E5b, E3]`(`D3.7` §I 组) |
| 现状(修订后) | `[E5b, E3, E4]` |
| 触发原因 | `W21-D1` 让 `E4` 不再被收益数值闸门误拦 ⇒ `I-01`「南方科技是什么公司?」首次真正走到**提示词红线 ②**(该红线**专为「名称查不到」设计**:不回绝、不反问、**不重复那个名称**、直接给最接近的正确事实) |
| 为什么改期望而不是改行为 | ① 考点的**两条**没变:禁忌字面「南方科技」**不出现**(`M-7` 无违反)、关键事实「南方基金」**命中**(`M-4` 通过);② 答案**更好** —— 旧 `E5b` 贴的是「6.2 核心科技平台 / 南方基金·智能服务系统」(答非所问),新 `E4` 给的是公司全称 / 成立日期 / 注册资本 / 业务范围;③ 若强行把 `E4` 拦回 `E5b`,等于**关掉**提示词红线 ② 的唯一落点 |
| 备选方案(未采纳,留档) | 加一条「问句主体名在证据中完全查不到 ⇒ `E4` 不接管」,可让 `M-1` 回到 `46/46` 而**不改金标**;代价是 `D3.7` 已定的 `I-03`/`I-04`(并列 `E4`)语义变窄,且把"能答的公司信息"降级成"贴一段原文" |
### 9.4 真 HTTP 复验(`cust_t` / 访客,`POST /api/v1/agent-runs`)
| # | 问句 | 出口 | 结果 |
|---|---|---|---|
| 1 | 南方现金添利怎么样 | `E4` | 生成作答:R1 / 代码 900501 / 起投 1 元 / 费率与申赎规则;**零收益数值**;含「不承诺收益,可能产生亏损」 |
| 2 | 买基金要手续费吗 | `E4` | 合并作答:交易环节 + 持有环节 + 赎回费 + 线上优惠 |
| 3 | 债基和货基哪个收益高 | `E4` | 明确「本公司不提供这类比较结论」+ 两类特征对比 |
| 4 | 科创债ETF南方怎么样 | `E4` | 诚实答「手册中查不到该名称」+ 列出手册里**确实存在**的示例产品;**未复述**不存在之名 |
| 5 | 你们的投诉电话是多少 | `E3` | 投诉渠道 + 热线;`transfer=False`(`C-10` 未回归) |
| 6 | 我就要人工 | `E5c` | `transfer=True`(`G-05` 白名单,未越界) |
| 7 | 什么样的基金不会亏钱? | `COMPLIANCE` | 合规拒答,**不转人工** |
| 8 | 南方科技是什么公司? | `E4` | 见 9.3;答复内无「南方科技」 |
| 9 | 南方现金添利怎么样(**访客**) | `E4` | 与客户档同答(`public` 档);**未越权** |
| 10 | `我想买个债基` → `它适合我吗` | `E3` → `E2d` | 第二轮开场即「您上一轮提到的是「南方稳健增利债券 A」,我就按它帮您核对:」(`W21-D2`) |
| 11 | `赎回费怎么算` → `持有 8 个月呢` | `E3` → `E3` | 追问继承谓词与参数,给分档表(`C-9` 未回归) |
### 9.5 诚实留痕(本轮新增,别漏)
1. **`D1` 是"降低概率"不是"消灭概率"**:只把 ④ 写进 user 模板时,三条问句仍各有约 1/3 会落到 `E5b`;把同义禁令补进 **system 红线 ②** 后测得 **9/9**。若某次生成仍写出收益数值,`_exit_partial` 的兜底照旧生效(**宁可兜底,不可输出**)。
2. **`I-01` 的金标期望被修订过**(9.3)。这是**判据变更**,不是"零回归" —— 请知情后使用 `M-1 46/46` 这个数字。
3. ~~**`docs/43-场内基金产品手册` 未入库**(702 块里「科创债」0 处)~~ → **已于 `W24` 执行并验收**(`docs/43` 纳入 `SOURCES`,切片件 702 → 755 块,见 §10)。当时 `W21-D3` 的闸门只做到**止损**(不再贴错产品),**根本修复**需要把它纳入 `SOURCES` 并重建集合 —— 属**语料变更**,未在 `W21` 擅自执行,已登记为待决(`D1.6` §10.4)。
4. **`E5b` 展示层仍是"原文直返"**:实测「买基金要手续费吗」的 `E5b` 分支正文里含「专户业绩报酬 | 按合同约定计提 | 超额收益的 10%—20%」,会被 `hits_zero_tolerance` 判 True(`收益`+数字% 陷阱)。**这是既有行为**(`drop_yield_claims` 只丢带收益指标名的行),治理层不对 `E5b` 正文做二次零容忍扫描,因此**不会拦单**;但**展示层净化是否要覆盖这一形态**,建议列入下一轮待决。
5. **改动只落在 `客服agent/` 与 `开发文档/` 两份权威副本 + 仓库同名镜像**,三处一致(`D1.1` §权威声明)。
---
## 10. `W24` · `docs/43` 场内基金手册入库的实施与验收(2026-09-21)
> **性质**:**补历史欠账**。`D3.4` 的 `B-02`(决 12)与 `D4.1` §B-02 早已写明「新增 `docs/43`+`docs/45` 入 `SOURCES`」,**从未执行**;`W21` §9.5 第 3 条首次把它暴露成实测缺口。甲方 2026-09-21 批准「**先把语料修复吧 在进行下一步 不单独立项了**」⇒ 本轮执行。
> **证据**:`_eval_harness/result_w24d.json` + `score_w24d.json`、`result_w24e.json` + `score_w24e.json`、`_w24_http.txt`(真 HTTP)、`knowledge/_chunks.jsonl`(755 块)。
### 10.1 落地清单(文件级)
| 文件 | 改动 |
|---|---|
| `tools/build_knowledge_chunks.py` | ① `SOURCES` 新增 `product/场内基金产品手册(知识库入库版).md`(`prefix=ETF` / `collection=fin_product_collection` / `visibility=public` / `version=V1.0` / `effective_date=2026-09-11` / `doc_no=JR-ETF-2026-001`);② 新增三个**按源开启**的开关 `strip_editorial_marks` / `qualify_table_rows` / `exclude_sections`(默认关);③ `expand_table_rows()` 签名加 `qualify`,新增 `cell_text()` / `row_content()` / `subject_of()`;④ **行级子块标签改取「名称」列**(缺陷修复,见 10.2) |
| `tools/load_knowledge_milvus.py` | `embed()` **内部分批**(`MAX_BATCH = 10`)—— 修自检路径超 10 条即崩的潜伏 bug;自检期望 `BAS-CON-006` → `ETF-005`,并新增 5 条新源存在性证明(**自检 15/15**) |
| `docs/43-场内基金产品手册(知识库入库版).md` + `knowledge/product/…md` | 费率表表头补「(%/年)」;两处**逐字一致**(有断言守着) |
| `knowledge/_chunks.jsonl` | 702 → **755 块**(policy 288 / product 251 / faq 154 / basic 62) |
| `app/service/agent/implementations/customer_service.py` | ① `_PRODUCT_NAME_SHAPE` 后缀集补 `定期开放混合\|股票\(LOF\)[ABC]?\|\(LOF\)\|原油[ABC]?`;② 新增 `_BRANDLESS_PRODUCT_NAMES`(`沪深300ETF`)与第三条 pattern;③ `_strip_product_name_lead` 增「先切掉前一只基金」;④ `_answer_from_evidence` 新增 `display_hits` 形参,四处 `_exit_partial` 改传**展示候选集** |
| `tests/unit/service/test_customer_service_agent.py` | **+2 条守卫**:20 只产品名全覆盖(语料驱动)、无厂商字样产品**不**在类目问句上开门 |
| `_eval_harness/cases_46.json` | `B-01` 证据期望补 `ETF`;`B-05` 出口期望补 `E4`(见 10.3) |
| `客服agent/D2.1` | 升 `v6.39` |
| `客服agent/D2.4` / `D2.8` / `D2.9` | 语料口径 675 → **755**;`D2.4` §4.4 场内基金手册切片预估「约 30—50」→ 实测 **53** |
| `开发文档/D1.1` / `D1.6` | 索引口径同步;`D1.6` 新增 §11,§10.4 标「已执行」、§10.6 第 1 条销账 |
| 本文件 | 新增 §10;§0 / §8 / §9.5 同步 |
### 10.2 执行中发现并修复的两个真实缺陷
| 编号 | 缺陷 | 现象(修复前) | 根因 | 修法 |
|---|---|---|---|---|
| **`W24-A`** | 🔴 **切片器:行级子块标签取错列** | 「科创债ETF南方怎么样」返回 **20 只产品的整张表**(真 HTTP + 进程内**双重**复现) | 上游 `_prefer_section` 用子块 `title` 末段与问句比对;多列表格**第 0 列是代码**(`159700[:2]` = `15`),**永不重合** ⇒ 每一问都被换成整节块 | `qualify` 模式下标签改取表头里含**「名称」**的那一列(两张表都有该列),无则退回第 0 列 |
| **`W24-B`** | 🔴 **`embed()` 自检路径未分批** | `CHECKS` 超过 10 条时,**数据已全部写完之后**崩(本次 14 条即触发) | `text-embedding-v3` **单请求最多 10 条**;原实现只有灌库路径分批 | 分批下沉进 `embed()`(`MAX_BATCH = 10`),两条路径同时受益 |
**另修一处 `M-4` 回归(`B-02`)**:
| 项 | 内容 |
|---|---|
| 现象 | `B-02`「基金申购和赎回有哪些费率?」(访客)在 `w24c` 由 `E5b` 展示 `POL-SPM-033-01`(**单行「赎回费率:<7 天 1.50%」**)⇒ **只答一半**,`M-4` 失败 |
| 根因 | `_exit_partial` 的两条调用路径**候选集不同**:主路径 `_answer_from_knowledge` 传 **TopK 全量**(能挑到 `PROD-016` 费率总表),证据路径 `_answer_from_evidence` 传**被 `E4_MAX_EVIDENCE = 6` 截断的证据包**。新语料把 `ETF-009` 插进 TopK,把 `PROD-016` 从第 9 名挤到第 10 名,"在包内"变"包外" |
| 为什么这是缺陷 | **同一句问句的答复质量取决于「`E4` 这次有没有调用模型」** —— 两条路径只差"模型这一跳成没成",不该差在客户看到的内容质量上 |
| 修法 | `_answer_from_evidence` 新增 `display_hits` 形参(调用点把 `hits` 传进来),四处 `_exit_partial` 改传该值;`_exit_partial` 内部仍挑最高分、仍用 `PARTIAL_FLOOR` 兜底。**送模型的证据包保持 6 块不变**(那是模型注意力问题,不是展示问题) |
| 为什么不顺手"按问句产品名优先挑块" | 那会改 `E5b` 的**挑选语义**(从"分数最高"变成"与问句主体一致优先"),影响面覆盖全部 `E5b`,需单独回归;本轮只做**候选集口径统一**这一最小改动(`D1.6` §10.6 第 3 条仍**部分未收口**) |
### 10.3 期望值登记修订(**判据变更,必须知情**)
| 项 | 原期望 | 修订后 | 理由 |
|---|---|---|---|
| `B-01`(**访客**,问句见 `cases_46.json`) | `expected_evidence = [PROD, FAQ]` | `[PROD, FAQ, ETF]` | 新语料让 `ETF-013`(0.7954)**合法**拿到 top1,`PROD-001-05`(0.7527)退到 #2;两者都答得到,**答案更完整**。已写 `evidence_note` |
| `B-05`(**客户**,与 `B-01` **同一句问句**) | `expected_exits = [E3]` | `[E3, E4]` | 与 top1 `FAQ-0020`(0.8286)只差 **0.033 < 0.07** ⇒ 走「并列 → 生成式作答」。已写 `exit_note` |
| `tools/load_knowledge_milvus.py` 自检 | `("场内基金和场外基金有什么区别", "BAS-CON-006")` | `("…", "ETF-005")` | `ETF-005` 0.861 vs `BAS-CON-006` 0.789,**更贴题**;已写明「期望值是**语料版本的函数**」 |
> ⚠️ 因此本轮**不适用**「零回归」表述。可复算的准确说法是:**在三条期望登记修订之后**,金标 `M-1`/`M-4` 均为 46/46。
### 10.4 验收(可复算)
| 项 | 结果 |
|---|---|
| 金标 46 条(进程内 `probe.py` + `score.py`) | `M-1 46/46`、`M-4 46/46`、`M-6 5/46`、`M-7/M-8/M-9/M-10 = 0`、`M-2 28/31`、`M-2b 15/18`、`M-3 4/4` —— **与 `w23` 基线逐项一致** |
| **两次独立复跑** | `result_w24d` 与 `result_w24e` **指标完全相同**(本仓库有"只跑一次就宣布修好是假绿"的历史教训,`D1` 类改动必须重复采样) |
| Milvus 自检 | **15/15**;切片件 755 = 四集合合计(**只信 `_chunks_report.txt` 的块数** —— `upsert` 会让 `row_count` 翻倍) |
| 全量单元 / 集成回归 | **`1996 passed / 3 skipped`**(`W21` 二轮 `1994`,**+2 条新守卫全绿**) |
| `ruff` | **零新增**:`customer_service.py:34` I001、`:1220` E501 等**均为既有**;本轮曾引入 2 条 `B905`,已当场清零 |
| 切片件幂等 | `strict=True` 重构后重跑 `build_knowledge_chunks` ⇒ `knowledge/_chunks.jsonl` **SHA256 逐字节相同**(无须重灌) |
| 真 HTTP | 见 10.5 |
### 10.5 真 HTTP 复验(`cust_t` / 访客,`POST /api/v1/agent-runs`)
| # | 问句 | 结果 |
|---|---|---|
| 1 | 南方金利定开债券A的费率是多少? | 单行费率 **带单位**:管理费率(%/年)0.50 / 托管费率(%/年)0.15 / 销售服务费率 无 |
| 2 | 科创债ETF南方怎么样 | **单只产品**(代码 159700 / 深交所 / ETF / **R2** / 净值 101.8009)—— 修复前是 20 只的整张表 |
| 3 | 场内基金有哪些 | 20 只清单 |
| 4 | 场内基金和场外基金有什么区别 | `ETF-005`:场内按市价撮合 / 场外按净值申赎,T+1 确认 |
| 5 | 南方现金添利怎么样 | `E4` 生成作答(R1 / 起投 1 元 / 费率 / 申赎规则),**零收益数值** |
| 6 | 买基金要手续费吗 | `E4` 合并作答(交易 + 持有 + 赎回费 + 线上优惠) |
| 7 | 赎回费怎么算 → 持有 8 个月呢 | `E5b` → `E5b`:**两轮都给出完整分档表**(`B-02` 同族修复的连带确认) |
| 8 | 我想买个债基 → 它适合我吗 | 指代继承 + **真实适当性裁决**(R2 vs C1 + 揭示书 + 双录);**未替客户确认** |
| 9 | 科创债ETF南方(**访客**) | 与客户档同答(`public` 档);**无档位越权** |
### 10.6 本轮裁定:`docs/45` **不入库**
`docs/45`(R1—R5 问答)与 `FAQ-0018`、`POL-AST-011` / `POL-AST-012` **高度重复**;单独成块会在「R1 到 R5 分别代表什么」这类问句上**抢走 top1**(该题 Top1 与次优原本只差 0.021,属已知的阈值敏感区)。**裁定:不纳入 `SOURCES`**;历史承诺里的 `docs/45` 部分**据此关闭**。
### 10.7 诚实留痕(本轮新增,别漏)
1. **本条是"补历史欠账",不是新增需求**:`D3.4 B-02`(决 12)与 `D4.1 §B-02` 早已承诺,从未执行。
2. **本轮有 3 条期望修订**(10.3),"零回归"不适用。
3. `_exit_partial` **仍按分数挑块**,不接收问句 ⇒ 「问句点名了产品 X,但第 1 名的近邻块更贴题」这一类**仍未收口**(`D1.6` §10.6 第 3 条,本轮只统一了候选集口径)。
4. **`E5b` 展示层净化仍不覆盖「收益 + 数字%」形态**(`W21` §9.5 第 4 条)—— 本轮**未动**,继续挂账。
5. 语料里仍有 **2 个零容忍地雷块**(`POL-SPM-016` / `POL-SPM-022-01`),是**禁令条款**,**故意保留**,不是缺陷。
6. **`upsert` 会让 `get_collection_stats().row_count` 翻倍**(实测 450 vs 251,`doc_id` 仍唯一)⇒ **只信 `_chunks_report.txt` 的块数**;改 `doc_id` 编号后**必须丢集合重建**(本轮重编号 `ETF-009`/`ETF-010` 时留过一条陈旧行)。
7. 改动只落在 `客服agent/` 与 `开发文档/` 两份**权威副本** + 仓库同名**镜像**,三处一致(`D1.1` §权威声明)。