Files
group_fqcd_jr/app/core/exit_codes.py
T
张胜宇 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

101 lines
5.2 KiB
Python
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.
"""客服 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",
})