Files
group_fqcd_jr/docs/evidence/20260919-t2k-c10-source-reference-downgrade.json
T
张胜宇 5d0becb67d 客服 Agent 重构收口:五出口决策链 + 知识库档位隔离 + 前端入参边界(答辩演示版本)
一、客服 Agent 智能增强(正面回应"不智能、动不动就转人工")
- 决策链由 2 个出口扩到 5 个:E1 澄清 / E2 计算型 / E3 知识直返 / E4 证据约束生成 / E5 分级回退
- 转人工从"默认动作"降为最后一档 E5c,只保留 4 类白名单:
  P0 反诈 / P1 账户与个人数据 / P2 写操作与争议 / 用户明确要求人工
- 46 条金标实测(修复前 → 修复后):
  转人工率 43.5% → 10.9%;出口准确率 45.7% → 100%;事实正确率 69.6% → 100%
  禁忌违反 1 → 0;档位越权 / 无出处数字 / 误拒 四项零容忍全 0
- 安全不变量 INV-1~INV-5;零容忍规则未删,改的是挂载点
  (输出侧字面黑名单 → 检索层档位隔离 + 判定层合规词表 + 输出守护)

二、知识库:档位单点化与物理隔离
- 新增 app/core/knowledge_tier.py 作为档位规则唯一落点(G-03),
  knowledge_contracts.py 原定义块改为显式再导出(X as X,非副本)
- 档位过滤由 bool 默认值(fail-open)改为 tiers 必填集合(缺参即 TypeError)
- Milvus 侧四集合按 visibility 分区键物理隔离;双 schema 收敛为一套
- 新增 app/core/actor.py:访客三元组与匿名判定的唯一构造/判定点(G-01/G-01b)
- 新增 app/core/fund_fee_rules.py:费率计算纯函数

三、前端入参边界对齐(本轮 W11 新修,4 处"校验宽于存储")
- message 加 max_length=8000(与浮窗 widget.js 的 maxlength 一致)
- session_id 加 1—64;idempotency_key 上限 128 → 64(对齐列宽 String(64))
- feedback_type 加 max_length=32(对齐列宽 String(32))
- 8 条路径参数补 min_length=1 + max_length=64 + 字符集正则
  ({session_id} / {run_id} / {handover_id})
- 改前超限值会落到 MySQL 才失败(500);改后一律 422 AGENT_INPUT_INVALID + 字段级定位
- 新增 tests/unit/api/test_frontend_boundaries.py(33 例),含"端点表 ↔ OpenAPI 全量对照"

四、投顾模块整体清除(D4.4 / D4.5)
- 删除投顾相关 controller / schema / model / repository / service 及门户页面
- tools/portal_api_check.py 同步作废 AD003/AD005/AD011/A047 四条用例与 advisor_t 登录
  (端点与账号均已不存在,此前稳定报 3 条假红)

五、验证(提交前实测)
- pytest -q:1856 passed / 2 skipped / 0 failed
- ruff check app tools tests:19(= 基线);mypy app:2(= 基线)
- 前端接口契约体检 portal_api_check.py:38 项,通过 34,失败 0,跳过 4
- 全链路冒烟 e2e_smoke_test.py --read-only:31/31
- HTTP 全链路探针 http_probe.py:11/11 succeeded
- 跨文档一致性 _consistency.py:GATE PASS
- 真机边界复验 12 条:12/12 符合预期

六、纪律与文档
- 可改文件白名单 A-09(docs/46)与底座会签申请单 A-10(docs/47,组 1—组 4 全部受理)
- 零 DDL:未新增/修改任何表结构,89 张业务表与基线一致
- 证据留痕:docs/evidence/**(含 46 条金标 score、快照、清除与重建记录)
- 未提交(刻意排除,见提交说明):仓库内 客服agent/ 与 开发文档/ 是 2026-09-16 前的
  过期副本(Todolist 440 行 vs 权威 D2.1 1167 行),权威正本在仓库外;
  _chunks_report.txt 是 tools/build_knowledge_chunks.py 生成的本地产物
2026-09-20 14:33:30 +08:00

166 lines
8.5 KiB
JSON
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.
{
"artifact": "t2k-c10-source-reference-downgrade",
"generated_at": "2026-09-19T01:41:43.772361+00:00",
"date_local": "2026-09-19",
"purpose": "执行 C-10「知识出口来源引用」(关键路径 C-01 → C-03 → C-09 → C-06 → C-07 → C-10 的收口步);裁定取「乙 · 本期降级」并加双向护栏",
"scope": [
"app/service/agent/implementations/customer_service.py(业务层:_references 降级为死代码并写明依据)",
"tests/unit/service/test_customer_service_agent.py(护栏 A:AST 守无调用点)",
"tests/unit/service/test_agent_governance.py(护栏 B:knowledge 来源必被拒)",
"开发文档对应的 D2.2 / D2.4(口径回写,另见 D2.1 v6.6 / D1.6 §4.19)"
],
"decision": {
"chosen": "乙 · 本期降级(不向客户展示来源引用)",
"rejected": "甲 · 底座方先登记知识来源并放行(需会签)",
"why_b": "甲依赖底座方改造 ToolExecutor 与治理层放行 knowledge 来源,属不可控外部依赖;本模块今天要交付,不能把交付卡在别人会签上。乙的代价只是「不展示出处」,可追溯性并未丢失(见下)。",
"why_not_silent": "降级不是静默丢弃:D2.2 的 FR-CS-010 与验收表、D2.4 的 §5.7 与 G4/AC-06 都写明了「本期降级」及其技术原因与启用前提,避免下一轮误当成已完成功能。",
"reversibility": "完全可逆:代码本体(_references)保留在位,护栏只是禁止调用;底座方放行后删掉护栏断言即可接通。"
},
"technical_reason": {
"gate": "app/service/agent/governance.py:314-316(review_output 的来源白名单只认 memory / tool)",
"failure_mode": "knowledge 来源会被判为「引用未来自本次已授权召回结果」,抛出 ForbiddenAgentError ⇒ **整个 run 失败**,不是降级、也不是少一个字段",
"red_line": "S-8",
"traceability_instead": "可追溯性由审计承接:agent.tool_executed 的工具调用记录含命中 doc_id 与分数;消息表亦留痕"
},
"implementation": {
"docstring": "_references() 的 docstring 重写为「保留待用的死代码 —— 本期(MVP)不向客户展示来源引用」,写明 governance.py:314-316、S-8、审计承接、启用前提须会签",
"helper_state": "函数体原样保留(source_type=\"knowledge\" 的 SourceReference 组装逻辑未改)",
"no_call_sites": "改用其他出口返回正文;零调用点"
},
"guardrails": {
"A_ast_no_call": {
"file": "tests/unit/service/test_customer_service_agent.py",
"test": "test_knowledge_exit_never_calls_the_disabled_reference_helper",
"method": "ast.walk 找 ast.Call 且 func.attr == \"_references\",断言 calls == []",
"why_ast": "该名字会合法出现在注释与文档字符串里,纯文本搜索会把「提一句」误判成「调用」",
"second_assert": "同时断言 _references 函数定义仍在位(清理时不得被连带删除)"
},
"B_governance_rejects": {
"file": "tests/unit/service/test_agent_governance.py",
"test": "test_knowledge_reference_is_rejected_so_it_must_stay_disabled",
"method": "以 source_type=\"knowledge\" 的 SourceReference 调 review_output,断言抛 ForbiddenAgentError",
"why": "把「本期为何必须降级」的技术原因钉成可执行断言:若将来底座放行,这个测试会失败并提醒同步移除护栏 A"
}
},
"html_downgrade_points": [
{
"doc": "D2.2",
"locator": "FR-CS-010 行",
"change": "追加 ⏸ 2026-09-19 本期降级说明 + <span class=\"pill p2\">⏸ 本期降级</span>"
},
{
"doc": "D2.2",
"locator": "US-CS-01 行",
"change": "期望值补「⏸ 本期不展示来源引用:C-10 乙」"
},
{
"doc": "D2.2",
"locator": "验收表 · 一 · 功能正确性",
"change": "「每条须带来源引用」→「⏸ 本期豁免(改由审计承接)」"
},
{
"doc": "D2.4",
"locator": "§5.7 末尾",
"change": "追加降级段:governance.py:314-316、S-8、_references() 死代码 + 护栏、启用前提须会签"
},
{
"doc": "D2.4",
"locator": "G4 行",
"change": "「可追溯」补「⏸ 本期:来源引用降级,可追溯性由审计承接」"
},
{
"doc": "D2.4",
"locator": "§5.7 三条要求 ①",
"change": "「知识类回答必须附来源」补「⏸ 本期降级,见下段」(本轮新发现的口径残留,与降级冲突)"
},
{
"doc": "D2.4",
"locator": "AC-06 行",
"change": "判据补「⏸ 本期豁免:C-10 乙不展示来源引用,改由审计承接」(同上,本轮新发现)"
}
],
"eol_check": {
"D2.2": "CRLF(本轮编辑后复核)",
"D2.4": "LF —— 编辑后实测 bytes=147917, bareLF=1758, CRLF=0,与改前同为纯 LF,EOL 未被改写"
},
"gates": {
"targeted_pytest": {
"files": 4,
"passed": 164,
"failed": 0,
"command": "pytest -q tests/unit/service/test_customer_service_agent.py tests/unit/service/test_agent_governance.py tests/unit/core/test_customer_service_rules.py tests/unit/core/test_compliance_context.py"
},
"ruff": {
"scope": "app tests tools",
"errors": 22,
"note": "等于基线,无新增"
},
"mypy": {
"scope": "app",
"errors": 3,
"note": "等于基线(agent_persistence_service.py:82 / run_query_service.py:79 / promotion_renderer.py:84,均为既有问题)"
}
},
"not_done_in_this_step": [],
"open_questions": {
"C-10_enablement": "若后续要展示出处,需底座方:① ToolExecutor 把工具返回的知识 doc_id 登记为本次可引用来源;② 治理层放行 knowledge 来源。两步都要会签。"
},
"closure_after": {
"document_writeback": "已完成:D2.1 升 v6.6(C-10 行标 [x] + 修订要点)、D1.6 新增 §4.19(第十五轮会话记录)",
"full_gate": "已完成,见 full_gate"
},
"full_gate": {
"pytest": {
"command": "pytest -q -p no:cacheprovider",
"result": "2 failed / 1579 passed / 2 skipped",
"note": "2 failed = T0 基线同两项(profile_snapshot_current_invariant / worker_runtime[False]),不计入本轮回归;passed 1577 → 1579,增量 = 本步新增的 2 条护栏"
},
"ruff": {
"command": "ruff check app tests tools",
"errors": 22,
"note": "= 基线"
},
"mypy": {
"command": "mypy app",
"errors": 3,
"note": "= 基线"
},
"authoritative_docs": {
"command": "python tools/check_authoritative_docs.py",
"result": "checked 50 documents, no number collision"
}
},
"environment_finding": {
"title": "残留 Worker 抢消费 outbox 事件 ⇒ 集成测试偶发失败(环境级,非代码回归)",
"first_symptom": "全量首跑多出第 3 个失败:tests/integration/test_memory_extraction.py::test_memory_is_not_recallable_across_customers",
"diagnosis_steps": [
"① 单文件重跑两次 → 3/3 通过 ⇒ 不稳定,不是确定性缺陷",
"② 按同一链路脚本复现 seed → enqueue → consume_once → 返回 True ⇒ 消费机制本身正常",
"③ 无人消费检测:插入一条 pending 事件后只观察、不消费 → 1 秒内被外部进程改成 published ⇒ 存在外部消费者"
],
"root_cause": "2026-09-18 17:52 启动的残留服务一直存活,其中 python -m app.worker 会抢消费 outbox 事件,与测试自身的 consume_once(aggregate_id=...) 竞争,先到先得 ⇒ 测试偶发拿不到事件而返回 False",
"processes": [
{
"pid": 27204,
"child": 8740,
"cmd": "uvicorn app.main:app --host 127.0.0.1 --port 8000"
},
{
"pid": 27084,
"child": 2204,
"cmd": "python -m app.worker"
}
],
"action": "经用户批准,已 Stop-Process 停掉 4 个 PID;停止后全量 pytest 回到 2 failed 基线",
"reusable_lessons": [
"与 A-02「门禁基线前先停 Worker」完全吻合 —— 该前置不是形式主义,不停就会污染集成测试",
"印证 S-7:知识集合重建后这两个服务早已过期",
"遇到集成测试失败,先做「无人消费检测」排除外部消费者,再怀疑代码"
]
},
"demo_precondition": {
"issue": "API 与 Worker 现已停止(8000 端口无监听)",
"must_do": "演示前按 S-7 重启 API + Worker,否则前端问答直接连不上"
}
}