设计与依据:新增 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)仍沿用
101 lines
5.2 KiB
Python
101 lines
5.2 KiB
Python
"""客服 Agent 的**出口码注册表**(`W27` 新增,`D3.9` §3.1 / §6.2)。
|
||
|
||
## 为什么要有这个模块
|
||
|
||
`D3.7` §3 记着一条老口径:「旧实现**没有** `E1`—`E5` 出口码」,评测探针只能靠"包装终止型
|
||
方法"给每条打标。那套包装法有两个代价:
|
||
|
||
1. **它测的是探针的包装,不是产品声明** —— 出口改名/换位置,标就变了,而 `D3.7` 的
|
||
验收结论是拿这些标算出来的;
|
||
2. 有些**行为正确但没出口方法**的分支(例如 `E8` 风险等级代办拦截)会被记成
|
||
「没有任何出口」,于是**做对的安全行为被判未达标**(`D3.9` §2 的度量失真)。
|
||
|
||
因此 `W27` 起改为:`CoreResult.exit_code` 由**出口自己声明**,读侧不反解文本。
|
||
第一个(也是目前唯一)消费方是门禁 `F5` 的**免责声明分档**(`DEC-W27-10`):
|
||
业务出口附完整投资免责话术,非业务出口只附轻量提示。
|
||
|
||
## 与 `D3.6` / `D3.7` 的编号关系
|
||
|
||
本模块的编号**直接沿用** `D3.6` §3 的出口码(`E1` 澄清 / `E2a`—`E2e` / `E3` / `E4` /
|
||
`E5a`—`E5c`),并补三个 `D3.6` 未给码的出口:
|
||
|
||
- `E0`:`L0` 表层判定层本身(`D3.9` §3.1)。它是**路由层**不是终态出口,因此
|
||
**不写入 `CoreResult.exit_code`**,只作为日志/审计的分组名;
|
||
- `E6`:行情出口(`D3.9` §4);
|
||
- `E8`:风险等级代办拦截(`D3.7` 的 `E-08` 红线 1 ④);
|
||
- 安全出口沿用 `SafetyRoute.priority`(`P0` / `P1` / `P2` / `COMPLIANCE` /
|
||
`PROMPT_INJECTION` / `ADVICE_BOUNDARY`),`D3.7` §3 已按这个口径给安全出口打标;
|
||
- `LOGIN` 沿用 `D3.7` 金标里已经在用的写法(`H-03` 的期望出口并列里就有它);
|
||
- `CHAT` / `CONTACT` 是本轮新增:闲聊与"问渠道联系方式"没有历史编号,但
|
||
`D3.9` §6.2 明确把它们列入**非业务档**,所以必须有一个可声明的码。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
# ---- `L0` 表层判定层(路由分组名,不写入 `CoreResult.exit_code`) ----
|
||
EXIT_SURFACE = "E0"
|
||
|
||
# ---- 出口 `E1` / `E2` 族:澄清与计算型 ----
|
||
#: `E1`:澄清。**非业务档**(`D3.9` §6.2)—— 「请问您想了解哪一只」不该挂投资风险提示。
|
||
EXIT_CLARIFY = "E1"
|
||
#: `E2a`:类别费率试算(给算法 + 区间,不给单一结论金额)。
|
||
EXIT_CALC_CATEGORY_FEE = "E2a"
|
||
#: `E2b`:单只产品赎回费递进。
|
||
EXIT_CALC_REDEMPTION = "E2b"
|
||
#: `E2c`:`C—R` 通用匹配规则(矩阵一格)。
|
||
EXIT_SUITABILITY_RULE = "E2c"
|
||
#: `E2d`:适当性裁决(产品风险等级 × 客户档案等级)。
|
||
EXIT_SUITABILITY_VERDICT = "E2d"
|
||
#: `E2c-my`(`W21` 新增):**以本人等级为对象**的可购买范围清单。
|
||
#: 与 `E2c` 的分工是"问句里有没有自带参数":`E2c` 收 `C1 能买 R3 吗` 这类自带 C/R
|
||
#: 的规则题,本码收 `我能买什么等级的产品` 这类**只问范围**、等级从画像里取的问法。
|
||
EXIT_ELIGIBLE = "E2c-my"
|
||
#: `E2e`:本人画像 / 客户分层作答(`query_customer_profile`)。
|
||
EXIT_PROFILE = "E2e"
|
||
|
||
# ---- 出口 `E3` / `E4`:知识直返与证据约束生成 ----
|
||
EXIT_KNOWLEDGE_DIRECT = "E3"
|
||
EXIT_EVIDENCE = "E4"
|
||
|
||
# ---- 出口 `E5` 族:分级回退 ----
|
||
#: `E5a`:知识侧的澄清(候选来自当前可见档位)。**非业务档**。
|
||
EXIT_CLARIFY_KB = "E5a"
|
||
#: `E5b`:部分答 + 引导。**业务档**—— 即使正文为空,客户拿到的也是"业务上答不上来",
|
||
#: 该给的投资风险提示一个字都不能少(这正是分档判据必须用出口码的原因)。
|
||
EXIT_PARTIAL = "E5b"
|
||
#: `E5c`:转人工。**业务档**(安全与责任披露最不能省的一档)。
|
||
EXIT_TRANSFER = "E5c"
|
||
|
||
# ---- 出口 `E6` / `E8` ----
|
||
#: `E6`:行情出口(净值序列 / 区间涨跌)。数字来自受控数据源,**业务档**。
|
||
EXIT_QUOTE = "E6"
|
||
#: `E8`:风险等级代办拦截(`D3.7` `E-08` 红线 1 ④)。业务档。
|
||
EXIT_RISK_LEVEL_GUARD = "E8"
|
||
|
||
# ---- 无编号出口:沿用既有语义名 ----
|
||
#: 闲聊。非业务档。
|
||
EXIT_CHITCHAT = "CHAT"
|
||
#: 引导登录(访客请求超出公开服务范围)。非业务档;`D3.7` 金标已用 `LOGIN` 这个名字。
|
||
EXIT_LOGIN = "LOGIN"
|
||
#: 问渠道联系方式(公开信息,不是提交投诉)。非业务档 —— 「电话是多少」后面跟一句
|
||
#: 投资风险提示属于挂错话术(`D3.9` §6.2 的例证之一)。
|
||
EXIT_CONTACT = "CONTACT"
|
||
|
||
#: **非业务档**出口集合:门禁 `F5` 只对这些出口追加**轻量提示**,其余一律追加完整
|
||
#: 投资免责话术。集合取"白名单式"是刻意的 —— 新增出口若忘记登记,默认落到**业务档**
|
||
#: (多一句披露),失败方向与 `INV-5`(收敛)一致。
|
||
NON_BUSINESS_EXIT_CODES = frozenset({
|
||
EXIT_CLARIFY,
|
||
EXIT_CLARIFY_KB,
|
||
EXIT_CHITCHAT,
|
||
EXIT_LOGIN,
|
||
EXIT_CONTACT,
|
||
})
|
||
|
||
#: 安全出口的优先级标签(与 `app/core/customer_service_rules.py` 的
|
||
#: `SafetyRoute.priority` 同源)。列在这里是为了让"哪些 priority 是安全出口"可被
|
||
#: 静态检查与测试引用,而不是散落在注释里。
|
||
SAFETY_PRIORITIES = frozenset({
|
||
"P0", "P1", "P2", "COMPLIANCE", "PROMPT_INJECTION", "ADVICE_BOUNDARY",
|
||
})
|