diff --git a/AGENTS.md b/AGENTS.md index fb1a253..7f96a56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,10 +23,11 @@ > 当前工作分支是 **`NL_develop`**(个人分支 → PR 合回 `qyqy_develop`),**不要再用 `6516ccb`**。 > 它是对"当前状态"最准确的一份,读完它再读下面这些。 > -> 2026-09-11 做过一次文档清理:**已删除 5 份编号文档**(`04`/`06`/`10`/`13`/`99`)与 10 份过期过程产物。 -> 删除理由与内容去向见 `docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md`。 -> ⚠️ 注意:架构师的分支上**仍保留这 5 份**(只是没人引用),合并时保留了他的版本以不干扰其工作线; -> 若要彻底删掉,请在合回 `qyqy_develop` 的 PR 里单独说明。 +> **⚠️ 文档现状(2026-09-11 第二次修订)**:本文件原先声明"已删除 5 份编号文档", +> 那条**已作废** —— 经评审,`docs/04`/`06`/`10`/`13`/`99` **全部保留**(架构师明确要求保留: +> 删除收益为零,而保留成本同样为零)。它们的内容**未被核对过、可能过期**, +> 因此**列在下面的 D 类"不要用来判断当前进度"**里,只作历史参考。 +> 被删除的只有 10 份**过程产物**,理由与清单见 `docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md`。 ### A. 核心 7 份(无论接手哪条线都必读) @@ -65,16 +66,25 @@ | 文件 | 为什么 | |---|---| | `TODO.md` | **自 2026-09-09 起未随 Phase 1 更新**:5 处"49 张表"(实为 51 张业务表)、T8.1 客服 Agent 整节未勾选但**已交付**、多处标"进行中"其实已完成。**当前进度一律以 `phase1-acceptance-report.md` 为准。** | -| 已删除的 5 份 | `docs/04`(历史评审评分)、`docs/06`(正文与附录自相矛盾)、`docs/10`(结论已整体失效)、`docs/13`(工具清单不完整)、`docs/99`(已废弃规范) | +| `docs/04` / `06` / `10` / `13` / `99` | 内容**未核对过、可能过期**(自相矛盾 / 结论失效 / 清单不全)。经 2026-09-11 评审**保留**(不再删除),仅作历史参考。**不要用它们判断现状** | ### E. 环境与命令口径(易错点) -- 解释器固定 **`.\.venv\Scripts\python.exe`**。**不是** `conda activate jr_py313`、**不是** `D:\conda\envs\jr_py313`(旧文档里有这些残留写法,已修正主要几处)。 +- 解释器:本机用 **`.\.venv\Scripts\python.exe`**;架构师环境用 `D:\conda\envs\jr_py313\python.exe`。 + 两者等价,**各用本机可用的那个**(`.venv` 被 `.gitignore` 忽略、不进仓库,不存在"需要统一"的问题)。 - 数据库现为 **52 张表**(含 `alembic_version`)= **51 张业务表**。核验命令:`.\.venv\Scripts\python.exe tools\audit_schema.py`。 - 已注册业务 Agent:`FundQueryDemoAgent`、`CustomerServiceAgent`、`RiskAgent`、`PlatformProbeAgent`(见 `app/service/agent/bootstrap.py`)。 - 已注册公共只读工具:`search_knowledge`(客服知识检索)、`check_suitability`、`query_customer_profile`(画像)、`query_fund_quote`; **工具可用范围 = 代码上限 ∩ 当前 active `config_release` 的发布白名单**,缺发布配置则失败关闭。 - 白名单现由 `tools/publish_customer_service_config.py` 发布(当前生效版本 `id=216`); - 发布时**同 key 的继承项必须被本次定义覆盖**,否则旧值会被子集校验 422 拦下(脚本已处理)。 -- 测试基线:`1 failed, 1013 passed, 2 skipped`(2026-09-11 合并后实测);唯一失败是 `tests/unit/repository/test_fund_readonly_contract.py`(**底座既有缺陷,不要修也不要报**)。 -- mypy 基线 **181 个错**(`mypy app`)——其中 135 个集中在 `app/model/fund.py`+`app/model/risk.py`(架构师侧模型文件),接手时不要让它再变多。 +- ⚠️ **`config_release` 是环境数据,不随代码合并**:本机 active 版本 id 与架构师环境**不同** + (本机是我方发布的客服白名单;他那边还有风控的 9 条白名单)。**"白名单已发布"必须带环境限定**,换环境要重发。 + 发布脚本 `tools/publish_customer_service_config.py`(**同 key 的继承项必须被本次定义覆盖**,否则旧值会被子集校验 422 拦下整次发布)。 +- ⚠️ **Milvus 集合 schema 也因环境而异**:本机是 `knowledge_id`/`snippet`(无 `visibility`), + 架构师环境是 `doc_id`/`content`/`visibility`/`chapter`…。**检索层已改为运行时探测字段名** + (`app/core/knowledge_schema.py`)——**不要在任何地方硬编码字段名**,那会把另一套环境打挂。 +- ⚠️ **Docker Desktop 不会常驻**:它没运行时 Milvus 不可用(`docker` CLI 报连不上守护进程)。 + 跑真机验证前先确认 Docker Desktop 在运行。 +- 测试基线:`1 failed, 1034 passed, 2 skipped`(2026-09-11 实测);唯一失败是 `tests/unit/repository/test_fund_readonly_contract.py`(**底座既有缺陷,不要修也不要报**)。 +- mypy:本机 `mypy app` 报 181 个错,其中 170 个集中在 `app/model/` 的模型文件(**本机未安装 `sqlalchemy2-stubs`**, + SQLAlchemy 的 `BIGINT`/`DATETIME` 被判成未类型化函数);架构师环境报 0 错。 + **这个数字双方不可比**,不要拿它当结论;只需保证"不比自己改动前更多"。 diff --git a/app/service/agent_persistence_service.py b/app/service/agent_persistence_service.py index 3fdbbea..9fe1cde 100644 --- a/app/service/agent_persistence_service.py +++ b/app/service/agent_persistence_service.py @@ -11,6 +11,26 @@ from app.model.audit import InteractionAudit from app.model.conversation import ConversationMessage from app.model.platform import AgentRun, DomainEventOutbox, RequestIdempotency +#: 治理层追加免责声明时使用的分隔形状(`app/service/agent/governance.py` 里定义)。 +#: 这里只用于**审计留痕**,不参与任何判定:判据是"末尾是否出现这个形状"。 +_GOVERNANCE_APPEND_MARKERS: tuple[str, ...] = ("\n\n本内容仅为投资分析参考",) + + +def _governance_rewrote(result: AgentResult) -> bool: + """治理层是否改写过这次输出(用于审计)。 + + 两条可观测痕迹(都不改协议、只读结果本身): + 1. **追加了固定免责声明**:正文末尾出现治理层使用的分隔形状; + 2. **拦截并替换**:命中禁用词/硬规则时治理层会把回复换成安全话术并置 `transfer_required`。 + + 保守取值:任一条成立即记 True。它只是审计信息,判错方向的代价是"多标了一次", + 不会影响业务行为——因此宁可宽一点,也不为了精确而改动治理协议。 + """ + text = result.result.text or "" + appended = any(text.endswith(marker) or marker in text + for marker in _GOVERNANCE_APPEND_MARKERS) + return appended or bool(result.result.transfer_required) + class AgentPersistenceService: def __init__(self, session: AsyncSession) -> None: @@ -61,7 +81,19 @@ class AgentPersistenceService: self.session.add(InteractionAudit( actor_type="agent", actor_id=run.user_id, target_customer_id=run.user_id, session_id=run.session_id, portal="agent", action_type="agent.run_completed", - detail={"run_id": run_id, "result_message_id": message.id}, created_at=now, + # `agent_type` 必须落进审计:治理层(`PlatformGovernance.review`)会**改写对外 + # 输出**(追加固定免责声明、命中禁用词时整条替换成安全话术),事后要能回答 + # "这次改写是哪个 Agent 触发的、改写到了什么程度"。 + # `governance_rewrite` 记录治理是否动过输出:正文里出现固定话术的追加形状, + # 或该次运行被标记为需转人工(拦截分支会置 `transfer_required`)。 + # 不改表结构:`detail` 是 JSON 列,加键不需要迁移(AGENTS.md 规则 4)。 + detail={ + "run_id": run_id, + "result_message_id": message.id, + "agent_type": run.agent_type, + "governance_rewrite": _governance_rewrote(result), + }, + created_at=now, )) await self.session.execute( update(RequestIdempotency) diff --git a/docs/04-开发文档评审报告.md b/docs/04-开发文档评审报告.md new file mode 100644 index 0000000..3248f3d --- /dev/null +++ b/docs/04-开发文档评审报告.md @@ -0,0 +1,521 @@ +# 开发文档评审报告 + +> 评审对象:`docs/01-通用Agent平台开发设计.md`、`docs/02-数据库建表设计.md`、`docs/03-平台端到端流程文档.md` +> 评审基准:docs 目录 2026-09-08 版三份文档(01 标称 v2.1;02、03 未标注版本) +> 评审方式:逐行通读 + 三份文档交叉引用核对 + 按文档内容推演可实施性 +> 评审结论:**三件套综合 80/100(B+),可作为实施基线;P0 项闭环前不建议派发给编码 Agent** +> +> **复审状态**:本文 §1-§6 与附录 A/B 针对 v2.1 版本,行号引用以 v2.1 为准。团队已完成修订,v3.0 复审结果见 **附录 C**;v3.0 综合评分 **90/100(A-)**,MVC+S 架构合规性 **92/100**。 + +--- + +## 1. 总评分 + +| 文档 | 得分 | 等级 | 结论 | +|---|---|---|---| +| `01-通用Agent平台开发设计.md` | **86 / 100** | A- | 架构与契约达到可直接编码水准,存在 3 处自相矛盾 | +| `02-数据库建表设计.md` | **77 / 100** | B+ | 6 张专项表 DDL 质量高,基础表基线不自包含,缺关键表 | +| `03-平台端到端流程文档.md` | **76 / 100** | B+ | 流程与职责边界清晰,落点与 02 冲突,缺幂等细节 | +| **三件套综合(01 占 40%,02/03 各占 30%)** | **80 / 100** | **B+** | 可进入实施,须先闭环 5 项 P0 | + +### 1.1 分维度得分 + +| 维度 | 权重 | 01 | 02 | 03 | +|---|---|---|---|---| +| 完整性与自包含性 | 20 | 16 | 13 | 14 | +| 内部一致性与准确性 | 20 | 16 | 15 | 15 | +| 架构与技术合理性 | 25 | 23 | 20 | 19 | +| 安全 / 合规 / 风控 | 15 | 14 | 13 | 13 | +| 可测试性与可验收性 | 10 | 9 | 8 | 8 | +| 工程可执行性 / 可维护性 | 10 | 8 | 8 | 7 | +| **合计** | **100** | **86** | **77** | **76** | + +### 1.2 等级标准 + +| 等级 | 分数 | 含义 | +|---|---|---| +| A | 90-100 | 优秀,可直接落地 | +| B | 75-89 | 良好,修订后可落地 | +| C | 60-74 | 需较大重构 | +| D | < 60 | 需重写 | + +--- + +## 2. 总体评价 + +### 2.1 值得肯定的部分 + +1. **架构分层可执行**。01 §3.1 的分层职责表明确列出每层"不允许承担的职责",比只写"职责"的文档强一个档次;§3.3 给出 Controller/Service 的最小代码约束。 +2. **模板方法约束到位**。`run_stream()` 标记 `@final`,业务子类只实现 `handle()`,§15.3 用契约测试扫描受保护方法(01:1296-1313),把"不许重写骨架"从口头约定变成可执行检查。 +3. **契约设计专业**。全部跨层对象使用 frozen dataclass,集合用 `tuple`/`frozenset`,依赖用 `Protocol` 倒置,Repository 与外部客户端全部隐藏在 Service 之后(01:491)。 +4. **配置权威链清晰**。01 §7.3 的 5 级优先级(安全硬约束 > 代码上限 > 数据库配置 > 代码默认值 > 环境变量),并明确"数据库只能缩小不能扩大",工具取三层交集(01:1050-1054)。 +5. **降级矩阵完整**。01 §12 与 03 §13 对 Redis/Milvus/Neo4j/模型/工具/审计/事件逐项给出处理策略,且区分"降级"与"阻断"。 +6. **编码 Agent 交接模板可直接使用**。01 §15.5 把任务正文、允许/禁止修改范围、必测项、验收命令、完成回报格式全部固化,这是同类文档中罕见的工程化细节,可当任务单直接派发。 +7. **专项表 DDL 规范**。02 §7 的 6 张表统一带 CHECK 约束、版本列、审核人、`created_at`/`updated_at`、审计语义,索引设计考虑了查询路径(队列索引 `(status, priority, created_at)`、前缀索引 + `phrase_hash` 规避长文本误判)。 +8. **职责边界写得好**。03 §2 的"禁止事项"列和 §10.3「Agent 不能确认、关闭或升级预警」把 AI 与人的决策边界钉死,这是金融场景的必备约束。 + +### 2.2 结构性问题(一句话概括) + +**01 是权威源,02 和 03 承接它的能力,但各自都漏了一个 01 已经承诺的持久化载体**——澄清轮次、通用领域事件、工具调用记录。这是"设计已完成、数据模型没跟上"的典型缺口,越晚补代价越大。 + +--- + +## 3. 分文档详评 + +### 3.1 `01-通用Agent平台开发设计.md`(86 / A-) + +**扣分明细** + +| 编号 | 问题 | 位置 | 严重度 | +|---|---|---|---| +| A1 | `clarification_round` 无持久化载体,澄清/转人工逻辑无法实现 | 01:245、744 | P0 | +| A2 | 通用领域事件要求 Outbox,但 02 无对应表 | 01:1125 | P0 | +| A3 | `allowed_tools` 按意图配置(02)与单集合消费(01)冲突 | 01:273、1261 | P0 | +| A4 | 合规策略来源不一致:主流程用 `config`,降级用 `definition` | 01:583 vs 816 | P1 | +| A5 | `CoreResult` 互斥语义矛盾:正文"不能同时为空",实现"恰好一个非空" | 01:368 vs 770 | P1 | +| A6 | 目录树缺 `bootstrap.py`、`AgentAuthorizer`,但 §7.1/§15.3 引用 | 01:139-152 vs 996、1292 | P1 | +| A7 | 伪流式与"首个安全 Token 延迟"指标冲突 | 01:579-636、907、1131 | P1 | +| A8 | `save_user_message` 在记忆召回前,与七步描述不符;异常路径遗留孤儿用户消息 | 01:573 vs 925 | P1 | +| A9 | `@final` 仅静态约束,契约测试用 `__dict__` 扫描可被 `setattr` 绕过 | 01:1312 | P2 | +| A10 | "核心公共逻辑覆盖率 ≥80%"未界定范围 | 01:1167 | P2 | +| A11 | 意图集合能否被数据库扩展未明确(§7.3 只列角色/入口/工具为代码上限) | 01:1040 | P2 | +| A12 | 幂等键仅提要求,无幂等表 / Redis key 规范 | 01:936 | P2 | +| A13 | Prompt 与模型参数"必须版本化并写入审计"无落地载体 | 01:1087 | P2 | +| A14 | 示例代码 `asdict` 在第 794 行使用,导入语句在第 853 行 | 01:794、853 | 文案 | + +**详细说明(关键三项)** + +- **A4 合规策略来源不一致**:主流程 `guard_output(raw_reply, config.compliance_policy, context)`(01:583)使用解析后的策略;降级分支 `fallback_service.reply_for(error.code, self.definition.compliance_policy)`(01:816)使用代码默认值。若数据库配置收紧过策略,降级路径会绕过该收紧。**建议**:统一改为 `config.compliance_policy`,或在 `_persist_degraded_result` 签名中传入 `config`。 +- **A5 互斥语义矛盾**:01:368 写"两者不能同时为空",01:770 的实现是 `if has_direct == has_material: raise`,即同时禁止两者**同时非空**。**建议**:正文改为"`direct_reply` 与 `generation_material` 必须恰好提供一个",与实现对齐。 +- **A7 伪流式**:当前顺序为「全量生成 → 合规校验 → 落库 → 审计 → 发事件 → 按 256 字符切片发 `delta`」(01:579-636)。SSE 的价值退化为分块传输,`delta` 并非 token 级增量,首字延迟 ≈ 全量生成 + 合规 + 落库时间。**建议**:要么在 §13 删除"首个安全 Token 延迟"指标并明确"合规优先、非真流式"的取舍;要么设计"完整句子缓冲 + 句级合规"方案并单独评估泄漏风险。 + +### 3.2 `02-数据库建表设计.md`(77 / B+) + +**扣分明细** + +| 编号 | 问题 | 位置 | 严重度 | +|---|---|---|---| +| B1 | 基础 33 张表以外部《合并后数据库表设计last.md》为基线,该文件不在仓库,§12 验收无法执行 | 02:5 | P0 | +| B2 | 无会话主表,`session_id` 仅为逻辑外键,澄清轮次与会话状态无处存储 | 02:97 | P0 | +| B3 | 无通用领域事件 Outbox 表,仅 `memory_sync_outbox` | 02:74 | P0 | +| B4 | `conversation_message` 缺 `tool_calls` 列,与 03:138 冲突 | 02:120-126 | P0 | +| B5 | `fin_knowledge_meta` 有效期过滤用 `CURRENT_DATE`,与"统一 UTC"约定冲突 | 02:172-173 vs 36 | P1 | +| B6 | `uk_feedback_message_customer(message_id, customer_id)`:MySQL 中 NULL 不参与唯一性,匿名反馈可重复刷票 | 02:334 | P1 | +| B7 | "同一时刻仅一个有效版本"仅靠服务层保证,缺数据库级加固 | 02:385、489 | P1 | +| B8 | 无归档 / 分区 / 容量策略;`conversation_message`、`interaction_audit`、`fin_market_price`、`memory_sync_outbox` 只增不减 | 02 全文 | P1 | +| B9 | 02、03 无版本号与变更记录(仅 01 标 v2.1) | 02 / 03 头部 | P1 | +| B10 | `ALTER TABLE ... AFTER trace_id` 未说明前置列存在性检查 | 02:132 | P2 | + +**详细说明(关键两项)** + +- **B6 NULL 唯一性陷阱**:MySQL 唯一索引中多个 NULL 视为互不相等,因此 `customer_id IS NULL` 的匿名反馈可无限重复插入,`uk_feedback_message_customer` 对其完全失效。文档虽要求"必须由 API 限流"(02:346),但把约束责任从数据库移到应用层。**建议**:`customer_id` 使用 `0` 作为匿名哨兵值,或增加 `client_fingerprint` 列参与唯一键。 +- **B7 有效版本唯一性**:`agent_reply_template` 与 `agent_intent_config` 的"同一时刻仅一个有效版本"依赖发布服务事务保证(02:385、489),并发发布存在窗口。**建议**:用生成列 + 唯一索引在数据库层强制(见 §5.1 补丁)。 + +### 3.3 `03-平台端到端流程文档.md`(76 / B+) + +**扣分明细** + +| 编号 | 问题 | 位置 | 严重度 | +|---|---|---|---| +| C1 | 要求落库 `tool_calls`,02 无该列 | 03:138 | P0 | +| C2 | 澄清轮次存储未定义,§5.3 "每轮只追问一个关键槽位"无法实现 | 03:112 | P0 | +| C3 | 异常矩阵缺"事件发布失败"行,与 01 §12 不对齐 | 03:361-372 vs 01:1125 | P1 | +| C4 | 无版本号与变更记录 | 03 头部 | P1 | +| C5 | 缺幂等、超时预算、重试上限的端到端数值 | 03 §4、§5 | P2 | +| C6 | 全程纯文本流程,跨 4 张表事务(§9)缺时序图 / 状态图 | 03 §9、§10 | P2 | +| C7 | Redis 实时行情不可用时交易流程如何处理未说明 | 03:248 | P2 | + +**说明**:03 的职责边界、异常矩阵、数据落点表(§14)质量高,主要问题是从 01/02 继承的落点缺口,而非自身叙述缺陷。 + +--- + +## 4. 问题清单汇总 + +### 4.1 P0:不闭环不能开工(5 项) + +| # | 问题 | 涉及文档 | 建议修复 | +|---|---|---|---| +| P0-1 | `clarification_round` 无持久化载体 | 01:245、744 / 02:97 / 03:112 | 新增 `svc_conversation_session` 表(见 §5.1) | +| P0-2 | 通用领域事件无 Outbox 表 | 01:1125 / 02:74 | 新增 `domain_event_outbox` 表(见 §5.1) | +| P0-3 | `tool_calls` 落点缺失,01/02/03 不一致 | 03:138 / 02:120-126 | `conversation_message` 增补 `tool_calls JSON`(见 §5.1) | +| P0-4 | `allowed_tools` 按意图配置 vs 单集合消费 | 02:402 / 01:273、1261 | 修改 `ResolvedAgentConfig` 契约(见 §5.2) | +| P0-5 | 数据库基线不自包含,验收无法执行 | 02:5 | 基线 DDL 并入 02 或作为附录提交 | + +### 4.2 P1:编码前应修正(8 项) + +| # | 问题 | 位置 | +|---|---|---| +| P1-1 | 合规策略来源不一致(`config` vs `definition`) | 01:583 vs 816 | +| P1-2 | `CoreResult` 互斥语义矛盾 | 01:368 vs 770 | +| P1-3 | 目录树缺 `bootstrap.py`、`AgentAuthorizer` | 01:139-152 vs 996、1292 | +| P1-4 | 伪流式与首字延迟指标冲突 | 01:579-636、907、1131 | +| P1-5 | 用户消息落库顺序与七步描述不符;异常路径孤儿消息 | 01:573 vs 925 | +| P1-6 | `CURRENT_DATE` 与 UTC 约定冲突 | 02:172-173 vs 36 | +| P1-7 | 匿名反馈唯一索引因 NULL 失效 | 02:334 | +| P1-8 | 无归档 / 分区策略;02、03 无版本号 | 02 全文、03 头部 | + +### 4.3 P2:建议增强(6 项) + +| # | 问题 | 位置 | +|---|---|---| +| P2-1 | 幂等键无表 / Redis key 规范 | 01:936 / 03:4.2 | +| P2-2 | Prompt 版本化无落地载体 | 01:1087 | +| P2-3 | 意图集合能否被数据库扩展未明确 | 01:1040 | +| P2-4 | `@final` 仅静态约束,契约测试可绕过 | 01:1312 | +| P2-5 | 覆盖率范围未界定;缺超时预算与 SLA 数值 | 01:1167 / 03 §4-5 | +| P2-6 | 缺时序图 / 状态图;Redis 行情故障路径未定义 | 03 §9、§10 | + +--- + +## 5. 建议修复方案 + +### 5.1 P0 表结构补丁 DDL + +> 以下 DDL 与 02 §7 风格一致,需通过 Alembic 生成迁移,并在空库、旧版本库、回滚路径各演练一次。 + +#### 5.1.1 `svc_conversation_session`(修复 P0-1) + +承载会话状态与澄清轮次,使 01:744 的低置信澄清/转人工逻辑可实现。 + +```sql +CREATE TABLE svc_conversation_session ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + session_id VARCHAR(64) NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + portal VARCHAR(32) NOT NULL, + agent_type VARCHAR(32) NULL, + status VARCHAR(16) NOT NULL DEFAULT 'active', + clarification_round TINYINT UNSIGNED NOT NULL DEFAULT 0, + message_count INT UNSIGNED NOT NULL DEFAULT 0, + last_intent VARCHAR(32) NULL, + started_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + last_active_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + ended_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) + ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (id), + UNIQUE KEY uk_session_id (session_id), + KEY idx_session_user (user_id, last_active_at), + KEY idx_session_status (status, last_active_at), + CONSTRAINT fk_session_user FOREIGN KEY (user_id) REFERENCES sys_user(id), + CONSTRAINT chk_session_status + CHECK (status IN ('active', 'ended', 'transferred', 'expired')), + CONSTRAINT chk_session_clarification + CHECK (clarification_round <= 10) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + COMMENT='Agent会话状态与澄清轮次'; +``` + +配套约定: + +- `RequestContextBuilder` 每次请求读取该行,将 `clarification_round` 写入 `RequestContext`。 +- 澄清轮次递增使用条件更新:`UPDATE ... SET clarification_round = clarification_round + 1 WHERE session_id = ? AND clarification_round = ?`,避免并发重复澄清。 +- Redis 作为热缓存,MySQL 为权威来源;Redis 丢失可从本表恢复。 +- `ended_at` 仅在 `end_session = true` 或超时过期时写入。 + +#### 5.1.2 `domain_event_outbox`(修复 P0-2) + +承载 01 §5.4 `EventPublisher` 与 01:882 `build_domain_events()` 产出的通用领域事件,与 `memory_sync_outbox` 职责分离。 + +```sql +CREATE TABLE domain_event_outbox ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + event_id CHAR(36) NOT NULL, + event_type VARCHAR(64) NOT NULL, + aggregate_type VARCHAR(32) NOT NULL, + aggregate_id VARCHAR(64) NOT NULL, + trace_id VARCHAR(64) NOT NULL, + payload JSON NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'pending', + retry_count INT UNSIGNED NOT NULL DEFAULT 0, + next_retry_at DATETIME(6) NULL, + last_error VARCHAR(500) NULL, + occurred_at DATETIME(6) NOT NULL, + published_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) + ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (id), + UNIQUE KEY uk_domain_event_id (event_id), + KEY idx_domain_outbox_pending (status, next_retry_at, id), + KEY idx_domain_outbox_aggregate (aggregate_type, aggregate_id), + KEY idx_domain_outbox_trace (trace_id), + CONSTRAINT chk_domain_outbox_status + CHECK (status IN ('pending', 'published', 'failed', 'dead')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + COMMENT='通用领域事件Outbox'; +``` + +配套约定: + +- 事件与业务写入(`conversation_message`、`interaction_audit`)在同一事务提交,满足 02 §10 的原子性要求。 +- `uk_domain_event_id` 保证消费者幂等,重复投递不会重复处理。 +- 重试采用指数退避写 `next_retry_at`,超过阈值置 `dead` 并告警。 + +#### 5.1.3 `conversation_message` 增补 `tool_calls`(修复 P0-3) + +```sql +ALTER TABLE conversation_message + ADD COLUMN tool_calls JSON NULL + COMMENT '脱敏后的工具调用摘要数组' AFTER source_references; +``` + +同步修正 03 §5.6 与 01 §6.2:`AgentResult.tool_calls` 序列化后写入该列,字段结构与 SSE `tools` 事件(01:628 `serialize_tool_calls`)保持一致,禁止写入未脱敏参数。 + +#### 5.1.4 有效版本唯一性加固(修复 P1-7 的同类问题) + +```sql +ALTER TABLE agent_reply_template + ADD COLUMN active_key VARCHAR(96) GENERATED ALWAYS AS + (IF(status = 'active', CONCAT(template_code, ':', locale), NULL)) STORED, + ADD UNIQUE KEY uk_reply_template_active_one (active_key); + +ALTER TABLE agent_intent_config + ADD COLUMN active_key VARCHAR(128) GENERATED ALWAYS AS + (IF(status = 'active', CONCAT(agent_type, ':', intent_code), NULL)) STORED, + ADD UNIQUE KEY uk_intent_config_active_one (active_key); +``` + +利用"唯一索引中 NULL 不参与比较"的特性,非 `active` 行 `active_key` 为 NULL 可无限共存,`active` 行则强制唯一,把服务层约定升级为数据库级约束。 + +#### 5.1.5 时间过滤修正(修复 P1-6) + +02:172-173 的 `CURRENT_DATE` 依赖会话时区,与 02:36"统一使用 UTC"冲突。修正为: + +```sql +review_status = 'published' +AND status = 'active' +AND (effective_date IS NULL OR effective_date <= UTC_DATE()) +AND (expire_date IS NULL OR expire_date > UTC_DATE()) +``` + +或在仓储层显式传入已计算的 UTC 日期参数,避免 SQL 依赖会话时区。 + +### 5.2 代码契约修正清单(01) + +| 编号 | 修正内容 | 位置 | +|---|---|---| +| F1 | `_persist_degraded_result` 使用 `config.compliance_policy`,签名增加 `config` 参数 | 01:802-817 | +| F2 | `CoreResult` 注释改为"`direct_reply` 与 `generation_material` 必须恰好提供一个" | 01:368 | +| F3 | 目录树补 `service/agent/bootstrap.py`、`service/agent/authorizer.py`,并定义 `AgentAuthorizer` 协议 | 01:139-152 | +| F4 | `ResolvedAgentConfig.allowed_tools` 改为 `Mapping[str, frozenset[str]]`(按意图),或在文档中明确"Agent 级并集 + 工具内二次校验" | 01:273 | +| F5 | §13 明确"合规优先、非真流式",删除或重新定义"首个安全 Token 延迟" | 01:1131 | +| F6 | §6.3 第 1 步补充"保存用户消息",与 §6.2 实际顺序一致;明确 `AgentRequestError` 后孤儿用户消息的处理策略 | 01:920-926 | +| F7 | §7.3 第 2 条补充 `supported_intents` 是否为代码级上限 | 01:1040 | +| F8 | 契约测试增加运行期守卫或 CI AST 检查,替代仅依赖 `__dict__` 扫描 | 01:1312 | +| F9 | §14.1 界定"核心公共逻辑"的具体模块范围 | 01:1167 | + +### 5.3 流程文档补强(03) + +| 编号 | 修正内容 | +|---|---| +| G1 | §13 异常矩阵增加"事件发布失败 → Outbox 异步重试,不丢失关键事件" | +| G2 | §5.3 补充澄清轮次的读取与递增路径,指向 `svc_conversation_session` | +| G3 | §9 补充 Redis 实时行情不可用时的处理(拒绝下单 / 降级为最近收盘价并明确提示,二选一) | +| G4 | 增加会话生命周期时序图与交易事务时序图 | +| G5 | 头部补充版本号、修订记录与关联文档索引 | + +--- + +## 6. 放行条件与后续动作 + +### 6.1 放行条件(全部满足才可派发编码) + +- [ ] P0-1 ~ P0-5 全部闭环,且 02 的基线 DDL 已并入或附于文档内 +- [ ] 三份文档的交叉引用一致性核对通过(见附录 A) +- [ ] P1 项已修正或已登记为有责任人的待办 +- [ ] 02、03 补齐版本号与修订记录 + +### 6.2 建议动作顺序 + +1. **补表**:执行 §5.1 的 4 组 DDL 补丁,更新 02 的表总览(39 → 42 张)。 +2. **修契约**:按 §5.2 修正 01 的 9 处代码级不一致。 +3. **对齐流程**:按 §5.3 补强 03 的异常矩阵与关键路径。 +4. **重跑核对**:以附录 A 为检查表,逐行核对三份文档的交叉引用。 +5. **派发**:使用 01 §15.5 的交接模板,附本次补丁后的表结构与契约。 + +### 6.3 工作量估算 + +| 阶段 | 内容 | 预估 | +|---|---|---| +| 补表 + 迁移脚本 | §5.1 四组 DDL 及 Alembic 版本 | 0.5 - 1 人日 | +| 契约修正 | §5.2 九项,以文档修改为主 | 0.5 人日 | +| 流程补强 | §5.3 五项,含两张时序图 | 0.5 人日 | +| 交叉核对 | 附录 A 全表 | 0.5 人日 | +| **合计** | | **约 2 人日** | + +--- + +## 附录 A:交叉引用一致性核对表 + +| 核对项 | 01 | 02 | 03 | 状态 | +|---|---|---|---|---| +| Agent 类型命名(`customer_service` / `advisor` / `risk` / `operations`) | §5.1 | 权限表 | §6.1 | ✅ 一致 | +| 客服五类意图(`faq`/`product_inquiry`/`policy_explain`/`chitchat`/`transfer_human`) | — | 02:429 | §6.1 | ✅ 一致 | +| `conversation_message` 字段(`intent`/`confidence`/`source_references`) | §6.3 | 02:120-126 | §5.6 | ⚠️ 缺 `tool_calls` | +| 转人工工单与风险工单分离 | — | 02:111、220 | §6.4、§10.2 | ✅ 一致 | +| 知识有效过滤条件(`published + active + 有效期`) | §12 | 02:170-174 | §6.2、§7 | ⚠️ `CURRENT_DATE` 时区 | +| 记忆提取触发条件 | §6.2 | — | §12.2 | ✅ 一致 | +| 降级策略矩阵 | §12 | — | §13 | ⚠️ 03 缺事件发布失败 | +| Outbox 可靠投递 | §12 | 仅 `memory_sync_outbox` | §5.7 | ❌ 缺通用事件表 | +| 会话状态与澄清轮次 | §5.2、§6.2 | ❌ 无表 | §5.3 | ❌ 缺表 | +| 工具白名单来源 | §7.3 | 按意图配置 | — | ❌ 语义冲突 | +| 适当性校验(C1-C5/R1-R5) | §10 | `fin_risk_assessment` | §8 | ✅ 一致 | +| 交易范围限定(场内模拟,不建场外表) | §1 | §2 | §9、§11 | ✅ 一致 | + +--- + +## 附录 B:评审发现统计 + +| 严重度 | 数量 | 分布 | +|---|---|---| +| P0(阻断实施) | 5 | 01: 3 项、02: 4 项、03: 2 项(含交叉项) | +| P1(编码前修正) | 8 | 01: 5 项、02: 3 项、03: 1 项 | +| P2(建议增强) | 6 | 01: 4 项、03: 2 项 | +| 文案级 | 1 | 01: 1 项 | + +> 注:单项问题可能跨多份文档,故分文档计数之和大于总数。 + +--- + +## 附录 C:v3.0 复审结果 + +> 复审对象:01(v3.0,1575 行)、02(v3.0,812 行)、03(v3.0,462 行)、新增 `docs/00-新数据库基线设计.md`(1137 行)与 `AGENTS.md` +> 复审方式:四份文档逐行通读 + 基线逐字段比对 + MVC+S 分层约束逐项核对 +> 复审结论:**综合 90/100(A-),MVC+S 架构合规性 92/100(A);上一轮 P0 项 5/5 全部闭环** + +### C.1 评分变化 + +| 文档 | v2.1 | v3.0 | 变化 | 等级 | +|---|---|---|---|---| +| `01-通用Agent平台开发设计.md` | 86 | **93** | +7 | A | +| `02-数据库建表设计.md` | 77 | **88** | +11 | B+ | +| `03-平台端到端流程文档.md` | 76 | **89** | +13 | A- | +| **三件套综合** | 80 | **90** | **+10** | **A-** | +| **MVC+S 架构合规性** | 未评 | **92** | — | **A** | + +### C.2 MVC+S 架构合规性检查表 + +| # | 检查项 | 判据 | 结论 | +|---|---|---|---| +| 1 | 四层职责边界明确 | 01 §3.1 同时给出职责与"不允许承担的职责" | ✅ 符合 | +| 2 | Controller 薄层,无业务判断 | 01 §3.3 示例仅路由 + 调 Service + 返回 View | ✅ 符合 | +| 3 | Controller 不访问 Model/ORM | 01 §3.1、02 §3 | ✅ 符合 | +| 4 | Service 编排业务,不解析 HTTP 细节 | 01 §3.1、§573 | ✅ 符合 | +| 5 | Model 仅做实体映射与持久化 | 01 §3.1、02 §3 | ✅ 符合 | +| 6 | View 仅做格式转换,无业务判断与 DB 访问 | 01 §3.1、§131 | ✅ 符合 | +| 7 | 依赖方向 Controller → Service → Repository → Model | 01 §227 | ⚠️ 方向正确,但"`Service -> View`"一句表述有歧义 | +| 8 | Agent 属于 Service 层,非第五层 | 01 §59、AGENTS.md 第 6 条 | ✅ 符合 | +| 9 | 业务 Agent 不得直连 MySQL/Redis/Milvus/Neo4j | 01 §227、§541 | ✅ 符合 | +| 10 | 业务 Agent 必须经 AgentFactory 创建,不绕过鉴权/记忆/合规/审计/事件 | 01 §6.1、§16.3 契约测试、AGENTS.md 第 7 条 | ✅ 符合 | +| 11 | 事务边界收敛在 Service 层 | 01 §5.4 `AgentPersistenceService`、02 §11 | ✅ 符合 | +| 12 | 新增 8 张底座表未破坏分层 | 02 §8,均由 Service 经 Repository 访问 | ✅ 符合 | +| 13 | 新增 6 个服务(Session/Idempotency/ConfigCenter/ModelRouter/Relationship/Persistence)均在 `app/service/` 下 | 01 §4 | ✅ 符合 | +| 14 | 横切关注点(合规、审计、事件)统一在底座,未下放业务 Agent | 01 §11、§13、§16.1 | ✅ 符合 | +| 15 | 场外运营流程独立,不写入场内交易表 | 01 §1、02 §2、03 §11、AGENTS.md 第 8 条 | ✅ 符合 | + +**架构层面待改进(不影响合规判定)** + +| # | 项 | 说明 | +|---|---|---| +| A-1 | 01 §227 表述歧义 | "`Service -> View` 仅返回中立结果,由 Controller 选择 JSON 或 SSE View" 中箭头方向与后半句矛盾,应改为"Service 不依赖 View;Controller 选择 View" | +| A-2 | Service 层粒度膨胀 | `app/service/` 下已有 15 个子包,应用层编排与基础设施适配器混放;建议在文档中区分 `service/application`(编排)与 `service/infrastructure`(模型网关、工具、存储适配) | +| A-3 | `AgentRequest` 定位 | HTTP 请求模型定义在 Service 层 `contracts.py`,Controller 与 Service 共用同一对象;严格 MVC+S 下 Controller 应持有 API DTO,Service 持有命令对象,建议补充设计说明 | +| A-4 | `app/model/dto/` 归属模糊 | DTO 位于 Model 层,可能造成 Model 被上层反向依赖,文档未说明其用途与依赖方向 | + +### C.3 上一轮问题修复验证 + +**P0(5 项):全部闭环 ✅** + +| 编号 | 问题 | 修复证据 | +|---|---|---| +| P0-1 | 澄清轮次无持久化载体 | 新增 `svc_conversation_session`(02 §8.1);01 §6.4 规定"从会话表读取、旧值条件原子递增、Redis 只做缓存";03 §4.2、§5.3 同步 | +| P0-2 | 通用领域事件无 Outbox | 新增 `domain_event_outbox`(02 §8.2);01 §5.4 新增 `AgentPersistenceService.complete_run` 同事务写 Outbox;03 §5.6、§5.7 | +| P0-3 | `tool_calls` 落点缺失 | 澄清基线已含 `tool_calls`(00:853),02 §6.1 明确沿用既有定义,01 `AgentResult` 保留该字段 | +| P0-4 | `allowed_tools` 按意图 vs 单集合 | `ResolvedAgentConfig.allowed_tools_by_intent: Mapping[str, frozenset[str]]`(01:307);§7.3 明确"按当前意图求交集,禁止先求并集";`RiskAgent` 示例已同步(01:1401) | +| P0-5 | 基线不自包含 | 新增 `docs/00-新数据库基线设计.md` 纳入项目,`AGENTS.md` 第 1 条固化为不可变基线 | + +**P1(8 项):修复 5、部分 1、未修 2** + +| 编号 | 问题 | 状态 | 说明 | +|---|---|---|---| +| P1-1 | 合规策略来源不一致 | ✅ 已修 | `_persist_degraded_result` 增加 `config` 参数并优先使用解析结果(01:862、870、896) | +| P1-2 | `CoreResult` 互斥语义矛盾 | ✅ 已修 | 01:408 改为"必须恰好提供一个" | +| P1-3 | 目录树缺 `bootstrap.py`/`authorizer` | ✅ 已修 | 01:157-158 已补 | +| P1-4 | 伪流式与首字延迟指标冲突 | ✅ 已修 | 01 §12 明示"合规优先、不承诺 Token 级真流式";指标改为"首个安全文本片段延迟"(01:1264) | +| P1-5 | 用户消息先于记忆召回落库、孤儿消息 | ⚠️ 部分 | 顺序未调整;`AgentRequestError` 后遗留用户消息仍未定义处理策略 | +| P1-6 | `CURRENT_DATE` 与 UTC 冲突 | ✅ 已修 | 02:199-200 改为 `UTC_DATE()` | +| P1-7 | 匿名反馈唯一索引 NULL 失效 | ❌ 未修 | 02:361 仍是 `(message_id, customer_id)`,02:373 仍依赖 API 限流 | +| P1-8 | 无归档/分区策略 | ❌ 未修 | 02 全文仍无分区、冷归档或容量规划 | + +**P2(6 项):修复 4、部分 1、未修 2** + +| 编号 | 问题 | 状态 | +|---|---|---| +| P2-1 | 幂等无落地载体 | ✅ 新增 `request_idempotency`(02 §8.3)+ 01 §6.4 幂等范围定义 | +| P2-2 | Prompt 版本化无载体 | ✅ 新增 `prompt_template_version`(02 §8.8) | +| P2-3 | 意图集合可否扩展未明确 | ✅ 01:1106 明确"数据库不能新增 `supported_intents` 之外的意图" | +| P2-4 | `@final` 可被绕过 | ❌ 仍为 `__dict__` 扫描(01:1455) | +| P2-5 | 覆盖率范围未界定;缺 SLA 数值 | ⚠️ 部分:已补模型超时 15s、重试 1 次、最多 3 端点(01:1168-1170);覆盖率范围仍未界定 | +| P2-6 | 缺时序图;Redis 行情故障未定义 | ⚠️ 部分:03:293 已补行情不可用拒绝下单;时序图仍未补 | + +### C.4 v3.0 新发现的问题 + +| 编号 | 问题 | 位置 | 严重度 | +|---|---|---|---| +| N1 | **`fin_knowledge_meta.content_text` 偏离基线**:基线定义为 `MEDIUMTEXT NOT NULL`,02 实施为 `NULL` 并声明"后续不得更改",属对基线字段定义的单方面变更,未登记偏差 | 00:875、1028 vs 02:181、192 | P1 | +| N2 | **`DomainEvent` 缺 `aggregate_type`**:Outbox 表要求该列 `NOT NULL`,但 01 的事件契约无此字段,填充规则未定义 | 02:503 vs 01:484-489 | P1 | +| N3 | **03 §3 流程顺序与 §5.6 矛盾**:总流程把"View 生成结果"排在"保存 `conversation_message`"之前,而 §5.6 与 01:963 要求持久化成功后才发送最终 SSE | 03:42-46 vs 03:153 | P1 | +| N4 | **02 关系图与 DDL 不一致**:关系图声明 `svc_conversation_session 1 --- N request_idempotency`,但 `request_idempotency` 表无 `session_id` 字段 | 02:132 vs 02:530-554 | P2 | +| N5 | `AgentRequest.metadata: dict[str, Any]` 与 §5.1"不传递无结构的顶层 dict"自相矛盾 | 01:237 vs 259 | P2 | +| N6 | `agent_reply_template` / `agent_intent_config` 的"同一时刻仅一个有效版本"仍靠服务层保证,未采用 `config_release` 已有的生成列唯一约束方案 | 02:412、02:575 | P2 | +| N7 | `model_routing_rule.fallback_endpoint_ids` 用 JSON 数组存储端点 ID,无法用外键保证端点有效性,应用层校验规则未定义 | 02:669 | P2 | +| N8 | 02 §13 验证清单口径与基线不一致:"原 39 张表"包含 02 自建的 6 张专项表,而基线仅有 33 张表 | 02:793 vs 00 §4 | P2 | + +### C.5 v3.0 剩余待办 + +| 优先级 | 事项 | +|---|---| +| P1 | N1 在 02 中登记基线偏差或提请修订基线;N2 为 `DomainEvent` 补 `aggregate_type` 或定义推导规则;N3 修正 03 §3 流程顺序;P1-5 明确孤儿用户消息策略 | +| P2 | P1-7 匿名反馈唯一性、P1-8 归档分区、P2-4 契约测试加固、N4-N8 | +| 架构 | A-1 修正 §227 表述;A-2 划分 Service 层应用/基础设施边界;A-3 说明 `AgentRequest` 定位;A-4 明确 `model/dto` 依赖方向 | + +### C.6 复审结论 + +v3.0 把上一轮的核心缺口全部补齐:会话状态、幂等、通用 Outbox、配置发布中心、多模型路由、Prompt 版本、Neo4j 受控关系视图均有了持久化载体与流程定义,并且新增的 8 张表全部是新增表、表名与基线无冲突,符合 `AGENTS.md` 的不可变基线约束。 + +**架构判定:符合 MVC+S**。四层职责、依赖方向、Agent 归属、横切关注点收敛均满足约束,业务 Agent 的越权路径在文档层面已被封堵(契约测试 + 工厂 + `@final` + 依赖注入)。 + +**放行判定:可作为实施基线,建议先修 N1-N3 三项 P1(约 0.5 人日)再派发编码 Agent。** + +## 附录 D:v3.1 整改记录 + +> 整改日期:2026-09-09 +> 决策依据:项目负责人对附录 C 问题逐项确认;本附录只记录 v3.1 的处理范围,不改写 v2.1/v3.0 的原始评审结论。 + +### D.1 已处理项 + +| 对应意见 | v3.1 整改结果 | +|---|---| +| 记忆提取事件事务一致性 | `complete_run()` 接收提取判定,并在保存助手消息、审计和幂等完成状态的同一事务内写入 `memory.extraction_requested` Outbox;提交后由 Worker 消费。 | +| N2 | `DomainEvent` 补齐 `event_id`、`aggregate_type` 和 `occurred_at`,所有示例事件均给出完整字段。 | +| N3 | 03 总流程调整为先提交消息、审计、幂等状态和 Outbox,再发送最终 JSON 或 SSE;仅 `start` 可以提前发送。 | +| N4 | `request_idempotency` 增加 `session_id`、索引及到 `svc_conversation_session` 的外键,DDL 与关系图一致。 | +| N5 | `AgentRequest.metadata` 改为禁止额外键的结构化模型,只允许 `locale`、`client_version` 和 `ui_entry`。 | +| N6 | 回复模板和意图配置增加 `active_key` 生成列及唯一索引,由数据库保证每个业务组合最多一个 `active` 版本。 | +| N7 | 新增 `model_routing_fallback` 权威关系表,对备用端点建立外键和顺序唯一约束;原 JSON 字段仅保留为兼容快照。 | +| 可恢复运行实体 | 根据接口设计新增 `agent_run`,保存运行状态、Worker 租约、结果定位和安全重试信息;`run_id` 与 `trace_id` 分离。 | + +### D.2 明确暂不处理项 + +| 对应意见 | 决策 | +|---|---| +| 缺少 33 张基础表的可执行 DDL | 本轮不处理。 | +| N1 `content_text` 基线差异 | 本轮不处理。 | +| N8 及表数口径文字问题 | 不专项处理;仅因新增一张平台表将实际总数同步为 48。 | +| 评分与等级不一致 | 本轮不处理,保留原评审记录。 | + +### D.3 v3.1 数据库规模 + +数据库设计现由 33 张基础业务表、6 张智能客服专项表和 10 张平台底座增量表组成,共 49 张。v3.1 仅增加字段和新表,不重命名、删除或改变任何已有表名及已有字段定义。 diff --git a/docs/06-底座代码测试报告.md b/docs/06-底座代码测试报告.md new file mode 100644 index 0000000..d1de675 --- /dev/null +++ b/docs/06-底座代码测试报告.md @@ -0,0 +1,491 @@ +# 底座代码测试报告 + +> 首版版本:v1.0(当前最新为 v3.0,结论见附录 D / 附录 E) +> 测试日期:2026-09-09 +> 测试对象:`jr-agent-platform` 底座代码(首版统计 86 个 Python 文件) +> 测试依据:`AGENTS.md`、`docs/00-新数据库基线设计.md`、`docs/01-通用Agent平台开发设计.md`、`docs/02-数据库建表设计.md`、`docs/03-平台端到端流程文档.md`、`docs/05-接口文档.md`、`TODO.md` +> 测试方式:自动化测试执行 + 静态与类型检查 + 真实 HTTP 接口冒烟 + 契约逐项核对 + +> **当前结论(v3.0,附录 E):综合 94/100(A)。** +> +> 本文档保留完整版本演进,**各节摘要与评分属于相应版本的口径,不要混读**: +> +> | 版本 | 综合分 | 口径说明 | +> |---|---|---| +> | v1.0 | 55(D+) | 首版,使用了未定义规则的归一计算 | +> | v1.1 | 48(D+) | 统一为"综合分 = 加权总分",契约完整度 25 → 45 | +> | v2.0(附录 D) | 90(A-) | 修复后独立复审:P0 三项 + P1 五项闭环 | +> | v3.0(附录 E) | **94(A)** | 能力接线复审:生产组装、意图分类、首个业务工具闭环 | +> +> 首版"不可用于任何真实流量"对应的 P0 缺陷已在附录 D 闭环。当前的**已知限制**已转移到 +> `docs/evidence/20260909-environment-facts.md`(模型端点与 embedding 端点未配置、 +> 语义召回通道待启用、Worker 需常驻等),不再由本文档承担。 + +--- + +## 1. 结论摘要 + +| 维度 | 得分 | 判定 | +|---|---|---| +| 工程基建(工具链 / 类型 / 测试骨架) | 85 | B+ | +| 安全与鉴权 | 20 | D | +| 契约实现完整度(按当前阶段口径) | 45 | C- | +| MVC+S 架构合规 | 60 | C | +| 端到端可运行性 | 35 | D | +| **综合** | **48** | **D+** | + +**一句话结论**:骨架搭起来了,承重墙没浇。`ruff` + `mypy --strict` 全绿、集成测试真连 MySQL,这层质量在同类项目里属上乘;但**任意登录用户可调用任意 Agent**(越权实测成功并落库),且**没有 worker 入口导致运行永远排队**,这两条使底座当前无法承载任何真实流量。 + +**缺陷统计**:P0 三项、P1 五项、P2 七项。 + +--- + +## 2. 测试范围与方法 + +| 层次 | 方法 | 覆盖 | +|---|---|---| +| 单元测试 | 执行仓库自带 `tests/unit` | 16 个测试文件 | +| 集成测试 | 执行仓库自带 `tests/integration`(真连本地 MySQL) | 7 个测试文件 | +| 静态检查 | `ruff check app tests` | 全部源码 | +| 类型检查 | `mypy app`(strict 模式) | 55 个源文件 | +| 接口冒烟 | 自建 `tools/smoke_check.py`,起真实 uvicorn 服务打真实请求 | 认证、越权、幂等、SSE、异常输入 | +| 契约核对 | 逐项比对 `docs/05-接口文档.md` 第 19 节接口总目录与代码路由 | 50 个接口 | +| 架构核对 | 比对 `01 §3.1/§4` 分层约束与代码目录、依赖方向 | MVC+S 四层 | + +**未覆盖**:Milvus / Neo4j 真实链路、模型供应商真实调用、压测与故障注入(`tools/performance_baseline.py` 未执行)。 + +--- + +## 3. 测试环境 + +| 项 | 值 | +|---|---| +| Python | 3.13.15(conda 环境 `jr_py313`,`D:\conda\envs\jr_py313`) | +| 测试框架 | pytest 8.4.2、pytest-asyncio 0.26.0 | +| Web 框架 | FastAPI 0.141.1、uvicorn 0.52.4 | +| 数据层 | SQLAlchemy 2.0.52、asyncmy 0.2.14、PyMySQL 1.2.0 | +| 静态检查 | ruff 0.16.6、mypy 1.20.2 | +| 数据库 | 本地 MySQL(`jr` 库,集成测试真实读写)、Redis、Milvus、Neo4j(配置就绪) | +| 服务地址 | `http://127.0.0.1:8099` | + +> **环境提示**:若使用 base 环境运行测试,因缺少 `pytest-asyncio`,16 个 `async def` 测试会被 pytest **静默跳过**(`PytestUnhandledCoroutineWarning`),仅显示 `17 passed, 16 skipped`。必须使用 `jr_py313`。 + +--- + +## 4. 测试执行结果 + +### 4.1 自动化测试 + +| 命令 | 结果 | 耗时 | +|---|---|---| +| `pytest tests/unit -q` | ✅ **33 passed**,0 skipped | 1.92s | +| `pytest tests/integration -q` | ✅ **9 passed** | 2.25s | + +集成测试**确认真连数据库**(非 mock):使用 `app.infrastructure.db.SessionFactory`、真实 ORM 模型、`commit()` 与 `finally` 清理,覆盖配置发布生命周期、幂等并发、Outbox 重放与 worker 租约。 + +**测试有效性评价**:测试本身质量良好,但**存在关键盲区**——`test_agent_factory.py` 只覆盖"已注册可创建 / 未注册报错",**没有任何角色或入口鉴权用例**,因此 P0-1 越权缺陷未被现有测试发现。 + +### 4.2 静态与类型检查 + +| 命令 | 结果 | +|---|---| +| `ruff check app tests` | ✅ All checks passed | +| `mypy app`(`strict = true`) | ✅ Success: no issues found in 55 source files | + +这两项是本项目最扎实的部分,说明类型契约与代码规范执行到位。 + +### 4.3 接口冒烟测试(真实服务) + +执行 `python tools/smoke_check.py`(服务已启动): + +| # | 用例 | 期望 | 实际 | 判定 | +|---|---|---|---|---| +| 1 | 无 token 调用 | 401 | 401 | ✅ | +| 2 | 无效 token | 401 | 401 | ✅ | +| 3 | 合法用户创建 `customer_service` run | 202 | 202 | ✅ | +| 4 | 普通客户调用 `risk` Agent(越权) | 403 | **202** | ❌ | +| 5 | 越权 run 是否真的落库 | 未落库 | **已落库 `agent_type=risk`** | ❌ | +| 6 | 未注册 `agent_type` | 400 | **202** | ❌ | +| 7 | `sub` 非数字 | 4xx | **500** | ❌ | +| 8 | 用户 2 读取用户 1 的 run | 404 | 404 | ✅ | +| 9 | SSE `Content-Type` | `text/event-stream` | `text/event-stream` | ✅ | +| 10 | SSE 首个事件 | `start` | `start` | ✅ | +| 11 | 同幂等键不同请求体 | 409 | 409 | ✅ | + +**合计 11 项,失败 4 项(对应 3 个真实缺陷)。** + +--- + +## 5. 缺陷清单 + +### 5.1 P0:阻断上线 + +#### P0-1 越权漏洞:任意登录用户可调用任意 Agent + +| 项 | 内容 | +|---|---| +| **现象** | 用户 `sub=1` 的合法 token 请求 `agent_type=risk`,返回 `202`;随后 `GET /api/v1/agent-runs/{run_id}` 返回 `agent_type=risk`,证明运行记录已真实落库 | +| **根因** | 四处叠加,形成完整漏洞链 | +| | ① `app/service/agent/factory.py:19-26` — `create()` 只校验 `agent_type` 是否注册,**无任何授权调用** | +| | ② `app/core/contracts.py:50-56` — `AgentDefinition` 无 `allowed_roles` / `allowed_portals` 字段,权限上限无处声明 | +| | ③ `app/service/agent/base.py:46-47` — `validate_access()` 为空实现(`del request, context`) | +| | ④ `app/core/security.py:57` — 认证只提取 `sub`,`RequestContext.roles` 恒为空元组 | +| **影响** | 角色隔离完全失效。客户可调用风控、投顾、运营 Agent;`01 §7.2` 权限映射表、`03 §4.2` 第 7 步、`AGENTS.md` 第 7 条全部落空 | +| **建议** | `AgentDefinition` 补 `allowed_roles` / `allowed_portals`;`AgentFactory.create()` 增加 `AgentAuthorizer.ensure_allowed(definition, context)`;JWT 认证后从 `sys_user_role` / `sys_role_permission` 加载有效角色写入 `RequestContext`(Redis 缓存 + 版本失效) | + +#### P0-2 `agent_type` 无白名单校验 + +| 项 | 内容 | +|---|---| +| **现象** | `agent_type=no_such_agent` 返回 `202` | +| **根因** | `app/service/agent_run_application_service.py:29-91` — `accept()` 全程不校验 `agent_type` 合法性,直接写 `agent_run` 并发 `agent.run_requested` 事件 | +| **影响** | 脏数据入库;worker 无法执行导致任务堆积;审计与指标被污染 | +| **建议** | `accept()` 前置校验 `AgentFactory` 注册表;未注册返回 `400` | + +#### P0-3 无 worker 入口,端到端不通 + +| 项 | 内容 | +|---|---| +| **现象** | 全项目 grep 无 `if __name__ == "__main__"`、`asyncio.run`、`def main`。run 创建后状态永久停留 `queued`,SSE 只推 `start` 后持续心跳直至超时 | +| **根因** | `app/worker/` 下有 `agent_run_worker.py`、`outbox_worker.py` 等类实现,但**没有任何可执行入口**;`app/main.py` 只注册三个路由,未启动 worker | +| **影响** | 底座核心链路(受理 → 执行 → 落库 → 推送)断裂,任何业务 Agent 都无法产生结果 | +| **建议** | 补 worker 启动入口(独立进程或 lifespan 内后台任务),并把 `agent.run_requested` Outbox 消费打通 | + +### 5.2 P1:编码前必须修复 + +#### P1-1 `sub` 非数字导致 500 + +- **现象**:token 的 `sub` 为字符串(如 `abc`)时返回 `500`,而非 `401`。 +- **根因**:`app/core/security.py:57` 将 `sub` 原样转为字符串存入 `user_id`;`app/service/agent_run_application_service.py:30` 执行 `int(context.user_id)` 未捕获 `ValueError`。 +- **影响**:500 泄露内部异常;不符合 `05-接口文档.md` 的错误码约定。 +- **建议**:认证阶段校验 `sub` 为数字,失败抛 `UnauthorizedAgentError`;`accept()` 内不再做隐式转换。 + +#### P1-2 BaseAgent 治理钩子全部为空实现 + +- **现象**:`resolve_config`、`recall_memory`、`check_compliance` 均为 `del ...` 空实现(`app/service/agent/base.py:49-57`)。 +- **影响**: + - **`check_compliance` 直接返回原结果 → 输出合规扫描完全缺失**(禁止表达、脱敏、适当性、跨客户检查全部未执行); + - `recall_memory` 空 → 记忆召回未接线; + - `resolve_config` 空 → 配置中心未接线。 +- **建议**:注入 `ComplianceService` / `MemoryService` / `ConfigService` 并实现钩子;在钩子为空期间禁止接入真实流量。 + +#### P1-3 SSE 缺失 `delta` 事件 + +- **现象**:`app/api/views/agent_run_sse.py:16-23` 仅生成 `start` / `tools` / `replace` / `done` / `error`,**从不生成 `delta`**。 +- **契约**:`docs/05-接口文档.md:400` 明确 `start -> tools(可选) -> delta(一个或多个) -> done -> 关闭`。 +- **影响**:按契约解析 `delta` 的客户端拿不到正文;流式体验退化为一次性替换。 +- **建议**:按安全分块发送 `delta`,或与接口文档同步改为"仅 `replace`"并更新第 19 节目录。 + +#### P1-4 Controller 直接访问 ORM(MVC+S 违规) + +- **证据**:`app/api/controllers/agent_runs.py:5,62-69`(`select(AgentRun)`);`app/api/controllers/conversations.py:37-45,68-73,112-119`(三处 `select`)。 +- **契约**:`01:131`"任何业务条件都不能写在 Controller 或 View 中";`02 §3`"Controller 不访问 ORM"。 +- **影响**:数据访问逻辑散落 Controller,权限过滤与事务边界难以统一;新增接口时容易绕过 Repository 约定。 +- **建议**:查询下沉到 `app/repository/`,Controller 只做路由、参数校验与 Service 调用。 + +#### P1-5 接口实现覆盖率:全量 20% / 当前阶段 55% + +- **契约**:`docs/05-接口文档.md:895-946` 定义 **50 个**接口(R001-R004 共 4、C001-C007 共 7、M001-M002 共 2、K001 共 1、A001-A033 共 33、O001-O003 共 3)。 +- **重要口径**:该 50 个接口**全部属于公共底座**(对应 `TODO.md` 阶段 3-7)。业务域接口在 `05-接口文档.md:948` 单独登记、不在本表内,由各业务模块负责。因此本项反映的是**底座自身进度**,不含业务接口。 +- **实现**:代码共 10 个路由。 + +| 控制器 | 已实现 | 对应编号 | 归属阶段 | +|---|---|---|---| +| `agent_runs.py` | 3 | R001、R002、R003 | 阶段 3、5 | +| `conversations.py` | 3 | C003、C005、C007 | 阶段 5 | +| `config_releases.py` | 4 | 提交/审核/激活/回滚(路径与 A004-A007 不完全一致) | 阶段 6(提前实现) | + +**双口径结论** + +| 口径 | 应交付 | 已实现 | 完成度 | +|---|---|---|---| +| 全量底座接口 | 50 | 10 | **20%** | +| 当前阶段(T3.1-T5.3,即 R001-R004 + C001-C007) | 11 | 6 | **55%** | + +- **说明**:阶段 6(平台管理面 A001-A033)与阶段 7(O001-O003)在 `TODO.md` 中尚未开始,不计入"当前阶段应交付"。按当前阶段口径,底座接口完成度为 55%。 +- **当前阶段缺失**:R004(取消运行)、C001(创建会话)、C002(会话详情)、C004(关闭会话)、C006(转人工详情)。 +- **建议**:按 `TODO.md` 阶段推进,并在每个接口完成时同步第 19 节目录与 OpenAPI。 + +### 5.3 P2:建议改进 + +| # | 问题 | 证据 | 建议 | +|---|---|---|---| +| 1 | 无 `svc_conversation_session` 表,澄清轮次无处存储 | `app/model/platform.py` 无该模型;`RequestContext` 无 `clarification_round` | 按 `02 §8.1` 补表;`RequestContext` 补字段 | +| 2 | `JwtAuthenticator` 每请求实例化并读公钥文件 | `app/api/dependencies/auth.py:18`、`app/core/security.py:27,29-36` | 单例化 + 公钥内存缓存 | +| 3 | 会话归属靠历史消息推断,新会话可被抢注 | `agent_run_application_service.py:35-42` | 改用会话表归属字段校验 | +| 4 | `docs/` 下两份 `05` 文档并存 | `05-公共Agent平台接口规范.md` 与 `05-接口文档.md` | 按 `TODO.md` T0.2 删除或归档废弃稿,并加检查脚本 | +| 5 | 目录结构偏离文档约定 | 代码 `app/api/controllers`、`app/api/views`、`app/repository`;文档 `01 §4` 为 `app/controller`、`app/view`、`app/model/repositories` | 统一其一并更新文档 | +| 6 | `TODO.md` 与设计文档数据不一致 | `TODO.md:34` 称 49 张表;`02` 称 47 张 | 以基线为准修正 TODO | +| 7 | 增量表模型未声明外键 | `app/model/platform.py` 各表无 `ForeignKey` | 与 `02 §8` 的 DDL 对齐或明确由应用保证 | + +--- + +## 6. 契约一致性核对 + +| 契约项 | 权威源 | 实现状态 | +|---|---|---| +| HTTP 前缀 `/api/v1` | `05-接口文档.md` | ✅ 一致 | +| Bearer JWT 认证 | `05-接口文档.md` §3 | ⚠️ 仅解析 `sub`,未加载角色 | +| 幂等范围 `user_id + agent_type + idempotency_key` | `01:984` | ✅ 一致(唯一约束 + 409 实测通过) | +| 运行受理原子写消息/幂等/run/Outbox | `05-接口文档.md:326` | ✅ 一致(`accept()` 单事务) | +| SSE 事件序列含 `delta` | `05-接口文档.md:400` | ❌ 缺 `delta` | +| 结果级恢复(不实现事件级续传) | `05-接口文档.md:423` | ✅ 一致(轮询 + 重发全量) | +| 配置发布需双人审核 | `02 §8.4` | ✅ 一致(集成测试覆盖 creator≠reviewer) | +| 七步执行骨架 | `01 §6.3` | ❌ 记忆召回、意图分类、合规校验未实现 | +| 工厂按角色/入口授权 | `01 §7.2` | ❌ 完全缺失 | +| 审计写入范围 | `01 §1.10`(接口规范) | ⚠️ 部分(配置生命周期已写审计) | + +### 6.2 TODO 阶段完成度校准 + +`TODO.md` 将阶段 0-7 归为底座、阶段 8 归为业务 Agent。下表按阶段核对"标记为已完成"的任务是否达到其**自己写明的验收标准**。 + +| 任务 | TODO 状态 | 验收标准(原文摘要) | 实测结果 | 判定 | +|---|---|---|---|---| +| T3.1 运行受理 | `[x]` | 返回 `202`、`run_id`、`trace_id`;同键同文返回原 `run_id`;同键异文 `409` | 全部通过 | ✅ 达标 | +| T3.2 AgentFactory 和 BaseAgent | `[x]` | "未注册 Agent、**非法角色、非法入口**和未授权工具均被拒绝";"实现 `BaseAgent.execute()` **七步流程**" | 非法角色/入口未被拒绝(越权返回 202);骨架仅 5 步,记忆召回、意图分类、合规校验为空实现 | ❌ 不达标 | +| T3.3 Worker 和运行租约 | `[x]` | "进程重启、租约过期和重复消费不会生成第二份最终消息" | 全项目无 worker 入口,链路无法启动,验收无法执行 | ❌ 不达标 | +| T4.1 `complete_run()` | `[x]` | 最终消息、审计、幂等完成、运行终态、记忆 Outbox 同事务 | 集成测试覆盖同事务提交 | ✅ 达标 | +| T4.2 Outbox Worker | `[-]` | — | 如实标注为进行中 | 状态一致 | +| T5.1 运行查询 | `[x]` | 越权资源统一返回 `404` | 实测 404 | ✅ 达标 | +| T5.2 结果级恢复 SSE | `[x]` | "输出 `start`、`tools`、`delta`、`replace`、`done`、`error`" | 从不生成 `delta` | ❌ 不达标 | +| T5.3 会话和消息 | `[-]` | — | 如实标注"会话关闭与转人工查询待补" | 状态一致 | + +**结论**:4 个标记为 `[x] 已完成并通过验收` 的任务中,**T3.2、T3.3、T5.2 三项未达到其自身验收标准**。这不是覆盖率问题,而是**验收流程问题**——若 T3.2 的"非法角色被拒绝"验收项真实执行过,越权缺陷不可能通过。 + +**建议**:将"重跑 T3.2 / T3.3 / T5.2 验收并修正 TODO 标记"列为 P0(见 §9),在修复 P0-1 至 P0-3 后重新走验收。 + +--- + +## 7. MVC+S 架构合规核对 + +| # | 检查项 | 结论 | +|---|---|---| +| 1 | 四层目录存在 | ⚠️ 存在但命名与 `01 §4` 不一致 | +| 2 | Controller 薄层、无业务判断 | ❌ 直接查询 ORM(P1-4) | +| 3 | Controller 不访问 Model / ORM | ❌ 违反 | +| 4 | Service 编排业务 | ✅ `agent_run_application_service.py` 事务边界正确 | +| 5 | Model 仅做映射 | ✅ 基本符合 | +| 6 | View 仅做格式转换 | ✅ `agent_run_sse.py` 无业务逻辑 | +| 7 | 依赖方向 `Controller → Service → Repository → Model` | ⚠️ Controller 直接依赖 Model(越层) | +| 8 | Agent 属于 Service 层 | ✅ `app/service/agent/` | +| 9 | Agent 不得直连存储 | ✅ 未发现直连 | +| 10 | Agent 必须经 `AgentFactory` 创建 | ⚠️ `AgentExecutor` 走工厂,但 `accept()` 阶段不校验 `agent_type` | +| 11 | 治理能力不可被子类绕过 | ✅ `__init_subclass__` 硬拦截,优于文档的 `@final` | +| 12 | 横切关注点统一在底座 | ❌ 合规/记忆/配置钩子为空 | + +**架构判定**:分层骨架与依赖注入设计正确,`__init_subclass__` 禁止覆盖治理方法的做法甚至比文档约定更硬。扣分集中在"Controller 越层访问 ORM"与"治理钩子未接线"两处。 + +--- + +## 8. 评分明细 + +| 维度 | 权重 | 得分 | 加权 | +|---|---|---|---| +| 工程基建(工具链 / 类型 / 测试骨架) | 20 | 85 | 17.0 | +| 安全与鉴权 | 25 | 20 | 5.0 | +| 契约实现完整度(当前阶段 55% 折算) | 20 | 45 | 9.0 | +| MVC+S 架构合规 | 20 | 60 | 12.0 | +| 端到端可运行性 | 15 | 35 | 5.25 | +| **合计** | **100** | — | **48.25 → 综合 48** | + +**计算口径修正(v1.1)**:v1.0 的"44.25 → 归一 55"使用了一个未定义规则的归一计算,v1.1 统一为"综合分 = 加权总分"。同时"契约实现完整度"按当前阶段口径从 25 上调至 45(见 §5.2 P1-5)。两项修正叠加后综合分为 **48**。 + +> 判定说明:工程基建单项优秀拉高了加权分,但安全(20)与端到端(35)为门槛项,任一不达标即不可上线,故综合评级为 **D+**。 + +--- + +## 9. 修复优先级 + +| 优先级 | 事项 | 预估 | +|---|---|---| +| P0 | `AgentDefinition` 补权限字段 + `AgentFactory` 接入授权校验 | 0.5 天 | +| P0 | JWT 加载有效角色 / 数据范围写入 `RequestContext` | 0.5 天 | +| P0 | `accept()` 校验 `agent_type` 已注册 | 0.2 天 | +| P0 | 补 worker 入口,打通 run 执行闭环 | 1 天 | +| P0 | 重跑 T3.2 / T3.3 / T5.2 验收,修正 TODO 的"已完成"标记 | 0.5 天 | +| P1 | `sub` 非数字返回 401;`JwtAuthenticator` 单例化与公钥缓存 | 0.3 天 | +| P1 | 接线 `check_compliance` / `recall_memory` / `resolve_config` | 1 天 | +| P1 | SSE 补 `delta`;Controller ORM 查询下沉 Repository | 0.5 天 | +| P2 | 会话表、废弃稿清理、目录对齐、TODO 修正 | 1 天 | + +**放行建议**:P0 全部闭环前,不得接入任何真实用户流量或对外暴露接口。 + +--- + +## 10. 附录:复现步骤 + +```powershell +# 1. 使用正确环境(必须 jr_py313,否则异步测试被静默跳过) +$py = 'D:\conda\envs\jr_py313\python.exe' + +# 2. 自动化测试 +& $py -m pytest tests/unit -q +& $py -m pytest tests/integration -q + +# 3. 静态与类型检查 +& $py -m ruff check app tests +& $py -m mypy app + +# 4. 启动服务 +& $py -m uvicorn app.main:app --host 127.0.0.1 --port 8099 + +# 5. 接口冒烟(另一个终端) +$env:PYTHONIOENCODING='utf-8' +& $py tools\smoke_check.py +``` + +**脚本说明**:`tools/smoke_check.py` 使用 `httpx.Client(trust_env=False)`,绕开 Windows 注册表代理设置——否则请求会被系统代理拦截并返回假性 `502`(本报告首次执行即遇到该问题)。 + +--- + +## 11. 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| v1.0 | 2026-09-09 | 首版:5 类测试执行结果、15 项缺陷清单、契约与架构核对、评分与修复优先级 | +| v1.1 | 2026-09-09 | 修正接口总数(46 → 50)并区分全量/当前阶段双口径;新增 §6.2 TODO 阶段完成度校准;修正评分计算口径(综合 55 → 48,契约完整度 25 → 45) | +| v2.0 | 2026-09-09 | 新增附录 D:gpt 修复后的独立复审结果(综合 48 → 90) | +| v3.0 | 2026-09-09 | 新增附录 E:能力接线复审(综合 90 → 94);生产组装、意图分类接入、首个业务工具闭环 | + +--- + +## 附录 D:v2.0 修复复审 + +> 复审对象:gpt 修复后的代码(82 个源文件,78 个测试用例) +> 复审依据:本报告 §9 修复清单 + `docs/07-测试问题修复记录.md` +> **复审结论:P0 三项 + P1 五项全部闭环,综合 48 → 90(A-)。** + +### D.1 复审方式 + +不复用开发方结论,独立执行四类验证: + +1. 全量回归(单元 + 集成); +2. 静态与类型检查(`ruff`、`mypy --strict`); +3. **独立验收脚本** `tools/acceptance_check.py`:真实 JWT + 真实 MySQL + 真实 HTTP 链路 + 显式注册的测试 Agent; +4. 真实 RBAC 数据加载验证 `tools/seed_test_rbac.py`。 + +### D.2 逐项验证结果 + +| 原缺陷 | 验证方式 | 结果 | +|---|---|---| +| P0-1 越权 | 真实 JWT(`sub=9001`,customer 角色)调用 `risk` Agent | ✅ **403**(修复前 202 + 落库) | +| P0-1 鉴权链路 | `IdentityService.resolve()` 真实读库 | ✅ `roles=('customer',)` / `('risk_operator',)` | +| P0-2 白名单 | 未注册 `agent_type` | ✅ **404** `AGENT_TYPE_NOT_FOUND`(依 05 权威规范,非 400) | +| P0-3 端到端 | HTTP 受理 → Outbox 消费 → worker 执行 → 结果查询 | ✅ `queued` → **`succeeded`**,`content` 正确 | +| P1-1 `sub` 非法 | 非数字 `sub` | ✅ **401**(修复前 500) | +| P1-2 治理钩子 | `AgentGovernance` 注入 + `resolve/recall/review` | ✅ 已接线(范围见 D.4) | +| P1-3 SSE `delta` | 运行中订阅 SSE | ✅ 含 `delta` 分块;终态重连走 `replace`(符合 05 §SSE 三场景) | +| P1-4 分层 | `tests/unit/api/test_architecture.py`(AST 扫描) | ✅ Controller 禁止 import `app.model` / `app.repository` / 直接操作 session | +| P1-5 接口覆盖 | `app.openapi()` 统计 | ✅ **41 paths / 50 operations,50/50 全覆盖** | +| 回归 | `pytest tests/unit tests/integration -q` | ✅ **77 passed**(修复前 42) | +| 静态 | `ruff check app tests` | ✅ 0 错误 | +| 类型 | `mypy app`(strict) | ✅ 82 文件 0 错误(修复前 55) | + +独立验收脚本输出(7/7 通过): + +```text +[PASS] 客户调用已授权 Agent 期望 202 / 实际 202 +[PASS] 客户越权调用 risk Agent 期望 403 / 实际 403 +[PASS] 未注册 agent_type 期望 404 / 实际 404 +[PASS] SSE 含 delta 事件 期望 yes / 实际 yes +[PASS] SSE 终止事件 期望 done / 实际 done +[PASS] 运行终态 期望 succeeded / 实际 succeeded +[PASS] 运行结果内容 期望 独立验收通过 / 实际 独立验收通过 +``` + +### D.3 评分更新 + +| 维度 | v1.1 | v2.0 | 依据 | +|---|---|---|---| +| 工程基建 | 85 | **90** | 测试 42→78,mypy 55→82 文件,新增 AST 架构测试 | +| 安全与鉴权 | 20 | **88** | 越权实测 403;RBAC 真实读库;拒绝写独立审计 | +| 契约实现完整度 | 45 | **95** | 50/50 接口全覆盖 | +| MVC+S 架构合规 | 60 | **92** | AST 测试机器强制 Controller 分层 | +| 端到端可运行性 | 35 | **85** | worker 闭环实测通过;扣分因生产零注册(见 D.4) | +| **综合** | **48** | **90** | A- | + +### D.4 遗留局限(不构成缺陷,但影响验收边界) + +| # | 局限 | 说明 | +|---|---|---| +| 1 | **生产代码零 Agent 注册** | `bootstrap.get_agent_factory()` 返回空工厂,正式注册入口与测试注册分离,业务 Agent 属 TODO 阶段 8。底座因此**无法独立端到端运行**,必须等组员注册后才能承接真实流量。 | +| 2 | 治理钩子部分覆盖 | `07` 文档自述:固定禁止表达、contains/exact、号码脱敏、记忆引用校验已接入;**适当性产品比较、真实模型意图分类、公共工具执行器未完成**;正则规则因无执行超时暂时失败关闭。 | +| 3 | RBAC 无初始化数据 | `sys_user` 等表为空,新环境需先造数据;仓库未提供 seed 脚本(本报告新增 `tools/seed_test_rbac.py` 可作参考)。 | +| 4 | 单元测试绕过真实认证 | `tests/unit/api/*` 用 `dependency_overrides` 注入 `RequestContext`,不覆盖 JWT→DB 身份链路;该链路仅由集成测试与本次独立验收覆盖。 | + +### D.5 复审交付物 + +| 文件 | 用途 | +|---|---| +| `tools/acceptance_check.py` | 独立端到端验收(越权、闭环、SSE) | +| `tools/seed_test_rbac.py` | RBAC 测试数据种子 + 身份加载验证 | +| `tools/smoke_check.py` | HTTP 冒烟(认证、幂等、异常输入) | + +--- + +## 附录 E:v3.0 能力接线复审 + +> 复审对象:新增能力接线与首个业务工具(89 个源文件,116 个测试用例) +> 复审依据:本报告附录 D 遗留 + `docs/10-业务域接入评估.md` 缺口清单 +> **复审结论:能力层与组装层全部闭环,综合 90 → 94(A)。** + +### E.1 上轮遗留的闭环情况 + +| 附录 D 遗留 | 本轮 | 证据 | +|---|---|---| +| 生产零注册,能力无法调用 | ✅ 组装入口补齐 | `bootstrap.get_agent_factory()` 注册 2 个内置工具、构造 `DatabaseModelGateway`、注入 `ModelGenerationService` / `ToolExecutor` / `IntentClassifier` | +| 治理钩子部分覆盖 | ✅ 补齐 | 模型调用、工具执行器、适当性校验、意图分类全部接线 | +| `IntentClassifier` 未接入骨架 | ✅ 已接入 | `base.py:104` 在 `execute()` 调用 `classify_intent()`;禁覆盖方法增至 13 个 | +| 健康检查 redis 硬编码 | ✅ 改真实 ping | `health_service.py:18,23-43` | +| 缺骨架契约测试 | ✅ 已补 | `tests/contract/test_agent_factory_contract.py`(遍历注册表) | +| `knowledge_service` 空壳 | ⚠️ 未处理 | 仍无条件 404(见 E.4) | + +### E.2 本轮新增能力 + +| 能力 | 实现 | 质量要点 | +|---|---|---| +| 模型网关 adapter | `OpenAICompatibleGateway` + `DatabaseModelGateway` | `secret_ref` 强制 `env:` 前缀,密钥不入库不入日志 | +| 工具执行器 | `ToolExecutor` + `ToolRegistry` | 强制只读、参数全 `[redacted]`、超时控制、审计 | +| 适当性校验 | `SuitabilityService` | 时区校验、C1-C5/R1-R5、通过与拒绝均留痕 | +| 意图分类 | `IntentClassifier` | 严格 JSON、意图白名单校验、低置信 `needs_clarification` | +| **首个业务工具** | `fund_quote_service.py`(209 行) | 交易时段判断、盘中/收盘分档缓存、降级标记、只读约束、延迟建连 | + +### E.3 独立验证 + +```text +pytest tests → 116 passed(附录 D 时 77) +ruff check → 0 +mypy app → 89 文件 0 错误(附录 D 时 82) + +开箱验证 tools/onboarding_check.py(不经替身): + 生产工厂 : ModelGenerationService + ToolExecutor + PlatformGovernance + create() 后 : 能力自动绑定 + 配置解析 : config_version 正常,tools_by_intent={'general': ()} + 工具调用 : ForbiddenAgentError 工具不在当前意图白名单(失败关闭,链路正确) +``` + +### E.4 遗留 + +| # | 项 | 影响 | 建议 | +|---|---|---|---| +| 1 | `knowledge_service.py` 仍无条件 404 | 客服 RAG 引用链路消费端为空 | 接 Milvus 检索后替换占位实现 | +| 2 | `milvus` 健康检查仍为 `True` | 有注释说明"optional projection",但探针仍会误判 | 改为 `not_probed` 或接入真实探测 | +| 3 | 开箱状态下工具不可用 | active release 的 `tools_by_intent` 为空 | 属正常安全设计,组员需先发布配置 | + +### E.5 评分更新 + +| 维度 | v2.0 | v3.0 | 依据 | +|---|---|---|---| +| 工程基建 | 90 | **92** | 测试 77→116,mypy 82→89 文件,新增契约测试 | +| 安全与鉴权 | 88 | 88 | 无变化 | +| 契约实现完整度 | 95 | 95 | 无变化 | +| MVC+S 架构合规 | 92 | 92 | 无变化 | +| 端到端可运行性 | 85 | **92** | 生产组装完整,开箱验证通过 | +| 能力实现质量 | — | **94** | 四个服务 + 首个业务工具样板 | +| **综合** | **90** | **94** | A | + +### E.6 复审交付物 + +| 文件 | 用途 | +|---|---| +| `tools/onboarding_check.py` | 开箱可用性验证(生产工厂 → 能力绑定 → 工具调用) | diff --git a/docs/10-业务域接入评估.md b/docs/10-业务域接入评估.md new file mode 100644 index 0000000..d118b96 --- /dev/null +++ b/docs/10-业务域接入评估.md @@ -0,0 +1,291 @@ +# 业务域接入评估 + +> 版本:v1.0 +> 评估日期:2026-09-09 +> 评估对象:客服、风控、投顾、运营四个业务域接入公共 Agent 底座的可行性 +> 评估依据:四份业务域工作流程文档 + `jr-agent-platform` 底座代码现状核对 +> **评估结论:四个域 100% 阻塞在同样两个公共出口上(模型调用、工具调用)。补完这两个出口前,交付给组员做业务实现会大面积返工。** + +--- + +## 1. 结论摘要 + +| 判定项 | 结论 | +|---|---| +| 底座公共能力(鉴权、运行闭环、SSE、会话、审计、配置) | ✅ 就绪,四域可直接依赖 | +| 模型生成 / 意图分类出口 | ❌ 缺失,**四域全部依赖** | +| 工具执行器 | ❌ 缺失,**四域全部依赖** | +| 适当性校验(C1-C5/R1-R5) | ❌ 缺失,投顾合规红线 | +| Milvus 知识检索 | ❌ 缺失,客服 RAG 阻塞 | +| 是否可交付组员做业务实现 | **暂不可**,建议先补 3 项 P0 公共能力 | +| 组员可并行开展的工作 | ✅ 业务 API、业务表、Agent 声明、契约测试 | + +--- + +## 2. 评估依据 + +### 2.1 需求来源(四份业务域文档) + +| 文档 | 规模 | 性质 | 详细度 | +|---|---|---|---| +| `智能客服Agent专项设计方案(2).html` | 191 KB / 42,166 字符 / 2,250 行 | 设计方案(含 Milvus 三集合路由代码、Go/No-Go 清单) | ★★★★★ | +| `投资顾问流程+(2).docx` | 16 KB / 1,688 字符 / 74 行 | 流程规范(四阶段 10 环节 + 适当性矩阵) | ★★★★ | +| `风控模块详细业务流程.md` | 5.5 KB / 175 行 | **当前系统现状描述**(8 个流程) | ★★★ | +| `基金运营流程.docx` | 13 KB / 905 字符 / 24 行 | 需求要点(2 个功能) | ★★ | + +> docx 与 html 的纯文本已提取至 `_tmp_extract/`(临时产物,可删除)。 + +### 2.2 底座现状核对方法 + +- 逐文件核对 `app/` 下 82 个源文件的服务、仓储、契约层实现; +- 关键词检索:`ToolExecutor` / `tool_executor` / `milvus` / `suitab` / `classify`; +- 复用 `docs/06-底座代码测试报告.md` 附录 D 的独立验收结论。 + +--- + +## 3. 底座能力就绪度矩阵 + +| 能力 | 实现证据 | 状态 | 四域可用性 | +|---|---|---|---| +| 鉴权 / RBAC / 数据范围 | `identity_repository.py`、`agent/authorizer.py` | ✅ | 全部 | +| 运行受理 + 幂等 | `agent_run_application_service.py`、`request_idempotency` | ✅ | 全部 | +| Outbox + 独立 Worker | `worker/runtime.py`、`worker/__main__.py` | ✅ | 全部 | +| SSE(`start/tools/delta/replace/done`) | `api/views/agent_run_sse.py` | ✅ | 风控文档事件序列与底座**完全对齐** | +| 会话状态 + 澄清轮次 | `model/session.py`、`svc_conversation_session` | ✅ | 客服、风控 | +| 转人工工单 | `svc_handover_ticket`、`public_platform_service.py` | ✅ | 客服 | +| 审计留痕 | `interaction_audit`、`AgentPersistenceService` | ✅ | 投顾"可追溯可举证"可满足 | +| 禁止表达合规 | `agent_negative_word` + `agent/governance.py` | ✅ | 客服 7 个零容忍词、投顾红线 | +| 配置发布 + 模型路由 + Prompt 版本 | `admin_service.py`、`config_release` 等 5 张表 | ✅ | 全部 | +| 记忆召回(含跨客户越界校验) | `memory_service.py`、`base.py:77` | ✅ | 投顾客户认知 | +| **模型生成 / 意图分类** | 仅 `model_gateway.py` 的 Protocol + 调度,**无 adapter、无 BaseAgent 出口** | ❌ | **全部阻塞** | +| **工具执行器** | `app/service/` 下**无 `tool/` 目录** | ❌ | **全部阻塞** | +| **适当性校验** | 全项目检索无实现 | ❌ | 投顾阻塞 | +| Milvus 知识检索 | 仅 `config.py:28-30` 配置项 | ❌ | 客服阻塞 | + +--- + +## 4. 四域需求映射 + +### 4.1 客服域 + +**需求要点**:5 类意图(`faq`/`product_inquiry`/`policy_explain`/`chitchat`/`transfer_human`)、3 个 Milvus 集合(FAQ/产品/政策)、转人工分级触发、负面词零容忍、七步骨架对齐。 + +| 需求 | 底座支撑 | 状态 | +|---|---|---| +| 5 类意图声明 | `AgentDefinition.supported_intents` | ✅ 可声明 | +| **意图分类** | 无分类器实现 | ❌ | +| **三集合 RAG 检索** | 无 Milvus 实现;`knowledge_service.py` 为空壳 | ❌ | +| **账户/持仓只读查询** | 无工具执行器 | ❌ | +| 转人工工单 | `svc_handover_ticket` + 接口 | ✅ | +| 负面词拦截 | 已审核规则真实生效 | ✅ | +| 会话状态机 | `svc_conversation_session` | ✅ | +| SSE 流式 | `delta` 分块 + `replace` 恢复 | ✅ | +| 低置信三档兜底 | 阈值逻辑待确认 | ⚠️ | + +### 4.2 风控域 + +> 注意:该文档描述的是**当前系统现状**(文末"程序只能由用户通过 `python -m app.main` 手动启动"),非新需求。迁移到 FastAPI 底座时需重新对齐启动方式。 + +| 需求 | 底座支撑 | 状态 | +|---|---|---| +| 工作台(统计/队列/通知) | 业务接口,属 B 类扩展 | 待组员实现 | +| 规则扫描 | 业务逻辑,不属于 Agent | 待组员实现 | +| 预警处置(确认/误报/升级) | **Agent 不可执行**,人工操作 → B 类接口 | ✅ 边界清晰 | +| **Agent 只读工具**(预警/证据/概览/列表) | 无工具执行器 | ❌ | +| **日报"建议优化方向"** | 无模型出口 | ❌ | +| SSE 事件序列 | 与底座契约完全一致 | ✅ | + +### 4.3 投顾域 + +**需求要点**:四阶段 10 环节、C1-C5/R1-R5 匹配矩阵(**文档标注"硬约束,不可绕过"**)、5 条合规红线、全流程留痕。 + +| 需求 | 底座支撑 | 状态 | +|---|---|---| +| **适当性校验** | 无实现 | ❌ **合规红线** | +| **产品筛选 / 组合构建** | 无工具执行器 | ❌ | +| **方案草案生成** | 无模型出口 | ❌ | +| 审核发布 | `client_facing_content` 表在 | ⚠️ 接口待确认 | +| 负面词(禁保本/稳赚) | ✅ | ✅ | +| 留痕(操作人/时间/输入/输出/审批) | `interaction_audit` | ✅ | +| 记忆召回(客户认知) | ✅ 含跨客户校验 | ✅ | + +### 4.4 运营域 + +**需求要点**:场外申购赎回单确认核对(申购 4 项 + 赎回 2 项 + 通用 2 项校验)、新成立产品推介材料自动生成,均需"人在回路确认"。 + +| 需求 | 底座支撑 | 状态 | +|---|---|---| +| 邮件接入与附件识别 | 无邮件适配器 | ❌ | +| **关键字段提取** | 无模型出口 | ❌ | +| **与实际产品比对(NL2SQL)** | 无工具执行器 | ❌ | +| 人在回路确认 | B 类业务接口 | 待组员实现 | +| 异常上报风控 | 事件/接口 | ⚠️ 待对接 | +| **推介材料生成** | 无模型出口 | ❌ | + +> 该文档仅 905 字符,无接口与字段定义,组员独立开工难度最大,建议补充需求说明。 + +--- + +## 5. 公共阻塞点 + +四个域的全部核心功能都落在同样两个出口上: + +```text +业务 Agent(客服/风控/投顾/运营) + │ + ├── 需要「生成/分类」 ──✗── 模型出口缺失 + │ (model_gateway 只有 Protocol,无 adapter; + │ BaseAgent 仅暴露 self.config / self.memories) + │ + └── 需要「查数据/调业务」 ──✗── 工具执行器缺失 + (app/service/ 下无 tool/ 目录, + allowed_tools 仅用于声明与交集校验) +``` + +**证据**: +- `model_gateway.py` 43 行,只有 `ModelGateway` Protocol、`ModelExecution` 数据类和 `ModelDispatchService` 调度逻辑; +- `base.py:26-27` 仅暴露 `self.config`、`self.memories`,无模型调用入口; +- `grep ToolExecutor` 仅命中 `allowed_tools` 的声明与校验(`contracts.py:59,71,74`、`admin_service.py:182,196`、`runtime_config_service.py:50`、`governance.py:48`),**无任何执行器类**。 + +--- + +## 6. 空壳接口与健康检查缺陷 + +本次核对新发现两处"路由存在但行为不正确"的实现,二者不影响接口覆盖率统计,但影响功能可用性判定。 + +### 6.1 `knowledge_service.py` 无条件返回 404 + +```python +class KnowledgeReferenceService: + async def resolve(self, context: RequestContext, token: str) -> dict[str, Any]: + await AuthorizationService.require(context, "knowledge:reference:read") + pieces = token.split(".") + ... + raise ResourceNotFoundError("引用不存在") # 第 20 行:函数末尾无条件抛出 +``` + +- 影响:K001 `GET /api/v1/knowledge-references/{reference_token}` 路由与 OpenAPI 均存在,但**永远不会返回数据**; +- 关联:客服域 RAG 引用链路的消费端为空。 + +### 6.2 健康检查硬编码 + +```python +checks["redis"] = True # health_service.py:18 +checks["milvus"] = True # health_service.py:19 +``` + +- 影响:`/internal/health/ready` 的 Redis / Milvus 检查为写死 `True`,非真实探测;**Milvus 或 Redis 故障时仍报 ready**,生产探针与熔断会失效; +- 建议:接入真实 ping,或在响应中显式标注为"未接入(optional projection)"而非 `True`。 + +### 6.3 使用文档描述了尚未实现的能力 + +`docs/09-底座使用文档.md` §9 写明模型调用链路: + +```text +ConfigRelease → ModelRouterService → ModelGateway → 主端点/受控 fallback +``` + +§13「当前边界」亦称底座负责"模型路由"。但当前实现中: + +- `app/service/model_gateway.py` 仅有 `ModelGateway` Protocol、`ModelExecution` 数据类与 `ModelDispatchService` 调度逻辑,**无任何供应商 adapter**; +- `BaseAgent` 未暴露模型调用入口(`base.py:26-27` 仅 `self.config`、`self.memories`); +- 该文档 §13 未标注"模型 adapter 与工具执行器尚未提供"。 + +**影响**:组员按该文档编写 `handle()` 时会认为可以调用模型,实际无处可调,容易诱发自行直连 HTTP 的绕行实现(违反 `01 §16.5`)。 + +**建议**:在该文档 §9、§13 增加显式缺口说明,或补齐实现后同步更新;两处内容必须与代码现状一致。 + +--- + +## 7. 补课顺序建议 + +| 优先级 | 补什么 | 解锁范围 | 预估 | +|---|---|---|---| +| **P0** | 模型网关 adapter(先接一个真实模型)+ `BaseAgent` 暴露调用入口 | 客服、投顾、运营、风控日报 **全部** | 1.5 天 | +| **P0** | 公共 `ToolExecutor` + 工具注册表 + 只读工具契约 | 客服查询、风控取证、投顾选基、运营 NL2SQL | 2 天 | +| **P0** | 适当性校验服务(C1-C5/R1-R5 矩阵) | 投顾合规红线(不可绕过) | 1 天 | +| P1 | 意图分类器(接模型路由) | 客服 5 类意图、风控意图分流 | 1 天 | +| P1 | Milvus 知识检索 + 修复 `knowledge_service` 空壳 | 客服 RAG | 2 天 | +| P2 | 健康检查真实探测 Redis / Milvus | 运维 | 0.5 天 | + +**总计约 8 人日**,其中 P0 三项约 4.5 人日。 + +--- + +## 8. 组员可并行开展的工作 + +以下工作**不依赖**模型与工具出口,可立即启动: + +| 工作项 | 说明 | +|---|---| +| 业务域 Controller / Service / Repository | B 类扩展,须遵守公共信封、幂等、分页、审计与 AST 分层约束 | +| 业务表设计与 Alembic 迁移 | 对照 `00-新数据库基线设计.md`,只增不改 | +| `AgentDefinition` 声明 | 角色、入口、意图集合、工具白名单(工具暂只声明不实现) | +| 业务 API 鉴权与契约测试 | 复用 `tests/unit/service/test_agent_authorization.py` 的参数化模式 | +| 前端联调 | 业务接口层(非 Agent 对话) | +| 需求补全 | 运营域文档过简,建议先补接口与字段定义 | + +**不可开展**:`handle()` 中任何需要模型生成或工具调用的业务逻辑。 + +--- + +## 9. 风险提示 + +| # | 风险 | 说明 | +|---|---|---| +| 1 | 业务 Agent 绕开底座 | 若模型/工具出口长期缺失,组员可能自行在 `handle()` 内直连 HTTP 或数据库,违反 `01 §16.5` 与 `AGENTS.md` 第 7 条 | +| 2 | 接口覆盖率被误读 | 50/50 只代表路由存在,`knowledge_service` 空壳说明需按行为验收,而非按路由计数 | +| 3 | 风控文档与底座启动方式冲突 | 现状文档为旧系统描述,迁移时需重新对齐 | +| 4 | 运营域需求不足 | 905 字符无字段定义,直接派发会导致返工 | + +--- + +## 10. 缺口闭环更新(v1.1) + +> 本节记录 v1.0 所列缺口在后续两轮开发中的闭环情况。§1-§9 保留为历史证据,不再代表当前状态。 + +### 10.1 缺口状态对照 + +| 原缺口 | v1.0 | 当前 | 证据 | +|---|---|---|---| +| 模型生成出口 | ❌ 缺失 | ✅ 闭环 | `OpenAICompatibleGateway`、`DatabaseModelGateway`、`BaseAgent.generate_with_model` | +| 工具执行器 | ❌ 缺失 | ✅ 闭环 | `ToolExecutor` + `ToolRegistry`,`bootstrap` 注册 2 个内置工具 | +| 适当性校验 | ❌ 缺失 | ✅ 闭环 | `SuitabilityService`(C1-C5/R1-R5 + 有效期 + 双向留痕) | +| 意图分类器 | ❌ 缺失 | ✅ 闭环 | `base.py:104` 骨架调用 `classify_intent()` | +| 生产组装入口 | ❌ 空工厂 | ✅ 闭环 | `bootstrap.get_agent_factory()` 完整组装并注入 | +| 骨架契约测试 | ❌ 缺失 | ✅ 已补 | `tests/contract/test_agent_factory_contract.py` | +| 业务工具样板 | ❌ 无 | ✅ 已提供 | `fund_quote_service.py`(基金行情,209 行) | +| 健康检查 redis | ❌ 硬编码 | ✅ 已修 | 真实 ping | +| 健康检查 milvus | ❌ 硬编码 | ⚠️ 保留 | 有注释说明,建议改 `not_probed` | +| Milvus 知识检索 | ❌ 缺失 | ⚠️ 未闭环 | 客服 RAG 仍阻塞 | +| `knowledge_service` 空壳 | ⚠️ 无条件 404 | ⚠️ 未处理 | 同上一项 | + +### 10.2 四域可开工判定(更新) + +| 业务域 | v1.0 判定 | 当前判定 | 说明 | +|---|---|---|---| +| **客服** | ❌ 阻塞 | ⚠️ 部分可开工 | 意图分类、转人工、负面词、SSE 已就绪;**RAG 检索仍缺 Milvus 实现** | +| **风控** | ❌ 阻塞 | ✅ 可开工 | 只读工具机制就绪;日报模型建议可用 | +| **投顾** | ❌ 阻塞 | ✅ 可开工 | 适当性硬约束、方案生成、工具调用均已就绪 | +| **运营** | ❌ 阻塞 | ⚠️ 部分可开工 | 模型提取/生成可用;**邮件接入与 NL2SQL 需组员自建工具**;需求文档仍过简 | + +### 10.3 剩余待办 + +| 优先级 | 事项 | 解锁范围 | +|---|---|---| +| P1 | Milvus 知识检索 + 修复 `knowledge_service` 空壳 | 客服 RAG | +| P2 | `milvus` 健康检查改为 `not_probed` 或真实探测 | 运维 | +| P2 | 运营域需求文档补充接口与字段定义 | 运营域开工 | + +### 10.4 结论 + +底座的两个公共出口(模型、工具)已闭环,并有首个业务工具样板可照抄。**§5 的"四域 100% 阻塞"结论在 v1.1 后不再成立**:风控与投顾已具备完整开工条件,客服与运营除各自一项外部依赖(Milvus / 邮件与 NL2SQL)外均可开工。 + +--- + +## 11. 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| v1.0 | 2026-09-09 | 首版:四域需求映射、公共阻塞点分析、空壳接口与健康检查缺陷、补课顺序建议 | +| v1.1 | 2026-09-09 | 新增 §10 缺口闭环更新:模型/工具/适当性/意图分类/组装/契约测试全部闭环,四域可开工判定更新,综合由"全阻塞"改为"风控投顾可开工" | diff --git a/docs/13-基金行情工具业务接入清单.md b/docs/13-基金行情工具业务接入清单.md new file mode 100644 index 0000000..efd884c --- /dev/null +++ b/docs/13-基金行情工具业务接入清单.md @@ -0,0 +1,87 @@ +# 基金行情工具业务接入清单 + +## 适用范围 + +客服、投顾、风控 Agent 如需查询场内基金行情,统一使用公共工具 `query_fund_quote`。 +本清单只规范接入方式,不创建业务 Agent,也不替业务组实现具体意图。 + +## 业务组员需要完成的事项 + +### 1. 在 AgentDefinition 声明工具 + +```python +allowed_tools=("query_fund_quote",) +``` + +工具权限不能通过数据库配置扩大。代码声明是上限,发布配置只能缩小范围。 + +### 2. 声明业务意图 + +在 `supported_intents` 中加入业务自己的意图,例如: + +```python +supported_intents=("fund_quote", "general") +``` + +然后在已发布配置中,将本 Agent 的 `fund_quote` 意图允许使用: + +```json +{ + "allowed_tools": ["query_fund_quote"] +} +``` + +配置键格式为: + +```text +agent_tools / :fund_quote +``` + +### 3. 在 handle 中调用公共工具 + +```python +quote = await self.call_tool( + "query_fund_quote", + {"fund_codes": ["159511"], "limit": 20}, + intent="fund_quote", + context=context, +) +``` + +不得导入 `hq.py`、`httpx`、东方财富 URL 或自行读取行情缓存。 + +### 4. 处理降级结果 + +必须识别以下字段: + +- `quote_source`:`eastmoney`、`cache` 或 `degraded`; +- `is_intraday`:是否盘中数据; +- `degraded`:是否处于降级状态; +- `nav_date`:净值对应日期。 + +降级行情只能用于说明或分析,不能当作成交、委托、持仓或实时保证。 + +## 业务边界 + +- 客服:可以解释行情字段和数据时间; +- 投顾:可以基于行情做分析,但仍须通过适当性校验; +- 风控:可以查询行情辅助风险分析; +- 所有 Agent:不得代客下单、修改持仓、确认成交或改变交易数据。 + +## 提交前检查 + +- [ ] `AgentDefinition.allowed_tools` 包含 `query_fund_quote`; +- [ ] `supported_intents` 包含实际使用的意图; +- [ ] 发布配置只允许必要的工具; +- [ ] 正常、未授权、未配置白名单、超时和降级测试齐全; +- [ ] 回答没有把行情当成成交确认; +- [ ] 没有直接调用外部行情接口; +- [ ] 注册表契约测试通过。 + +底座负责人验收命令: + +```powershell +python -m pytest tests/contract/test_agent_factory_contract.py -q -p no:cacheprovider +python -m ruff check app tests tools alembic +python -m mypy app +``` diff --git a/docs/99-已废弃-公共Agent平台接口规范.md b/docs/99-已废弃-公共Agent平台接口规范.md new file mode 100644 index 0000000..b8cf3b5 --- /dev/null +++ b/docs/99-已废弃-公共Agent平台接口规范.md @@ -0,0 +1,713 @@ +# 公共 Agent 平台接口规范(历史稿,已废弃) + +> 版本:v1.0 +> 修订日期:2026-09-08 +> 文档性质:历史评审稿,不再作为实现依据 +> 适用对象:底座负责人、业务模块开发者、前端与客户端、编码 Agent +> 关联文档:`00-新数据库基线设计.md`(数据基线)、`01-通用Agent平台开发设计.md`(架构与内部契约权威源)、`02-数据库建表设计.md`(增量表)、`03-平台端到端流程文档.md`(业务流程) + +> **废弃声明(2026-09-09)**:本文已被 [`05-接口文档.md`](05-接口文档.md) 完整取代,不得据此新增或修改接口。两份文档冲突时,以 `05-接口文档.md` 为唯一 HTTP 接口权威源;本文仅保留用于追溯早期设计决策。 + +--- + +## 0. 文档定位 + +### 0.1 本文负责 + +- HTTP 信封、认证方式、幂等、分页、追踪和 SSE 传输规则。 +- 公共 Agent 平台接口:运行、会话、记忆、配置查询、模型路由查询、Prompt 引用、知识引用、转人工入口、审计查询。 +- 平台管理面接口:配置发布与回滚、模型端点、模型路由规则、Prompt 版本、意图配置、禁止表达。 +- 业务接口的**扩展格式、命名映射、权限声明和契约测试**要求。 + +### 0.2 本文不负责 + +- 内部 Service / Repository / 事件类型定义:见第 4 章索引,本文不复制。 +- 业务域字段、状态机和业务校验规则:由各业务模块文档定义。 +- 数据库表结构与迁移:以 `00-新数据库基线设计.md` 为不可变基线,增量见 `02`。 +- Agent 执行骨架、配置权威链、工具与合规策略:以 `01` 为权威源。 + +### 0.3 权威源矩阵 + +| 契约项 | 唯一权威源 | 本文的处理方式 | +|---|---|---| +| HTTP 路径、信封、认证、幂等、分页、追踪、SSE 传输 | **本文** | 唯一定义 | +| SSE 事件名与载荷 | 01 §12 | 索引,不重复定义 | +| Service Protocol(9 个依赖) | 01 §5.4 | 索引 | +| 错误分类 `AgentError` 体系 | 01 §5.5 | 索引;本文只定义 HTTP 状态码映射 | +| `AgentRequest`、`RequestContext`、`AgentResult`、`CoreResult` 等 | 01 §5.2、§5.3 | 索引 | +| 执行骨架与七步顺序 | 01 §6 | 索引 | +| 配置权威链与发布回滚 | 01 §7.3、§7.4 | 索引 | +| `DomainEvent` 与 Outbox 语义 | 01 §5.3、02 §8.2 | 索引;本文只定义“何时产生” | +| 表结构、字段、约束 | 00 基线 + 02 | 索引 | +| 业务字段、状态机、业务校验 | 各业务模块文档 | 本文只列入口与 Agent 边界 | + +### 0.4 边界裁决规则 + +1. **骨架优先**:本文定义的通用约定(信封、错误码、认证、幂等、分页、追踪、SSE 传输)优先于任何业务文档;业务文档不得在信封之外新增顶层字段。 +2. **载荷归属**:业务字段、状态机、业务校验一律由业务文档定义,本文只引用。 +3. **冲突处理**:同一契约项出现两处定义时,以《权威源矩阵》指定的文档为准,另一处必须删除并改为引用。禁止两个定义同时进入实现或迁移。 +4. **新增错误码**:本文若需新增错误码,必须先在 01 §5.5 注册,再在本文映射 HTTP 状态码;未注册的 `code` 不得出现在响应中。 + +--- + +## 1. 通用约定 + +### 1.1 前缀与版本 + +- 所有接口统一前缀 `/api/v1`,网关按前缀做统一限流、大小限制和来源校验。 +- 版本策略:路径版本。不兼容变更升为 `/api/v2`,同一版本内只允许向后兼容的新增字段。 +- 01 §3.3 示例中的 `/agents/{agent_type}/chat` 在本文固化为 `POST /api/v1/agent-runs`(见 2.1)。 + +### 1.2 认证与上下文 + +- 认证方式:`Authorization: Bearer `,所有接口强制,无匿名接口。 +- **JWT 载荷**: + +| 声明 | 含义 | 说明 | +|---|---|---| +| `sub` | `user_id` | 唯一身份标识 | +| `jti` | 令牌唯一 ID | 用于吊销与审计 | +| `iat` / `exp` / `iss` / `aud` | 标准声明 | 标准校验 | +| `portal` | 入口标识 | **签发时绑定**,请求参数不得覆盖 | + +- **不得放入 JWT 的字段**:`roles`、`data_scope`、`assigned_customer_ids`。 + 依据 03 §4.2“FastAPI 认证依赖解析令牌,并重新加载有效角色”:角色与数据范围每次请求从 `sys_user_role` / `sys_role_permission` / `sys_customer_assignment` 加载(Redis 缓存 + 版本失效),保证权限降级即时生效;客户归属是动态集合,无法进令牌。 +- 01:319 的约束在 HTTP 层落地为:`user_id`、`roles`、`portal`、`data_scope`、`clarification_round` **只来自服务端上下文**,客户端通过 query、header 或 body 提交这些字段一律忽略并记安全审计。 + +**网关与应用层职责(防御纵深)** + +| 层 | 职责 | 不承担 | +|---|---|---| +| 网关 | JWT 签名校验、过期校验、限流、请求大小、来源白名单、`/api/v1` 前缀路由 | 业务授权、数据范围过滤 | +| 应用层 | 加载有效角色与数据范围、构造 `RequestContext`、`AgentFactory` 授权、工具与数据层过滤 | 不做令牌签名校验的替代 | + +### 1.3 请求与响应信封 + +**请求头** + +| 头 | 必填 | 说明 | +|---|---|---| +| `Authorization` | 是 | `Bearer ` | +| `Content-Type` | 写操作是 | `application/json; charset=utf-8` | +| `Idempotency-Key` | 写操作是 | 长度 8–64,见 1.5 | +| `X-Request-Id` | 否 | 客户端链路号,仅用于关联,不替代 `trace_id` | + +**成功响应** + +```json +{ + "code": "OK", + "message": "成功", + "data": {}, + "trace_id": "b7f1c2e0-4a6d-4f2b-9c31-0d5e7a8b9c01" +} +``` + +**错误响应** + +```json +{ + "code": "AGENT_INPUT_INVALID", + "message": "请求参数不正确", + "trace_id": "b7f1c2e0-4a6d-4f2b-9c31-0d5e7a8b9c01", + "error_id": null, + "details": [] +} +``` + +- `details` 只在参数校验失败时填充字段级错误,禁止回传堆栈、SQL 或依赖原始响应。 +- SSE 接口不使用该信封,见 1.8。 + +### 1.4 错误码与 HTTP 状态映射 + +错误码取值以 01 §5.5 为权威源,本文只做 HTTP 映射。 + +| code | HTTP | 场景 | 来源 | +|---|---|---|---| +| `OK` | 200 | 成功 | 本文 | +| `AGENT_INPUT_INVALID` | 400 | 参数、编码、长度校验失败 | 01:506 | +| `AGENT_PERMISSION_DENIED` | 403 | 角色、入口或数据范围不允许 | 01:511 | +| `AGENT_SESSION_NOT_FOUND` | 404 | 会话不存在**或**不属于当前用户 | **本文新增,需在 01 §5.5 注册** | +| `AGENT_IDEMPOTENCY_CONFLICT` | 409 | 同幂等键但请求体不同 | 01:984 | +| `AGENT_IDEMPOTENCY_PROCESSING` | 202 | 同幂等键请求仍在处理中 | 01:984 | +| `AGENT_RATE_LIMITED` | 429 | 网关或应用层限流 | 本文 | +| `AGENT_INTERNAL_ERROR` | 500 | 未预期异常,响应含 `error_id` | 01:669 | + +**重要约定** + +- **降级不是错误**:模型失败、Milvus 超时等可恢复故障经安全模板降级后,HTTP 返回 `200`,结果中带 `degraded: true` 与 `degradation_reason`(01 §6.2 的 `REPLACE` + `done` 路径)。客户端不得把 `degraded` 当作失败处理。 +- **会话不存在与越权统一返回 404**:避免通过 403/404 差异探测他人会话是否存在。 +- `AGENT_SESSION_NOT_FOUND` 注册前,实现方应临时使用 `AGENT_INPUT_INVALID`,不得自定义其他 `code`。 + +### 1.5 幂等 + +- 所有写操作(`POST` / `PUT` / `PATCH` / `DELETE`)必须携带 `Idempotency-Key`。 +- 幂等范围:`user_id + agent_type + idempotency_key`(01:984),落表 `request_idempotency`(02 §8.3)。 +- 行为: + +| 情况 | 响应 | +|---|---| +| 键不存在 | 抢占记录(`processing`),正常执行 | +| 键存在、`request_hash` 不同 | `409 AGENT_IDEMPOTENCY_CONFLICT` | +| 键存在、`completed` | 返回原结果,`code=OK` | +| 键存在、`processing` 且租约未过期 | `202 AGENT_IDEMPOTENCY_PROCESSING` | +| 键存在、`processing` 且租约已过期 | 允许接管,重新执行 | +| 键存在、`failed` | 按业务决定是否允许重试,默认允许 | + +- `request_hash` 由服务端对规范化后的请求体计算,客户端不得提交该字段。 + +### 1.6 分页 + +- 统一游标分页:`?limit={1..100}&cursor={opaque}`,`limit` 默认 20,上限 100。 +- 响应结构: + +```json +{ + "code": "OK", + "message": "成功", + "data": { "items": [], "next_cursor": "eyJpZCI6MTIzfQ", "has_more": false }, + "trace_id": "..." +} +``` + +- `cursor` 为不透明字符串,客户端不得解析或构造。禁止使用 `offset` 深分页。 + +### 1.7 追踪 + +- 服务端为每个请求生成唯一 `trace_id`,贯穿 API、模型、工具、数据库与事件日志(01 §6.4)。 +- 响应头必须回传 `X-Trace-Id`,响应体 `trace_id` 与之一致。 +- `trace_id` 同时是 Agent 运行的对外标识(见 2.1),本文不引入独立的 `run_id`。 + +### 1.8 SSE 传输规则 + +事件名称与载荷以 01 §12 为权威源,本节只定义如何通过 HTTP 传输。 + +**响应头** + +```text +Content-Type: text/event-stream; charset=utf-8 +Cache-Control: no-cache +Connection: keep-alive +X-Accel-Buffering: no +X-Trace-Id: +``` + +**认证**:与 1.2 一致,`Authorization: Bearer `。不接受把令牌放在 query 参数中。 + +**事件帧格式** + +```text +id: +event: +data: + +``` + +- `id` 为单调递增序号(从 1 开始),供客户端排序与去重;即使 MVP 不实现事件级续传,服务端也必须提供该字段。 +- `data` 为单行 JSON,不得包含裸换行;多行文本按 JSON 字符串转义。 + +**事件顺序与终止条件** + +| 阶段 | 事件 | 说明 | +|---|---|---| +| 1 | `start` | 首帧,携带 `trace_id`、`session_id` | +| 2 | `tools` | 可选,工具调用摘要(已脱敏) | +| 3 | `delta` | 0..N 帧,安全文本分块 | +| 4 | `replace` | 可选,降级时用完整安全文本替换 | +| 5 | `done` | 正常终止,携带 intent、confidence、sources、suggestions、degraded | +| — | `error` | 异常终止,携带 error_code、message、trace_id | + +- `done` 或 `error` 之后服务端必须关闭连接,不再发送任何帧。 +- 合规约束:`delta` 只在输出合规检查完成后发送(01:987);MVP 为“全量生成、全量合规、持久化后分块发送”,不承诺模型 Token 级真流式(01 §12)。 +- 持久化约束:`AgentPersistenceService.complete_run()` 成功返回后才允许发送最终 SSE(01:963、03 §5.6)。 + +**心跳** + +- 空闲超过 15 秒发送注释行:`: keep-alive` + 空行。 +- 注释帧不占 `id` 序号,客户端必须忽略。 + +**重连与断流恢复(结果级)** + +- MVP **不实现事件级续传**。客户端可选携带 `Last-Event-ID`,服务端若无法续传,应按 2.1 的规则正常结束连接,不返回部分流。 +- 断流后的恢复方式:客户端调用 `GET /api/v1/agent-runs/{trace_id}` 获取完整结果。 +- 该规则与 01 §6.4“同一 `trace_id` 的重复请求返回已有结果”一致:运行结果在发送 `delta` 之前已落库,因此恢复查询总是能拿到最终内容。 +- 客户端断开后,服务端仍须完成归档与审计(01 §6.4、03 §13)。 + +**超时** + +- 服务端整体 SSE 生命周期上限 120 秒;超时发送 `error`(`AGENT_INTERNAL_ERROR`)并关闭。 +- 模型单次调用超时 15 秒、最多 3 个端点(01:1168-1170)。 + +### 1.9 时间、金额与编码 + +| 项 | 约定 | +|---|---| +| 时间 | ISO 8601 UTC,微秒精度,如 `2026-09-08T12:00:00.000000Z`,与 02 §3 的 `DATETIME(6)` 对齐 | +| 金额 | 字符串,两位小数,语义同 `DECIMAL(18,2)`;禁止用浮点数 | +| 价格 | 字符串,六位小数 | +| 数量 | 字符串,四位小数 | +| 编码 | UTF-8;`Content-Type: application/json; charset=utf-8` | +| 空值 | 统一使用 `null`,禁止空字符串与 `null` 混用表达“无值” | + +### 1.10 审计写入范围 + +审计表 `interaction_audit` 只追加、不可改(02 §12)。为避免技术噪声淹没受监管记录,写入范围固定如下。 + +**必须写入 `interaction_audit`** + +| # | 类别 | 示例 | +|---|---|---| +| 1 | 受监管业务状态变化 | 委托、成交、资金、持仓、预警处置、工单流转 | +| 2 | 配置发布 | `config_release` 的创建、校验、审核、激活、回滚 | +| 3 | 权限决策 | 越权拒绝、适当性拦截、跨客户访问拒绝 | +| 4 | 人工处置 | 工单分配/接单/解决/关闭、投顾方案审核 | +| 5 | 对客内容与运行留痕 | `client_facing_content` 发布、Agent 运行结果 | + +**只写运行日志与指标,不写审计** + +| # | 类别 | 示例 | +|---|---|---| +| 1 | 心跳与租约 | SSE 心跳、幂等租约续期 | +| 2 | 幂等重试 | 抢占失败、`processing` 轮询 | +| 3 | 缓存操作 | 配置缓存命中/失效、记忆热缓存刷新 | +| 4 | 传输状态 | SSE 游标、断流、重连 | +| 5 | 运维流量 | 健康检查、限流拒绝、网关拦截 | + +**边界情况** + +- `request_idempotency` 的**完成**随 `complete_run` 在同一事务写审计(与业务结果绑定);**租约续期与接管**只写日志。 +- 降级运行写审计(`outcome="degraded"`,01:888),但降级原因的技术细节写日志。 +- 审计写入失败时,受监管业务不得返回成功(01 §13)。 + +--- + +## 2. 公共平台接口 + +统一约定:均需认证;均返回 1.3 信封(SSE 除外);均按 `data_scope` 过滤;均写 `trace_id`。 + +### 2.1 Agent 运行 + +#### 2.1.1 发起运行(SSE) + +```text +POST /api/v1/agent-runs +``` + +**权限**:由 `AgentFactory` 按 `AgentDefinition.allowed_roles` + `allowed_portals` 判定;本文不在此处重复权限规则。 + +**幂等**:`Idempotency-Key` 必填。 + +**请求体** + +```json +{ + "agent_type": "customer_service", + "session_id": "session-uuid", + "message": "基金赎回多久到账?", + "idempotency_key": "client-request-uuid", + "end_session": false, + "metadata": {} +} +``` + +**与内部契约的映射(重要)** + +`agent_type` 不在 01 §5.2 的 `AgentRequest` 中。为避免修改该契约,Controller 使用独立的 HTTP DTO `AgentRunRequest`,再映射为 Service 层命令对象: + +```python +class AgentRunRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + agent_type: str + session_id: str + message: str + idempotency_key: str + end_session: bool = False + metadata: dict[str, Any] = Field(default_factory=dict) + + def to_agent_request(self) -> AgentRequest: + return AgentRequest( + session_id=self.session_id, + message=self.message, + idempotency_key=self.idempotency_key, + end_session=self.end_session, + metadata=self.metadata, + ) + + +@router.post("/agent-runs") +async def create_agent_run( + payload: AgentRunRequest, + context: RequestContext = Depends(build_request_context), + service: AgentService = Depends(get_agent_service), +) -> StreamingResponse: + events = service.run_stream( + payload.agent_type, payload.to_agent_request(), context + ) + return AgentSseView.response(events) +``` + +该写法符合 MVC+S:Controller 持有 API DTO,Service 持有命令对象,Controller 不含业务判断(01:131)。 + +**响应**:`200` + `text/event-stream`,事件见 1.8 与 01 §12。 + +#### 2.1.2 查询运行结果(断流恢复) + +```text +GET /api/v1/agent-runs/{trace_id} +``` + +**权限**:仅可查询本人或职责范围内的运行;越权统一返回 `404 AGENT_SESSION_NOT_FOUND`。 + +**数据来源**:由 `conversation_message` + `request_idempotency` + `interaction_audit` 按 `trace_id` 聚合,不新增运行实体表。 + +**响应 `data`** + +```json +{ + "trace_id": "b7f1c2e0-...", + "session_id": "session-uuid", + "agent_type": "customer_service", + "status": "completed", + "reply": "……", + "intent": "faq", + "confidence": "0.8231", + "source_references": [], + "tool_calls": [], + "suggestions": [], + "transfer_required": false, + "degraded": false, + "degradation_reason": null, + "created_at": "2026-09-08T12:00:00.000000Z" +} +``` + +- `status` 取值:`processing` / `completed` / `failed`(由 `request_idempotency.status` 映射)。 +- `tool_calls` 为脱敏后的摘要,字段结构以 01 §5.3 `ToolCallRecord` 为权威源。 +- 运行不存在或尚未落库时返回 `404 AGENT_SESSION_NOT_FOUND`。 + +#### 2.1.3 运行列表(可选) + +```text +GET /api/v1/agent-runs?session_id=&agent_type=&limit=&cursor= +``` + +按 1.6 分页;仅返回数据范围内的运行。 + +### 2.2 会话与消息 + +| 方法 | 路径 | 说明 | 关联表 | +|---|---|---|---| +| `GET` | `/api/v1/conversations` | 会话列表,按 `last_active_at` 倒序 | `svc_conversation_session` | +| `GET` | `/api/v1/conversations/{session_id}` | 会话详情,含状态与 `clarification_round` | 同上 | +| `GET` | `/api/v1/conversations/{session_id}/messages` | 消息列表,游标分页 | `conversation_message` | +| `POST` | `/api/v1/conversations/{session_id}/end` | 结束会话,幂等 | `svc_conversation_session` | +| `POST` | `/api/v1/conversations/{session_id}/feedback` | 提交反馈 | `conversation_feedback` | +| `POST` | `/api/v1/conversations/{session_id}/handover` | 发起转人工 | `svc_handover_ticket` | + +**约束** + +- `clarification_round` 为只读字段,只能由服务端在澄清路径中原子递增(01:985),客户端不得提交。 +- 反馈接口按 `message_id` 去重;匿名场景的唯一性由业务文档定义(见 7.2 遗留项)。 +- 会话归属校验失败统一返回 `404`。 + +### 2.3 记忆 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/api/v1/memories` | 记忆列表,按状态与有效期过滤 | +| `GET` | `/api/v1/memories/{memory_id}` | 记忆详情 | +| `GET` | `/api/v1/memories/{memory_id}/evidences` | 证据列表 | +| `GET` | `/api/v1/memories/conflicts` | 冲突列表(仅员工角色) | +| `POST` | `/api/v1/memories/{memory_id}/forget` | 发起遗忘,异步清理 Milvus/Neo4j/Redis | + +**约束** + +- 客户只能访问自己的记忆;员工按 `data_scope` 与客户归属过滤。 +- 遗忘是异步流程(03 §12.5),接口返回受理状态,不代表物理删除已完成。 +- 依法必须保留的交易、审计与对话归档不参与遗忘。 + +### 2.4 配置查询 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/api/v1/agent-configs/{agent_type}/resolved` | 返回解析后的 `ResolvedAgentConfig`(脱敏) | +| `GET` | `/api/v1/agent-configs/releases/active` | 当前 active 发布批次摘要 | + +- 响应中不得出现 `secret_ref` 的实际值、密钥、供应商完整地址。 +- 配置的写操作见第 3 章;本组接口只读,用于排障与前端展示。 + +### 2.5 模型路由查询 + +```text +GET /api/v1/model-routes/resolved?agent_type=&task_type= +``` + +- 返回本次会选中的策略名、主端点代号、备用链代号、`max_attempts`、`latency_budget_ms`。 +- 不返回 `secret_ref`、完整 `base_url`、成本单价。 +- 端点与路由的增删改见第 3 章。 + +### 2.6 Prompt 引用 + +- 公共侧不提供 Prompt 内容的读取接口;每次运行使用的 Prompt 版本通过 2.1.2 的 `trace_id` 关联审计记录获取(01 §7.4)。 +- Prompt 的创建与版本管理见 3.4。 + +### 2.7 知识引用 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/api/v1/knowledge/{knowledge_id}` | 查询已发布知识元数据(不含全文) | +| `GET` | `/api/v1/agent-runs/{trace_id}` | 已包含本次运行的 `source_references` | + +- 只返回 `review_status='published'` 且 `status='active'` 且在有效期内的条目(02 §6.2)。 +- 知识上传、审校与发布流程由知识运营业务文档负责(03 §7)。 + +### 2.8 转人工(通用入口) + +```text +POST /api/v1/conversations/{session_id}/handover +``` + +**请求体** + +```json +{ + "reason_code": "complaint", + "reason_detail": "客户投诉资金争议" +} +``` + +**响应 `data`** + +```json +{ "ticket_no": "HT20260908000001", "status": "pending", "priority": "P0" } +``` + +**边界** + +- 本接口只负责“发起转接”这一跨 Agent 骨架动作,落 `svc_handover_ticket`。 +- 工单的队列、分配、接单、解决、关闭状态机与字段由客服业务文档定义(02 §7.2、03 §6.4)。 +- 转人工事件 `conversation.transfer_requested` 由底座在 `complete_run` 时同事务写入 Outbox(01:937)。 + +### 2.9 审计查询 + +```text +GET /api/v1/audit-records?actor_id=&action_type=&target_customer_id=&from=&to=&limit=&cursor= +``` + +- 权限:`admin`、`super_admin`;`risk_operator` 按风控数据范围。 +- 只读;`interaction_audit` 不允许任何更新或删除接口(02 §12)。 +- `detail` 字段的结构由产生该审计的业务模块文档定义。 + +--- + +## 3. 平台管理面接口 + +统一前缀 `/api/v1/admin`,权限限 `admin` / `super_admin`。所有写操作需 `Idempotency-Key`,全部写审计(1.10 第 2 类)。 + +### 3.1 配置发布批次 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `POST` | `/admin/config-releases` | 创建草稿 | +| `GET` | `/admin/config-releases` | 批次列表 | +| `GET` | `/admin/config-releases/{id}` | 批次详情与配置项 | +| `POST` | `/admin/config-releases/{id}/validate` | Schema 与安全上限校验 | +| `POST` | `/admin/config-releases/{id}/review` | 审核(`reviewer_id <> created_by`) | +| `POST` | `/admin/config-releases/{id}/activate` | 原子激活 | +| `POST` | `/admin/config-releases/{id}/rollback` | 回滚到指定历史版本 | + +**约束** + +- 状态机以 02 §8.4 的 CHECK 取值为准:`draft` / `validating` / `pending_review` / `approved` / `active` / `superseded` / `rejected` / `rolled_back`。 +- 全局同一时刻只允许一个 `active` 批次,由 `uk_config_release_active_one` 在数据库层强制(02:582)。 +- 回滚是重新激活历史不可变版本,不原地修改历史记录(01 §7.4)。 +- 激活后必须使 `agent-config:{agent_type}:{release_id}` 缓存失效。 + +### 3.2 模型端点 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/admin/model-endpoints` | 端点列表(`secret_ref` 只回显引用名) | +| `POST` | `/admin/model-endpoints` | 新建端点 | +| `POST` | `/admin/model-endpoints/{id}/status` | 启用/禁用/归档 | + +- 禁止通过任何接口读取或写入明文密钥(02 §8.6、§12)。 + +### 3.3 模型路由规则 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/admin/model-routes?release_id=` | 规则列表 | +| `POST` | `/admin/model-routes` | 在指定 release 内新增规则 | + +- `max_attempts` 取值 1–3,由 02:683 的 CHECK 强制。 +- `fallback_endpoint_ids` 为 JSON 数组,端点有效性由应用层在发布校验阶段验证(见 7.2 遗留项)。 + +### 3.4 Prompt 版本 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/admin/prompts?task_type=&agent_type=` | 版本列表 | +| `POST` | `/admin/prompts` | 新增不可变版本 | + +- `prompt_code + version` 唯一(02 §8.8),版本一经创建不可修改。 + +### 3.5 意图配置 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/admin/intent-configs?agent_type=` | 配置列表 | +| `POST` | `/admin/intent-configs` | 新增版本 | + +- 不得新增 `AgentDefinition.supported_intents` 之外的意图(01:1106)。 +- 工具白名单按意图配置,解析时与代码上限、角色权限求交集(01:1099)。 + +### 3.6 禁止表达 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/admin/negative-words` | 规则列表 | +| `POST` | `/admin/negative-words` | 新增规则(初始 `draft`) | +| `POST` | `/admin/negative-words/{id}/status` | 启用/停用/归档 | + +- 只有审核通过的规则才可置为 `active`(02 §7.1)。 + +--- + +## 4. 内部契约索引 + +本章只做索引,不复制任何定义。修改内部契约必须修改权威源,并同步本节引用。 + +| 契约 | 权威源 | 变更要求 | +|---|---|---| +| `AgentRequest`、`RequestContext`、`AgentDefinition`、`ResolvedAgentConfig` | 01 §5.2 | 改动需评估 HTTP 映射影响 | +| `SseEvent`、`IntentResult`、`SourceReference`、`ToolCallRecord`、`ToolResult`、`CoreResult`、`AgentResult` | 01 §5.3 | 本文 2.1.2 响应结构随之更新 | +| 依赖协议(`ModelGateway`、`MemoryService`、`ComplianceService`、`ToolExecutor`、`AgentPersistenceService` 等 9 项) | 01 §5.4 | 不影响 HTTP 层 | +| 错误分类 `AgentError` 体系 | 01 §5.5 | 新增 `code` 必须在此注册 | +| 执行骨架与七步顺序 | 01 §6 | 决定 SSE 事件时序 | +| `AgentFactory` / `AgentRegistry` | 01 §7 | 决定 2.1.1 的权限判定入口 | +| 配置权威链与发布回滚 | 01 §7.3、§7.4 | 决定第 3 章接口语义 | +| `DomainEvent` | 01 §5.3 | 本文只定义何时产生 | +| `domain_event_outbox`、`request_idempotency`、`svc_conversation_session` 等增量表 | 02 §8 | 字段变更以 02 为准 | +| 基线表与字段 | 00 基线 | 只增不改(`AGENTS.md` 第 2–4 条) | + +--- + +## 5. 扩展规范 + +### 5.1 A 类扩展:Agent 能力 + +新增某类 Agent 的意图与 `handle()` 时: + +- **不新增 HTTP 接口**,复用 `POST /api/v1/agent-runs`。 +- 变更范围限定为:`implementations/_agent.py`、`bootstrap.py`(一行注册)、单元测试、契约测试数据(01 §16.4)。 +- 禁止修改 `base.py`、`factory.py`、`contracts.py`、`app/controller/**`、`app/view/**`(01 §16.5)。 + +### 5.2 B 类扩展:业务 API + +新增领域接口(如模拟下单、预警处置、方案审核)时: + +- **新增 Controller 与 Service**,且 Controller 只做路由、参数校验、调用 Service、返回 View。 +- 必须继承以下公共骨架,缺一不可: + +| # | 约束 | 依据 | +|---|---|---| +| 1 | 统一前缀 `/api/v1`、统一信封与错误码 | 1.1、1.3、1.4 | +| 2 | 认证走统一上下文,禁止自建鉴权 | 1.2 | +| 3 | 写操作必须幂等 | 1.5 | +| 4 | 列表必须游标分页 | 1.6 | +| 5 | 响应回传 `trace_id` | 1.7 | +| 6 | 按 1.10 写入审计 | 1.10、02 §12 | +| 7 | Controller 禁止访问 Model / ORM,禁止业务条件 | 01:131、02 §3 | + +- 业务字段、状态机、业务校验规则必须在对应业务模块文档中定义,本文不预先写死。 + +### 5.3 命名映射规则 + +| 场景 | 规则 | 示例 | +|---|---|---| +| `agent_type` | snake_case,正则 `[a-z][a-z0-9_]{1,31}` | `customer_service` | +| HTTP 路径段 | kebab-case,资源用复数 | `/api/v1/risk-alerts` | +| 域路径 | 仅在该域存在独立业务 API 时使用,与 `agent_type` 无强制同名 | `customer_service` → `/api/v1/customer-service/**` | +| 状态流转 | 资源 + 子资源 | `/risk-alerts/{id}/actions` | +| 数据库表名 | 沿用基线前缀约定 | `fin_`、`sys_`、`svc_`、`agent_` | + +- `operations` 仅作为 `agent_type` 保留,对话走统一 `agent-runs`,**不设** `/operations/**` 域前缀。 +- 场外运营业务 API 使用语义明确的独立资源名,例如 `/api/v1/offshore-subscriptions`、`/api/v1/fund-operation-mails`,并遵守 `AGENTS.md` 第 8 条(独立建表、独立接口)。 + +### 5.4 权限声明格式 + +每个 B 类接口必须在业务文档中声明: + +```text +接口:POST /api/v1/sim-orders +allowed_roles: customer, operator, admin +allowed_portals: web, app +data_scope: own_only +幂等: 必需,幂等键 = client_order_no +审计: 是(1.10 第 1 类,受监管业务状态变化) +Agent 边界: Agent 只读查询委托与成交,不得代客下单 +``` + +### 5.5 契约测试要求 + +每个 B 类接口至少覆盖: + +- 未认证 → `401`;令牌无效 → `401`。 +- 角色/入口不允许 → `403`。 +- 越权访问他人数据 → `404`(统一口径)。 +- 写操作缺少 `Idempotency-Key` → `400`;重复提交 → 幂等命中。 +- 分页边界:`limit=0`、`limit=101`、非法 `cursor`。 +- 审计写入成功且不可被业务接口修改。 +- 响应信封与错误码符合第 1 章。 + +--- + +## 6. 业务接口清单 + +本清单只列入口、归属文档和 Agent 边界;详细字段与状态机由归属文档负责。 + +| 入口 | 类型 | 归属文档 | Agent 边界 | +|---|---|---|---| +| `POST /api/v1/sim-orders` | B | 交易域文档 | Agent 只读查询委托/成交,**不得代客下单**(03 §2) | +| `GET /api/v1/sim-orders`、`/api/v1/holdings` | B | 交易域文档 | 只读,按客户归属过滤 | +| `POST /api/v1/risk-alerts/{alert_id}/actions` | B | 风控域文档 | Agent **不能**确认、关闭或升级预警(03 §10.3) | +| `POST /api/v1/risk-scan-runs` | B | 风控域文档 | 规则引擎产生预警,模型只做辅助研判(03 §10.1) | +| `POST /api/v1/advisory-plans/{plan_id}/reviews` | B | 投顾域文档 | Agent 只生成草案,审核由持证投顾执行(03 §8) | +| `GET/POST /api/v1/client-facing-contents` | B | 投顾域文档 | 草稿状态内容不得作为正式建议返回(03 §8) | +| `GET/POST /api/v1/handover-tickets` | B | 客服域文档 | 状态流转由人工执行(02 §7.2、03 §6.4) | +| `GET/POST /api/v1/faq-synonyms` | B | 知识运营文档 | 只有 `approved` 参与检索(02 §7.3) | +| `GET/POST /api/v1/knowledge-documents` | B | 知识运营文档 | 发布需审校(03 §7) | +| `GET/POST /api/v1/offshore-subscriptions` | B | 场外运营文档 | 人工在回路确认后才提交清算(03 §11) | +| `GET/POST /api/v1/feedback-reviews` | B | 客服域文档 | 质检池归人工处理(03 §6.5) | + +> 本清单为**索引**,不是接口定义的权威源。新增业务接口时在此登记一行,并在归属文档中定义字段。 + +--- + +## 7. 边界裁决与遗留项 + +### 7.1 裁决规则 + +见 0.4。补充两条操作要求: + +- 修改权威源后,必须同步本文第 4 章索引,并在提交说明中写明“权威源 + 同步位置”。 +- 任何契约项若出现两处定义,实现与迁移脚本必须停止推进,先完成裁决再继续。 + +### 7.2 已知遗留项 + +| # | 遗留项 | 影响 | 处理 | +|---|---|---|---| +| 1 | `AGENT_SESSION_NOT_FOUND`、`AGENT_RATE_LIMITED` 尚未在 01 §5.5 注册 | 错误码权威性 | 提请在 01 §5.5 补注册 | +| 2 | 匿名反馈的唯一性依赖 API 限流,`uk_feedback_message_customer` 对 `customer_id IS NULL` 不生效 | 重复刷票风险 | 由客服业务文档定义哨兵值或指纹方案 | +| 3 | `fallback_endpoint_ids` 为 JSON 数组,无外键约束 | 备用端点可能失效 | 3.3 的发布校验必须验证端点存在且状态为 `active` | +| 4 | `fin_knowledge_meta.content_text` 基线定义为非空,02 实施为可空 | 基线一致性 | 由 02 登记偏差或提请基线修订 | +| 5 | 事件级 SSE 续传未实现 | 重连体验 | MVP 采用结果级恢复(1.8) | + +--- + +## 8. 变更记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| v1.0 | 2026-09-08 | 首版:确立 HTTP 层权威源边界、七章结构、SSE 传输规则、审计写入范围、A/B 类扩展规范 | diff --git a/docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md b/docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md index f7d091d..d255837 100644 --- a/docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md +++ b/docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md @@ -1,5 +1,13 @@ # 文档清理归档(2026-09-11) +> ⚠️ **2026-09-11 第二次修订(评审后)**:本文原先记录的"**5 份编号文档已删除**" +> (`docs/04`/`06`/`10`/`13`/`99`)**已被推翻** —— 架构师在评审中明确要求保留, +> 理由:删除收益为零(那几份无人引用),而保留成本同样为零;且 `docs/10`(业务域接入评估) +> 本身有内容价值。**这 5 份现已全部恢复**,只作**历史参考**,并在 `AGENTS.md` 的 +> D 类"不要用来判断当前进度"里列明。本文 §1.5 与 §3.1 的描述相应作废。 +> +> 真正删除的是 **10 份过程产物**(§1.1–§1.4)与 2 个孤儿字节码,那些不恢复。 + > 本文记录 **2026-09-11 一次文档/文件清理**中删除的内容,用于保留可追溯性。 > 删除依据是两份只读审计报告: > - `.superpowers\sdd\2026-09-10-客服Agent与RAG-qyqy版\audit-docs.md` diff --git a/tests/unit/service/test_agent_persistence_audit.py b/tests/unit/service/test_agent_persistence_audit.py new file mode 100644 index 0000000..c8bab2f --- /dev/null +++ b/tests/unit/service/test_agent_persistence_audit.py @@ -0,0 +1,48 @@ +"""治理审计落库的字段契约(`agent_type` + 治理是否改写过输出)。 + +**为什么需要这个文件**:治理层(`PlatformGovernance.review`)会**改写对外输出** —— +追加固定免责声明(门禁 F5),命中禁用词时还会把整条回复替换成安全话术。 +架构师评审明确要求:"治理层改写了对外输出,事后必须能追溯到是哪个 Agent 触发的。" + +所以 `agent.run_completed` 那条审计的 `detail` 里必须带 `agent_type`; +本文件锁住这个契约,避免以后有人"顺手精简"掉它。 +""" + +from app.core.contracts import AgentResult, CoreResult +from app.service.agent_persistence_service import _governance_rewrote + + +def _result(text: str, *, transfer_required: bool = False) -> AgentResult: + return AgentResult( + run_id="r", + result=CoreResult(text=text, transfer_required=transfer_required), + ) + + +def test_plain_answer_without_disclaimer_counts_as_rewritten() -> None: + """客服出口的正文**不带**免责声明(话术由治理层统一注入)。 + + 所以"Agent 产出的纯粹正文"在治理前后的差异就是那句被追加的话术—— + 见到治理后的正文(含话术)即说明治理改写过。 + """ + assert _governance_rewrote( + _result("交易日 15:00 前提交,T+1 日确认份额。\n\n本内容仅为投资分析参考,不构成任何直接投资建议。") + ) is True + + +def test_transfer_required_counts_as_rewritten() -> None: + """拦截分支:治理层把回复替换成"该内容需要人工核实"并置 `transfer_required`。""" + assert _governance_rewrote( + _result("该内容需要人工核实。基金投资存在风险,本系统不代客交易。", + transfer_required=True) + ) is True + + +def test_untouched_text_is_not_marked_as_rewritten() -> None: + """没有任何治理痕迹时不标记(避免"全部记 True"让这个字段失去信息量)。""" + assert _governance_rewrote(_result("交易日 15:00 前提交,T+1 日确认份额。")) is False + + +def test_empty_text_does_not_crash() -> None: + """空正文(异常路径)不得让审计写入抛异常。""" + assert _governance_rewrote(_result("")) is False