Files
group_fqcd_jr/开发文档/D3.9-客服Agent智能路由与行情出口设计-2026-09-21.md
张胜宇 3e24033f72 feat(W27): L0 表层判定层 + 出口 E6 行情 + 收益过滤槽位白名单 + 免责声明分档(设计与代码同轮完成)
设计与依据:新增 D3.9-客服Agent智能路由与行情出口设计-2026-09-21.md(CS-ARCH-2026-024)
DEC-W27-1~12 全部批准并落地。

## 新增(3 个源文件 + 3 个测试文件)
- app/core/exit_codes.py:出口码注册表(E0/E1/E2a~E2e/E3/E4/E5a~E5c/E6/E8/CHAT/LOGIN/CONTACT)
  + NON_BUSINESS_EXIT_CODES 白名单(免责分档的判据面)
- app/service/fund_trend_service.py(E6):summarize_trend() 纯函数 + query_fund_trend 工具
  读 fin_nav_history;按净值日个数(5/20/60/120)给区间涨跌 / 区间高低 / 来源与区间
- tests/unit/{service,tools} 新增 45 条守卫单测(E6 出口 / L0 表层判定 / 工具配置)

## 改动(重点)
- customer_service.py:
  · _route_surface()(L0-a 闲聊 / L0-b 行情 / L0-e 无信息量),位置在安全路由之后
  · drop_yield_claims() 重写为「入口闸 × 槽位白名单」+ fail-closed
    (旧黑名单会误删权重:A-06 业绩基准公式整行消失)
  · allowed_tools 补 query_fund_trend —— 修掉发布被拒 422「配置超出 Agent 工具上限」
    (该 422 是**子集**校验而非数量上限,根因即此项漏配)
  · 25 处 CoreResult 全部补 exit_code(AST 守卫保证零遗漏)
- customer_service_rules.py:闲聊判定重写(业务实体边界 + 词表 + 特征串 + 语气词)
  + is_low_information_message() + is_contact_inquiry()
- governance.py:免责声明按 exit_code 分档(业务档完整 / 非业务档轻型)
- actor.py:VISITOR_PERMISSIONS 补 fund:quote:read(E6 对访客开放,DEC-W27-11)
- tools/:seed_compliance_baseline 新增 TPL_DISCLAIMER_LIGHT;publish 工具改为
  「从生效版本派生原列表再追加」(_inherit_tools 取不到返回 None,防静默改窄)

## 实测
- 金标扩容 46 → 55(新增 Q 组行情 5 条 + C 组闲聊 4 条)
- M-1 55/55、M-4 55/55;M-5 与四项零容忍(M-7/M-8/M-9/M-10)全 0
- 原 46 条可比基线逐项不变(46/46);M-6 9.1%(5 条全在转人工白名单内)
- 全量 pytest 2094 passed / 3 skipped;ruff 零新增(余 5 条与 HEAD 逐条对应)
- 配置版本 244(cs-tools-75813de45421)已发布并激活

## 文档
- 新增 D3.9(设计专册);D1.1 §34 补「代码实施已同轮完成」并把旧表述作废
- D3.7:新增 Q 组判据表 + §6.4 第四次实测 + M-3/M-9 口径澄清
- D2.9:新增 §2.11(9 条新增用例的实测答复原文)+ 汇总表改双列口径
- D2.1:新增 v6.41 执行条目
- 权威副本(D:\桌面\金融\)→ 仓库镜像 全量比对一致(0 缺失 / 0 不一致)

## 诚实未做项
- 知识出口(E3/E4)侧的实体锚点闸门未做(故金标扩容 9 条而非 12 条,A-11~A-13 未成集)
- 阈值未重标(D3.9 §3.4 只给方法);M-3 分母未改(仅明示口径);
  D1.1 §0 与 §4.0 历史计数偏移(差 1)仍沿用
2026-09-21 22:32:39 +08:00

395 lines
30 KiB
Markdown
Raw Permalink 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.
# D3.9 · 客服 Agent 智能路由与行情出口设计
> **体系编号**:`D3.9` · 域:三、现行权威·完整版与专项 · 编号体系见 `D1.1` §4.0
> **编号**:CS-ARCH-2026-024 | **版本**:v1.0 | **日期**:2026-09-21 | **状态**:**现行(`W27` 轮设计与实施依据)**
> **性质**:本文件是 `W26` 会话中甲方提出三项异议后的**设计答复 + 实施依据**。它不重复 `D3.6`(五出口决策链)与 `D3.7`(金标判分),只回答四件事:① 三项异议的**实测根因**(含反证);② 新增的 **`L0` 表层判定层** 与 **能力路由**;③ 新增 **出口 `E6` 行情**;④ 把"精确性"从**分数阈值**迁到**实体锚点 + 证据结构**的可证伪口径。
> **读法**:先读 §0 → §2(诊断)→ §3(架构)→ §8(决策登记)→ §9(验收)。证据表在 §2,文档对齐清单在 §10。
> **上游**:`D3.6`(五出口)、`D2.4` / `D3.2`(知识库)、`D2.2` `FR-CS-008`(分级回退)、`D3.7`(金标)。**本件对 `FR-CS-008` 的"跨集合回退(阈值 0.65)"作口径更正**,见 §3.5。
---
## 0. 一句话结论
甲方三项异议的根因**不是一个**,不能用一个"意图识别不准"解释:
1. **"阈值能不能保证精确检索"** —— **不能,且前提不成立**。我们自己的阈值标定件实测:应当直答的 26 条 `score ∈ [0.6115, 1.0]`,而**明确不该直答**的 20 条 `score ∈ [0.5049, 0.8226]` —— **两个区间大幅重叠**,任何单点阈值都无法把"该答 / 不该答"分开。
2. **"回退降阈值会不会让答案不精准"** —— 顾虑正确,但**实现里根本没有"降阈值回退"**:`knowledge_search_service.FALLBACK_COLLECTIONS` 是**死代码**;`FR-CS-008` ①「跨集合回退(阈值 0.65)」只存在于文档。**本轮裁定:不实现降阈值回退,改文档口径。**
3. **"走势答不对"** —— 根因是**数据源缺失**,不是判定不准:库内 `fin_product` **20 条全是场内 `ETF`/`LOF`**(每只 122—160 个净值日),而手册里的示例产品(`南方稳健增利债券A` 等)**不在 `fin_product`**,KB 侧只有静态描述。`走势` 在金标 46 条中出现 **0 次** —— 这是个**从未被建模、也从未被测试**的能力。
4. **"闲聊总触发知识库检索"** —— 分类器其实判对了(实测 9/9 条 `chitchat`);隐患在**兜底**:确定性判据 `is_chitchat_message()` 只用于算 `chitchat_streak`,**没接进路由**,一旦分类失手就落进 `faq` 检索。
5. **顺带挖出一处合规泄漏(优先级高于以上)**:收益过滤 `drop_yield_claims()` 是**关键词黑名单**(只认 `收益率 / 年化 / 回报率 / 涨幅 / 业绩`),实测答复漏出 **「近一年表现(虚构) | 约 4.12%」**。同一句写成"近一年收益率 4.12%"会被整行删除,换个说法就放行 —— **黑名单可被改写绕过**,必须升级为**槽位白名单**(§5)。
---
## 1. 甲方原话(逐字留痕)
> 我现在答辩遇到问题了 我现在的意图识别做的很不清晰,比如用户问某个基金的走势怎么样 回答的不是我想要的结果 而且分级的回退那个规则怎么去确定他那个阈值就一定能精确的检索出那个答案,然后回退之后到第一层那个降低阈值的话 答案不是就不精准了吗 这个规则我觉得是不合理的 我希望在做意图识别之前 再加一层规则 或者把意图识别做得更精准 用户闲聊的时候总是触发知识库检索这种问题 请你跟我讨论修改的方案
同轮附图为 `D2.2` `FR-CS-008` 截图(等级 `P0`,期望口径:检索为空或最高分低于阈值 → ① 跨集合回退(阈值 `0.65`)→ ② 部分答 + 引导(`E5b`)→ ③ 白名单命中才转人工(`E5c`))。
---
## 2. 实测诊断(`W26` 轮新跑,非文档转述)
### 2.1 分数阈值不可分(**这是本轮最硬的一条**)
| 来源 | 组 | 条数 | `score` 区间 | 极值案例 |
|---|---|---|---|---|
| `_eval_harness/threshold_calibration.json` | 应当直答(`E3`) | 26 | **0.6115 — 1.0** | 最低 `E-02` |
| 同上 | **明确不许直答** | 20 | **0.5049 — 0.8226** | 最高 `C-03` |
**判据**:若"应直答"的**下限**(`0.6115`)**低于**"不许直答"的**上限**(`0.8226`),则**不存在**能把两组分开的单点阈值。`0.65` / `0.75` 都只是"当时调出来的数",**不是精确性保证**。
**结论**:精确性不能由分数承担。当前实现之所以能 `M-1` 46/46,靠的是分数之上叠加的**结构判据**(间隙 `MIN_GAP`、主体相关性闸门、同族合并 `E4`、澄清 `E5a`、档位裁剪),**不是**那一个阈值。
### 2.2 "跨集合回退(阈值 0.65)"在代码中不存在
| 事实 | 位置 | 说明 |
|---|---|---|
| `FALLBACK_COLLECTIONS` **定义了但无人调用** | `app/service/knowledge_search_service.py:41` | 死代码 |
| 唯一的"跨集合"动作**不降阈值** | `customer_service.py` `_supplement_basic_explain()` | 条件触发(仅通用常识题)、**且要求命中严格高于主检索 top1** 才前置 |
| 跨集合分数**不可比**(已实测) | `knowledge_search_service.py:25-37` 注释 | 把第 4 集合并入默认检索面,`M-1` **100% → 91.3%**(把次优分抬到 0.69—0.79,撞坏 `MIN_GAP`) |
| 文档却写着"阈值 0.65" | `D2.2` `FR-CS-008` / `D3.1` `FR-CS-008` / `D2.4` | **文档与实现不一致** —— 答辩现场被追问即失分 |
**裁定**:`FR-CS-008` ① 作废,改为「**回退不降阈值、不改档位,只做档位内部分作答 + 引导**」(§3.5)。
### 2.3 "走势"三类问法的实测答复(同一轮 HTTP 实测,客户身份)
| 问句 | 期望 | 实测 | 判定 |
|---|---|---|---|
| `南方稳健增利债券A最近走势怎么样?` | 走势/净值区间 | 返回**整张产品卡**(产品代码 / 风险等级 / 起投 / 经理 / 规模 / 费率…),**与"走势"无关** | ❌ 答非所问 |
| `159382这只ETF最近走势怎么样?` | 走势/净值区间 | 返回一句话静态快照「净值 2.5841 / 净值日期 2026-09-11」,前面挂「关于这一点,公开资料里的口径是:」 | ⚠️ 有数据、无走势 |
| `季季盈90天最近的净值表现如何` | —— | 走成 **`E5a` 澄清**,候选是「升降级规则 / 七日年化 / 赎回费率」等**不相干项** | ❌ 澄清误触发 |
> 补充事实:`季季盈`、`年年盈` 已按 `D2.2` `I-02` 列为**禁词**(品牌口径整改产物),KB 中确实**不存在**;实测 `季季盈90天的起投金额是多少` 返回"查无该产品"属**正确行为**,不是回归。
### 2.4 数据源事实(决定 `E6` 能答什么)
| 事实 | 数值 | 依据 |
|---|---|---|
| `fin_product` 产品数 | **20** | DB 直查(`jr_agent`) |
| 产品类型分布 | **全部为场内 `ETF` / `LOF`**(`SSE` / `SZSE`) | 同上 |
| 净值序列 | **每只 122—160 个净值日**,区间 `2026-03-18` — `2026-09-13` | `fin_nav_history`(2494 行) |
| 手册示例产品(`南方稳健增利债券A` / `南方平衡优选混合` / `南方红利价值股票` / `南方全球精选QDII` / `南方智能科技股票A` / `南方现金添利`) | **不在 `fin_product`**,**无净值序列** | 同上 |
**推论**:`E6` 能给出**真实走势**的只有场内 20 只;对示例产品**必须如实说"暂无公开净值序列"**,**不得**用手册里的虚构数字拼走势(否则等于编造行情)。
### 2.5 闲聊实测(9 条全判对,隐患在兜底)
| 问句 | 分类结果 | 出口 | 观察 |
|---|---|---|---|
| `在吗` / `讲个笑话` / `谢谢你` / `哈喽` / `嗯嗯` / `你忙吗` / `你是谁呀` / `你们公司的人都好有趣啊` / `你好呀今天心情不错` | 9/9 = `chitchat` | 闲聊出口 | 答复自然 ✅ |
| 同上 9 条的**尾缀** | —— | —— | **每条都追加完整投资免责声明**("本内容仅为投资分析参考…"),`讲个笑话` 后接风险提示,体验割裂 ⚠️ |
**隐患**:`app/core/customer_service_rules.py:708` 的 `is_chitchat_message()` 只在 `chitchat_streak()` 里被用,**未进路由**。分类器一旦把寒暄判成 `faq`(如"有人吗"、"在不在"、"收到"),就真的去检索知识库。
### 2.6 合规泄漏(收益数值过滤可被改写绕过)
| 问句 | 实测答复片段 | 问题 |
|---|---|---|
| `南方季季盈90天怎么样` | 「… 近一年表现(虚构) | 约 **4.12%** | ★★★★ …」 | 收益数值**已漏出**给客户 |
| 对照 | 「近一年**收益率** 4.12%」会被 `drop_yield_claims()` **整行删除** | 说明过滤是**字面黑名单**,换个措辞即绕过 |
根因:`_YIELD_NUMBER_A/B` 只认 `收益率 / 年化 / 回报率 / 涨幅 / 业绩` 五个词。`表现 / 累计收益 / 涨了 / 回报` 等改写形态全部放行。
---
## 3. 目标架构
### 3.1 新增 `L0` 表层判定层(出口 `E0`)
**位置**:**安全路由之后、意图分类之前**。
```text
① 确定性安全路由(P0 反诈 / 注入 / 合规 / P1 账户 / P2 写操作与争议) ← 仍在最顶层,不可越过
② L0 表层判定层(本件新增,E0) ← 不问模型、不看分数
├─ L0-a 闲聊寒暄(is_chitchat_message 扩展表)────────────────► 闲聊出口
├─ L0-b 行情走势(词法 + 实体)─────────────────────────────► E6 行情出口
├─ L0-c 本人数据(画像 / 适当性 / 可买清单)─────────────────► 既有 E2* 出口
├─ L0-d 计算型参数位(费率 / 赎回费 / C—R 规则)─────────────► 既有 E2a—E2c 出口
└─ L0-e 无信息量短句(纯符号 / 纯表情 / 单字语气词)──────────► 澄清或闲聊出口
③ 身份与档位边界(visitor / customer)
④ 意图分类(**降级为增益 + 兜底**,见 §3.2)
⑤ 知识检索 → E3 直返 / E4 证据约束生成 / E5a 澄清 / E5b 部分答 + 引导 / E5c(白名单)
```
**为什么加这一层**:现在 `_route_and_answer()` 里**已经有**五个确定性前置分支(安全路由、风险等级代办、画像、计算型、可买清单),但它们是"散装"的、没有统一名字与优先级文档 —— 于是"闲聊"和"走势"这两类**本可确定性判定**的问法,只能靠**意图分类 + 检索分数**去碰运气。
**`L0` 三条纪律**(照 `INV-5` 失败方向 = 收敛):
1. **只做判定,不做结论**:`L0` 命中后**转交既有出口**执行,自己不生成事实。
2. **判据必须可证伪**:每条 `L0` 判据都要能在评测集上量出"命中率 / 误命中率",不接受"感觉能行"。
3. **不得放大权限**:`L0` 出口的可见档位与主路径**同一映射**(沿用 `FR-CS-008` 的硬约束)。
**`L0-a` 闲聊判据(扩展)**:现表 `CHITCHAT_MESSAGES` 12 条 + `CHITCHAT_PHRASES` 5 条,扩展为四组 —— ① 招呼/告别(在吗 / 有人吗 / 在不在 / 拜拜 / 再见);② 致谢/确认(谢谢 / 收到 / 好的 / 嗯嗯 / ok);③ 关于助手(你是谁 / 你叫什么 / 你会什么);④ 情绪寒暄(你忙吗 / 今天天气 / 讲个笑话)。**边界**:句中含业务实体(产品名 / 代码 / 条款号 / 费率 / 账户词)时**不判闲聊**,交 `L0` 其余分支或知识检索。
### 3.2 意图分类:从"路由器"降级为"增益 + 兜底"
**现状**:`intent` 真正的杠杆只有两处 —— ① 访客意图白名单;② 工具白名单 key(`customer_service:<intent>`)。其余一律落知识检索。**结论:现在是"意图在替一个它不该做的决定背锅"**。
**改法(能力路由)**:按"回答这句话需不需要外部数据"分流,而不是按"这句话属于哪一类":
| 能力 | 需要的权威来源 | 出口 | 判定者 |
|---|---|---|---|
| `kb` | 知识库(档位裁剪后) | `E3` / `E4` / `E5*` | 检索 + 结构判据 |
| `profile` | `query_customer_profile` | `E2e` | `L0-c`(确定性) |
| `suitability` | `check_suitability` / `query_eligible_products` | `E2c` / `E2d` / `E2c-my` | `L0-c` + 意图增益 |
| `calc` | 档位内参数位(纯函数) | `E2a` / `E2b` | `L0-d`(确定性) |
| `quote` | 行情接口 + 库内净值序列 | `E6` | `L0-b`(确定性) |
**意图分类保留什么**:① 与 `INV-1` 相关的**档位与主体判断**;② `needs_clarification` → `E5a` 的一个触发条件(`H-01` DoD ①);③ 兜底(`L0` 未命中且检索证据不足时,用意图选默认能力面)。
### 3.3 判据迁移:精确性交给"实体锚点 + 证据结构"
**可判的锚点**(问句 → 命中块必须一致):
| 锚点类型 | 例 | 一致性判据 |
|---|---|---|
| 产品名 | `南方稳健增利债券A`、`创业板人工智能ETF南方` | 命中块 `title`/`content` 必须含同一名称(现有 `_names_other_product` 的推广) |
| 产品代码 | `159382`(6 位) | 数字串精确匹配,且**不得**把日期/金额切成假代码 |
| 条款号 | `第十六条`、`5.2` | 命中块 `title` 的条款号一致 |
| 字段名词 | `起投金额` / `赎回费率` / `风险等级` | 命中块含该字段标签(现有 `_prefer_section` 的推广) |
**判定顺序(取代"分数是否 ≥ 阈值"作为主判据)**:
```text
有锚点 且 命中含同锚点 ──► 可答(E3 直返 / E4 合并)
有锚点 但 命中块是别的实体 ──► 判"没找到"(E5b,不拿相近实体替答)
无锚点 且 有同族多块 ──► E4 合并作答
无锚点 且 分数低于中位 ──► E5a 澄清(四类触发条件不变)
其余 ──► E5b 部分答 + 引导
```
**阈值只保留一件事**:作为**安全下限**(低于它一律不给结论)。它**不再承担"精确性保证"**。
### 3.4 阈值该怎么标定(可证伪的口径)
**当前不可分,故只能分层标定**:
1. 金标从 46 条扩到 **≥ 80 条**(§9.2),按**出口分组**统计 `score` 分布;
2. 对"必须答对"的题取**下限 `L`**(要求 `recall = 100%`);对"不许直答"的题取**上限 `U`**;
3. **仅当 `L > U`** 时,单点阈值才成立并可写进文档;
4. `L ≤ U`(**当前实测就是这种情况**)⇒ 阈值**必须**与结构判据联合使用,且**按意图分别取值**(`faq` / `product_inquiry` / `policy_explain` 各自的 `L`/`U` 不同)。
**验收**:标定脚本产出 `threshold_calibration.json` 的新版本,含每组的 `L` / `U` / 是否可分,**可分性结论必须写进 `D2.4`**,不得只留一个"0.65"。
### 3.5 `FR-CS-008` 口径更正(本轮裁定)
| 项 | 原文(作废) | 新口径 |
|---|---|---|
| ① 触发 | 检索为空或最高分低于阈值 → **触发跨集合回退(阈值 0.65)** | 检索为空或最高分低于阈值 → **档位内部分作答 + 引导**(不跨集合、不降阈值、不改档位) |
| ② 失败 | 回退仍失败 → 部分答 + 引导(`E5b`) | 保持(`E5b`) |
| ③ 转人工 | 仅白名单命中才转(`E5c`) | 保持(`E5c`) |
| 硬约束 | 回退不得跨档位 | 保持,并**扩充**:回退不得跨集合、不得改判据 |
**理由(已有实测)**:跨集合余弦分数量纲不可比,混比会撞坏 `MIN_GAP` 判据(`M-1` 100% → 91.3%)。**回退的正确形态是"改答法",不是"改检索参数"** —— 阈值一旦放松,等于用精度换命中率,与 `INV-2`(无证据不生成事实)冲突。
---
## 4. 出口 `E6` 行情(走势 / 净值 / 涨跌)
### 4.1 数据源(先说清能答什么)
| 对象 | 数据源 | 状态 |
|---|---|---|
| 场内 `ETF` / `LOF`(20 只) | `fin_nav_history`(122—160 个净值日) | ✅ 可算区间走势 |
| 场内实时/收盘行情 | `query_fund_quote`(东财适配器 + Redis 缓存,15s 超时预算,`allowed_roles` 已含 `customer`) | ✅ 工具已在仓,**投顾 Agent 已在用**;客服 Agent **未接** |
| 手册示例产品(6 只) | **无** | ❌ **必须如实说没有** |
### 4.2 触发与取数
- **触发(确定性)**:问句含走势类词(`走势` / `涨跌` / `净值` / `行情` / `最近表现` / `涨了` / `跌了` / `今天多少`)**且**能抽出实体(产品名或 6 位代码)。
- **取数顺序**:① 库内净值序列(区间涨跌 / 期初期末 / 最高最低 / 数据区间);② 需要最新一档时补 `query_fund_quote`。
- **不调模型**:与 `E2` 同族,纯函数 + 受控数据源,数字直接来自数据,**不生成**。
- **超时与降级**:取不到 → `E5b` 如实告知(**绝不**回退到知识库里的静态快照充当"走势";**绝不**猜一个数)。
### 4.3 渲染与数字放行
| 规则 | 说明 |
|---|---|
| 固定模板 | 名称 + 代码 + 最新净值 + 净值日期 + 区间(近 5/20/60/120 个净值日)涨跌 + 区间最高/最低 + 数据来源 |
| 必带 | 数据日期与来源(可核对性),以及"不构成投资建议"提示(由治理层统一追加) |
| 禁止 | 跨产品比高低("比 X 强")、收益承诺措辞、把区间涨跌表述为预期、把示例产品的虚构数字当行情 |
### 4.4 示例产品(无数据源)的答复口径
必须**先说明数据可得性**,再给能给的(费率 / 门槛 / 风险等级,来自手册):
> 「`南方稳健增利债券A` 在演示数据里是**手册示例产品**,没有公开净值序列,因此无法给出走势;我可以按手册告诉你它的风险等级、起投金额与费率。」
**这条口径同时是答辩加分项**:它证明系统**知道自己的数据边界**,而不是拿相近内容硬答。
---
## 5. 收益数值过滤升级:关键词黑名单 → 槽位白名单
**问题**:`drop_yield_claims()` 是"收益词 + 数字"的**字面黑名单**,已被实测绕过(§2.6)。
**改法(按槽位判定)**:先把一行里的数字归入**槽位**,再决定放行/拦下:
| 槽位 | 例 | 处置 |
|---|---|---|
| 费率 | `申购费率 0.08%`、`管理费 0.30%/年`、`赎回费 1.5%` | **放行** |
| 权重/基准 | `沪深300指数收益率×60%` | **放行**(现有 `_YIELD_FORMULA_MARKERS` 豁免的推广) |
| 门槛/规模/日期 | `1,000 元起投`、`规模 186 亿元`、`2026-09-11` | **放行** |
| 风险指标 | `最大回撤 -0.75%`、`夏普比率 1.62`、`波动率` | **按业务口径裁定**(建议放行并标注口径,见 §8 `DEC-W27-9`) |
| **收益 / 涨跌** | `近一年表现 4.12%`、`累计收益 12%`、`涨了 3.2%` | **默认拦下**;仅当来源为 `E6` 行情出口(受控数据 + 日期)时**放行** |
**判据实现要点**:不再依赖"关键词命中",而是**先识别数字的语义槽位**(同行内的标签词 + 单位 + 位置),**未识别出的槽位一律按收益槽位处理(fail-closed)**。
---
## 6. 闲聊收口与免责声明分档
### 6.1 闲聊
- 确定性判据(§3.1 `L0-a`)前置,**先于意图分类**;
- 连续闲聊计数沿用 `chitchat_streak`(上限 5),第 4 轮起做**业务引导**(现有行为不变);
- **不得**因闲聊而建单(现行为即是,只做文档固化)。
### 6.2 免责声明分档(**门禁口径变更,已获甲方批准**)
| 答复类型 | 话术 | 例 |
|---|---|---|
| 业务事实(`E2*` / `E3` / `E4` / `E6`) | **完整**投资免责话术 | 产品费率、条款解释、行情数据 |
| 非业务(闲聊 / 联系方式 / 引导登录 / 澄清) | **轻量**提示 | 寒暄、`400-889-8899`、"请问您想了解哪只产品" |
**理由**:现门禁 `F5` 要求"面向客户输出 100% 附固定话术",于是 `讲个笑话` 后面跟投资风险提示;**分档不减少业务答复的披露**,只是**不再给非业务答复挂不相关的话术**。
**实施约束**:分档判据必须是**出口码**(`CoreResult` 携带出口),**不得**用"文本里有没有数字"这类间接判据(否则 `E5b` 空答会掉档)。
---
## 7. 安全不变量增量
现有 `INV-1` — `INV-5` 全部保留(`D3.6` §4.1)。本件新增两条:
| 编号 | 不变量 | 判据 | 反例(本件引入它的原因) |
|---|---|---|---|
| **`INV-6`** | **行情数字必须来自受控数据源** | 答复中每个净值/涨跌数字可解析到 `fin_nav_history` 行或行情工具返回 | §2.3 的静态快照 + §2.6 的虚构数字泄漏 |
| **`INV-7`** | **数据边界必须自陈** | 无数据源的产品被问到时,答复必须显式说明"无公开净值序列" | §2.4 示例产品无净值;若硬答即编造行情 |
---
## 8. 决策登记(`W26` 会话 · 已按建议批准)
| 编号 | 事项 | 裁定 | 落地位置 |
|---|---|---|---|
| `DEC-W27-1` | 是否新增 `L0` 表层判定层(`E0`) | **做** | §3.1;`customer_service.py` 新增 `_route_surface()` |
| `DEC-W27-2` | 意图分类降级为"增益 + 兜底"、改能力路由 | **做** | §3.2 |
| `DEC-W27-3` | `FR-CS-008` 改文档 vs 补实现跨集合回退 | **改文档**(不实现降阈值) | §3.5;`D2.2` / `D3.1` / `D2.4` |
| `DEC-W27-4` | 阈值是否继续作主判据 | **迁到实体锚点 + 结构**;阈值只作安全下限 | §3.3 / §3.4 |
| `DEC-W27-5` | 金标是否从 46 扩到 ≥ 80 | **扩**(补走势 / 闲聊 / 行情) | §9.2;`D3.7` / `D2.9` |
| `DEC-W27-6` | 是否新增 `E6` 行情出口 | **做** | §4 |
| `DEC-W27-7` | 行情数字是否可对客输出 | **可**:限来源 + 带日期 + 不解读者为预期;禁止跨产品比高低 | §4.3 |
| `DEC-W27-8` | 示例产品问走势怎么答 | **如实说无公开净值序列** + 给手册可答项 | §4.4 |
| `DEC-W27-9` | `最大回撤` / `夏普比率` 是否属可放行槽位 | **放行**(风险指标,非收益承诺),但**必须带口径标注**(如"近一年,虚构") | §5 |
| `DEC-W27-10` | 免责声明是否分两档(门禁 `F5` 口径变更) | **批准分两档** | §6.2 |
| `DEC-W27-11` | 访客档是否开放 `E6` | **开放**(行情是公开事实,不需要画像) | §4 |
| `DEC-W27-12` | 是否先立专册再动代码 | **是**(本件即专册) | 本文件 |
---
## 9. 实施与验收
### 9.1 实施顺序(按"收益 / 风险比"排)
| 序 | 事项 | 依赖 | 回归口径 |
|---|---|---|---|
| 1 | `L0-a` 闲聊确定性 + 出口标记 | 无 | 9 条闲聊实测全走闲聊出口;`M-1` 不动 |
| 2 | 收益过滤**槽位白名单** | 无 | 旧黑名单用例全过;新增"近一年表现 4.12%"必须被拦 |
| 3 | 实体锚点闸门(产品名 / 代码 / 条款号 / 字段名) | 无 | `M-1` / `M-4` 不得下降 |
| 4 | `E6` 行情出口(库内净值序列) | 工具白名单 | 新增行情用例全过;无数据源产品如实答复 |
| 5 | 免责声明分档 | 出口码 | 业务答复仍 100% 带完整话术 |
| 6 | 阈值分层标定 + `threshold_calibration.json` 新版 | 金标扩容 | 输出可分性结论 |
### 9.2 金标扩容(`D3.7` / `D2.9`)
新增用例组(建议 **≥ 12 条**):行情走势 `Q-01`—`Q-05`(场内代码 / 产品名 / 无数据源示例产品 / 区间词 / 无实体)、闲聊寒暄 `C-10`—`C-13`(招呼 / 致谢 / 关于助手 / 含业务词不判闲聊)、判据迁移 `A-11`—`A-13`(有锚点不一致必须判没找到)。
### 9.3 验收(DoD)
1. `M-1` / `M-4` **不低于**改造前(46/46);四项零容忍**保持 0**;
2. `E6` 类答复中**每个数字**可解析到 `fin_nav_history` 或行情工具(`INV-6`);
3. 无数据源产品被问到 → 答复含"无公开净值序列"(`INV-7`);
4. 闲聊答复**不出现**完整投资免责话术;
5. 收益数值泄漏用例(§2.6)**被拦**;
6. 全量 `pytest` 通过(基线 1997 passed / 3 skipped),`ruff` 零新增。
---
## 10. 文档对齐清单(本件生效后必须同步)
| 文档 | 改动 |
|---|---|
| `D2.2` `FR-CS-008` | 删除"跨集合回退(阈值 0.65)",改为"档位内部分作答 + 引导";硬约束补"不跨集合、不改判据" |
| `D3.1` `FR-CS-008` | 同上(完整版口径一致) |
| `D2.4` / `D3.2` | 补"阈值分层标定 + 可分性结论";说明第 4 集合不进默认检索面的实测依据(已有,需与之对齐新表述) |
| `D3.6` §3.4 | `E5` 口径与新表述对齐;出口表补 `E6` 与 `L0` |
| `D3.7` / `D2.9` | 补行情 / 闲聊 / 判据迁移用例 |
| `D2.1` | 新增 `W27` 执行条目 |
| `D1.1` | 登记本件;计数与「合计」行同步 |
---
## 11. 诚实未做项(本轮声明)
1. **未做全量文件普查**:`D1.1` 的历史计数偏移(`§0` 与 `§4.0` 长期差 1)**沿用不追改**;
2. **未引入跨集合分数归一/重排**:`E6` 走独立数据源,不回退到第 4 集合;
3. **场外真实基金无行情源**:`fin_product` 只有场内 20 只,示例产品无法给走势(属数据边界,非实现缺陷);
4. **`E6` 区间口径**:按**净值日个数**(5/20/60/120)而非自然日,文档与答复统一写明;
5. **阈值可分性**:§3.4 给出的方法需在金标扩容后**重跑一次**才能给出最终结论 —— 金标已扩容至 **55 条**(见 §12),但**阈值仍未重标**,故本条**依然未做**;
6. **实体锚点闸门只落到 `E6`**:§3.3 的「判据由阈值迁到实体锚点」本轮只实现了行情侧(`_trend_entity` 三级取实体),**知识出口(`E3`/`E4`)侧的锚点闸门未做** —— 这也是金标扩容只加 9 条(`Q-01`—`Q-05` / `C-10`—`C-13`)而非 12 条的原因:`A-11`—`A-13` 考的就是这条未实现的判据。
---
## 12. 实施与验收实绩(`W27` · 2026-09-21 同轮完成)
> 📌 本节是**闭环记录**:§1—§11 是设计与依据,本节记录"真的做完了没有、量出来是多少"。证据全部是可复算的落盘文件。
### 12.1 落地清单
| 项 | 落地物 | 状态 |
|---|---|---|
| `L0` 表层判定层 | `_route_surface()`(`L0-a` 闲聊 / `L0-b` 行情 / `L0-e` 无信息量)+ `app/core/exit_codes.py` 出口码注册表 | ✅ |
| 出口 `E6` 行情 | `app/service/fund_trend_service.py`(`summarize_trend()` 纯函数 + 工具)+ `query_fund_trend` 注册(`fund:quote:read`,含 `visitor`) | ✅ |
| 收益过滤升级 | `drop_yield_claims()` 重写为**入口闸 × 槽位白名单**、fail-closed | ✅ |
| 免责声明分档 | `governance.review_output()` 按 `exit_code` 分档;模板 `TPL_DISCLAIMER_LIGHT` 已入库 | ✅ |
| 访客权限面 | `VISITOR_PERMISSIONS` 补 `fund:quote:read`;`query_fund_quote` 的 `allowed_roles` **不含 visitor**(逆向守卫单测) | ✅ |
| 发布配置 | 版本 **`244`**(`cs-tools-75813de45421`)已激活;`customer_service:faq` 派生继承 + 追加 | ✅ |
### 12.2 验收(`§9.3` DoD 逐条对账)
| DoD | 结论 | 证据 |
|---|---|---|
| 1. `M-1`/`M-4` 不低于改造前(46/46);四项零容忍保持 0 | ✅ **`M-1 55/55`、`M-4 55/55`**;`M-7`/`M-8`/`M-9`/`M-10` **全 0**;46 条可比基线**逐项不变** | `_eval_harness\score_w27d_55.json` / `score_w27d_46.json` |
| 2. `E6` 类答复中每个数字可解析到 `fin_nav_history` 或行情工具(`INV-6`) | ✅ `M-9` = 0(评分器取证面已含**工具原始载荷** + 数值化比对) | `D3.7` §4 / §6.4 |
| 3. 无数据源产品被问到 → 答复含「无公开净值序列」(`INV-7`) | ✅ `Q-03` / `Q-04` 实测命中 | `result_w27c.json` |
| 4. 闲聊答复不出现完整投资免责话术 | ✅ 闲聊 3 条只附**轻型**话术;业务档仍附**完整**话术(单测钉死) | `test_governance_disclaimer.py`(45 条) |
| 5. 收益数值泄漏用例(§2.6)被拦 | ✅ 槽位白名单生效;`A-06` 权重豁免(不再误删整行) | `test_customer_service_rules.py`(194 条) |
| 6. 全量 `pytest` 通过、`ruff` 零新增 | ✅ **2094 passed / 3 skipped**;`ruff` 余 5 条与 `HEAD` 逐条对应(仅行号平移) | 仓库根实测 |
### 12.3 金标从 46 扩到 55
| 组 | 条数 | 说明 |
|---|---|---|
| `Q` 组 · 行情走势(`E6`) | +5(`Q-01`—`Q-05`) | `Q-01`/`Q-03` 走**访客档**(`DEC-W27-11`);`Q-05` 是**反向守卫**(定义题不得被行情出口抢走) |
| `C` 组 · 闲聊收口(`L0-a`) | +4(`C-10`—`C-13`) | `C-13` 是**反向守卫**(含产品名 ⇒ 永不判闲聊) |
逐条答复原文见 `D2.9` §2.11;判据表见 `D3.7` §2 `Q` 组与 `C` 组补注。
### 12.4 实施中发现并修掉的真缺陷
1. 🔴 **`risk_level` 类型假设错误**:`fund_trend_service` 原本写 `int(product.risk_level)`,而 `fin_product.risk_level` 存的是字符串 `'R4'` ⇒ 冒烟当场抛 `ValueError`。改为与 `public_product_service` 同口径的 `str(...)`,并加回归测试钉住;
2. 🔴 **发布被代码上限挡住(422)**:`admin_service` 的校验是**子集**判断而非数量上限(见 §8 决策 `W27` 条 8),`query_fund_trend` 漏进 `definition.allowed_tools` ⇒ 配置发布直接被拒。已补入并加单测钉子;
3. 🟡 **评分器取证面偏窄**:`M-9` 只看 Milvus 命中,会把行情答复里的 26 个净值数字整片误判成「无出处数字」(**评分器口径**问题,非产品缺陷)。已补「工具原始载荷」取证面 + 数值化比对;
4. 🟡 **探针缺权限点**:进程内探针的合成上下文漏了 `fund:quote:read`,会让 `E6` fail-closed(表现成「行情功能坏了」)。已补,并在注释里写明理由。
### 12.5 本轮诚实未做项(与 §11 呼应)
1. 知识出口(`E3`/`E4`)侧的**实体锚点闸门**未做 —— 故 `A-11`—`A-13` 未成集;
2. **阈值未重标**(§3.4 的方法只在文档里);
3. `M-3` 分母未改(只明示口径为「考检索的 C 组条目」);
4. `D1.1` §0 与 §4.0 的历史计数偏移(差 1)仍沿用不追改。