## 为什么做这一步 权威文档 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...`), 末尾带省略号,是接口文档的示意值,**不是可用凭据**。
609 lines
94 KiB
Markdown
609 lines
94 KiB
Markdown
# 客服 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 零改动清单。
|