上一轮我只更新了接手文档,**漏了交付说明**——它的末次更新(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。
34 KiB
交付说明 · 客服 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 份全部恢复(§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_requestedapp/worker/knowledge_vector_worker.py→ 消费事件 →MilvusKnowledgeWriter写 Milvusapp/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 个测试文件全绿即为证)。
新增:
- 出口零:画像问答(
handle()最前面加一次is_profile_question()判定); allowed_tools增加query_customer_profile(代码上限);- 删掉 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)。
已处理:
- 环境升到 SQLAlchemy 2.0.52 + mypy 1.20.2(均在
pyproject.toml约束内)→ 184 → 3 个错; - 没有把
sqlalchemy2-stubs写进依赖(按你的明确要求); - 我文件里那 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. 需要你裁决 / 知晓的事项汇总
AgentGovernance.review()新增agent_type参数 —— ✅ 你已同意;审计留痕已按你要求补上 (§4.1,真机验证通过)。若你更希望走别的判定途径(例如让 Agent 自己声明customer_facing),告诉我,我改。- 检索字段名 —— ✅ 已按你 §1.3 改为运行时探测,不再是"三选一"(§4.2)。
删除 5 份编号文档—— ✅ 你驳回,已撤回,5 份全部恢复。docs/26-JWT密钥管理与轮换.md—— 是重命名(原docs/21)。✅ 你在第二轮批准, 并指出docs/21在我让号之前就已经是两份了(你的守卫脚本当时是失败的)—— 即我的让号顺手修好了你那边一个既有故障。config_release是环境数据 —— ✅ 归你(第二轮确认):你那台合并后补发一版, 把query_customer_profile加进customer_service:faq,其余 8 条原样继承。 你已记下"同 key 的继承项必须被本次新定义覆盖"这个坑。fund_query_demo:fund_quote白名单 —— 在我方环境缺失(你那边 201 里有)。属环境差异, 我方需补发;你那台不受影响。- 同事那条线带入的文档重号 —— ✅ 你在第二轮批准我让
docs/15→docs/27(依据:手册被docs/16/17/AGENTS.md三处引用,改名代价更大)。 - 同事那条线带入 11 个 alembic 迁移 —— ✅ 两边的坏状态都靠
alembic upgrade heads收敛: 我方是"版本号跑了、表没建",你方是"表没建、版本号也没跑"。你合并后需补跑(已在你的清单里)。 docs/00要不要补那 17 张表 —— ✅ 你在第二轮裁决:不动docs/00,另立登记。 我方已按此落地:新增docs/28-场外与推广域数据表登记.md(17 张表逐表登记 + 规则 8 两向边界核对), 并把docs/08的审计口径改为「场内 51 + 场外/推广 17 = 68」。docs/00一个字段都没动。
7. 已知限制(未做,不是遗漏)
-
画像链路"生产端已接、消费端未接" —— ✅ 归属已定(第二轮):消费端归你。 判定依据是你给的(生产者在我这边、你那边什么都没有 ⇒ 缺的是装配层的消费端,而
app/worker/正是你在维护)。我方保持不动,等你合并后接。你那台(你 grep 的结论) 我这台(实测) MemorySyncOutbox生产者无 有: profile_repository.py:142表内数据 空 2 行待处理( MILVUS/NEO4J各 1)消费端 无 有服务定义( ProjectionReconciliationService)但全仓无实例化点GraphProjectionWorker无实例化点(你发现) 同样无实例化点(我复核一致) 具体要接的三处(供你参考):
GraphProjectionWorker的实例化、与runtime.py的装配、ProjectionReconciliationService的接线。 -
mypy —— ✅ 已收敛:见 §5.2。环境升到位后从 184 → 3 个错,且剩余 3 个都在 同事那条线的文件里(非本线代码,你合并前那边没有这些文件)。没有擅自改 mypy 配置。
-
Redis 限流降级:容器以
--requirepass 123456启动而.env无密码 ⇒ 每次请求一条AuthenticationError堆栈、限流形同虚设(不阻断业务)。属本地环境配置,未擅自修改。 -
Docker Desktop 不常驻:它没运行时 Milvus 不可用(
dockerCLI 报连不上守护进程)。 我方已把这条写进AGENTS.md的环境口径。 -
5 份恢复的文档没有内容校对 ——
docs/04/06/10/13/99只做了"恢复",未逐字核对 其正确性(它们此前被判定为内容过期)。已在AGENTS.md的 D 类里标注"仅作历史参考", 但不排除其中仍有会误导读者的内容——若你要用它们,建议先过一遍。
本说明由 2026-09-11 的收尾会话产出;第二次修订在评审意见之后(见 §0)。
若你发现与代码不一致,以代码与 docs/05 为准。