docs: 新增给架构师的交付说明(PR 评审用)

- 与'接手 AI 交接文档'定位区分:这份讲'动了架构师什么、为什么、要不要他点头'
- 逐文件列出对其 7 个核心文件的改动与理由(base.py 协议变更、knowledge_search_service
  字段映射的三选一裁决、governance 免责声明范围、model_gateway 映射并入、bootstrap/runtime 并集)
- 明示 6 项待裁决/知晓事项(含 5 份文档删除、config_release 216 已生效、fund_query_demo 白名单缺失)
- 证据段附可复现命令与真机结果;mypy 181 的错数按'谁引入'拆分为 170/8/3
- 已知限制 4 条(memory_sync_outbox 无消费者、mypy 归属、Redis 降级、文档删除的连带说明)
This commit is contained in:
qyqy
2026-09-11 15:59:29 +08:00
parent 96a6e01634
commit ac0b973006
@@ -0,0 +1,357 @@
# 交付说明 · 客服 Agent + RAG + 画像(`NL_develop` → `qyqy_develop`)
> **收件人**:架构师(`qyqy_develop` 维护者)
> **来源分支**:`NL_develop` @ `96a6e01` **合并目标**:`qyqy_develop`
> **基线**:本分支已包含 `qyqy_develop` 的 `d2cdbba`(即合并时你的最新版本,落后 0)
> **一句话**:以**你已有的客服实现为骨架**,把本线独有的**画像问答出口**、**知识库文档管理三端点**、
> **合规语境豁免**嫁接进去,并修掉合并过程中暴露的 3 个"单测全绿但真机必挂"的问题。
>
> 📌 **本文档是给评审者看的**,与给接手开发的 `docs/superpowers/handoff/2026-09-11-交接文档-客服Agent与RAG收尾.md`
> 定位不同:那份讲"怎么继续开发",这份讲"**动了你什么、为什么、要不要你点头**"。
---
## 1. 请你重点看的四件事(都在 §4 有逐条说明)
| # | 事项 | 为什么要你确认 |
|---|---|---|
| 1 | **改了 `AgentGovernance.review()` 的协议签名**(新增可选 `agent_type`) | 这是**公共契约**,会影响所有实现该协议的替身/子类;我同步改了 5 处测试替身 |
| 2 | **`knowledge_search_service.py` 的字段映射**(`doc_id`→`knowledge_id`、`content`→`snippet`) | 你的实现是按 `tools/load_knowledge_milvus.py` 的 schema 写的,而现库集合**是另一套字段** —— 详见 §4.2,这里有个需要你裁决的口径问题 |
| 3 | **删除了 5 份编号文档**(`docs/04`/`06`/`10`/`13`/`99`)+ 1 份过程产物 | 你分支上仍在,我的分支删了;若你希望保留,请在 review 时驳回这一项 |
| 4 | **`docs/26-JWT密钥管理与轮换.md` 是新文件** | 我新写的(原 `21`→`25`→`26` 两次让号,因为编号被占) |
---
## 2. 净差异总量(相对 `qyqy_develop` 的 `d2cdbba`)
```
92 项:新增 50 / 修改 35 / 删除 6 / 重命名 1
```
| 类别 | 数量 | 说明 |
|---|---|---|
| 新增 `app/` 模块 | **16** | 画像生成与投影、知识入库/检索/管理、Milvus 读写适配、文档解析与落盘、合规语境 |
| 新增 `tests/` | **17** | 与服务一一对应;另有 1 个 MySQL 集成测试 |
| 新增 `tools/` | **5** | 知识集合建表、种子导入、画像演示数据、合规种子、QA 素材解析 |
| 新增文档 | **13** | 交接文档、工作报告、验收证据(从被 gitignore 的 `.superpowers/` 复制过来)、ARCHIVE 归档、设计 spec |
| 修改 | 35 | 其中 **7 个是你的核心文件**(见 §4) |
| 删除 | 6 | 5 份编号文档 + 1 份过程产物(见 §1.3) |
**未新增任何数据库表、未新增迁移**:`fin_knowledge_meta` 等本就在 `docs/00` 基线内,本次只补 ORM 映射。
`.venv\Scripts\python.exe tools\audit_schema.py` → `51 business tables, no missing or unexpected tables`。
---
## 3. 本线交付了什么(按功能,附真机证据)
### 3.1 画像问答出口(你的实现里没有这个出口)
- 新增 `app/service/customer_profile_service.py`:`query_customer_profile` 只读工具。
权限 `memory:read:self`;作用域校验 `self`/`own_customers`/`all`;审计 `memory.profile_read`;
**`fields` 参数拒绝任何 PII 字段**;快照缺失/为空**失败关闭**(不返回空对象假装成功)。
- 新增 `app/core/profile_projection.py`:画像字段白名单投影,**PII 全剔除**,`assessment_expired` 重算。
- 新增 `app/service/profile_generation_service.py` + `app/repository/profile_repository.py`:
一次 MySQL 事务内 **新快照 + 2 条同步事件 + 旧快照 `is_current` 置 0**(符合 `docs/00` §6.4.6)。
- 修掉 `app/service/public_platform_service.py` 的 `memory()` **恒返回 `{}`** 静默故障 → 现返回投影。
- 在 `customer_service.py` 中新增**出口零**:`is_profile_question()` 确定性关键词识别(不经意图分类),
命中即查本人权威字段;失败关闭为转人工。
**真机证据**:
```
客户 9101(C5)→ succeeded 「您的风险测评等级是 进取型(C5)。」
客户 9001(测评过期)→ succeeded 「…保守型(C1)。注意:您的风险测评已过有效期,需要重新完成测评…」
规则类问题「风险等级怎么划分」→ 未被画像分支截走,走知识检索
跨客户读取 → GenericResourceNotFoundError: 客户不可访问
```
### 3.2 知识库文档管理三端点(老师验收标准第 7 条)
`app/api/controllers/knowledge_management.py` + `app/service/knowledge_management_service.py`
```
POST /api/v1/knowledge/upload → 201(切分 → 逐块写 fin_knowledge_meta → 投向量同步事件)
GET /api/v1/knowledge/list → 200(只返 content_preview 前 200 字,不做正文旁路)
DELETE /api/v1/knowledge/{id} → 200(status='expired' + 投向量删除事件;不存在 → 404)
```
**真机证据**(含向量的**真实进出**,不只是返回码):
```
客户 9001 上传 → 403 ✅ 权限隔离正确
管理员上传 → 201 ids=['369']
消费同步事件 → Milvus 出现 ✅
列表 → 200 命中本次上传 1 项 ✅
删除 → 200 {"status":"expired","vector_delete_event":"knowledge.vector_delete_requested"}
消费删除事件 → Milvus 向量消失(轮询 2s)✅
不存在 id → 404 ✅
```
> 📌 路径口径:老师草稿写的是 `POST /api/knowledge/upload`(无 `/v1`),实现为 `/api/v1/knowledge/upload`。
> `docs/05` §8.3 与 §19 目录(K002/K003/K004)均以带 `/v1` 的路径为权威。
### 3.3 合规语境豁免(恢复政策问答)
`app/core/compliance_context.py`(新增)+ `app/service/agent/governance.py`(改)
**问题**:零容忍词库是朴素子串匹配,它分不清"作出承诺"与"禁止承诺/谈论该表述"。
政策原文里含「严禁承诺保本保收益」「禁止使用『保证收益』」时,平台会**用自己的规则拦下自己的合规教材** ——
实测「基金销售有哪些合规要求」的回答从 1097 字被替换成 83 字的"该内容需要人工核实"。
**修法**:按**句界**限定窗口(12 字符)的否定/元语言线索豁免。第一版没限句界,
把 `严禁承诺保本。但这只基金保本` 错误豁免了 —— 已收紧为句内才认,并有 17 条单测锁定。
### 3.4 知识向量链路(入库 → 同步 → 检索)
- `app/service/document_parser.py`(txt/md/docx,512 字符切块 / 64 重叠)
- `app/service/knowledge_ingest_service.py` → 写 `fin_knowledge_meta` + 投 `knowledge.vector_sync_requested`
- `app/worker/knowledge_vector_worker.py` → 消费事件 → `MilvusKnowledgeWriter` 写 Milvus
- `app/service/knowledge_retrieval_service.py` → 读路径(Milvus 失败降级 MySQL LIKE,并如实标 `degraded`)
- 读写**物理隔离**(写适配器与召回客户端不共用连接)
**顺带修掉的 4 个真机静默故障**(都是"单测绿、真机挂"):
| # | 位置 | 缺陷 | 原后果 |
|---|---|---|---|
| 1 | `milvus_knowledge_writer.py` | 写字段名 `vector`,集合 schema 是 `embedding` | **所有**向量写入失败 |
| 2 | `knowledge_vector_worker.py` | Milvus VARCHAR 上限是 **UTF-8 字节**,未按字节截断 | `title` 346 字节 > 256 → 整批失败 |
| 3 | 同上 | 乱序到达的旧同步事件会把已删向量**复活** | 已删知识又出现在检索结果 |
| 4 | `knowledge_tool.py` | `async with _session_factory()` 少一层调用 | 工具 100% 抛错 |
---
## 4. 我对**你的代码**做的修改(逐文件说明)
### 4.1 `app/service/agent/base.py`(+6 −1)⚠️ 公共契约
```python
# 原来
result = await governance.review(result, context, config, memories)
# 现在
result = await governance.review(
result, context, config, memories, agent_type=self.definition.agent_type
)
```
**理由**:门禁 F5(面向客户输出 100% 附固定话术)只该对**面向客户**的 Agent 生效。
你的风控 Agent 输出是字段化摘要(预警编号/级别/建议动作),被追加一句面向投资者的免责声明后,
`tests/contract/test_risk_agent_contract.py` 直接失败(实测)。`agent_type` 从**定义**取最可靠
(不需要查库、与发布配置无关)。
**协议同步**(`governance.py`):`AgentGovernance.review` 与 `PlatformGovernance.review` 均新增
关键字参数 `agent_type: str = ""`,**带默认值**,所以旧的调用点不会 TypeError。
判定口径是**确认式白名单**:只有 `CUSTOMER_FACING_AGENT_TYPES = {customer_service, fund_query_demo}`
才注入;空串(未声明)**不注入**——理由见代码注释(测试替身刻意不连库,若默认注入则每个替身测试
都会被塞进话术,用测试噪声换假安全感)。
**受影响的测试替身已同步更新(5 处)**:`tests/conftest.py`、`tests/contract/test_fund_query_demo_agent_contract.py`、
`tests/contract/test_risk_agent_contract.py`、`tests/unit/service/test_agent_governance.py`
(`test_risk_agent_contract.py` 里那个 `StubGovernance` 也补了转发 `agent_type`)。
### 4.2 `app/service/knowledge_search_service.py`(+79 −47)⚠️ 需你裁决
**现象**:你的 `search_knowledge` 在本环境**零召回**且**整条链路失败** —— Milvus 报
`field doc_id not exist` / `field visibility not exist`,三个集合全失败 → `degraded=True` →
客服对**所有**知识问题一律"引导人工"(实测连「基金申购后多久确认」都 `failed`)。
**原因**:你的实现按 `tools/load_knowledge_milvus.py` 的 schema 读取
(`doc_id` / `content` / `chapter` / `section` / `doc_no` / `visibility`),
而**现库三个集合的真实字段**是(`describe_collection` 实测):
```
knowledge_id | title | snippet | tags | version | intent | embedding
```
那个建库脚本自述"**临时脚本,跑完即删**",所需的 `knowledge/_chunks.jsonl` **不在仓库里**;
**本环境从未按它建过集合**(现有 106/177/73 行数据是用另一套 schema 灌进去的)。
**我的处理**:**只改读取侧**,不改检索逻辑、不改 `KnowledgeHit` 的对外形状
(`doc_id` 字段名保留,你的 `_parent_of`、去重、父子块选择、字面召回逻辑一行没动):
```python
_FIELD_ALIASES = { # 逻辑名 → 现库真实字段名
"doc_id": "knowledge_id",
"content": "snippet",
"title": "title", "tags": "tags", "version": "version", "intent": "intent",
}
_HAS_VISIBILITY = False # 现库无此字段:拼 visibility == "public" 会让整次检索失败
```
**代价(必须让你知道)**:`_HAS_VISIBILITY=False` ⇒ 检索层**没有**内部资料硬隔离。
当前 356 行均为对外知识(已核对),所以不影响现状;但**你原设计的"反洗钱手册标 internal、
客服侧过滤"这条安全能力在本环境是关闭的**。
**三个选项,请你选一个**:
| 选项 | 做法 | 代价 |
|---|---|---|
| **A(当前采用)** | 保持字段映射,`visibility` 过滤关闭 | 内部资料隔离靠"入库侧只放对外知识"保证 |
| B | 按 `load_knowledge_milvus.py` 重建三集合并**重灌 356 行** | 需要恢复 `_chunks.jsonl`(不在仓库)+ 重新向量化 + 重建索引 |
| C | 给现库集合**新增** `visibility`/`chapter`/`section`/`doc_no` 字段并回填 | 需要一个 migration + 回填脚本;好处是 A 的代价消失 |
我选 A 是因为它**当天可用、可逆**(改回映射即可切换),且不引入迁移。
### 4.3 `app/service/agent/implementations/customer_service.py`(+137 −9)⚠️ 你的骨架 + 我的出口
**保留你的全部实现**(三档置信阈值、适当性裁决、闲聊、话题矩阵、`_topic_of`/`_search_query`/
`_suitability_text` 等方法**一字未改**——你的 3 个测试文件全绿即为证)。
**新增**:
1. **出口零:画像问答**(`handle()` 最前面加一次 `is_profile_question()` 判定);
2. `allowed_tools` 增加 `query_customer_profile`(代码上限);
3. **删掉 Agent 自己拼的 `DISCLAIMER` 常量及 3 处 `f"...\n{DISCLAIMER}"`** —— 见 §4.5。
> `PROFILE_WHITELIST_INTENT = INTENT_FAQ`:画像工具调用复用**已发布**的 `faq` 意图 key。
> 换新意图码会让 `allowed_tools_by_intent` 缺 key → 交集为空 → `AGENT_PERMISSION_DENIED`。
### 4.4 `app/service/model_gateway.py`(+53 −35)⚠️ 我改了你的映射表
**你的版本**把 `intent_classification` 映射成同名的 `intent_classification`;**现库没有任何端点声明该能力**
(实测 `embedding-primary=["embedding"]`、`chat-primary=["chat"]`)⇒ 筛选得空集,靠你新加的
"空集 → 回退全部端点"才不至于失败。我改为映射到 `chat`(意图分类走 `/chat/completions`)。
**同时并入你新增的 5 个风控 `task_type`**,与我的映射合并成**同一张表**(`REQUIRED_CAPABILITY_BY_TASK_TYPE`,
`TASK_CAPABILITY` 保留为别名指向它 —— **两个名字不要各存一份内容**,那正是"修复被后续合并悄悄回退"的成因)。
你新加的"未登记 task_type 要告警留痕"我保留了(并补上了被你删掉的 `logger` 定义)。
### 4.5 `app/service/agent/governance.py`(+141 −5)
| 改动 | 说明 |
|---|---|
| 语境豁免 | `_first_violation` 走 `app/core/compliance_context.py`,见 §3.3 |
| 客服热线白名单 | `CUSTOMER_SERVICE_HOTLINE = "15936583816"` 不再被脱敏成 `[手机号已脱敏]`(实测:客户拿不到联系方式) |
| 免责声明限定范围 | `agent_type` 判定,见 §4.1 |
| **免责声明重复的修复** | 见下 |
**免责声明曾出现两条**(Agent 自己拼一句 + 治理层追加权威话术):
```
## 第三章 客户风险等级划分
(以上内容由智能客服依据公司公开资料整理,不构成投资建议) ← Agent 拼的
本内容仅为投资分析参考,不构成任何直接投资建议… ← 治理层追加的
```
**修法**:话术**只由治理层注入**。理由:合规文案属**发布配置**,改文案不该改代码;
且治理层无法判断"业务是不是已经加过了"(它只认自己追加过的形状)。
副作用:`customer_service.py` 的 `DISCLAIMER` 常量被删除 → 你的 `test_customer_service_topic_matrix.py`
原本 `from ...customer_service import DISCLAIMER`,已改为从 `governance` 取 `FALLBACK_DISCLAIMER`
(该测试只用它拼"治理后"的正文形状,断言对象是 `_topic_of`,语义不变)。
### 4.6 `app/service/agent/bootstrap.py`(+40 −0)
- 补回 `get_milvus_knowledge_writer()`(知识向量**写**适配器装配入口;我的 Worker 依赖它,
返回 `None` 表示显式降级 —— 语义与你的 `projection_cleaner` 一致)。
- 注册 `query_customer_profile` 工具(`required_permission="memory:read:self"`)。
**你新增的平台验证探针**(`PlatformProbeAgent` + 两个探针工具)全部保留,与我的改动无交集。
### 4.7 `app/worker/runtime.py`(+50 −0)
- 新增知识向量同步的装配:`knowledge_writer` / `knowledge_embedder` / `knowledge_endpoint_resolver`
三个注入点 + 降级告警只打一次。**你新增的 `relationships`(图关系服务)保留**,两段是并集。
### 4.8 其他被修改的你的文件(改动很小)
| 文件 | 改动 |
|---|---|
| `app/core/knowledge_contracts.py` | **两条链路的契约并存**:你的 `KnowledgeSearchInput` + 本线的 `ALLOWED_COLLECTIONS`/`VECTOR_DIM`/`intent_for_qa_id`/`KnowledgeQuery`/`KnowledgeHit`/`KnowledgeSearchResult`。合并时一度误删后者,导致 **23 个测试模块收集失败**,已恢复并在文件头注明"删任何一半前先看两份引用点" |
| `app/main.py` | 挂载知识库管理路由(`knowledge_management_router`) |
| `tests/conftest.py` | 替身 `review` 转发 `agent_type` |
| `tools/publish_customer_service_config.py` | 见 §4.9 |
| `tests/unit/service/test_knowledge_keyword_recall.py` | 替身字段名改成现库 schema(否则测试假绿:业务读到空 `snippet` 静默丢弃,断言又只查 `doc_id`) |
### 4.9 配置发布(**这是运行期必须的一步,请留意**)
工具白名单是**失败关闭**的:`ToolExecutor` 取「代码 `allowed_tools` ∩ 发布配置
`agent_tools/<agent>:<intent>`」的交集,缺配置时交集为空 ⇒ 任何工具调用都被拒。
**问题**:合并后代码上限变成 `search_knowledge` / `check_suitability` / `query_customer_profile`,
而生效版本 `186` 的白名单是 `query_knowledge` ⇒ **交集为空** ⇒ 所有知识问题 `AGENT_PERMISSION_DENIED`。
**已发布并激活 `config_release id=216`**:
```
customer_service:faq = [search_knowledge, query_customer_profile]
customer_service:product_inquiry = [search_knowledge]
customer_service:policy_explain = [search_knowledge]
customer_service:suitability_check = [search_knowledge, check_suitability]
```
**顺带修了发布脚本 `tools/publish_customer_service_config.py` 的一个坑**:
`config_release` 是**整版本替换**语义,脚本会把当前生效版本的配置项原样搬进新版本 ——
但**同 key 的继承项必须被本次新定义覆盖**,否则旧值(含已从代码上限移除的 `query_knowledge`)
会被 admin 端的子集校验 422 拦下,**整次发布失败**,而报错只有"配置超出 Agent 工具上限",
看不出是继承造成的。已改为"同 key 覆盖"并打印被替换的旧值。
> ⚠️ **注意**:`fund_query_demo:fund_quote` 这条白名单在合并前的生效版本里**就已经不存在**了
> (我发布时打印的"当前生效版本配置项"只有 1 条)。这不是本次引入的,但会让
> `FundQueryDemoAgent` 的工具调用失败 —— 如果你那边有依赖它的验收项,请确认是否要补发。
---
## 5. 验证证据(均可复现)
```powershell
# 测试(合并后基线)
.\.venv\Scripts\python.exe -m pytest -q
# → 1 failed, 1013 passed, 2 skipped
# 唯一失败 tests/unit/repository/test_fund_readonly_contract.py 是既有的空集缺陷(与本线无关)
# 结构审计(证明未动 docs/00 基线)
.\.venv\Scripts\python.exe tools\audit_schema.py
# → schema audit passed: 51 business tables, no missing or unexpected tables
# 文档守卫(编号无冲突)
.\.venv\Scripts\python.exe tools\check_authoritative_docs.py
# → checked 23 documents, no number collision
# 静态检查(基线由 151 → 181;181 个里只有 11 个落在本线碰过的文件上,见下)
.\.venv\Scripts\python.exe -m mypy app
```
**mypy 181 个错的归属**(`mypy app | 按文件计数` 实测):
| 归属 | 文件 | 错数 |
|---|---|---|
| **架构师侧**(本线未改) | `app/model/fund.py` 105、`app/model/risk.py` 30、`app/model/configuration.py` 14、`app/model/session.py` 9、`app/model/platform.py` 5、`app/model/episode.py` 4、`app/model/conversation.py` 2、`app/repository/platform_repository.py` 1 | **170** |
| 本线新增文件 | `app/service/knowledge_retrieval_service.py` 5、`app/api/controllers/knowledge_management.py` 3 | **8** |
| 双方都改过的文件 | `app/service/model_gateway.py` 2、`app/worker/runtime.py` 1 | **3** |
即:**170 个是合并带入的既有问题,11 个落在本线改过的文件上**(新增文件只占 8 个)。若要清零,我可以单独开一个提交处理这 11 个。
**真机链路**(真实 MySQL / Redis / Milvus / DashScope / HTTP,非单测替身):
| 场景 | 结果 |
|---|---|
| 知识问答「基金申购后多久确认」 | `succeeded`,**免责声明仅 1 条** |
| 知识问答「风险等级怎么划分」 | `succeeded`(政策集合真实命中) |
| 画像问答 9101 / 9001(测评过期) | `succeeded`,过期被**明说** |
| 知识库三端点 | 403 / 201 / 200 / 200+expired / 404 全绿 |
| 跨客户读画像 | 被拒(`客户不可访问`) |
---
## 6. 需要你裁决 / 知晓的事项汇总
1. **`AgentGovernance.review()` 新增 `agent_type` 参数** —— 公共契约变更,虽带默认值。
若你更希望走别的判定途径(例如让 Agent 自己声明 `customer_facing` 属性),告诉我,我改。
2. **`knowledge_search_service.py` 的字段映射** —— §4.2 的 A/B/C 三选一,**当前是 A**。
3. **删除 5 份编号文档 + 1 份过程产物** —— 若你希望保留,请驳回这一项(它们在无人引用,
但删除会出现在 PR diff 里)。
4. **`docs/26-JWT密钥管理与轮换.md` 是新增文件** —— 原 `docs/21`→`docs/25`→`docs/26` 两次让号。
5. **`config_release 216` 已在共享库生效** —— 若你那边有其它 Agent 依赖旧的 `query_knowledge`
工具名,会一并受影响。
6. **`fund_query_demo:fund_quote` 白名单缺失**(§4.9 末尾)—— 可能影响 `FundQueryDemoAgent` 验收。
---
## 7. 已知限制(未做,不是遗漏)
1. **`memory_sync_outbox` 没有消费者** ⇒ 画像**没有真的同步到 Milvus / Neo4j**(事件写入了,没人消费)。
⚠️ 你最近那批提交里有"记忆→画像→图全自动触发",**这两处可能重叠**,请先确认再动手。
2. **`mypy` 由 151 → 181** —— 其中 **170 个是合并带入的既有问题**(`app/model/` 下 7 个文件 +
`platform_repository.py`),落在本线碰过的文件上的只有 **11 个**(新增文件占 8 个)。
若你希望本线把这 11 个清掉,我单独开一个提交处理(不动你的模型层)。
3. **Redis 限流降级**:容器以 `--requirepass 123456` 启动而 `.env` 无密码 ⇒ 每次请求一条
`AuthenticationError` 堆栈、限流形同虚设(不阻断业务)。属本地环境配置,未擅自修改。
4. **`docs/04`/`06`/`10`/`13`/`99` 若保留**,则 `AGENTS.md` 里"已删除"的说明需要同步改回。
---
*本说明由 2026-09-11 的收尾会话产出。若你发现与代码不一致,**以代码与 `docs/05` 为准**。*