复核时发现四处「交付件里的事实错误」,逐条修: - D2.4 §AC-04/AC-05 段:正文写「本仓库 _chunks.jsonl 当前 617 块全部为 public」, 与本文档自己的 v1.6/v1.8 版本行(public 730 / registered 25)自相矛盾。 改为标「设计当时」+ 补 v1.8 现状(755 块),并写明 AC-04/AC-05 现已可证伪、 不再需要「造一条测试块」(双向验证已于 2026-09-18 实测通过)。 - D2.5 演示脚本:「族判定(family_id 628 块全覆盖)」→ 755 块(W24 复测)。 这是答辩当天要照着念的稿子,块数必须与实库一致。 - D4.8 §9.2:把「新集合条数 basic 105 / product 398 / faq 300 / policy 576」 当成真实条数引用 —— 那组数字是 upsert 留下的墓碑行被计入 get_collection_stats().row_count 的结果(同 D2.1 v6.9:298/576/382 = 存活行 149/288/191 的两倍)。已补口径更正段,写明本轮复测的存活行数 251/154/288/62 = 755,并定下「引用块数一律用 _chunks.jsonl,不用 row_count」。 - D2.6 答辩报告:补一条 2026-09-21 W24 状态更新 —— ① docs/43 场内基金手册已入库 (20 只场内基金,702 → 755 块),「问在库产品却答另一只」的根因已根除; ② 出口经 E2c-my 细分后共六个(E1/E2/E2c-my/E3/E4/E5),转人工只在 E5c。 已复核:Milvus 四集合存活行数与 knowledge/_chunks.jsonl 逐集合一致(288/251/154/62 = 755), 无墓碑行残留;recalls_customer_memory 仍为 False,与 D2.7 记载一致。 D2.4 HTML 结构自检通过(table/tr/td 标签配平)。
41 KiB
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 天区间)的赎回费率…」 |
两个根因(不同,必须分开治):
- 主语反解取不到。
_topic_of只在答复前 4 行的固定形状里取主语(X:…/### 2.1 X/…为 R2)。上一轮是 FAQ 型答复(首行是「问:…」)时反解为空,本出口于是回一句"请告诉我具体的基金名称或代码"—— 而客户刚刚才被告知是那只产品。 - 意图分类把短追问标成
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条目,不在此重复。⚠️ 口径更正(2026-09-21 补注):上面那组「集合条数」
105 / 398 / 300 / 576不是存活行数 —— 它是upsert留下的墓碑行被计入get_collection_stats().row_count的结果(同D2.1v6.9条目:「重灌后脚本打印 298 / 576 / 382 = 存活行 149/288/191 的两倍」)。判据:按doc_id查单条只返回 1 行。本轮(W24)复测的存活行数是product 251 / faq 154 / policy 288 / basic 62 = 755(docs\evidence\knowledge-collections.json),与knowledge\_chunks.jsonl逐集合一致。引用块数时一律用_chunks.jsonl/_chunks_report.txt,不要用row_count。
8. 诚实留痕(别漏)
- 打标口径变更:
W20版把认不出的归「其它」(10 条盲区,逐条读原文发现 9 条其实是正确作答);W22版翻转为"只识别退化形态"。两次分布不可相减,§2.3 已写明。 - C-11 是"未修复"而不是"不需要修"。它已被精确定位到代码行与触发条件,只差一个合规口径的批准(§6 第 1 项)。
- 本轮体检样本是本报告自己构造的(81 条 + 8 组多轮),不是甲方给定的验收集。金标 46 条仍是唯一验收依据,体检集只用于发现缺陷。
- 代码里 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。) - 真机复验依赖本地四依赖(MySQL / Redis / Milvus / API+Worker)。复跑前须确认
/internal/health/ready三依赖全绿,否则E5b会被误读成"缺陷"。 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 诚实留痕(本轮新增,别漏)
D1是"降低概率"不是"消灭概率":只把 ④ 写进 user 模板时,三条问句仍各有约 1/3 会落到E5b;把同义禁令补进 system 红线 ② 后测得 9/9。若某次生成仍写出收益数值,_exit_partial的兜底照旧生效(宁可兜底,不可输出)。I-01的金标期望被修订过(9.3)。这是判据变更,不是"零回归" —— 请知情后使用M-1 46/46这个数字。→ 已于docs/43-场内基金产品手册未入库(702 块里「科创债」0 处)W24执行并验收(docs/43纳入SOURCES,切片件 702 → 755 块,见 §10)。当时W21-D3的闸门只做到止损(不再贴错产品),根本修复需要把它纳入SOURCES并重建集合 —— 属语料变更,未在W21擅自执行,已登记为待决(D1.6§10.4)。E5b展示层仍是"原文直返":实测「买基金要手续费吗」的E5b分支正文里含「专户业绩报酬 | 按合同约定计提 | 超额收益的 10%—20%」,会被hits_zero_tolerance判 True(收益+数字% 陷阱)。这是既有行为(drop_yield_claims只丢带收益指标名的行),治理层不对E5b正文做二次零容忍扫描,因此不会拦单;但展示层净化是否要覆盖这一形态,建议列入下一轮待决。- 改动只落在
客服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 诚实留痕(本轮新增,别漏)
- 本条是"补历史欠账",不是新增需求:
D3.4 B-02(决 12)与D4.1 §B-02早已承诺,从未执行。 - 本轮有 3 条期望修订(10.3),"零回归"不适用。
_exit_partial仍按分数挑块,不接收问句 ⇒ 「问句点名了产品 X,但第 1 名的近邻块更贴题」这一类仍未收口(D1.6§10.6 第 3 条,本轮只统一了候选集口径)。E5b展示层净化仍不覆盖「收益 + 数字%」形态(W21§9.5 第 4 条)—— 本轮未动,继续挂账。- 语料里仍有 2 个零容忍地雷块(
POL-SPM-016/POL-SPM-022-01),是禁令条款,故意保留,不是缺陷。 upsert会让get_collection_stats().row_count翻倍(实测 450 vs 251,doc_id仍唯一)⇒ 只信_chunks_report.txt的块数;改doc_id编号后必须丢集合重建(本轮重编号ETF-009/ETF-010时留过一条陈旧行)。- 改动只落在
客服agent/与开发文档/两份权威副本 + 仓库同名镜像,三处一致(D1.1§权威声明)。