Files
group_fqcd_jr/开发文档/D3.4-客服Agent重构Todolist.md
张胜宇 bc61d5c579 docs: 入库权威文档目录(客服agent/ 24 份 + 开发文档/ 50 份,替换旧命名的过期副本)
## 为什么做这一步

权威文档 74 份此前**只在本机**,评审者 clone 分支后看不到任何设计文档;而仓库里那两份同名目录
是 **2026-09-16 之前的过期副本,连文件名都是旧的**(无体系编号)。本次按「**权威覆盖过期**」入库。

## 入库内容

| 目录 | 文件数 | 体积 | 说明 |
|---|---|---|---|
| `客服agent/` | 24 | 0.77 MB | `D2.1`~`D2.6` 对外交付四件套 + 演示脚本/答辩报告 + `_build` 构建工具 |
| `开发文档/` | 50 | 2.16 MB | `D1.x` 索引与决策、`D3.x` 方案、`D4.x` 清除与重构留痕、`D5.x` 业务流程、`D6.x` 业务事实基座、`D7.x` 交付物、`D8.x` 规范 |

**旧的过期副本整体移除**(`客服Agent执行Todolist.md` → `D2.1-客服Agent执行Todolist.md` 之类
的改名 + 新增 `D2.5`/`D2.6`),入库后目录内容与权威副本**逐文件一致(零差异,已复核)**。

## 入库前的安全扫描(必须留痕)

- 扫描规则:`sk-` 类密钥 / `Bearer` 长串 / `password=`、`api_key=` 赋值 / 会话中出现过的两把明文 key 片段。
- 结论:**真实密钥只出现在 `.env`**(已被 `.gitignore` 命中,未入库);`.env.example` 与
  `config/risk.env.example` 只有**空占位**。
- 文档内唯一命中是 `D3.1` 里一处**截断的示例 JWT**(`Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...`),
  末尾带省略号,是接口文档的示意值,**不是可用凭据**。
2026-09-20 15:03:15 +08:00

94 KiB
Raw Permalink Blame History

客服 Agent 重构 Todolist(执行看板 · v5.1)

体系编号:D3.4 · 域:三、现行权威·完整版与专项 · 编号体系见 D1.1 §4.0

文档编号:CS-REFACTOR-2026-005 日期:2026-09-16 性质:执行看板 —— 取代 v4.0、v3.0 及 客服Agent模块重构方案与Todolist.md §9 的任务清单

✅ 2026-09-17 更新:本文件已升为 v5.1,§5 / §6 / §7 / §8 已按报告回写并入(必读)

v5.1 做的事:把 开发文档/D4.1-客服Agent重构报告-2026-09-16.md(CS-REFACTOR-2026-010)的修订结果 并入本文件本身——§5 的 20 项失效落点已按报告改写、§6 的 S-2/S-3 已改并新增 S-10/S-11、 §7 判据已补、§8 风险已重排。执行时以本文件为唯一入口,报告降为「逐项取证与理由」的支撑材料。

v5.1 新增「批次 G · 访客与鉴权」(2 项可执行 + 1 项挂起):把「访客与角色分离」落为可执行任务。 该批次主动扩张了底座接触面(触碰 §1.5 零改动清单中的 app/api/**、app/worker/**、app/service/agent/base.py), 是本次唯一需要单独会签的一组,见 §1.6 与批次 G 说明。

⚠️ 以下为 v5.0 原文告示(背景保留)

客服 Agent 模块已于 2026-09-16 按形态A整体清除(见 CS-PURGE-2026-008)。本文件写于清除之前, 因此 §5 执行看板中有 20 项任务的「落点」指向已不存在的文件,§6 关键路径 / §7 完工判据 / §8 风险登记 也有若干条需要修订。

现行权威 = 开发文档/D4.1-客服Agent重构报告-2026-09-16.md(CS-REFACTOR-2026-010,v5.1 级修订): 它逐项给出 20 项的新落点、新增前置项 P-6~P-9(游客链路恢复 / 发布脚本重建 / 配置重发 / 热线孤岛清除)、 并改写了串行约束 S-2/S-3、新增 S-10。

本文件继续有效的部分:§0 规范依据(N-1N-14)、§1 底座接触面总表、§2 用法、§3 决策落位索引、 §4 需求覆盖矩阵、§6 的 S-1/S-4S-9、§9 判据校正、§10 待决 20 结论、附录 A/B/C。

C-07 已完成(一期路由 + 兼容桩由形态A代删,词表已留痕于 docs/customer-service-routing-legacy-keywords.md)。 配套:客服Agent清除与重构方案(底座层与业务层分层).md(CS-REFACTOR-2026-006,分层方案)、客服Agent模块重构方案与Todolist.md(v2.0,方案与取证)、待决事项清单.md(CS-REFACTOR-2026-004,19 项决策)、D5.1-业务流程-MVP版-最终交付-2026-09-15.md(MVP 业务基线)、客服与投顾模块重构前代码清理建议-2026-09-15.md(死代码清单) 状态:19 项决策已全部定案;底座接触面已逐文件取证完毕(本轮新增 §1) 规模:51 项(49 项可执行 + 2 项挂起:E-07、G-02),分 7 个批次;关键路径 12 步;十二条硬串行约束(S-9 会签门、S-10 ~ S-12) 底座触碰面(v5.1 修订为两组):组 1 = 原 6 个底座文件 / 8 处(§1.1);组 2 = 批次 G 的 4 个文件(§1.6,含 3 个原属「零改动清单」);+1 处条件触发(C-10 选甲);1 项走数据层(零代码,C-05);2 项降级为不改底座(D-07、D-04 备选乙)


v5.0 本轮修订要点(7 条)

# 修订 依据
1 新增 §1《底座接触面总表》与附录B《会签申请单》——把「哪些地方要动底座」逐文件、逐函数、逐行钉死,可直接抄送底座 owner 用户要求「按最优方案开发、不影响整体项目」
2 新增 §0.1《底座规范依据》——每一条约束都落到 docs/** 与 AGENTS.md 的原文位置,不凭印象 用户要求「与 docs 文件夹中的底座使用规范保持一致」
3 D-02 判据校正:从「混合 schema → 整片失败」改为「缺 visibility 的集合被同一表达式打挂 → 被 except 吞成 failures → degraded=True / partial_collection_failure → 客服走兜底」;风险由 中高 → 中 knowledge_search_service.py:138-154 / 202-217 / 269-271 逐行实证
4 D-01 落点收窄:不再包含 app/core/knowledge_contracts.py——档位由 context.roles 推导(tool_executor.py:116 已把 context 传给 handler),KnowledgeSearchInput 保持 extra="forbid" 不变 tool_executor.py:116、docs/18 §4.2
5 C-01 语义纠错:不再把 ZERO_TOLERANCE_WORDS 改名为 OUTPUT_BAN_WORDS——它是输入侧零容忍集(route_message() 唯一消费方),名字准确;改名会破坏 test_customer_service_rules.py:87-92 的相等断言且无收益 customer_service_rules.py:160-162、test_customer_service_rules.py:87-92
6 D-07 降级:原落点含 app/infrastructure/milvus_knowledge_writer.py,与 A-09 ④「禁止修改 infrastructure/**」冲突 → 改为门禁侧解决(B-01 要求显式声明档位)+ 文档登记,不改底座 AGENTS.md 规则 7、docs/14 §12
7 B-05 范围收窄:原 DoD「全仓 grep 15936583816 = 0」不成立(docs/** 有 14 处历史与要求记载,其中 docs/演示用/软件需求文档-2026-09-14.md 把它写成要求)→ 范围收窄为 app/ tests/ tools/ knowledge/;docs/** 由 F-05 回写;并补齐 v4.0 遗漏的 3 处测试内联值 全仓 grep 实证

0. 规范依据与全局前提

0.1 底座规范依据(本轮新增)

口径:凡与本文冲突的,以 docs/** 与 AGENTS.md 原文为准。下表逐条给出原文位置与它对本看板的硬约束。

# 规范条目 原文要点 对本看板的约束
N-1 docs/14 §1「你负责什么,底座负责什么」 底座负责:JWT/RBAC/入口与客户范围校验、配置快照与记忆召回、意图分类与低置信处理、模型路由与密钥引用、工具权限/超时/审计和来源引用、适当性校验、合规审查/敏感信息脱敏、运行持久化/Outbox/SSE/恢复 凡落在上述 8 项的改动 = 底座件,须会签。这直接决定了 §1 的 6 个文件
N-2 docs/14 §3 「业务类只实现 handle()。不要覆盖 execute()、鉴权、配置、记忆、意图分类、模型、工具或合规方法。」 D 批次不得在 customer_service.py 内自建检索实现或档位隔离;只能通过 call_tool() 走公共工具
N-3 docs/14 §8 公共只读工具表 search_knowledge(正式名,knowledge:reference:read,customer/advisor/operator/admin);query_knowledge(别名,knowledge:query,visitor/customer,同一 handler) D-04 的落点必须在既有工具 handler 内,不得新增工具、不得改权限码/角色/超时
N-4 docs/14 §12 禁止事项 禁止绕过 AgentFactory/BaseAgent/统一鉴权/记忆/模型/工具/合规/审计/事件流程;禁止直接创建 Session、访问 MySQL/Redis/Milvus/Neo4j Driver;禁止重命名/删除/修改已有表与字段 全批次零 DDL、不新增架构层;数据访问一律经既有 Service
N-5 AGENTS.md 规则 1/3/4 docs/00 为不可变业务基线;禁止重命名/删除已有表;禁止重命名/删除/复用字段、改变类型/可空性/业务含义 E-01 的 priority/reason_code 必须用既有字段(已确认存在,零 DDL)
N-6 AGENTS.md 规则 7 业务 Agent「不得绕过公共鉴权、记忆、模型路由、工具、合规、审计和事件流程」 与 N-2 同源,是 D-04 必须走会签而非业务侧自制的直接依据
N-7 AGENTS.md 环境口径(工具白名单) 工具可用范围 = 代码上限 ∩ 当前 active config_release 的发布白名单;缺发布配置则失败关闭;知识类意图必须同时发布 search_knowledge 与 query_knowledge A-03 快照必须含白名单全文;A-07 必须实测;E-02 必须按 reason 区分日志
N-8 AGENTS.md 环境口径(Milvus) 「检索层已改为运行时探测字段名(app/core/knowledge_schema.py)——不要在任何地方硬编码字段名,那会把另一套环境打挂」 D-01/D-02/D-04 一律用 schema.resolve()/schema.has() 取字段,禁止字面量
N-9 AGENTS.md 环境口径(记忆范围) 「记忆可读范围只有一个判定口径:app/core/memory_scope.py……改召回、改引用校验、改范围守卫,三处必须一起走这个模块」 F-01 必须调用 memory_scope,不得自写一套主体判据
N-10 AGENTS.md 环境口径(RBAC) 权限码定义源是 tools/seed_test_rbac.py(DELETE 重建语义);「没并进它的权限码重建一次就没了」 本期不新增权限码;E-03 明确「无需新增端点/权限码」
N-11 docs/18 §4.2 集合内字段名不得硬编码,用 knowledge_schema.FIELD_CANDIDATES 探测;KnowledgeSearchInput 契约 extra="forbid" D-01 不改 KnowledgeSearchInput;档位不经入参(避免身份进入模型可控的参数面)
N-12 docs/05 §8.4 四个只读工具的接口面与两段式白名单 A-03/A-07 的核对面;C-10 甲若启用须遵守同一登记口径
N-13 docs/14 §11 / knowledge_tool.py 文件头 「Agent 只能提出转人工请求,不能分配/接单/解决/关闭工单」;「source_references 由基座统一附加,业务代码不能伪造来源引用」 E-01 只在持久化层赋 priority;C-10 甲必须由底座方实现,客服侧实现反而违规
N-14 docs/14 §13 统一验收命令 pytest -q -p no:cacheprovider、ruff check app tests tools alembic、mypy app、tools/check_authoritative_docs.py、tools/audit_schema.py A-02 门禁基线的命令集;§6 判据 7/10

分层判据(一句话):「被多个 Agent 使用 / 在 ToolRegistry 注册为公共只读工具 / 承载鉴权·记忆·意图·模型·工具·合规·审计·持久化 8 项中的任一项」→ 底座层,须会签;否则 → 业务层,自主重构。

0.2 五项全局前提(适用于全部任务,不再逐条重复)

# 前提 来源 落地方式
P-1 「底座不可修改」的精确含义 = 本次只允许触碰 §1 列明的 6 个文件 8 处;其余底座件一律不动 决 1 + N-1 由 A-09《可改文件白名单》 落为纪律凭据;白名单外文件一律不动
P-2 知识库与数据库以仓库现状为准(knowledge/ 6 源文件 + _chunks.jsonl 617 块 + alembic/baseline_generated.sql),不再等待外部交付 决 11 B 批次与 D 批次直接对现有对象作业
P-3 测试策略:关键路径真实、其余 Mock——检索层与规则层用真实实现(可离线断言),LLM 生成用 Mock(固定返回),端到端演示走真实调用 决 15 各任务「验证」列均按此执行
P-4 🔴 底座分层:底座层须会签,业务层自主重构——判据见 §0.1。本次仅 6 文件 / 8 处落在底座公共件上(§1),+1 处条件触发(C-10 甲)、1 项走数据层(C-05)、2 项降级不改底座(D-07、D-04 备选乙) CS-REFACTOR-2026-006 §2/§3 + N-1~N-6 业务层与底座只允许通过 4 个接触面交互(工具调用、context、config、持久化),见 §1.3
P-5 🔴 会签门(v5.1 扩为两组):组 1 = §1.1 的底座改动;组 2 = §1.6 的访客与鉴权改动(批次 G)。两组都先提案、后动手——会签未过不得开工(S-9)。未获受理的项按 §1.4 的降级方案执行,不得静默绕过。⚠️ 组 2 的特殊性:它触碰的是原本被声明为「零改动」的文件,属于主动扩张,因此必须单独一个 PR,不得与组 1 混提交 用户「不影响整体项目」+ 组 2 的扩张须显式披露 A-10 产出会签申请单(附录B 即模板),组 1 与组 2 各一份,提交底座 owner

开工纪律:若某项前置未完成,不要绕过——本项目的静默降级几乎都来自「先绕过、以后再补」。底座件的「绕过」形态尤其危险:它不会报错,只会让另一套环境或另一个 Agent 静默失效。


1. 底座接触面总表(v5.0 核心)

1.1 六文件 / 八处(须会签)

# 底座文件 触碰项 改什么(精确到函数/行) 规范依据 为什么是公共缺陷(而非客服私需) 最小化边界 影响面
1 app/service/knowledge_search_service.py D-01 search() L156-163:include_internal: bool = False → tiers: frozenset[str](必填、无默认值);_visibility_filter() L138-154 同步按 tiers 拼表达式;非法值/缺失 → 收敛为 {"public"} N-1(工具权限)、N-8(字段探测)、docs/18 §4.2 search_knowledge/query_knowledge 是公共只读工具(服务 customer/advisor/operator/admin + visitor);布尔默认值 = fail-open,语义上「不传就是全开」 取值域保留 {"public","registered"};不新增字段、不改 KnowledgeHit 形状、不改 KnowledgeSearchInput 呼叫点:knowledge_tool.py:29、tools/seed_knowledge_r1r5_faq.py:97;单测:test_knowledge_schema.py、test_knowledge_keyword_recall.py、test_knowledge_granularity.py
1 同上 D-02 ① _visibility_filter() L138-154 + search() L202-217:表达式按集合逐个拼(filter=expression if schema.has("visibility") else None);② _hit_from_row() L507:visibility=value("visibility") or "public" → 缺字段时不再回落 public(改为空串 + outcome.reason 标记不可判定);③ 补 search() L269-271:degraded=failures>0 保留,但增加「因缺 visibility 而跳过过滤」的独立计数 N-8;docs/18 §4.2 见 §9 校正 1:现码把「有 visibility 的集合」算出的表达式无差别传给全部集合,缺字段的集合被 Milvus 报错 → 被 except 吞成 failure → degraded=True → 客服走「引导人工」;且 L153 注释声称「各自调用处跳过拼接」与实现不符。三集合任一环境异构即触发,与「是否为客服」无关 不改返回形状;degraded/reason 既有取值不删;新增计数放在 reason 之外(不改既有语义) 同上
2 app/service/knowledge_tool.py D-04 L25 删除 del context;由 context.roles 推导档位(visitor → {"public"}、customer → {"public","registered"}、未知 → {"public"})后调 search(..., tiers=...) N-1(工具权限)、N-2、N-6、N-3 档位隔离属工具层职责(N-1 明列)。若退到客服侧自行过滤,投顾/运营将来各自再实现一遍,最先漏的恰是没实现的那一方;且 docs/14 §3 不允许业务类覆盖工具方法 不改工具名/权限码/角色/超时;不改 KnowledgeSearchInput;不新增工具。(tool_executor.py:116 已把 context 传入 handler,零契约变更) bootstrap.py:318-337(注册不变)、customer_service.py:289/455(工具名分支不变)
3 app/core/compliance_context.py C-04 NEGATION_CUES L43-48 补短否定式(多字):不保本、不保收益、不保本保收益、非保本、并非保本、不确保、不担保 N-1(合规审查)、docs/14 §1 review_output 对全部 Agent 生效(governance.py:9,317,322)。现表只有 不承诺/不保证/不作/不能/绝不/从不/且不/并不/未保证/不予保证,不含「不保本/不保收益」→ FAQ-0018/FAQ-0036 的答案会被整条替换成兜底话术,对所有 Agent 同理 不动 ZERO_TOLERANCE_WORDS 的 11 条与 governance.hard_patterns 5 条;CONTEXT_WINDOW/SENTENCE_BOUNDARIES/QUOTE_CHARS 不变;禁止加裸「不」 tests/unit/core/test_compliance_context.py(须新增反例守卫);全部 Agent 的输出治理
4 app/service/agent/governance.py B-05(底座侧) ① 删 L46 CUSTOMER_SERVICE_HOTLINE;② 删 _mask_mobile 内的白名单分支 L332-339 → 内联为 return "[手机号已脱敏]" N-1(合规审查/脱敏)、N-5 它是第 2 份热线副本(第 1 份在 customer_service_rules.CONTACT_PHONE),L44-45 的注释自己写着「改一处必须同步另一处」——这正是「改漏即错」的公共隐患。热线改为 400-826-9518 后不再匹配手机号正则 (?<!\d)1[3-9]\d{9}(?!\d),白名单分支变为不可达死代码 → 删除比改值更干净,且脱敏口径只可能更严 不改正则、不改 FALLBACK_DISCLAIMER、不改 hard_patterns;不改任何函数签名 无直接断言该常量的单测(已全仓确认);test_milvus_profile_projection.py:182 用的是另一套脱敏(记忆投影),与本项无关
5 app/service/agent_run_application_service.py D-06 + F-01 ① D-06:受理时 customer_id 按身份取值——"visitor" in roles → None,否则 user_id(改 L213 与 agent_persistence_service.py:93 的写入口径);② F-01:_load_mysql_prior_context() L98-116 增加主体过滤,判据必须调用 app/core/memory_scope.py(客户:customer_id == user_id;访客:customer_id IS NULL;员工:customer_id IN scope) N-1(客户范围校验)、N-9(记忆范围唯一口径)、N-4 受理链路服务全部 Agent(api/controllers/agent_runs.py:41、app/worker/runtime.py、tools/acceptance_check.py、tools/memory_chain_probe.py)。F-01 现状只按 session_id 取历史(L100-108),跨主体可读;D-06 让访客的匿名标识污染 customer_id 语义 零 DDL(conversation_message.customer_id 本就可空、无外键);ConversationSession.user_id/RequestIdempotency.user_id/AgentRun.user_id 均 NOT NULL → 保持现状;不改函数签名(scope 由 accept() 内取好传入) tests/integration/test_agent_run_acceptance.py、test_run_cancellation_mysql.py、test_worker_runtime_mysql.py;tests/unit/service/test_agent_authorization.py、test_customer_service_chitchat_metadata.py;tests/conftest.py:102
6 app/service/agent_persistence_service.py E-01 complete_run() 工单段 L116-153:① 建单白名单(决 6);② 赋 priority(决 5,HandoverTicket.priority 已存在,String(8),model default P1);③ reason_code 枚举化(L132 现为 transfer_reason or "agent_requested",四套词汇混入同一 VARCHAR(64)) N-1(运行持久化)、N-5(零 DDL)、N-13(Agent 只提出转人工) complete_run 服务全部 Agent(app/worker/runtime.py:920)。「答不上来也建单」是所有 Agent 的公共出口行为;priority 有 CHECK 与索引却无任何代码赋值(队列索引白建) 不改表结构(决 5/6 零 DDL);不改函数签名;⚠️ 白名单必须限定 run.agent_type == "customer_service"——否则会改变风控/投顾等其它 Agent 的既有建单行为(这是「不影响整体项目」的关键约束) tests/unit/service/test_agent_persistence_handover.py(6 处)、test_agent_persistence_audit.py、tests/integration/test_complete_run.py(4 处)

1.2 条件触发(+1 处)

# 底座文件 触碰项 触发条件 说明
7 app/service/tool_executor.py L129-131 C-10(甲) 仅当待决 20 选「甲·实现」且底座方受理时 现状只产出一条 SourceReference(source_type="tool", source_id=f"{trace_id}:{name}")(L129-131),没有 doc 级引用;governance.review_output L305-309 的引用校验只认 memory/tool 来源 → 知识类引用会让整个 run 失败。按 N-13,来源引用是基座职责 → 必须底座方实现,客服侧自行实现反而违规。同批还需在 governance.py 增加 source_type == "knowledge" 的放行分支(与第 4 项同文件)

1.3 业务层与底座的 4 个接触面(唯一合法交互通道)

接触面 形态 本看板的使用方
C-a 工具调用 await self.call_tool(name, args, intent=..., context=context) C-09(输出规则)、D-04(档位经 handler 内 context)
C-b 上下文 只读 request / context / self.config / self.memories E-02、E-04、D-06
C-c 发布配置 config_release 的 agent_tools 白名单(改环境数据,不改代码) A-03、A-07、E-02、F-04
C-d 持久化 complete_run 同事务写 conversation_message + svc_handover_ticket + 审计 E-01、E-04

1.4 数据层项与降级项(不改底座代码)

# 触碰项 类型 做法 为什么这样最优
D-a agent_negative_word 种子(C-05) 数据层(零代码) 新增三类词:收益比较 / 稀缺性 / 费率误导。必须:applicable_agents JSON 数组非空且含 customer_service、match_type ∈ {exact, contains}、status='active'、reviewer_id/reviewed_at 非空 底座已提供「禁用表达」的可配置能力(N-1 归底座、词表内容归业务线);不改代码即零回归风险。⚠️ 三条硬约束:
① governance.resolve() L118-128 对空数组是失败关闭跳过(配置为空 = 规则永不生效),所以 applicable_agents 必须真的填;
② 不得改写既有 11 行的 word_pattern(test_customer_service_rules.py:87-92 断言相等);
③ 按承诺性短语收录而非形容词——用「承诺高收益/保证高收益/较 X 收益高/收益高 N 个 BP/稀缺额度/优先认购权/优先配置权/X 折优惠」,不要只收「高收益」(否则「高收益伴随高风险」这类风险提示会被一并拦下,与决 8「不拦风险形容词」冲突)
D-b agent_reply_template 种子的两条话术(B-05 数据侧) 数据层 tools/seed_compliance_baseline.py:99,101 的 15936583816 改为 400-826-9518 话术内容变更须保持 status='active' + reviewer_id/reviewed_at 非空(governance.resolve() 与 _load_template_text() 同口径),否则回退 FALLBACK_DISCLAIMER
D-c knowledge/ 语料 + _chunks.jsonl(B-01~B-04) 数据层 见批次 B 改内容不改结构;SOURCES 是 tools/build_knowledge_chunks.py 的业务数据
D-1 D-07 降级 不改底座 不碰 app/infrastructure/milvus_knowledge_writer.py(A-09 ④ 禁止修改)。改为 B-01 语料门禁要求「每块 visibility 必须显式声明」+ 文档登记「API 入库路径的默认档位问题本期不修」 原方案改 FIELD_DEFAULTS(现默认 public)会影响全部写入方(knowledge_ingest_service、knowledge_vector_worker、管理面);而 MVP 不新增 registered 内容(决 10)→ 风险远大于收益。明确记为「已知未修」,不是遗漏
D-2 D-04 备选乙 不改底座 仅在会签不受理时启用:customer_service.py 对工具返回的 hits[].visibility 做访客侧后置过滤 + 文档登记 明确标注为降级:违反「检索层硬隔离、不依赖上层自觉」(knowledge_search_service.py 文件头)与 N-2/N-6;仅作过渡,且必须在 F-05 文档里写明

1.5 底座零改动清单(本次明确不碰 —— 用于向底座 owner 交底)

app/service/agent/base.py、app/service/agent/factory.py、app/service/tool_executor.py(除 C-10 甲的 L129-131)、app/core/memory_scope.py、app/core/knowledge_schema.py、app/core/knowledge_contracts.py、app/api/**、app/infrastructure/**、app/model/**、app/worker/**、alembic/**、tools/seed_test_rbac.py、docs/00 基线、check_suitability/query_customer_profile/query_fund_quote 的实现、投顾与风控模块全部文件、app/core/customer_service_rules.py(该文件属客服业务层,仅由客服使用——见 §9 校正 13)。

说明:app/core/knowledge_schema.py 的 FIELD_CANDIDATES/detect_schema/SchemaCache 已是底座能力,本次不需要改——这一条与分层方案 CS-REFACTOR-2026-006 中「字段探测需业务侧补齐」的表述不同,以代码为准(见 §9 校正 1/2)。


1.6 访客与鉴权接触面(组 2,v5.1 新增 · 修订 §1.5)

为什么单列一组:这 4 个文件不在 §1.1 的 6 文件名单内,其中 3 个还明确写在 §1.5「底座零改动清单」里。 把它们单列出来,是为了让「本次主动扩张了底座接触面」这件事可见、可审、可单独回滚,而不是藏在某处顺手改掉。

# 底座文件 触碰项 改什么 规范依据 为什么必须动它 最小化边界
2-1 app/core/security.py L92-97 G-01 访客分支改为调用新增的 actor.anonymous_context()(行为逐字不变,只改「值从哪来」) docs/33 §1.2(单点鉴权)、docs/29 §1(令牌只带 id) 它是访客三元组的第一份定义,不消掉就无法实现「改一处即生效」 不动 jwt.decode 参数、不动 sub 校验、不动 visitor claim 语义
2-2 app/worker/runtime.py L179-184 G-01 同上——第二份副本改为调用同一构造点 同上 两份逐字相同的三元组、无一致性测试 ⇒ 改一处不生效(例如只改权限就会让 run 卡在 queued 且无报错) 不动 restore_context() 签名、不动 resolve_identity() 分支
2-3 app/api/dependencies/auth.py L52-53 G-01b 「跳过身份解析」的条件改为语义化谓词 docs/33 §1.2 现为字面量 "visitor" not in context.roles,身份模型一变就漏 不动鉴权入口结构、不动 401 口径、不新增任何路由
2-4 app/service/agent/base.py L147 G-01b 访客不召回的谓词改为 actor.is_anonymous(context) docs/34 §0 统一判定口径;位置与层级保持不变 ⚠️ 仍留在基类、仍不依赖 Agent 声明位(不变量②)——这是底线,不是可选项

与 §1.5 的关系(重要):§1.5 原文把 app/api/**、app/worker/**、app/service/agent/base.py 列为「本次明确不碰」。 v5.1 明确修订该声明:这三项由批次 G 触碰,依据是 FR-CS-043/FR-CS-044(需求文档 §1.12)。 除这 4 个文件外,§1.5 的其余全部条目继续有效。

⚠️ 额外复核项:docs/33/docs/34 对 security.py 做过逐行实证(L43 / L57-61 / L87-91 / L92 / L53 等)。 本组改动会使这些行号漂移,因此 G-01 完成后必须复核一遍那些逐行结论是否仍成立,并在会签单里说明。


2. 怎么用这份看板

用法 说明
开工顺序 批次 A → B → C → D → E → F;批次内按 ID 顺序,标注「可并行」的可同时开
🔴 会签前置 标 〔需会签〕 的任务先提案、后动手(P-5 / S-9);提案模板见附录B
每项四件套 落点(改哪个文件)|DoD(做完怎么算完成,已含决策定值)|验证(单测 / 脚本 / 手工)|依赖
勾选 每项前的 [ ] 直接改成 [x],可作为交付证据
红标项 🔴 = 客户可见或合规级,不能跳过、不能降级
决策标记 DoD 中的「决 N」指向 §3 的 19 项决策;「**校正 N」指向 §9 的 v5.0 判据校正
底座标记 〔需会签〕= §1.1 的 6 文件 8 处;〔数据层〕= §1.4 的 D-a/D-b;〔降级〕= §1.4 的 D-1/D-2

3. 19 项决策的落位索引(定值已写进各任务 DoD)

决 决策 定值 落入任务
1 「底座不可修改」边界 精确为 §1.1 的 6 文件 8 处(其余底座件不动) A-09(白名单)、P-1
2 热线号码 400-826-9518 B-05(定值)、B-02、B-01
3 服务时间 人工客服每日 7:00–22:00;线下门店 9:00–17:30;VIP 7×24 B-05、B-02
4 品牌域名与邮箱 nanfangwm.com、complaint@nanfangwm.com B-05、B-02、B-01
5 工单 priority 映射 反诈 P0;投诉 / 建议请求 P1;客户主动要求转人工 P2;其他 P2 E-01
6 「答不上来」建单 不建单(仅审计 + 未命中指标) E-01
7 访客主体 不新增 visitor_id,只用 customer_id IS NULL(零 DDL) D-06、F-01
8 输出侧新增禁用边界 只拦收益比较 / 稀缺性 / 费率误导;不拦风险形容词 C-05、C-06
9 输出侧豁免实现 否定线索 + 指标名白名单,两者都做,同句生效 C-04、C-06
10 registered 档位 保留在取值域,内容为空(护栏先有) D-01、E-07
11 资源以哪一版为准 以仓库现状为准 P-2
12 行业术语条目 补:docs/43、docs/45 纳入 SOURCES B-02、B-04
13 证据链执行时点 立即 --dry-run,不依赖代码 F-06
14 适当性留痕落点 conversation_message.tool_calls + interaction_audit(零 DDL) E-04
15 测试策略 关键路径真实、其余 Mock P-3
16 文档回写 重构完成后一次性回写 v2.3 F-05
17 演示账号来源 用仓库现有 seed 脚本生成 F-04
18 24 个失效锚点 修(仅改 href 对齐正文 id) F-07
19 遗留死代码 本期只记录不删;但核对是否与检索实现重复 D-01、F-05

4. 需求覆盖矩阵(「需求是否覆盖完整」的凭据)

4.1 MVP 三条业务线 × 演示九步

MVP 要求 验收口径 覆盖任务 覆盖状态
游客线:公司公开信息正常展示、无投资建议类内容 演示第 1 步 B-01B-05(内容一致)、C-09(推介拦截)、D-01D-04(档位)、F-02 ✅ 完整
客服线:金融基础问答、上下文延续 演示第 2 步 B-04、D-01~D-06、F-01 ✅ 完整
客服线:问「我的持仓」→ 说明无权限 + 引导自助页面 演示第 3 步 E-02(auth_required)、B-03(界面指代)、F-02 ✅ 完整
客服线:发起工单 → 工单生成、投顾侧可见 演示第 5 步 E-01(建单 + priority)、E-03(可见方落文) ✅ 完整
客服不查持仓 / 不代买 三条红线 ③ E-08(红线对照验证) ✅ 完整
投顾 9 步第 ⑥⑦ 步需产品候选 > 0 演示第 6/7 步 F-06(证据链,唯一硬阻断点) ✅ 完整

4.2 需求文档 §4 Phase 1 功能需求

需求 内容 覆盖任务
F1.1 项目框架与目录规范 A-09(白名单证明不改分层)
F1.2 RAG 知识库搭建(解析 / Embedding / 管理接口) 批次 B(语料与灌库)、批次 D(检索与档位)
F1.3 智能客服 Agent(意图 / 检索 / 记忆 / 合规 / 转人工) 批次 C(规则与安全)、批次 E(闭环与出口)
§7.1 交付物 开发计划、每日纪要、演示材料 F-03、F-04、F-05

4.3 三条红线(客服侧落点)

红线 客服侧约束 覆盖任务
① 风险等级唯一来源是问卷测评 对话中不得给出等级结论、不得引导「重做测评以提级」 E-04(只读口径)、E-08(逐条验证)
② 先风险揭示、后客户确认 客服侧不得代客户确认;不匹配场景必须「揭示 → 主动确认 → 留痕」 E-04、E-08
③ 不生成交易指令,交易跳转 对话中不得输出可执行交易要素(代码 / 金额 / 份额 / 确认动作) E-08(route_message P0 已拦代办交易,需逐条验证)

4.4 客服方案 v2.2 需求域 A—G

域 名称 覆盖任务
A 对话接入与意图识别 C-01~C-03、C-09、E-02
B RAG 知识检索 批次 B 全部、D-01~D-05、D-07
C 会话记忆 D-06、F-01
D 合规与安全 C-04、C-05、C-09、E-08
E 转人工与服务闭环 E-01、E-03
F 跨 Agent 协作 E-03(工单可见方)
G 双角色与访客能力(FR-CS-029~042) D-04、D-06、E-02、C-09、F-02

覆盖结论:三条业务线、九步演示中客服承担的 4 步、F1.1—F1.3、三条红线、A—G 七域均有对应任务。


5. 执行看板(51 项 · v5.1 新增批次 G)

批次 A · 冻结与准备(10 项,立即开动,不依赖任何前置)

批次目标:把「当前是什么样」钉死,后续任何「红了」都能区分是我改坏的还是本来就有的。

ID 任务 落点 DoD 验证 风险
[ ] A-01 安全用例集基线复现 docs/客服Agent一期_合规红队与业务评测集_v1.md RT-001~010 逐条记录当前通过/失败,预期 RT-009/010 失败(注入拦截缺失),输出留痕文档 脚本 LOW
[ ] A-02 门禁数字基线 仓库根 按 N-14 跑 5 项并记录原始输出 + 日期 + 解释器路径 + SQLAlchemy 补丁版:ruff check app tests tools alembic、mypy app、pytest tests/unit tests/contract、pytest tests/integration、python tools/audit_schema.py(预期 89 张业务表,这是零 DDL 的对照基线);并记录 P-3 测试策略已落到测试目录的约定。⚠️ 跑验收前先停 Worker(否则抢队列) 脚本 LOW
[ ] A-03 配置与环境快照 config_release / Milvus / embedding 端点 ① active 版本 id + 条数 + customer_service:* 与访客工具的 agent_tools 白名单全文(须同时含 search_knowledge 与 query_knowledge——缺后者访客线全线失败,见 N-7);② 三集合的字段名 + 行数(doc_id/content 与 knowledge_id/snippet 两套命名都要确认);③ embedding 模型名 + 维度(预期 1024) 脚本 LOW
[ ] 🔴 A-04 业务一致性三查基线 全仓 grep / knowledge/_chunks.jsonl / app-shell.js 三份清单:① 品牌名(南方财富 / 南方科技)出现位置与数量;② 含 XXX/400-XXX-XXXX 的块号列表;③ 语料提到的界面名 vs app-shell.js 链接表的差集。另加第四份:15936583816 在 app/ tests/ tools/ knowledge/ 的全部位置(预期:代码 2 处 = customer_service_rules.py:35、governance.py:46;种子 2 处;测试 1 处断言 + 2 处内联占位符)——它是 B-05 的对照基线 脚本 LOW
[ ] A-05 演示前检查清单固化 docs/44-演示流程.md 同款 五项:Docker Desktop 运行 → Milvus 可连 → Worker 在跑 → 行情未过期(15 分钟内)→ .env 的 MILVUS_LOCAL_URI 为空。产物是文档;执行在 F-04 文档 LOW
[ ] A-06 分支与 PR 策略 qyqy_develop → cs-refactor 分支建立;约定「每批次一个 PR 回主干」;底座会签项单独一个 PR(便于底座 owner 只审那一份 diff);门禁口径确认 — LOW
[ ] 🔴 A-07 访客链路可用性实测 访客令牌 → agent-runs → query_knowledge 实跑一次访客请求,确认:① query_knowledge 在发布配置白名单内、不抛 ForbiddenAgentError;② 访客意图白名单生效;③ 记录访客的 context.user_id 形态(agent_run_application_service.py:151 有 int(context.user_id),须确认是数字串,否则 D-06 的前提失效);④ 记录「哪些日志能区分『配置错』与『正常引导登录』」 脚本 + 手工 中
[ ] A-08 前端约束与配合点复核 docs/41-客服Agent前端开发约束_v1.md、widget.js、app-shell.js 产出「需前端配合项清单」:至少核对 ① 访客点转人工的既有行为(已知为非客户跳转登录页);② auth_required 话术是纯文本、是否需要登录引导组件;③ 界面名以 app-shell.js 为准;④ 热线与域名若出现在前端文案里,同步改(B-05 的第七处候选)。结论可以是「0 项」——但必须是核对后的 0 项 文档 + 手工 LOW
[ ] 🔴 A-09 《可改文件白名单》落文(决 1) docs/ 新增一份纪律凭据 四类:① 纯新增(客服模块自有新文件);② 允许修改(客服业务层:implementations/customer_service.py、core/customer_service_rules.py、customer_service_session_memory_service.py、转人工三件套、app/static/portal/common/customer-service-widget/**);③ 提案后由底座方修改——精确为 §1.1 的 6 文件 8 处,每项须写明为什么属公共缺陷而非客服私需(照抄 §1.1 第 6 列);④ 禁止修改(§1.5 全清单 + app/core/customer_service_rules.py 之外的一切 app/core/**)。并附「本次零 DDL」声明与 tools/audit_schema.py 基线截图 文档 LOW
[ ] 🔴 A-10 底座会签申请单落文(分层方案调整 2) docs/ 新增一份会签依据(附录B 即模板) 对 §1.1 的 6 处(按文件计 6 个) 与 §1.2 的 1 处条件触发逐项写清六件事:改什么(精确到函数/行)、为什么是公共缺陷(对全部 Agent 的失效路径)、最小化边界(不新增字段、不改返回形状、不改表结构、不改签名)、规范依据(N-1~N-14 的对应条)、影响面(哪些 Agent、哪些测试)、降级方案(不受理时怎么退)。用于向底座 owner 提交(在 A-09 之后执行) 文档 LOW

批次 A 交付判据:10 份基线产物齐备;A-04 四份清单已提交;A-07 实测通过;A-09 四类白名单与 A-10 会签申请单已落文并提交底座方。


批次 B · 语料与话术(5 项,串行,只重跑一次灌库)

批次目标:把客户可见的跨层不一致一次清干净。本批次内严禁中途重跑灌库——必须等 B-02 全部改完后由 B-04 统一执行。

⚠️ 顺序不可调:B-01 门禁 → B-02 改语料 → B-05 热线定值 → B-03 P1 话术 → B-04 重跑 + 三查。

🔴 B-05 含 1 处底座改动(governance.py,§1.1 第 4 项)——须会签;业务侧 2 处与数据层 2 条可自主执行。

ID 任务 落点 DoD 验证 依赖 风险
[ ] 🔴 B-01 语料入库门禁(四查) tools/build_knowledge_chunks.py 四条件任一不满足即中止写入并报出块号:① 内容含 XXX/400-XXX-XXXX 等占位符;② 出现品牌白名单以外的公司名——白名单 = 南方基金(平台品牌与产品管理人实体)+ nffund.com(域名与邮箱域)〔🔁 2026-09-19 口径更正(乙-18/乙-24):底稿原写 南方财富 + nanfangwm.com,旧值会放行错误品牌——「南方财富」非本公司名、nanfangwm.com 非本公司域名;§3 定值表第 2/4 行的历史值按 乙-24 裁定保留作溯源,不回改,替换矩阵见 D1.4 §2.1〕;③ 源文件/章节落在禁止类目;④ (本轮新增,承接 D-07 降级)每块的 visibility 必须显式声明,缺省即拒绝写入(不改 milvus_knowledge_writer 的 FIELD_DEFAULTS,把「默认 public」的 fail-open 挡在门禁这一侧) 单测 A-04 LOW
[ ] 🔴 B-02 语料一次性修正(含决 2/3/4/12) knowledge/ 下 5 个源文件 + SOURCES ① 「南方科技」→「南方财富」,域名 www.nanfangtech.com → www.nanfangwm.com、complaint@nanfangtech.com → complaint@nanfangwm.com;② 热线统一 400-826-9518、服务时间统一 人工 7:00–22:00 / 线下 9:00–17:30 / VIP 7×24——涉及位置已定位 4 处:company/企业信息.md:19、policy/D6.3.1-理财产品销售管理办法.md:415、product/D6.2.3-高净值客户服务规范.md:129、faq/高频问答对.txt:9,26,37;③ FAQ-0009/0026/0035 的界面名与投诉入口与前端一致;④ product/D6.2.3-高净值客户服务规范.md 的 allow_chapters 由 ["一、","二、"] 改为 ["一、"](HNW「二、各层级专属权益」整章下线);⑤ 新增两个源:docs/43-场内基金产品手册(知识库入库版).md、docs/45-风险等级R1到R5问答(知识库入库版).md 纳入 SOURCES(决 12),入库前按决 8 的规则逐块检查无收益比较 / 稀缺性表述 脚本(重跑后校验) B-01 LOW
[ ] B-03 P1_REPLY 界面指代修正 customer_service_rules.py(业务层,可自改)、tests/unit/core/test_customer_service_rules.py:154 按数据类型分别指向实际存在的页面(我的持仓 / 收益明细 / 交易记录 / 资产总览);同步改被锁死的断言;新增断言:话术里的页面名必须存在于 app-shell.js 的链接表 单测 A-04 LOW
[ ] B-04 🔴 重跑灌库 + 四查验收(唯一一次) tools/build_knowledge_chunks.py → tools/load_knowledge_milvus.py ① 重跑后 _chunks.jsonl 0 个 XXX、0 个「南方科技」、0 个 nanfangtech、0 个 400-XXX-XXXX;② HNW-004~007 不再存在;③ 新增的场内基金与 R1–R5 条目已入库且抽查 5 块无收益比较类表述;④ 检索自检 7 个用例全过;⑤ 公开问题召回量与 A-04 基线一致(证明没误伤);⑥ 每块 visibility 字段都在(B-01 第四查的产物核对) 脚本 B-02、B-05 中
[ ] B-05 热线与服务时间单点化(决 2/3/4) 见右栏五组落点 真源已在 customer_service_rules.py:35,38(CONTACT_PHONE/CONTACT_HOURS,业务层),且 customer_service.py:157-165 已转发(不重复定义)——所以本项要清的是下面五组:
① 〔业务层·可自改〕 customer_service_rules.py:35,38 改定值(400-826-9518 / 新服务时间),并同步 P0/P1/P2/P4_REPLY、COMPLIANCE_REPLY 里的引用(它们都拼 CONTACT_PHONE,自动生效);
② 〔需会签〕 governance.py:46 删 CUSTOMER_SERVICE_HOTLINE + _mask_mobile 白名单分支(§1.1 第 4 项);
③ 〔数据层〕 tools/seed_compliance_baseline.py:99,101 的两条 agent_reply_template 话术改值(保持 active + 已审核字段非空);
④ 〔业务层·测试〕 tests/unit/core/test_customer_service_rules.py:67 的相等断言改定值;tests/unit/service/test_customer_service_suitability.py:14、test_customer_service_search_query.py:19 的 400-XXX-XXXX 内联占位符同步;
⑤ 〔前端〕 按 A-08 结论同步前端文案(若有)。
验收:grep -r "15936583816" app/ tests/ tools/ knowledge/ = 0;grep -r "400-XXX-XXXX" app/ tests/ tools/ knowledge/ = 0;docs/** 的历史记载不动(由 F-05 回写,见 §9 校正 12)
单测 + 脚本 A-03、A-04 LOW

批次 B 交付判据:MVP 演示前 5 步已可安全演示(品牌名、热线、界面指代、投诉入口、违规内容全部一致)。


批次 C · 规则重构与安全(9 项,一次定稿关键词表,一次双向验证)

批次目标:把零容忍规则从「一个常量担两用」拆成输入/输出两套,同时补齐安全缺口与访客侧推介边界,最后统一验证两个方向。

⚠️ 本批次集中在 customer_service_rules.py(业务层)+ compliance_context.py(底座,1 处会签)+ 数据层(agent_negative_word),不要拆成多次提交——测试彼此交叠,拆开会导致反复红。

⚠️ 删除类动作(C-07)必须在 C-06 验证通过之后。

关于编号:本批次没有 C-08——原 C-08(删兼容桩)已并进 C-07。C-09/C-10 是复核新增项,编号沿用 v3.0 续号,避免与既有引用冲突。映射见附录A。

ID 任务 落点 DoD 验证 依赖 风险
[ ] C-01 拆输入/输出两套常量(语义已校正,见 §9 校正 5) customer_service_rules.py:47-62(业务层) ① 不动 ZERO_TOLERANCE_WORDS 的名字与 11 条内容——它的唯一消费方是 route_message()(输入侧),名字准确;改名为 OUTPUT_BAN_WORDS 是语义错误(输出侧用的是 governance.hard_patterns + DB 规则),且会破坏 test_customer_service_rules.py:87-92 的相等断言;
② 新增 INPUT_PROMISE_WORDS(8 条字面承诺词)+ INPUT_PROMISE_PATTERNS(句式),由 hits_zero_tolerance() 一并消费
单测 A-02 中
[ ] C-02 输入侧改句式判定 customer_service_rules.py ① `(年化 七日年化 预期)收益率+(是多少 有多少
[ ] 🔴 C-03 输入侧补安全缺口 customer_service_rules.py ① 移植提示词注入拦截 8 条,插在 P0 之后、合规拦截之前(P0 优先:混合输入要拿反诈话术);② 补「推荐」「收益最高」「纠纷」;③ 补裸词账户问法(持仓/收益/订单/投资 类,覆盖「我持仓有多少钱」这类不含「我的持仓」的问法);④ 保留 _CHITCHAT_MESSAGES/_CHITCHAT_PHRASES 不动(chitchat_streak 依赖) 单测 C-01 中高
[ ] 🔴 C-04 输出侧补豁免线索(决 9)〔需会签·§1.1 第 3 项〕 app/core/compliance_context.py:43-48 ★ 属底座公共件。定性为公共缺陷:短否定式(不保本 / 不保收益 / 非保本)不在 NEGATION_CUES 覆盖内 → FAQ-0018/FAQ-0036 会被整条替换——对所有 Agent 同理(review_output 是全局治理)。DoD:① NEGATION_CUES 补多字短否定式(不保本/不保收益/不保本保收益/非保本/并非保本/不确保/不担保);② 否定线索与指标名白名单两者都做且同句生效(沿用 SENTENCE_BOUNDARIES/CONTEXT_WINDOW)。最小化边界:不触碰 ZERO_TOLERANCE_WORDS 的 11 条、governance.hard_patterns 的 5 条、正则与窗口常量。验收:两条 FAQ 答案能完整返回 单测 — 中高
[ ] 🔴 C-05 输出侧补收益比较类规则(决 8)〔数据层·§1.4 的 D-a,零代码〕 DB 表 agent_negative_word 的种子数据 + docs 口径说明 ★ 不改任何 .py。三条硬约束:① applicable_agents JSON 数组非空且含 customer_service(governance.resolve() L118-128 对空数组是失败关闭跳过,填错=规则永不生效);② 不得改写既有 11 行的 word_pattern(test_customer_service_rules.py:87-92 断死);③ 按承诺性短语收录(承诺高收益/保证高收益/较 X 收益高/收益高 N 个 BP/稀缺额度/优先认购权/优先配置权/X 折优惠),不要只收「高收益」(否则「高收益伴随高风险」这类风险提示会被一并拦下)。仅此三类,明确不纳入风险形容词(稳健/低风险/低波动)。补完须跑 tests/integration/test_compliance_seed_mysql.py 确认种子与代码镜像仍对账 集成测试 — 中
[ ] C-06 双向验证(本批次唯一放行闸门) 红队集 + tests/unit/core/test_customer_service_rules.py + test_compliance_context.py 方向 A(放开):「七日年化收益率是什么」「结构性存款安全吗」「预期收益率是什么意思」→ 可答;方向 B(收紧):5 条诱导性问法(这个产品保本吗/有什么年化5%以上的理财/有没有年化5%以上的产品/有没有稳赚不赔的基金/推荐一个无风险的产品)仍拒答;方向 C(访客):访客问「推荐一只基金」→ 不产生推介措辞;方向 D(反例守卫,C-04 新增):本产品不保本 → 不判违规;本产品保本 → 判违规;严禁承诺保本。但这只基金保本 → 仍判违规(同句窗口不得跨句);红队 RT-001~010 与 A-01 基线逐条对比,只允许更严 单测 + 脚本 C-01~C-05、C-09 中高
[ ] C-07 删除一期确定性路由 + 兼容桩(合并原 C-07 与 C-08) app/service/agent/customer_service_routing.py、app/service/agent/customer_service_agent.py、tests/unit/service/test_agent_governance.py ① 删 classify()、CustomerServiceRoute、6 组只服务它的关键词常量;必须保留 _CHITCHAT_MESSAGES/_CHITCHAT_PHRASES(chitchat_streak 在用);② 删除兼容桩文件,import 指向 app.service.agent.implementations.customer_service;③ 一期词表原样留痕到 docs/(新增 customer-service-routing-legacy-keywords.md,不要只留在 git 历史);④ 全量单测绿 单测 C-06 中
[ ] 🔴 C-09 访客侧推介边界(决 8 同源) customer_service_rules.py、customer_service.py、compliance_context.py(均为业务层可自改;第三项的改动归入 C-04 的会签) 这是 MVP 第 1 步「无投资建议类内容」的唯一落点。现状:VISITOR_INTENTS(customer_service.py:50)含 INTENT_PRODUCT,访客问「买哪只基金好 / 推荐一只」会走知识检索并返回产品内容,无输出侧推介拦截。DoD:① 输入侧识别访客的推介请求句式(推荐 / 买哪个好 / 该买什么 / 帮我选)→ 返回「不提供投资建议」的边界话术 + 可答范围说明;② 输出侧按主体分化——subject_type=guest 时禁止出现「推荐」「适合您」「建议购买」「建议配置」及排序性表述;客户侧不受此限(客户侧风险是承诺收益,二者不能共用一套规则);③ 客户侧行为不变(回归验证) 单测 C-01 中高
[ ] C-10 source_references 定向落地或降级(待决 20,结论见 §10) 〔甲〕tool_executor.py + governance.py(需会签·§1.2);〔乙〕docs/ + 护栏断言 按 §10 的结论执行:
(甲·请底座方实现)① ToolExecutor 登记本次 run 可引用的 doc_id;② governance.review_output 放行 knowledge 来源(L305-309 增加分支);③ 知识出口返回 references。★ 必须由底座方实现——N-13 明示来源引用是基座职责、「业务代码不能伪造来源引用」。验收:答案带 1–3 条来源,且不导致 run 失败。
**(乙·降级)**① 文档显式标注「本期不向客户展示来源引用」;② 加护栏断言:禁止从知识出口调用 _references()(它当前是死代码——customer_service.py:727 全仓无调用点——误启用会让整个 run 失败);③ 可追溯性由审计承接
单测 C-06 中

批次 C 交付判据:① 概念题可答;② 诱导性问法仍拒答;③ 访客问「推荐一只」不产生推介;④ 注入拦截就位(RT-009/010 转绿);⑤ 反例守卫(方向 D)全绿;⑥ 全量单测 0 failed。


批次 D · 架构护栏(7 项,为「加档位/加角色」备前置)

批次目标:把「权限边界落在检索层」真正做完。当前 registered 无内容,但护栏必须先有——否则将来加内容时就是裸奔。

🔴 本批次整批属底座公共件改动,须 A-10 会签通过后才能开工(P-5 / S-9)。依据:search_knowledge 是公共只读工具(允许 customer/advisor/operator/admin,N-3),knowledge_search_service.py 是两套环境共用的检索实现(其文件头 L24-35 自述)。明确否决「在客服模块内新建检索实现」——违反 N-2/N-4/N-6,且会形成两条检索路径,其中一条必然漏掉可见性过滤。

不阻塞整体进度:A/B/C(F-06) 可并行推进,只有本批次在等会签。

ID 任务 落点 DoD 验证 依赖 风险
[ ] D-01 检索签名 fail-closed(决 10、决 19)〔需会签·§1.1 第 1 项〕 app/service/knowledge_search_service.py:138-163 include_internal: bool = False → tiers: frozenset[str](必填、无默认值);非法值/缺失 → 归 {"public"};取值域保留 {"public","registered"}(决 10:registered 内容为空但档位保留);所有调用点同步(knowledge_tool.py:29、tools/seed_knowledge_r1r5_faq.py:97)。
★ 落点已收窄(校正 3):不改 app/core/knowledge_contracts.py——档位由 context.roles 推导(tool_executor.py:116 已把 context 传给 handler),KnowledgeSearchInput 保持 extra="forbid" 与三字段不变(N-11)。
并核对:app/service/knowledge_retrieval_service.py(KnowledgeRetrievalService)是未接入的第二条检索实现(有单测、无生产调用方;且它不做 visibility 过滤,走 MySQL fin_knowledge_meta 的 review_status/有效期)——本期只登记不改造(决 19),但必须在 F-05 的待清理清单里点名,因为它是「加档位时最容易漏掉的第二条入口」
单测 A-03 中
[ ] 🔴 D-02 按集合逐个拼过滤表达式(缺陷已重新定性,见 §9 校正 1)〔需会签·§1.1 第 1 项〕 knowledge_search_service.py _visibility_filter() L138-154、search() L202-217、_hit_from_row() L507 ① 表达式按集合逐个拼:filter=expression if schema.has("visibility") else None(现码把「有 visibility 的集合」算出的表达式无差别传给全部集合,缺字段的集合被 Milvus 报错 → except 吞成 failures → degraded=True / partial_collection_failure → 客服走「引导人工」);② 修正 L153 与实现不符的注释(它声称「不带该字段的集合由各自调用处跳过表达式的拼接」,但调用处并未跳过);③ _hit_from_row() 的 visibility=value("visibility") or "public" → 缺字段时不再回落 public(不可判定不得冒充 public);④ 补「因缺 visibility 而跳过过滤」的独立计数,使「不可判定」不再与「正常」混在一个 degraded 里。
替身测试需覆盖两种情形:Ⅰ 部分集合有 / 部分没有 visibility;Ⅱ 字段名不同(doc_id/content vs knowledge_id/snippet)——后者是本项目两套环境的既有差异(N-8)
单测 D-01 中
[ ] D-03 over-fetch + 独立指标 同上 有过滤时 limit = top_k × 2(上限 20),过滤后截断到 top_k;「过滤后为空」独立计数 单测 D-01 LOW
[ ] D-04 档位判定纯函数 + handler 内取身份〔需会签·§1.1 第 2 项〕 app/service/knowledge_tool.py:25(+ 可选新增 app/core/knowledge_tier.py 纯函数) 删除 del context;档位由 context.roles 推导,与既有访客口径同源(visitor → {public},customer → {public, registered},未知 → {public},失败关闭);不新增 KnowledgeSearchInput 字段(tool_executor.py:116 已把 context 传给 handler,零契约变更);不改工具名/权限码/角色/超时(N-3);单测覆盖访客/客户/未知角色。
备选乙(不受理时):见 §1.4 的 D-2,必须文档标注为降级
单测 D-01 LOW
[ ] D-05 缓存键含档位 检索/embedding 缓存 构造「客户查过 → 访客再查」用例,断言不命中缓存 单测 D-04 LOW
[ ] D-06 访客主体口径统一(决 7)〔需会签·§1.1 第 5 项〕 app/service/agent_run_application_service.py:151,213、app/service/agent_persistence_service.py:93 不新增 visitor_id、零 DDL(决 7)。访客 conversation_message.customer_id = NULL(该列本就可空、无外键);「customer_id IS NULL = 访客」成为唯一判据,与工单侧(已刻意置 NULL)一致;访客在审计侧以 session_id 留痕。
★ 边界(校正 10):ConversationSession.user_id/RequestIdempotency.user_id/AgentRun.user_id 均 NOT NULL → 保持现状(只改消息表);L174-181 的会话归属校验仍用既有 user_id 口径,不改其语义
单测 A-02、A-07 低–中
[ ] D-07 API 入库路径的档位(已降级,改为门禁 + 文档,见 §9 校正 14) 不碰 app/infrastructure/milvus_knowledge_writer.py;落点为 B-01 的第四查 + docs/ 登记 ★ 降级方案:FIELD_DEFAULTS(现默认 public)不改——它会同时影响 knowledge_ingest_service、knowledge_vector_worker 与管理面,而 MVP 不新增 registered 内容(决 10),风险远大于收益。
DoD:① B-01 增加「visibility 必须显式声明,缺省即拒绝写入」;② docs/ 登记「API 入库路径的档位默认值问题本期不修,属已知未修项」;③ 若将来启用 registered 内容,本项必须重新评估并走会签
文档 + 单测 B-01 低(原「中」下调)

批次 D 交付判据:① 访客检索 registered 返回空、客户同期正常;② 公开问题召回量与 A-04 基线一致;③ 混合 schema 与字段名差异两类替身测试通过;④ 「缺 visibility 的集合」不再污染 degraded(新增独立计数可证)。


批次 E · 业务闭环与出口(8 项,含 1 项挂起)

批次目标:让工单分类正确、访客边界清晰、Agent 出口稳健、红线可举证。

ID 任务 落点 DoD 验证 依赖 风险
[ ] 🔴 E-01 工单建单条件 + priority + reason_code 枚举化(决 5、决 6)〔需会签·§1.1 第 6 项〕 agent_persistence_service.py:82-153(底座)、customer_service.py:685-700(业务层)、customer_service_rules.py(业务层) ① 建单白名单(决 6,必须限定 run.agent_type == "customer_service",否则会改其它 Agent 的建单行为):P0_safety_risk / P2_human_requested / advice_requested 建单;知识库未命中 / 置信度不足 / 检索降级 / 画像查询失败 不建单(仅留审计 + 未命中指标)——修正当前「答不上来也建单」;② priority 映射(决 5,字段已存在,零 DDL):反诈 / 安全风险 → P0;投诉 → P1;客户要建议(advice_requested)→ P1;客户主动要求转人工 → P2;其他 → P2(当前恒为 model default P1,队列索引白建);③ reason_code 枚举化(当前四套词汇混入同一 VARCHAR(64),含中文自由文本);④ 不改函数签名、不改表结构 单测 + 集成 C-03 中
[ ] E-02 auth_required 运行时意图 customer_service.py(业务层) 复用既有出口 _guide_to_login(reason)(已有两处触发点)——本项把触发面扩到「账户 / 持仓 / 收益 / 工单 / 风评 / 密码类问法」。不动 supported_intents、不重发 config_release;反例(主语为「我」但内容属公开知识)不触发。并补:_guide_to_login 须按 reason 区分日志/指标,使「配置错误」与「正常引导登录」可被区分 单测 D-04 LOW
[ ] E-03 工单可见方落文 docs(无需新增端点/权限码,N-10) 明确表述「投顾侧可见 = 管理面可见 + 可指派给投顾账号」;assigned_to(已有 sys_user 外键)承载指派;投顾专属队列与自动分派归未来扩展 文档 E-01 LOW
[ ] E-04 适当性「不匹配告知 + 主动确认」(决 14) customer_service.py _answer_suitability else 分支 提示超出风险承受能力 + 主动确认路径 + 留痕落 conversation_message.tool_calls 与 interaction_audit(零 DDL);与三条红线逐条对照(尤其「先风险揭示、后客户确认」的顺序) 单测 — 中
[ ] E-05 _topic_of 文本反解收口 customer_service.py 出口结构 出口显式声明主题,不再从回答文本反解(该机制已咬 5 次);test_customer_service_topic_matrix.py 全绿且不再需要新增格式分支 单测 — 中
[ ] E-06 长答案截断提示 customer_service.py:339 当前 content[:1200] 无省略号静默截断,而 POL-AST-009 长 2828 字符且是高频命中块 → 截断处加提示,或对超长块走「整节 + 可追问」 单测 — LOW
[ ] E-07 ⏸ registered 档位内容标注(挂起,不执行) build_knowledge_chunks.py 的 SOURCES 暂不执行(决 10):唯一候选(HNW 权益细节)已因合规问题下线(B-02 ④),其余属 MVP 明确不做的卡级体系。保留 registered 为「已支持但为空」的档位,等卡级体系进范围再启用 — D-01 —
[ ] 🔴 E-08 三条红线客服侧对照验证 customer_service.py、customer_service_rules.py、docs/ 留痕 逐条验证并留痕:① 风险等级唯一来源——对话中不给等级结论、不引导「重做测评以提级」(现有 _answer_profile 只读、route_message 有部分拦截,需逐条确认);② 先揭示后确认——客服侧不得代客户确认;③ 不生成交易指令——对话中不输出可执行交易要素(代码 / 金额 / 份额 / 确认动作),只做跳转引导。发现的缺口按「补拦截」而非「补话术」处理 单测 + 手工 E-04 中

📌 E-02 / E-04 / E-05 / E-06 / E-08 同文件(customer_service.py),建议并入同一 PR,避免对同一文件多次改动与多次测试。


批次 F · 演示与收口(7 项)

ID 任务 落点 DoD 验证 依赖 风险
[ ] F-01 MySQL 回退路径主体隔离〔需会签·§1.1 第 5 项〕 agent_run_application_service._load_mysql_prior_context() L98-116 增加主体过滤(客户 customer_id == user_id;访客 customer_id IS NULL;员工 customer_id IN scope);主体判据必须调用 app/core/memory_scope.py,不得自写一套(N-9 明令「改召回、改引用校验、改范围守卫三处必须一起走这个模块」);构造「两主体同 session_id」用例断言读不到对方历史(当前 Redis 侧有 sha256(actor+session) 隔离,MySQL 回退侧没有——L100-108 只按 session_id + role="user" 取) 单测 D-06 中
[ ] F-02 访客专项端到端验证 手工清单 + 脚本 五条:越权(访客问「我的持仓」)、限流、会话隔离、登录后上下文继承、推荐类请求不产生投资建议(C-09 的端到端确认) 手工 D-04、D-06、E-02、C-09 LOW
[ ] 🔴 F-03 MVP 九步演示脚本与彩排 docs/44-演示流程.md 同款 九步全部可见;含第 1 步「游客浏览无投资建议类内容」、第 3 步「客服明确说明无权限 + 引导至正确页面」;含排障表与账号速查 手工 B-04、C-06、E-01、E-03、F-04、F-06 中
[ ] F-04 演示前五项自检 + 账号速查(决 17) A-05 的清单 + tools/seed_*.py ① Docker → Milvus → Worker → 行情 → MILVUS_LOCAL_URI 全部通过;② 用现有 seed 脚本生成客户 / 访客 / 管理员三类账号,产出「账号速查表」并实测可登录(可复现,环境重置后能重建) 手工 A-05、A-07 LOW
[ ] F-05 文档回写 v2.3(决 16、决 19) D3.1-客服Agent需求开发文档与设计方案.html + docs/** 重构完成后一次性回写:① v1→v2 的 15 项更正、N-01N-15、T-01T-11 定论;② §5.5.2 / §5.5.3 整套变更流程作废说明(目标表不存在);③ registered 空转的事实;④ 决 1~19 的定案 + E-03 的工单可见方表述;⑤ 死代码待清理清单(含 knowledge_retrieval_service.py(未接入的第二条检索实现)、?v= 版本号、_references()、D-07 的 API 入库默认档位),并注明「本期不删」;⑥ 修设计文档自身 3 处债:§1.4 说「分为 6 个功能域」但实际 A—G 七个、source_references 的可达性结论、来源引用需求若降级须显式标注;⑦ 本轮新增 4 处代码注释债(见 §9 校正 6/13):governance.py:52 与 customer_service_rules.py 模块 docstring 里「治理层是朴素子串匹配、没有否定式豁免」已过期(实际走 _first_violation);knowledge_search_service.py:153 的注释与实现不符(D-02 一并修);⑧ 口径冲突回写:docs/演示用/软件需求文档-2026-09-14.md §421/§596 与 docs/验收与审计/phase1-acceptance-criteria.md:55 把 15936583816 记为「真实号码要求」——与决 2 冲突,须改为 400-826-9518 并标注决策来源 文档 全部 LOW
[ ] F-06 🔴 产品证据链(决 13 · 甲) tools/sync_nanfang_official_product_governance.py 立即 --dry-run(不依赖任何代码改动):① 确认 19 只 ETF/LOF 已在库;② dry-run 验证抓取与解析;③ 正式执行;④ 演示第 ⑥⑦ 步 candidate_count > 0。降级阶梯:(甲)自动 → (甲)人工导 2–3 只真实证据 → (丙)演示时说明。不做乙(source_url NOT NULL,编造会让以后分不清真假) 脚本 + 手工 无需代码前置 中
[ ] F-07 D7.1-需求文档.html 的 24 个失效目录锚点修复(决 18) D7.1-需求文档.html 24 个 href 对齐正文 id(只改 href,不动正文 id,不影响任何引用);点击目录全部可跳转 脚本 — LOW

可提前:F-06 与 A 批次同时启动(不依赖代码,越早跑越有余量走降级)。


批次 G · 访客与鉴权(5 项:4 可执行 + 1 挂起,v5.1 新增 · 独立会签组)

为什么单独成批:本批次要碰的 4 个文件不在 §1.1 的 6 文件名单内,其中 3 个还明确列在 §1.5「底座零改动清单」里(app/api/**、app/worker/**、app/service/agent/base.py)。 这是本次唯一一处「主动扩张底座接触面」——因此必须单独一列、单独会签、单独一个 PR,不得混入 §1.1 那批。

定案前置:必须先跑 G-00(达成可运行的 pytest/ruff/mypy 环境)。本条不是形式要求——G-01 要改鉴权入口与 Worker 执行路径, 没有可运行的测试套件就等于在鉴权链路上盲改;而当前环境依赖零安装,只能做语法与静态扫描。

ID 任务 落点 DoD 验证 依赖 风险
[ ] 🔴 G-00 测试环境就位(门禁可运行) pyproject.toml / venv 按 N-14 跑通 pytest tests/unit tests/contract、ruff、mypy app,记录基线数字 脚本 — LOW
[ ] 🔴 G-01 访客权威单点化(方案乙) 新增 app/core/actor.py;改 app/core/security.py:92-97、app/worker/runtime.py:179-184 ① 访客三元组(roles/permissions/data_scope)只有一个构造来源;② security.py 与 runtime.py 均调用它;③ 对外行为零变化(访客问答、限流、令牌 TTL 全部不变);④ 新增接缝单测:同一输入下两侧产出必须相等(这正是当前缺失的测试) 单测 + 端到端访客问答 G-00 中
[ ] 🔴 G-01b 判定口径单点化(G-01 完整版) 改 app/api/dependencies/auth.py:52-53、app/service/agent/base.py:147、app/worker/runtime.py:193,221、app/service/agent_run_application_service.py:141 6 处 "visitor" in context.roles 全部改走 actor.is_anonymous(context);⚠️ base.py 那处的「位置与层级」不得变(不变量②):仍在基类、仍不依赖 Agent 声明位 单测 + 「访客五查」(见报告 §5.3) G-01 中
[ ] ⏸️ G-02(挂起) 身份轴(方案甲) app/core/contracts.py(RequestContext 增 subject_type)、authorizer.py、tool_executor.py、bootstrap.py、app/core/actor.py roles 回归纯 RBAC(访客 roles=());20 余处 allowed_roles 中的 "visitor" 迁至身份轴声明;query_knowledge 工具同步 「访客五查」+ 全部既有 Agent 的角色回归 G-01b、MVP 演示跑通 高
[ ] 🔴 G-03 档位推导单点化(承接 D-04) 新增 app/core/knowledge_tier.py(§6.3 通道 3 已规划的落点) 档位由 actor.is_anonymous() 推导,不由工具层自行判 roles;⇒ 将来 G-02 落地时只需改 actor.py 一处 单测 + 档位双向验证(见 D 批次) G-01b、D-01 LOW

批次 G 交付判据:G-01 后访客链路端到端行为与动手前逐项一致(含令牌 TTL、限流阈值、agent_type 投影);grep -rn '"visitor" in context.roles' app/ 的结果只出现在 actor.py 内部的兼容分支(G-01b 后);G-02 挂起待 MVP 演示通过。

⚠️ 与 D 批次的串行关系(新增 S-11):G-01b → G-03 → D-04。D-04 不得自行判断身份字段,必须调用 knowledge_tier。 ⚠️ agent_run_application_service.py 成为三处交叉点:G-01b(:141)、D-06(:213)、F-01(历史读取)——执行顺序固定为 G-01b → D-06 → F-01。


6. 关键路径、串行约束与并行通道

6.1 关键路径(9 步 + 1 个会签等待窗口)

[会签窗口:A-10 提交 → 底座方受理 §1.1 的 6 处]  ──┐
                                                    ↓
A-02 → A-07 → D-01 → D-02 → D-04 → D-06 → E-01 → E-03 → F-03 ──→ 演示跑通
                                                             ↑
                    B-01 → B-02 → B-05 → B-04 ───────────────┤
                                                             │
                    C-01 → C-03 → C-09 → C-06 ───────────────┘

最长链:A-02 → A-07 → G-01 → G-01b → G-03 → D-01 → D-02 → D-04 → D-06 → E-01 → E-03 → F-03(并入批次 G 后为 12 步;若 G-00 与 A-02 合并执行则回到 11 步) v5.0 原最长链:A-02 → A-07 → D-01 → D-02 → D-04 → D-06 → E-01 → E-03 → F-03 = 9 步(已被上面取代) 新增等待窗口:A-10 的会签往返(不影响步数,但是唯一的外部队列——越早提交越早有回音,故 A-10 排在 A 批次内优先执行)。

并行条件:C 组可与 D 组并行,因为改动文件不重叠——C 改 customer_service_rules.py(业务)/ compliance_context.py(底座 1 处)/ 数据层,D 改 knowledge_search_service.py / knowledge_tool.py。 ⚠️ 但按 P-4/P-5,两组的「底座公共件」部分均需会签:C-04(compliance_context)、B-05 底座侧(governance)、D 组(knowledge_search_service / knowledge_tool)、D-06/F-01(agent_run_application_service)、E-01(agent_persistence_service)——不能因「并行」而跳过会签;C-05(数据层)与 D-07(降级)不受影响。 例外:C-10 会碰 governance.py 与 tool_executor.py,建议排在 D-04 之后,避免与身份口径变更交叉。

6.2 九条硬串行约束(违反即返工)

# 约束 违反后果
S-1 B-01 门禁 → B-02 改语料 → B-04 重跑 顺序颠倒会把问题再灌一遍
S-2 B-05 热线定值 → B-02 填值 语料里会填进旧的/占位的号码
S-3 C-06 验证 → C-07 删除 直接删一期 = 提示词注入拦截消失,唯一带安全性的删除
S-4 D-02 修过滤表达式 → 任何集合加 visibility/改字段名 缺字段的集合被同一表达式打挂 → 被吞成 failure → degraded=True → 客服全员「引导人工」且不报错
S-5 D-01 fail-closed → D-04 传身份 身份传进没有 fail-closed 语义的签名 = 把「记得过滤」交回调用方
S-6 D-03 over-fetch 与 D-01/D-02 同批 只有过滤没有 over-fetch → 过滤后 TopK 不足 → 上层误判「无答案」
S-7 集合 schema 变更后必须重启 API + Worker get_knowledge_search_service() 是进程级 @lru_cache 单例,SchemaCache 永不失效
S-8 禁止在知识出口启用 _references()(除非 C-10 选甲并完成全部三项改动) governance 只认 memory/tool 来源 → 返回 knowledge 引用会让整个 run 失败
S-9 🔴 底座 6 处未获会签前不得动手(P-5)。业务侧可先行,但不得为「绕过会签」而在业务层重造同一能力(N-2/N-4/N-6) 绕过 = 形成第二套实现,其中一套必然漏掉隔离;且违反项目级强制约束
S-10 🔴 P-9 未做严禁开始 F-02 / F-03 第 1 步(游客链路开关) 客服 Agent 的 allowed_roles 缺 "visitor" → 游客线永久失效,且失败表现是「请登录」,与设计如此无法区分
S-11 🔴 G-00 → G-01 → G-01b → G-03 → D-04(v5.1 新增)。即:测试环境就位 → 三元组单点化 → 判定口径单点化 → 档位推导单点化 → 再改检索 ① 无测试环境改鉴权链路 = 盲改;② G-01 不做就改 D-04,档位会依赖两份可能不一致的身份定义;③ D-04 若自行判 roles,则 G-02 落地时必须改两处,分离收益归零
S-12 🔴 agent_run_application_service.py 三处交叉点顺序固定:G-01b(:141) → D-06(:213) → F-01(历史读取) 三处同文件不同段,倒序会互相覆盖;且 D-06 的 customer_id 口径依赖身份判定已单点化

6.3 若多人并行:按「文件不相交」切四路

通道 负责批次 独占文件(不与其它通道冲突)
通道 1 · 语料 批次 B 全部 + F-06 knowledge/*、tools/build_knowledge_chunks.py、tools/sync_nanfang_*
通道 2 · 规则与数据层 批次 C 全部 + E-01(业务侧) app/core/customer_service_rules.py、agent_negative_word 种子
通道 3 · 检索护栏 批次 D 全部 + F-01 app/service/knowledge_search_service.py、app/core/knowledge_tier.py、app/service/knowledge_tool.py
通道 4 · 出口与文档 批次 A + E-02/E-04/E-05/E-06/E-08 + F 文档类 app/service/agent/implementations/customer_service.py、docs/*
通道 0 · 底座会签 §1.1 的 6 文件 8 处(单独一个 PR,由底座 owner 审) app/service/knowledge_search_service.py、app/service/knowledge_tool.py、app/core/compliance_context.py、app/service/agent/governance.py、app/service/agent_run_application_service.py、app/service/agent_persistence_service.py

⚠️ 三处交叉点需串行:① agent_run_application_service.py 由通道 3(D-06)与通道 4(F-01)先后改,D-06 → F-01;② agent_persistence_service.py 只由通道 2(E-01)改;③ tool_executor.py 仅由 C-10 碰,且 C-10 排在 D-04 之后。


7. 整体完工判据(Definition of Done)

# 判据 怎么验
1 业务一致性三查全绿 语料 0 个 XXX、0 个「南方科技」、0 个 nanfangtech、0 个 400-XXX-XXXX;热线/服务时间代码与语料一致且 app/ tests/ tools/ knowledge/ 内无 15936583816;语料提到的界面名全在 app-shell.js 链接表内
2 安全只增不减 红队 RT-001~010 与 A-01 基线逐条对比,无一条变松;新增注入类用例全绿;C-06 方向 D 的反例守卫全绿
3 规则三个方向都对 概念题可答 + 5 条诱导性问法仍拒答 + 访客推荐类请求不产生推介
4 护栏生效 访客检索 registered 返回空、客户同期正常;公开问题召回量与基线一致;混合 schema 与字段名差异两类替身测试通过;「缺 visibility 的集合」有独立计数、不再污染 degraded
5 访客主体一致 conversation_message.customer_id IS NULL = 访客;工单侧同为 NULL;MySQL 回退路径读不到他人历史(走 memory_scope)
6 工单分类正确 「答不上来」不建单;反诈工单 priority=P0;投诉/建议请求 P1;客户主动要人工 P2;reason_code 为枚举值;其它 Agent 的建单行为未变
7 门禁干净 按 N-14:ruff 0 错、mypy app 0 错、pytest tests/unit tests/contract + tests/integration 0 failed(绝对用例数会随开发增减,判据是 0 failed)
8 演示跑通 MVP §4.1 九步全部可见,含第 1 步「无投资建议类内容」、第 ⑥⑦ 步 candidate_count > 0
9 红线可举证 E-08 的三条验证结果留痕;适当性不匹配场景可在消息表与审计表中还原「揭示 → 确认」顺序
10 🔴 底座纪律达标(本轮新增) ① 实际改动文件 ⊆ A-09 白名单;② §1.1 的 6 处每一处都有会签记录(或走 §1.4 的降级并文档标注);③ 零 DDL 证据:python tools/audit_schema.py = 89 张业务表、alembic/baseline_generated.sql 无 diff;④ 未绕过 AgentFactory/BaseAgent/工具/合规/审计流程(N-2/N-4/N-6)
11 🔴 访客与角色已分离(v5.1 新增) ① 访客权益三元组只有一个构造来源(grep -rn "agent:run", "knowledge:query" app/ 应只命中 app/core/actor.py 与新构造点);② 访客链路端到端行为与动手前逐项一致(令牌 TTL / 限流阈值 / actor_type 投影 / 记忆不召回);③ grep -rn '"visitor" in context.roles' app/ 结果为空(判定已全部走 actor.py);④ 档位由 knowledge_tier 推导,工具层不含身份字段字面量;⑤ 三条不变量逐条可举证(单点鉴权 / 兜底仍在基类 / 最小权限与 data_scope="public" 未变)

8. 风险登记(重点盯防 7 项)

排名 任务 最高风险点 对策
1 D-02 过滤器按集合逐个拼 改错会让客服全员转人工且不报错(degraded 的语义被稀释) 先补「部分集合有 / 部分没有」与「字段名不同」两类替身测试,再改实现;上线后立刻验「公开问题召回量不变」;保留「缺字段跳过」的独立计数
2 C-06 四向验证 收窄输入侧可能放过诱导性问法;新增访客方向可能漏;C-04 补线索补宽了会放过真实承诺 四个方向的用例都必须有;反例守卫(严禁承诺保本。但这只基金保本 必须仍判违规);红队逐条对比
3 C-09 访客推介边界 按主体分化输出规则时,可能把客户侧误伤(或反之) 客户侧必须做回归验证;规则按 subject_type 分支而非合并词表
4 C-05 数据层词表 applicable_agents 填错(空数组)= 规则永不生效且不报错;只收形容词会误伤风险提示 会签单内写明三条硬约束;新增词必须为承诺性短语;跑 test_compliance_seed_mysql.py 对账
5 E-01 建单白名单 若不限定 agent_type,会改动其它 Agent 的建单行为(跨模块回归) DoD 明确「仅对 agent_type=="customer_service" 生效」;把风控/投顾的既有建单用例纳入回归
6 A-07 访客链路实测 若 query_knowledge 未在白名单,全线失败表现为「请登录」,与设计如此无法区分 实测 + 日志按 reason 区分(E-02 补);A-03 快照留白名单全文
7 B-04 / F-06 两次「跑数据库/外部服务」 环境问题伪装成功能缺陷 先跑 A-03/A-05 的快照与自检;B-04 前停 Worker 以免抢队列;F-06 先 dry-run

9. v5.0 复核:以代码为准的判据校正(15 项)

复核方法:不看文档,逐条回到代码取证。凡与 客服Agent清除与重构方案(底座层与业务层分层).md(CS-REFACTOR-2026-006)或 v4.0 表述冲突的,一律以代码为准并在下表登记。

# 原判据(v4.0 / 分层方案) 代码实证 校正结果
1 分层方案 D-02:「_visibility_filter 只算一个表达式却无差别应用到全部集合 → 混合 schema 下整片失败」 _visibility_filter() L138-154 确实只算一个表达式且在 L202 应用;但 L212 把它传给全部集合,缺 visibility 的集合被 Milvus 报错 → L214 except 吞掉 → failures += 1 → L269 degraded = failures > 0、reason="partial_collection_failure"。不是「整片失败」,而是部分集合静默失空 + 降级标记 → 客服走兜底 表述校正 + 风险下调(中高 → 中);修法更小:filter=expression if schema.has("visibility") else None
2 L153 注释:「只要有一个集合带该字段就过滤;不带该字段的集合由各自调用处跳过表达式的拼接」 调用处 L212 并未跳过,用的是同一个 expression 代码与注释不符 → 并入 D-02 一并修正
3 D-01 落点含 app/core/knowledge_contracts.py:45-58(改 KnowledgeSearchInput) tool_executor.py:116 await definition.handler(validated, context) —— handler 已能拿到 context 落点收窄:只改 knowledge_search_service.py 的签名;KnowledgeSearchInput 保持 extra="forbid" + 三字段不变(N-11)
4 D-01:「核对 knowledge_retrieval_service.py(死代码)是否与 knowledge_search_service 存在重复的检索实现」 app/service/knowledge_retrieval_service.py 存在,有单测(tests/unit/service/test_knowledge_retrieval_service.py)但无生产调用方;它不做 visibility 过滤,走 MySQL fin_knowledge_meta 的 review_status/有效期 定性修正:不是「重复的可见性过滤实现」,而是「未接入的第二条检索入口」——加档位时确实最易漏 → 本期只登记不改造(决 19),写入 F-05 待清理清单
5 C-01:ZERO_TOLERANCE_WORDS → OUTPUT_BAN_WORDS 该常量的唯一消费方是 hits_zero_tolerance() ← route_message()(输入侧);输出侧走 governance.hard_patterns + DB 规则;test_customer_service_rules.py:87-92 断言其名字与 11 条内容 语义纠错 + 不改名:改名是错的(把输入侧常量叫成输出侧),且无收益、有成本
6 分层方案 / governance.py:52 / customer_service_rules.py 模块 docstring:「治理层 review_output() 是朴素子串匹配、没有否定式豁免」 governance.py:9 from app.core.compliance_context import first_violation as _first_violation;L317/L322 实际调用它(有豁免) 注释已过期 → 列入 F-05 文档债;但 customer_service_rules.py 硬约束 2 的结论仍成立(话术文本仍不该命中禁用字面)
7 C-04 落点 compliance_context.NEGATION_CUES L43-48 有 不承诺/不保证/不作/不能/绝不/从不 等,无 不保本/不保收益/非保本 → FAQ-0018/0036 会被整条替换 成立,且只改 compliance_context.py;governance.py 无需改
8 C-05「改走数据层(零代码)」 governance.resolve() L111-114 的 SQL 带 status='active' AND reviewer_id IS NOT NULL AND reviewed_at IS NOT NULL;L118-128 对 applicable_agents 空数组失败关闭跳过;L129-131 只接受 exact/contains 成立,但 v4.0 未写清三项必填约束 → 已补进 C-05 与 §1.4 D-a
9 E-01「工单 priority 有 CHECK 与索引但无任何代码赋值」 app/model/platform.py:106 priority: Mapped[str] = mapped_column(String(8), nullable=False, default="P1");complete_run L140-153 的 HandoverTicket(...) 未传 priority → 恒为 model default P1 成立,零 DDL(字段已在)
10 E-01「建单白名单」 complete_run 对所有 transfer_required=True 的 Agent 都建单(L116) 成立,但须补约束:白名单必须限定 run.agent_type == "customer_service",否则改到风控/投顾
11 D-06「访客匿名 id 被当 customer_id 写入消息表」 agent_run_application_service.py:151 user_id = int(context.user_id);L213 customer_id=user_id;agent_persistence_service.py:93 customer_id=run.user_id 成立;补充边界:ConversationSession.user_id/RequestIdempotency.user_id/AgentRun.user_id 均 NOT NULL → 只改消息表
12 B-05 验收「全仓 grep 15936583816 = 0」 app/ 2 处(customer_service_rules.py:35、governance.py:46)、tools/ 2 处(种子话术)、tests/ 1 处断言,另加 2 处内联占位符;但 docs/** 有 14 处,其中 docs/演示用/软件需求文档-2026-09-14.md §421/§596 与 docs/验收与审计/phase1-acceptance-criteria.md:55 把它写成要求 范围收窄为 app/ tests/ tools/ knowledge/;docs/** 由 F-05 回写;补齐 3 处测试内联值(校正 13)
13 v4.0 未列的两处热线位置 ① tests/unit/core/test_customer_service_rules.py:67 assert CONTACT_PHONE == "15936583816";② tests/unit/service/test_customer_service_suitability.py:14 与 test_customer_service_search_query.py:19 的 FALLBACK 含 400-XXX-XXXX 补进 B-05 的第 ④ 组
14 D-07 落点含 app/infrastructure/milvus_knowledge_writer.py:72-74 该目录属 A-09 ④「禁止修改」,且 FIELD_DEFAULTS 默认 public 被全部写入方共用 降级为不改底座(B-01 门禁 + 文档登记),风险由「中」下调为「低」
15 customer_service_rules.py 属底座还是业务层 全仓消费方只有 implementations/customer_service.py 与测试;它承载的是客服自己的话术与关键词(非 N-1 的 8 项公共职责) 定性为业务层,可自改——这是 C-01/C-02/C-03/B-03/B-05 业务侧不需会签的依据

10. 待决 20 的结论(source_references:实现还是降级)

项 内容
事项 source_references 的取舍:由底座方实现,还是本期显式降级?
现状证据 ① _references()(customer_service.py:727)全仓无调用点(死代码);② tool_executor.py:129-131 只产出一条 SourceReference(source_type="tool", source_id=f"{trace_id}:{name}"),没有 doc 级引用;③ governance.review_output L305-309 只认 memory/tool 来源 → 知识类引用会让整个 run 失败;④ N-13:knowledge_tool.py 文件头自述「Agent 拿到的 source_references 也由基座统一附加,业务代码不能伪造来源引用」;docs/14 §1 亦把「工具权限、超时、审计和来源引用」列为底座职责
结论(v5.0 定稿) 请底座方提供(甲),客服侧不自行实现。理由:① 甲的两处半改动(tool_executor.py 登记 doc_id、governance.py 放行 knowledge 来源)都在底座件上,客服侧实现会直接违反 N-13 与 N-2;② v2.2 §3.3 把来源引用写进了设计,直接降级会留下文档与实现的口径差,而本次重构的目的正是清除这类差;③ 三处改动全在既有机制内,不新增架构层。
执行方式 由 A-10 会签申请单提出(§1.2 的条件触发项),排在 C-06 之后执行。
兜底 必须保留 S-8——若底座方本期不提供,则退回乙:C-10 只做「加护栏断言(禁止从知识出口调用 _references())+ 文档显式降级标注」,工作量约为甲的三分之一;可追溯性由审计承接(现状已能追溯到命中块)。

附录A · 与 v4.0 / v3.0 的编号映射

v4.0 / v3.0(CS-R 体系) 本版 v5.0 变化
CS-R-0005 / A-01A-06 A-01~A-06 对应(A-04 增第四份清单、A-07 增访客 user_id 形态核对)
— A-07、A-08、A-09 新增(复核 R-3、R-5、R-6)
— A-10 新增(会签申请单;附录B 为模板)
CS-R-10~19 B-01~B-05 合并为 5 项(原 3 次灌库 → 1 次);B-01 增第四查、B-05 范围与落点重排
CS-R-20~24 C-01~C-05 对应(C-01 语义纠错:不改名;C-05 落点改数据层)
CS-R-14/15 的验证部分 C-06 收敛为唯一放行闸门;新增方向 D 反例守卫
CS-R-40(挂起) E-07 挂起(决 10 确认保留档位)
CS-R-41~45 E-01~E-06 对应(E-01 增 agent_type 限定)
— E-08 新增(复核 R-4)
CS-R-50~57 F-01~F-07 对应(F-06 提前启动;F-05 增 3 组文档债)
原 C-07 + C-08 C-07 合并
— C-09、C-10 新增(复核 R-1、R-2;C-10 结论见 §10)
— D-07 → 降级 由「改底座」改为「门禁 + 文档」

方案文档 客服Agent模块重构方案与Todolist.md 的 CS-R-xx 编号已由本表取代;分层方案 CS-REFACTOR-2026-006 的 D-02 判据与 D-01 落点以本版 §9 为准。


附录B · 底座会签申请单(模板 —— 可直接抄送底座 owner)

用途:A-10 的产物。每处(每个文件)一张单,共 6 张 + 1 张条件触发(C-10 甲)+ 1 张数据层(C-05,仅需知会)。 纪律:先提案、后动手(P-5 / S-9)。提案通过前,客服侧不得为「绕过」而重造同一能力。

### 会签申请 <序号> · <底座文件名>

**申请人**:客服 Agent 重构(CS-REFACTOR-2026-005 v5.0)
**对应任务**:<D-01 / D-02 / D-04 / C-04 / B-05 / D-06+F-01 / E-01 / C-10甲>

1. 改什么(精确到函数/行)
   - <函数名>(L<起>-L<止>):<现状 → 改后>

2. 为什么是公共缺陷而非客服私需(对全部 Agent 的失效路径)
   - 影响面:<哪些 Agent / 哪些环境>
   - 失效路径:<不报错的静默失效,还是显式报错?>

3. 规范依据
   - docs/14 §<x>:<引文要点>
   - AGENTS.md 规则 <n>:<引文要点>
   - docs/18 §<x> / docs/05 §<x>:<引文要点>

4. 最小化边界(写清「不改什么」)
   - 不新增表字段 / 不改返回形状 / 不改函数签名 / 不改权限码与角色 / 不改 MAX 值与阈值常量
   - 零 DDL(`tools/audit_schema.py` 仍为 89 张业务表)

5. 影响面清单
   - 调用点:<文件:行>
   - 受影响的测试:<文件>

6. 验证方式(会签后由谁跑什么)
   - 单测 / 集成 / 脚本;判据 = 0 failed + 召回量与基线一致

7. 降级方案(若本期不受理)
   - <退化为:门禁 + 文档登记 / 业务层后置过滤(明确标注为降级) >

会签单清单(6 + 1 + 1)

单号 文件 任务 会签后谁执行
1 app/service/knowledge_search_service.py D-01 + D-02(同文件合并为一张单,减少往返) 底座 owner(或授权客服侧按单实施)
2 app/service/knowledge_tool.py D-04 同上
3 app/core/compliance_context.py C-04 同上
4 app/service/agent/governance.py B-05(底座侧:删副本 + 删白名单分支) 同上
5 app/service/agent_run_application_service.py D-06 + F-01(同文件合并) 同上
6 app/service/agent_persistence_service.py E-01(建单白名单 + priority,须限定 agent_type) 同上
7(条件) app/service/tool_executor.py(+ governance.py) C-10 甲(仅当底座方愿意提供来源引用) 底座 owner
知会 agent_negative_word 种子 C-05(数据层,零代码,仅需知会合规话术审核流程) 客服侧

附录C · 底座零改动清单(本次明确不碰)

类别 文件
Agent 骨架 app/service/agent/base.py、factory.py、bootstrap.py(除注册表不变)、app/service/tool_executor.py(除 C-10 甲)
公共契约与能力 app/core/memory_scope.py、knowledge_schema.py、knowledge_contracts.py、app/core/contracts.py
路径与基础设施 app/api/**、app/infrastructure/**、app/model/**、app/worker/**、alembic/**、app/main.py
权限与基线 tools/seed_test_rbac.py、docs/00-新数据库基线设计.md、alembic/baseline_generated.sql
其它公共工具 check_suitability、query_customer_profile、query_fund_quote 的实现与注册
其它业务模块 投顾模块全部文件、风控模块全部文件、场外/推广/NL2SQL/探针模块全部文件
说明 app/core/customer_service_rules.py 不在此清单——它是客服业务层(§9 校正 15),可自改

文档结束 —— 本看板为重构执行的唯一权威清单。 规模:46 项(45 项可执行 + 1 项挂起 E-07);6 个批次;关键路径 9 步 + 1 个会签等待窗口;九条硬串行约束;完工判据 10 条;重点盯防 7 项;底座触碰面 = 6 文件 / 8 处(须会签)+ 1 处条件触发 + 1 项数据层 + 2 项降级;待决 0 项(待决 20 已定稿:请底座方提供,否则降级乙)。 v5.0 修订小结:新增 §0.1 规范依据(N-1~N-14)与 §1 底座接触面总表;15 处判据以代码为准校正(含 D-02 重新定性、C-01 语义纠错、D-07 降级、B-05 范围收窄);新增 P-5 会签门与 S-9;新增附录B 会签申请单模板与附录C 零改动清单。