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

609 lines
94 KiB
Markdown
Raw Permalink 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 重构 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-1~N-14)、§1 底座接触面总表、§2 用法、§3 决策落位索引、
> §4 需求覆盖矩阵、§6 的 S-1/S-4~S-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 归底座、词表**内容**归业务线);不改代码即零回归风险。**⚠️ 三条硬约束**:<br>① `governance.resolve()` L118-128 对**空数组**是**失败关闭跳过**(配置为空 = 规则永不生效),所以 `applicable_agents` 必须真的填;<br>② 不得改写既有 11 行的 `word_pattern`(`test_customer_service_rules.py:87-92` 断言相等);<br>③ 按**承诺性短语**收录而非形容词——用「承诺高收益/保证高收益/较 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-01~B-05(内容一致)、**C-09**(推介拦截)、D-01~D-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` **已转发**(不重复定义)——所以本项要清的是**下面五组**:<br>① **〔业务层·可自改〕** `customer_service_rules.py:35,38` 改定值(`400-826-9518` / 新服务时间),并同步 `P0/P1/P2/P4_REPLY`、`COMPLIANCE_REPLY` 里的引用(它们都拼 `CONTACT_PHONE`,自动生效);<br>② **〔需会签〕** `governance.py:46` 删 `CUSTOMER_SERVICE_HOTLINE` + `_mask_mobile` 白名单分支(§1.1 第 4 项);<br>③ **〔数据层〕** `tools/seed_compliance_baseline.py:99,101` 的两条 `agent_reply_template` 话术改值(保持 active + 已审核字段非空);<br>④ **〔业务层·测试〕** `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` 内联占位符同步;<br>⑤ **〔前端〕** 按 A-08 结论同步前端文案(若有)。<br>**验收**:`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` 的相等断言;<br>② **新增** `INPUT_PROMISE_WORDS`(8 条字面承诺词)+ `INPUT_PROMISE_PATTERNS`(句式),由 `hits_zero_tolerance()` 一并消费 | 单测 | A-02 | 中 |
| `[ ]` **C-02** | 输入侧改句式判定 | `customer_service_rules.py` | ① `(年化|七日年化|预期)收益率` + `(是多少|有多少|能达到|多高)` → 拦;+ `(是什么|什么意思|含义|怎么算|如何计算)` → 放行;② `安全` + `(保证|承诺|确保|一定|绝对)` → 拦;单独出现 → 放行;③ 保留原「收益+数字」句式(`YIELD_TRAP_PATTERNS` 四条不动) | 单测 | C-01 | 中 |
| `[ ]` 🔴 **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 的结论执行**:<br>**(甲·请底座方实现)**① `ToolExecutor` 登记本次 run 可引用的 `doc_id`;② `governance.review_output` 放行 `knowledge` 来源(L305-309 增加分支);③ 知识出口返回 `references`。**★ 必须由底座方实现**——N-13 明示来源引用是基座职责、「业务代码不能伪造来源引用」。验收:答案带 1–3 条来源,且**不导致 run 失败**。<br>**(乙·降级)**① 文档显式标注「本期不向客户展示来源引用」;② **加护栏断言:禁止从知识出口调用 `_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`)。<br>**★ 落点已收窄(校正 3)**:**不改 `app/core/knowledge_contracts.py`**——档位由 `context.roles` 推导(`tool_executor.py:116` 已把 `context` 传给 handler),`KnowledgeSearchInput` 保持 `extra="forbid"` 与三字段不变(N-11)。<br>**并核对**:`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` 里。<br>替身测试需覆盖**两种情形**:Ⅰ 部分集合有 / 部分没有 `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);单测覆盖访客/客户/未知角色。<br>**备选乙(不受理时)**:见 §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` 留痕。<br>**★ 边界(校正 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),风险远大于收益。<br>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-01~N-15、T-01~T-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-00~05 / A-01~A-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)。提案通过前,客服侧不得为「绕过」而重造同一能力。
```markdown
### 会签申请 <序号> · <底座文件名>
**申请人**:客服 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 零改动清单。