Files
group_fqcd_jr/docs/交付说明-NL_develop-给架构师.md
T
qyqy 58849addcc docs: 交付说明补第二轮批复(含 mypy 归因纠正);修正残留过期数字
上一轮我只更新了接手文档,**漏了交付说明**——它的末次更新(19:15)早于架构师 19:43 的
第二轮答复,导致它还写着已被推翻的结论。本次补齐:

- 新增 §0-B:架构师第二轮的 9 条批复逐条落地对照(含"mypy 归因纠正""docs/00 不改另立登记"
  "两件事归他""那 2 个失败按新引入对待")
- §5.2 整体重写:**认错并给出四组对照实验**(SQLAlchemy 2.0.34+1.14→173、2.0.52+1.14→6、
  2.0.52+1.20→3),说明真因是 SQLAlchemy **补丁版**而非"缺类型存根";环境已升级到
  2.0.52+1.20.2,mypy 184→3;并列出剩余 3 个错全在同事那条线的文件里
- §5.1 接受架构师口径:那 2 个失败**不是"双方共有的既有失败"**,应作为合并后**新引入的失败**跟踪
- §2 净差异分两段重算:本线增量 16 模块/17 测试/5 工具 vs 同事那条线 334 文件/11 迁移
- §6/§7 更新为第二轮后的状态(哪些已批准、哪些归他、哪些已落地)
- 修正残留过期数字:结构审计 51→68 张表、去掉"缺 SQLAlchemy 2.0 类型信息"的错误论断
- 章节号整理:两个同名 "## 0" → "## 0-A / ## 0-B"

文档守卫 35 份无重号;mypy 3 错;测试 1218 passed。
2026-09-11 20:06:23 +08:00

34 KiB
Raw Blame History

交付说明 · 客服 Agent + RAG + 画像(NL_develop → qyqy_develop)

收件人:架构师(qyqy_develop 维护者) 来源分支:NL_develop 合并目标:qyqy_develop 基线:本分支已包含 qyqy_develop 的 3f7c5ca(含你与同事各自的最新推送,落后 0) 版本:第三版(§0-A 第一轮评审的落实 → §0-B 你第二轮答复的落实) 一句话:以你已有的客服实现为骨架,把本线独有的画像问答出口、知识库文档管理三端点、 合规语境豁免嫁接进去;按你的评审意见把检索字段名改为运行时探测; 并修掉合并过程中暴露的多个"单测全绿但真机必挂"的问题。

📌 本文档是给评审者看的,与另外两份的分工: docs/接手文档-NL_develop-给架构师.md(接手维护用)、 docs/评审意见回复-NL_develop.md(对照你的评审意见)、 docs/superpowers/handoff/…(给下一位开发者:怎么继续写代码)。


0-A. 第一轮修订:你首轮评审意见的落实情况(2026-09-11 晚)

你那份《评审意见》里的每一条我都核对过实测证据,除 §3.3 我照你的要求撤回之外,其余全部采纳。

你的意见 我的处理 落地位置
§1.1 我说的 active 216 在你那边不存在 ✅ 你是对的。我这边 config_release 总共只有 4 条(最高 id=216),你那台有 201/198/197… 与 9 条 agent_tools 白名单 —— 两台机器连的不是同一套 MySQL/Milvus。已把"216 已在共享库生效"整体改写为"我方环境已发布,你那台需在合并后补发" §4.9
§1.2 合并后画像出口会 AGENT_PERMISSION_DENIED ✅ 采纳。customer_service:faq 必须补 query_customer_profile。你已提出"我这边可以出脚本"——接受,由你来发(config_release 是环境数据,不随代码合并) §4.9
§1.3 字段映射不是 A/B/C,应改运行时探测 ✅ 已按你的方案实现:新增 app/core/knowledge_schema.py,describe_collection 拿真实字段名建映射、按集合缓存;visibility 有才过滤;缺必需字段的集合明确判为不可用。两套 schema 各有单测(任何回退到硬编码都会让其中一侧红) §4.2
§1.4 解释器不统一、mypy 数字不可比 ✅ 你的方向对;但我的归因在第二轮被你纠正了:我说"缺 SQLAlchemy 2.0 类型信息"是错的 —— 真因是我的 SQLAlchemy 补丁版太旧(2.0.34→2.0.52 使报错 173→6)。环境已升级,184 → 3 个错。详见 §5.2 §5.2
§2 docs/26 会和 docs/21 重复 ⚠️ 核对后是同名重命名而非并存:你那台的 docs/21-JWT密钥管理与轮换.md 与我这台的 docs/26-… 是同一份内容,我这边 21 已被你的《风控业务第二版迁移清单》占用,所以只能让号。合并后是一次 rename(删 21、加 26),不会出现两份同主题文档 §2 末
§3.1 agent_type 同意,但要写进审计 ✅ 已加:interaction_audit.detail 增 agent_type + governance_rewrite(不改表结构,detail 是 JSON 列);真机已验证最新审计行含这两个键 §4.1
§3.3 驳回删除 5 份文档 ✅ 撤回,5 份已全部恢复(git show 从你分支取回,逐字节相同)。AGENTS.md 改为"保留但仅作历史参考"并入 D 类 §1 第 3 项
§4 memory_sync_outbox ⚠️ 两台的结论不同,我这边更"进一步":我方有生产者(profile_repository.py:142)、表里已有 2 行待处理,但消费端从未被实例化(ProjectionReconciliationService( 全仓无调用点);你说的 GraphProjectionWorker 无实例化点在我这边同样成立。同一类问题:组件写好了、线没接。归属待定,见 §7 §7
§5 合并顺序 ✅ 按你的顺序:先对齐环境口径 → 改字段探测 → 撤回删除项 → 合代码 → 补发配置(你来) 全文

一句话总结这次核对:不是谁配错了,而是两台机器各自有独立的 MySQL + Milvus。 由此推出的第一条纪律:"配置已发布""数据是某 schema"这类结论必须带环境限定, 否则每次合并都要重吵一遍——这正是你 §1.4 想避免的事。


0-B. 第二轮修订:你答复里的批复已全部落地(2026-09-11 晚)

你在《架构师答复 · 第二轮》里的每一条都已按此执行,本文件是最终版:

你的批复 落地位置 / 状态
§1 mypy 根因是我的环境版本,不是缺存根 ✅ 我认错并复现:SQLAlchemy 2.0.34→2.0.52 使报错 173→6,mypy 1.14→1.20 再降到 3。环境已升级,mypy 184 → 3 个错。详见 §5.2
§1 不要把 sqlalchemy2-stubs 写进依赖 ✅ 已卸载、未写入任何依赖文件
§0 docs/26 让号批准(且修好了你的既有故障) ✅ 保留
§0 末 docs/27 让号批准 ✅ 保留
§2.1 画像白名单补发归你 ✅ 我这边不再重发;已把我的发布脚本坑(同 key 覆盖)说明给你
§2.2 memory_sync_outbox 消费端归你 ✅ 我方保持不动;要接的三处已在 §7 列出
§3 docs/00 不改,另立登记 + 更新审计口径 ✅ 新增 docs/28-场外与推广域数据表登记.md;docs/08 口径改为「场内 51 + 场外/推广 17 = 68」
§4 那 2 个失败不是"双方共有的既有失败" ✅ 接受你的口径:它们随本分支合并才出现,请当新引入的失败跟踪。详见 §5.1
§6 文档体系两套口径不需要改写索引 ✅ 维持现状
§7 环境重建 → 推送 NL_develop ✅ 已完成(环境升级 + 本文件与其余三份文档已推送)

还剩你那四件事(你答复 §5 自列的清单),我这边已全部就绪、不需要你再等我: ① alembic upgrade heads → ② 补发配置 → ③ 接 outbox 消费端 → ④ 补表登记。 其中 ④ 我已替两边做好(docs/28 + docs/08 随代码合并给你),你只剩前三件。


1. 请你重点看的四件事(都在 §4 有逐条说明)

# 事项 状态
1 AgentGovernance.review() 新增可选 agent_type 你已同意;审计留痕已按你要求补上(§4.1)
2 检索字段名改为运行时探测 ✅ 已按你的 §1.3 实现,替换掉原先的硬编码映射(§4.2)
3 删除 5 份编号文档 ✅ 已撤回,5 份全部恢复(§3.3 你驳回)
4 docs/26-JWT密钥管理与轮换.md 是重命名(原 21,因 21 被你占用),非新增(§2 末)

2. 净差异总量

⚠️ 本节已按最新状态重算(原文写的是相对 d2cdbba 的 93 项,那已过时)。 那次统计之后发生了两件事:同事那条线(袁聪,11 提交 / 334 文件)被并进来, 以及按你的两轮评审意见做的整改。所以"净差异"要分两段看:

2.1 我这条线独有的增量(相对你的客服/知识线)

类别 数量 说明
新增 app/ 模块 16 画像生成与投影、知识入库/检索/管理、Milvus 读写适配、文档解析与落盘、合规语境
新增 tests/ 17 与服务一一对应;另有 1 个 MySQL 集成测试
新增 tools/ 5 知识集合建表、种子导入、画像演示数据、合规种子、QA 素材解析
新增文档 5 交接文档、本交付说明、评审意见回复、接手文档、docs/28 表登记
修改你的核心文件 7 逐文件说明见 §4

2.2 同事那条线带入的量(不是我写的,但随本分支一并过来)

类别 数量
提交 11(含 6 个 merge/辅助)
文件 334
alembic 迁移 11(新建 17 张 offsite_* / promotion_* 表)
影响 表数 51 → 68(见 docs/28-场外与推广域数据表登记.md)

⚠️ 表数跳变不是"有人偷偷建表":tools/audit_schema.py 的期望集合是动态推导的 (读 baseline_generated.sql + 扫描 alembic/versions/*.py 的 CREATE TABLE), 随迁移自动增长。你合并后会看到 68 business tables,那是这 11 个迁移的预期结果。 .\.venv\Scripts\python.exe tools\audit_schema.py → 68 business tables, no missing or unexpected tables

我这条线本身未新增任何数据库表、未新增迁移:fin_knowledge_meta 等本就在 docs/00 基线内, 本次只补 ORM 映射。17 张新表全部来自同事那条线。


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)⚠️ 公共契约

# 原来
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)。

根因:两台机器的集合 schema 不同(这正是你评审 §1.3 指出的核心事实,我实测确认):

我这台(describe_collection 实测) 你那台(你实测)
标识字段 knowledge_id doc_id(主键)
正文 snippet content
可见性 无 visibility
来源/章节 无 source_file / chapter / section / doc_no
行数 106 / 177 / 73 125 / 297 / 214

所以"改读取侧字段名"这个方向本身是错的 —— 无论改成哪一套,都会把另一套打挂。 采纳你的方案(运行时探测),已实现:

# app/core/knowledge_schema.py(新增)
FIELD_CANDIDATES = {          # 逻辑名 → 该字段在各环境里可能的物理名(按优先级)
    "doc_id": ("doc_id", "knowledge_id"),
    "content": ("content", "snippet"),
    "visibility": ("visibility",),      # 可选:有就过滤,没有就跳过
    "source_file": ("source_file",),    # 同上(章节/编号同理)
    ...
}
REQUIRED_LOGICAL_FIELDS = ("doc_id", "content")   # 缺这两个 ⇒ 该集合判为不可用
# app/service/knowledge_search_service.py(改造)
schemas = {c: self._schema_for(client, c) for c in targets}   # 逐集合探测,按集合缓存
expression = self._visibility_filter(schemas, include_internal)  # 有 visibility 才拼
raw = client.search(..., output_fields=list(schema.output_fields))  # 只请求存在的字段

代价与边界(如实说明):

  • visibility 缺失的集合没有检索层内部资料隔离(我方 356 行均为对外知识,已核对); 你那台有该字段,过滤照常生效——所以这条能力不是被关掉,而是"按环境自动启用"。
  • 既无 doc_id 又无 knowledge_id(或既无 content 又无 snippet)的集合会被明确判为 不可用并记 degraded=True,不静默零召回——这是刻意的:静默零召回最难定位。

测试:新增 17 个探测单测;并把你那三个关键词召回用例参数化为两套 schema 各跑一遍 (tests/unit/service/test_knowledge_keyword_recall.py 的 SCHEMAS)。 任何回退到硬编码字段名的改动,都会让其中一侧立刻变红。

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>」的交集,缺配置时交集为空 ⇒ 任何工具调用都被拒。

⚠️ 首先纠正我上一版的错误表述:config_release 是环境数据,不随代码合并。 我上一版写"216 已在共享库生效"是错的——你那台根本没有 216(你实测最高 201), 两台机器各自有独立的 config_release 表。下面这张表只描述我方环境:

环境 生效版本 customer_service:faq 白名单
我方(我发布的) id=216(我发的) [search_knowledge, query_customer_profile] ✅ 画像出口可用
你方(你维护的) id=201(你发的) [search_knowledge] ❌ 缺 query_customer_profile

⇒ 合并后你那台必须补发一版,把 customer_service:faq 改成 [search_knowledge, query_customer_profile],其余 8 条原样继承 (config_release 是整版本替换,漏带会清空别人的白名单)。 这一步由你来发——你已在评审里提出"我这边可以出脚本",我接受:配置属环境数据, 谁的环境谁发布,代码合并不该携带它。

发布脚本的一个坑(与我方无关,但你会踩到):tools/publish_customer_service_config.py 会把当前生效版本的配置项原样搬进新版本,而同 key 的继承项必须被本次新定义覆盖—— 否则旧值(例如已从代码上限移除的 query_knowledge)会被 admin 端的子集校验 422 拦下, 整次发布失败,而报错只有"配置超出 Agent 工具上限",看不出是继承造成的。 我方已改为"同 key 覆盖"并打印被替换的旧值,这个改动随代码合并给你。

⚠️ 顺带:fund_query_demo:fund_quote 这条白名单在我方环境的生效版本里不存在 (我发布时"当前生效版本配置项"只打印出 1 条),而你那边 201 里是有的 —— 这又是两台环境不一致的证据。我方的 FundQueryDemoAgent 需要补发才能调用工具; 你那台不受影响。


5. 验证证据(均可复现)

# 测试(合并后基线)
.\.venv\Scripts\python.exe -m pytest -q
# → 3 failed, 1218 passed, 2 skipped
#   1 个是既有缺陷(test_fund_readonly_contract 的空集问题,与本线无关)
#   2 个是环境相关(§5.1 已说明:断言方式依赖 JSON 序列化配置,非代码缺陷)

# 结构审计(**注意:表数已从 51 变为 68**,原因见 §2.2)
.\.venv\Scripts\python.exe tools\audit_schema.py
# → schema audit passed: 68 business tables, no missing or unexpected tables

# 文档守卫(编号无冲突)
.\.venv\Scripts\python.exe tools\check_authoritative_docs.py
# → checked 35 documents, no number collision
#   顺带修了一处**同事那条线带入的重号**:`docs/15-金融NL2SQL工具接入说明.md` 与既有
#   `docs/15-Agent组员详细开发与使用手册.md` 撞号 → 新的那份让号到 `docs/27`(详见 §6)

5.1 两个环境相关失败(你提醒得对:它们不是"双方共有的既有失败")

tests/unit/service/test_offsite_document_recognition_adapter.py 的 2 个用例断言 请求体里是中文原文("产品代码".encode() in requests[1].content), 而本机 httpx 把中文序列化成 \uXXXX,字节序列自然不匹配。

你指出的关键事实我接受:这 2 个用例在你那条分支上根本不存在(同事那条线没进来), 所以它们不是"双方共有的既有失败",而是随本分支合并才会出现的。 ⇒ 合并后请当新引入的失败跟踪,不要按"历史遗留"放过。我这边已按此口径记录。

判定它不是代码缺陷的两条证据(供你复核): ① 该测试文件与 origin/qyqy_develop 逐字节相同(git diff 无输出)—— 非我方改动; ② 本机 .pytest_cache 的 lastfailed 里早已记录这两个用例(合并前的运行结果)。

功能无影响(OCR/LLM 请求本身正常)。建议改为断言 json.loads(body) 后的字段值 —— 字节级断言本来就不该用来测 JSON(你在答复里也是这个意见)。

5.2 mypy:你在第二轮的纠正成立,我原归因错了 ✅ 已收敛

先认错:我原先写"根因是本机缺 SQLAlchemy 2.0 的类型信息",方向对了一半、结论反了。 你的纠正成立 —— SQLAlchemy 2.0 自带 py.typed,sqlalchemy2-stubs 是给 1.4 用的 (装上后"181→43 看着变好",实际是换了一批按 1.4 API 核对的错)。

我做了决定性实验(四组对照):

组合 mypy app
SQLAlchemy 2.0.34 + mypy 1.14.1 ← 我原来的环境 173
SQLAlchemy 2.0.34 + mypy 1.20.2 173
SQLAlchemy 2.0.52 + mypy 1.14.1 6
SQLAlchemy 2.0.52 + mypy 1.20.2 3

⇒ 主因是 SQLAlchemy 的补丁版本(173 → 6):旧补丁版的类型标注不完整, BIGINT/DATETIME 被判成未类型化函数,于是 app/model/*.py 每处列定义都报一条。 mypy 版本是次因(6 → 3)。

已处理:

  1. 环境升到 SQLAlchemy 2.0.52 + mypy 1.20.2(均在 pyproject.toml 约束内)→ 184 → 3 个错;
  2. 没有把 sqlalchemy2-stubs 写进依赖(按你的明确要求);
  3. 我文件里那 8 个真实错误已修(你已看过,认为改法正确):
文件 错数 处理
app/service/knowledge_retrieval_service.py 4 ✅ 已修(Mapping→dict 等)
app/api/controllers/knowledge_management.py 3 ✅ 已修(工厂返回类型)

剩余 3 个错(都在同事那条线的文件里,非本线代码,你合并前那边没有这些文件):

app/service/offsite_document_recognition_adapter.py:788  redundant-cast
app/service/offsite_fund_service.py:1203                 return-value(dict[str, bool|None])
app/service/offsite_fund_service.py:1240                 attr-defined(Message.get_content)

按你"先放着"的意见,model_gateway.py 与 runtime.py 那两处我没动(也没用 cast 掩盖)—— 环境升上来之后它们确实自己消失了,与你的预判一致。

一个建议(供你裁决,我没擅自改):pyproject.toml 的 sqlalchemy>=2.0,<3 允许范围内 补丁版差异会带来 173 vs 3 的量级差异。若希望门禁数字稳定,建议把 SQLAlchemy 钉到具体补丁版 (例如 >=2.0.52,<2.1),否则同样的门禁命令在两个人的机器上可能给出完全不同的结论。

真机链路(真实 MySQL / Redis / Milvus / DashScope / HTTP,非单测替身):

场景 结果
知识问答「基金申购后多久确认」 succeeded,免责声明仅 1 条
知识问答「风险等级怎么划分」 succeeded(政策集合真实命中)
画像问答 9101 / 9001(测评过期) succeeded,过期被明说
知识库三端点 403 / 201 / 200 / 200+expired / 404 全绿
跨客户读画像 被拒(客户不可访问)

6. 需要你裁决 / 知晓的事项汇总

  1. AgentGovernance.review() 新增 agent_type 参数 —— ✅ 你已同意;审计留痕已按你要求补上 (§4.1,真机验证通过)。若你更希望走别的判定途径(例如让 Agent 自己声明 customer_facing),告诉我,我改。
  2. 检索字段名 —— ✅ 已按你 §1.3 改为运行时探测,不再是"三选一"(§4.2)。
  3. 删除 5 份编号文档 —— ✅ 你驳回,已撤回,5 份全部恢复。
  4. docs/26-JWT密钥管理与轮换.md —— 是重命名(原 docs/21)。✅ 你在第二轮批准, 并指出 docs/21 在我让号之前就已经是两份了(你的守卫脚本当时是失败的)—— 即我的让号顺手修好了你那边一个既有故障。
  5. config_release 是环境数据 —— ✅ 归你(第二轮确认):你那台合并后补发一版, 把 query_customer_profile 加进 customer_service:faq,其余 8 条原样继承。 你已记下"同 key 的继承项必须被本次新定义覆盖"这个坑。
  6. fund_query_demo:fund_quote 白名单 —— 在我方环境缺失(你那边 201 里有)。属环境差异, 我方需补发;你那台不受影响。
  7. 同事那条线带入的文档重号 —— ✅ 你在第二轮批准我让 docs/15 → docs/27 (依据:手册被 docs/16/17/AGENTS.md 三处引用,改名代价更大)。
  8. 同事那条线带入 11 个 alembic 迁移 —— ✅ 两边的坏状态都靠 alembic upgrade heads 收敛: 我方是"版本号跑了、表没建",你方是"表没建、版本号也没跑"。你合并后需补跑(已在你的清单里)。
  9. docs/00 要不要补那 17 张表 —— ✅ 你在第二轮裁决:不动 docs/00,另立登记。 我方已按此落地:新增 docs/28-场外与推广域数据表登记.md(17 张表逐表登记 + 规则 8 两向边界核对), 并把 docs/08 的审计口径改为「场内 51 + 场外/推广 17 = 68」。docs/00 一个字段都没动。

7. 已知限制(未做,不是遗漏)

  1. 画像链路"生产端已接、消费端未接" —— ✅ 归属已定(第二轮):消费端归你。 判定依据是你给的(生产者在我这边、你那边什么都没有 ⇒ 缺的是装配层的消费端,而 app/worker/ 正是你在维护)。我方保持不动,等你合并后接。

    你那台(你 grep 的结论) 我这台(实测)
    MemorySyncOutbox 生产者 无 有:profile_repository.py:142
    表内数据 空 2 行待处理(MILVUS/NEO4J 各 1)
    消费端 无 有服务定义(ProjectionReconciliationService)但全仓无实例化点
    GraphProjectionWorker 无实例化点(你发现) 同样无实例化点(我复核一致)

    具体要接的三处(供你参考):GraphProjectionWorker 的实例化、与 runtime.py 的装配、 ProjectionReconciliationService 的接线。

  2. mypy —— ✅ 已收敛:见 §5.2。环境升到位后从 184 → 3 个错,且剩余 3 个都在 同事那条线的文件里(非本线代码,你合并前那边没有这些文件)。没有擅自改 mypy 配置。

  3. Redis 限流降级:容器以 --requirepass 123456 启动而 .env 无密码 ⇒ 每次请求一条 AuthenticationError 堆栈、限流形同虚设(不阻断业务)。属本地环境配置,未擅自修改。

  4. Docker Desktop 不常驻:它没运行时 Milvus 不可用(docker CLI 报连不上守护进程)。 我方已把这条写进 AGENTS.md 的环境口径。

  5. 5 份恢复的文档没有内容校对 —— docs/04/06/10/13/99 只做了"恢复",未逐字核对 其正确性(它们此前被判定为内容过期)。已在 AGENTS.md 的 D 类里标注"仅作历史参考", 但不排除其中仍有会误导读者的内容——若你要用它们,建议先过一遍。


本说明由 2026-09-11 的收尾会话产出;第二次修订在评审意见之后(见 §0)。 若你发现与代码不一致,以代码与 docs/05 为准。