From 3a7eee8af4db1e27b60fcb2b9869abdf7ca66d31 Mon Sep 17 00:00:00 2001 From: zhuyongxin Date: Wed, 29 Jul 2026 19:04:44 +0800 Subject: [PATCH] docs(mvp): add harness design and progressive guides --- mvp/engineering/README.md | 17 + .../Session-Run-Trace隔离-从串线到可回放.md | 315 +++++++++++ mvp/engineering/harness/CONTEXT.md | 378 +++++++++++++ ...rness-Tool双视图-从原始结果到可验证证据.md | 280 ++++++++++ ...arness信息增益停止-让无证据诊断正常收敛.md | 302 +++++++++++ .../harness/Harness生命周期与状态.md | 440 +++++++++++++++ .../Harness组件全景-职责-设计原因与边界.md | 374 +++++++++++++ ...rness设计-非确定性Agent的确定性控制边界.md | 503 ++++++++++++++++++ ...arness证据安全链-从引用真实到结论可发布.md | 276 ++++++++++ mvp/engineering/harness/README.md | 77 +++ .../01-运行控制-让一次请求有边界.md | 121 +++++ ...gent接入与收敛-让推理可以工作也可以停止.md | 148 ++++++ ...实边界-让模型看到必要信息而系统保留真相.md | 135 +++++ ...验证与发布-让未经证明的结论无法越过出口.md | 162 ++++++ mvp/engineering/harness/components/README.md | 22 + 15 files changed, 3550 insertions(+) create mode 100644 mvp/engineering/diagnosis/Session-Run-Trace隔离-从串线到可回放.md create mode 100644 mvp/engineering/harness/CONTEXT.md create mode 100644 mvp/engineering/harness/Harness-Tool双视图-从原始结果到可验证证据.md create mode 100644 mvp/engineering/harness/Harness信息增益停止-让无证据诊断正常收敛.md create mode 100644 mvp/engineering/harness/Harness生命周期与状态.md create mode 100644 mvp/engineering/harness/Harness组件全景-职责-设计原因与边界.md create mode 100644 mvp/engineering/harness/Harness设计-非确定性Agent的确定性控制边界.md create mode 100644 mvp/engineering/harness/Harness证据安全链-从引用真实到结论可发布.md create mode 100644 mvp/engineering/harness/README.md create mode 100644 mvp/engineering/harness/components/01-运行控制-让一次请求有边界.md create mode 100644 mvp/engineering/harness/components/02-Agent接入与收敛-让推理可以工作也可以停止.md create mode 100644 mvp/engineering/harness/components/03-Tool事实边界-让模型看到必要信息而系统保留真相.md create mode 100644 mvp/engineering/harness/components/04-验证与发布-让未经证明的结论无法越过出口.md create mode 100644 mvp/engineering/harness/components/README.md diff --git a/mvp/engineering/README.md b/mvp/engineering/README.md index 26913c8..ac52cc4 100644 --- a/mvp/engineering/README.md +++ b/mvp/engineering/README.md @@ -40,11 +40,28 @@ --- +## Harness + +| 文档 | 内容 | +|---|---| +| [harness/README.md](harness/README.md) | **从这里开始**:用一次诊断请求理解 Harness,不要求先掌握组件和状态 | +| [harness/components/README.md](harness/components/README.md) | **组件渐进式导读**:沿一次请求分四步理解运行控制、Agent 收敛、Tool 事实边界和验证发布 | +| [harness/CONTEXT.md](harness/CONTEXT.md) | Harness 统一术语、命名规则、状态维度与已知命名债务 | +| [harness/Harness生命周期与状态.md](harness/Harness生命周期与状态.md) | Run、Tool、Progress、Guard、Release、SSE 和持久化生命周期及状态映射 | +| [harness/Harness设计-非确定性Agent的确定性控制边界.md](harness/Harness设计-非确定性Agent的确定性控制边界.md) | Harness 根问题、设计不变量、方案取舍、关键决策、代价与真实问题反推 | +| [harness/Harness组件全景-职责-设计原因与边界.md](harness/Harness组件全景-职责-设计原因与边界.md) | 当前 Harness 10 个职责域、全部生产类型、设计原因、作用与边界 | +| [harness/Harness-Tool双视图-从原始结果到可验证证据.md](harness/Harness-Tool双视图-从原始结果到可验证证据.md) | canonical truth、Control View、Agent Observation 与 metadata audit 的数据边界 | +| [harness/Harness证据安全链-从引用真实到结论可发布.md](harness/Harness证据安全链-从引用真实到结论可发布.md) | EvidenceGuard、EvidenceRepair、SemanticGuard 和唯一 Release Policy | +| [harness/Harness信息增益停止-让无证据诊断正常收敛.md](harness/Harness信息增益停止-让无证据诊断正常收敛.md) | GAINED/NO_GAIN、重复检测、协议停止、ProgressSnapshot 与过程型 Fallback | + +--- + ## 诊断 / E2E | 文档 | 内容 | |---|---| | [diagnosis/一次诊断全流程-E2E导读.md](diagnosis/一次诊断全流程-E2E导读.md) | SUCCESS 全流程:阶段、token、timeline、字段 | +| [diagnosis/Session-Run-Trace隔离-从串线到可回放.md](diagnosis/Session-Run-Trace隔离-从串线到可回放.md) | 多轮串线问题、身份拆分、协议迁移与 exact-run E2E | 架构对照: diff --git a/mvp/engineering/diagnosis/Session-Run-Trace隔离-从串线到可回放.md b/mvp/engineering/diagnosis/Session-Run-Trace隔离-从串线到可回放.md new file mode 100644 index 0000000..bf0683a --- /dev/null +++ b/mvp/engineering/diagnosis/Session-Run-Trace隔离-从串线到可回放.md @@ -0,0 +1,315 @@ +# Session、Run、Trace 隔离:一次身份建模错误的修复 + +**更新日期**:2026-07-29 +**性质**:MVP 工程问题与架构决策复盘 +**结论状态**:Session / Run 身份模型沿用至当前架构 + +--- + +## 1. 问题到底是什么 + +一句话定义:**系统用同一个 `sessionId`,同时标识“多轮对话”和“一次诊断执行”,导致一次 Trace 不再对应一次真实执行。** + +问题由一个两轮 E2E 暴露。用户在同一会话中连续发起两次请求: + +```text +Round 1:诊断支付接口超时 +Round 2:基于上一轮结论,列出还缺哪些证据 +``` + +两轮复用 `sessionId` 是正确的,因为第二轮需要上一轮上下文。但当时持久化和 Trace 查询也只使用 `sessionId`: + +- `diagnosis_session` 是覆盖写,第二轮把第一轮的 query、answer、status 覆盖掉; +- `agent_step` 和 `tool_invocation` 是追加写,两轮明细累积在同一个 `sessionId` 下; +- Trace API 再按 `sessionId` 聚合主表和明细。 + +结果不是简单的“重复数据”,而是一个系统中从未真实发生过的混合执行: + +```mermaid +flowchart LR + R1["Round 1
query A / steps A / tools A"] --> SID["同一个 sessionId"] + R2["Round 2
query B / steps B / tools B"] --> SID + + SID --> Main["diagnosis_session
只剩 query B / answer B"] + SID --> Steps["agent_step
steps A + steps B"] + SID --> Tools["tool_invocation
tools A + tools B"] + + Main --> Trace["混合 Trace"] + Steps --> Trace + Tools --> Trace + + Trace --> Error["无法回答:
哪组证据支持了哪次回答?"] +``` + +这个错误会沿数据链继续放大: + +| 消费方 | 错误结果 | +|---|---| +| Trace 回放 | 一条 Trace 混合两轮步骤和 Tool | +| Verifier / Gatekeeper | 当前轮可能读到上一轮证据 | +| Evaluation | 评分对象和证据集合不再属于同一次执行 | +| Feedback | 无法确定用户评价的是哪一轮回答 | +| CaseLibrary | 可能把反馈沉淀到错误的 query/answer 上 | + +因此,核心问题不是某个 Repository 的更新方式,而是**会话边界被错误地当成了证据与审计边界**。 + +--- + +## 2. 设计必须守住什么 + +修复前先定义四条不变量。后续方案不是凭表结构偏好选择,而是看能否同时满足这些约束。 + +### 不变量 1:一次执行只有一个稳定身份 + +从请求被接受到最终 `SUCCESS / FALLBACK / FAILED / CANCELLED`,必须有一个不可变 ID。模型步骤、Tool 调用、预算、最终答案和反馈都属于它。 + +### 不变量 2:一条 Trace 只描述一次执行 + +Trace 中的 Run、AgentStep、ToolInvocation 和生命周期事件必须使用同一个 exact ID 聚合,不能依赖时间邻近或“最新一条”猜测归属。 + +### 不变量 3:上下文连续不等于执行合并 + +同一个 Session 可以包含多个 Run。后一个 Run 可以读取前一轮安全发布结果,但不能继承前一轮的 Tool、Trace、评分或失败状态。 + +### 不变量 4:兼容不能伪造精度 + +旧客户端可以有迁移期 fallback,旧数据也可以保留;但系统必须让歧义可观察,不能把无法恢复的历史混合数据伪装成精确多轮记录。 + +这四条不变量共同导出一个结论:系统需要两个身份,而不是给 `sessionId` 增加更多解释。 + +--- + +## 3. 为什么不是在旧表上继续修 + +设计阶段考虑的不是“拆表还是不拆表”这一个问题,而是如何建立稳定的执行边界。 + +| 候选方案 | 能解决什么 | 为什么没有选择 | +|---|---|---| +| 继续复用 `sessionId`,修正覆盖逻辑 | 避免主表被覆盖 | 明细仍无法区分轮次,根因未解决 | +| 增加 `round_no` | 可以表示第几轮 | 并发请求、重试和多个执行入口下顺序不稳定;外部引用仍需复合身份 | +| 按时间窗口拆分历史 Step/Tool | 无需改协议 | 时间不能证明归属,会制造看似精确的错误 Trace | +| 每次请求插入一条新的 `diagnosis_session` | 形成一行一次执行 | 实际上已经引入 Run 概念,但名称和 Session 生命周期仍混淆,也缺少会话主实体 | +| 拆分 `chat_session` 与 `diagnosis_run` | 显式表达一对多生命周期 | 需要 Schema、API 和客户端迁移,但能满足全部不变量 | + +最终选择最后一种。它的判断依据不是“范式更规范”,而是只有它能让对话连续性和执行可审计性同时成立。 + +--- + +## 4. 核心设计 + +目标模型是一个清晰的一对多关系: + +```mermaid +flowchart TB + Client["Client"] --> Session["chat_session
sessionId:多轮对话目录"] + Session --> Run1["diagnosis_run
runId 1:一次执行"] + Session --> Run2["diagnosis_run
runId 2:另一次执行"] + + Run2 --> Step["agent_step
模型步骤 metadata"] + Run2 --> Tool["tool_invocation
Tool 审计 metadata"] + Run2 --> Event["diagnosis_trace_event
生命周期 Timeline"] + Run2 --> Reasoning["agent_reasoning_audit
受限原文"] + + Run2 --> Trace["普通 Trace API"] + Step --> Trace + Tool --> Trace + Event --> Trace + Reasoning --> Audit["独立 Reasoning API"] +``` + +这里有三个不同的职责: + +- `chat_session` 回答“哪些 Run 属于同一段对话”; +- `diagnosis_run` 回答“这次请求的输入、状态、结果和资源消耗是什么”; +- Trace 回答“这个 Run 具体经历了什么”,它是按 Run 聚合的读模型。 + +--- + +## 5. 六项关键决策 + +### 决策 1:引入正式的 `runId` + +**选择**:每次有效 Chat 执行创建新的 `runId`,并通过 SSE metadata 返回 `sessionId + runId`。 + +**理由**:Run 必须能被 API、数据库、日志、Feedback 和评测独立引用。数据库自增 ID 不适合作为外部协议;轮次编号又不能稳定处理并发和重试。 + +**代价**:客户端必须保存并向后续 Trace/Feedback 请求传递 `runId`。 + +**边界**:`runId` 是不透明标识。早期实现使用 `run-` + UUID,后续格式发生过演进,客户端不得解析其前缀或长度。 + +### 决策 2:拆分会话态与运行态 + +**选择**:新增 `chat_session` 和 `diagnosis_run`,旧 `diagnosis_session` 停止承载新的运行写入。 + +**理由**:两者生命周期不同。 + +| 对象 | 保存内容 | 更新特点 | +|---|---|---| +| `chat_session` | 会话状态、轮次数、最近活动时间等目录信息 | 跨多轮持续更新 | +| `diagnosis_run` | 单次 query、answer、终态、intent、release outcome、预算 | 一次执行内从 RUNNING 走向唯一终态 | + +完整多轮正文没有因为拆表就复制到 MySQL。身份拆分解决的是审计归属,不应顺带扩大长期数据保存范围。 + +### 决策 3:Trace 是 Run 的聚合视图,不另造身份 + +**选择**:第一阶段复用 `agent_step` 和 `tool_invocation`,增加 `run_id`;不为了修复隔离问题再创建一个独立 `traceId`。 + +**理由**:隔离所缺的是执行外键,不是第三套身份。引入 `traceId` 只会产生 `sessionId / runId / traceId` 的映射问题。 + +后续单 Agent + Harness 重构新增 `diagnosis_trace_event`,用于表达统一生命周期 Timeline。这是 Run 下的新明细,不是新的聚合根,也没有改变 `runId` 的边界。 + +### 决策 4:exact Run 是目标协议,latest Run 只是迁移桥梁 + +**选择**:目标查询使用: + +```http +GET /api/diagnosis/{sessionId}/trace?runId={runId} +``` + +服务端同时验证 Run 存在且属于 path 中的 Session,防止跨 Session 串读。 + +旧客户端暂时只传 `sessionId` 时,可以解析 latest Run;但这是显式兼容路径,不是新的业务语义。如果必须计算 latest,应按: + +```text +created_at DESC, id DESC +``` + +而不是 `updated_at`。旧 Run 可能因 Feedback 或异步处理再次更新,最近修改不等于最近执行。 + +### 决策 5:所有下游语义绑定 Run + +**选择**:Trace、Feedback、Evaluation、Tool evidence 和新 Case provenance 都以 `runId` 为执行边界。 + +**理由**:这些对象评价或引用的是一次回答,不是整段会话。 + +Feedback 在迁移期缺少 `runId` 时可以绑定 latest Run,但响应必须暴露 `fallbackToLatestRun=true`。兼容如果不可观察,就会从临时措施变成永久歧义。 + +`case_library.diagnosis_id` 因复用旧列,在过渡期存在历史 `session_id` 和新 `run_id` 两种语义。这是明确接受的迁移成本,而不是应被隐藏的数据一致性。 + +### 决策 6:历史混合数据不做推测性拆分 + +**选择**:每条旧 `diagnosis_session` 最多映射为一个 compatibility Run,不根据时间或 Agent 名称猜测真实轮次。 + +**理由**:旧数据没有记录边界,任何自动拆分都只能产生无法证明的归属。审计系统宁可明确“不知道”,也不能制造虚假的精确回放。 + +--- + +## 6. 协议和影响范围 + +这是一次有意的行为与协议变化,不是纯内部重构。 + +| 范围 | 变化 | 受影响方 | +|---|---|---| +| Chat / SSE | metadata 增加 `runId` | 前端、脚本、调用方 | +| Trace API | 支持 exact `runId` 查询 | Trace UI、排障工具、评测 | +| Run API | 提供 Session 下的 Run 列表 | 多轮历史浏览 | +| Feedback | request/response 增加 Run 绑定和 fallback 标志 | 前端、CaseLibrary | +| 数据库 | 新增两张主表,明细增加 `run_id` | 持久化、迁移、查询脚本 | +| 其它入口 | 当时的 AIOps 同步采用 Run 边界 | SSE 消费方、Trace | + +之所以把当时的 AIOps 一并迁移,是因为它同样会产生可回放执行;只修 Chat 会留下第二条具有同类缺陷的数据链。后续 ISS-014 删除了旧 AIOps 双入口,但这不改变当时“所有执行入口必须共享 Run 边界”的设计判断。 + +--- + +## 7. 风险如何处理 + +### 风险 1:兼容路径继续产生歧义 + +控制方式是让 fallback 可观察,并把 exact `runId` 定义为目标协议。兼容是迁移机制,不能反向成为领域模型。 + +### 风险 2:异步链路丢失或串用身份 + +`sessionId` 与 `runId` 必须作为同一执行上下文传播。当前架构将二者放入显式 `RunContext`,ToolBoundary、审计 Hook 和持久化都校验当前 Run,避免只依赖线程隐式状态。 + +### 风险 3:历史和新 provenance 共用旧列 + +保留旧列降低了迁移破坏性,但查询和文档必须承认双语义,不能把旧 `session_id` 当作非法 `run_id` 清理。 + +### 风险 4:旧混合 Trace 永远无法恢复 + +这是明确接受的事实。系统保留 compatibility 访问和回滚能力,但不承诺不存在的历史精度。 + +--- + +## 8. 如何证明设计成立 + +验收问题不是“接口里有没有 `runId`”,而是下面五个条件是否同时成立: + +```text +同一 Session 连续执行两轮 + + 两轮获得不同 runId + + 每个 exact Trace 只返回本 Run 明细 + + 数据库不存在跨 Run 混合行 + + 第二轮仍能使用会话上下文 +``` + +真实 E2E 使用: + +```text +sessionId = e2e-phase6-chat-codex-20260710-2120 +run1 = run-e2a97696-4398-4abc-90e4-28f45c838f92 +run2 = run-76ce6a6e-92ab-40c9-800a-eca0c1bb5172 +``` + +结果: + +- 两轮 Chat 成功并复用同一个 `sessionId`; +- 两轮返回不同 `runId`; +- run1 exact Trace 只返回 run1,run2 exact Trace 只返回 run2; +- `diagnosis_run` 中存在两条独立运行记录; +- AgentStep:run1 为 10 行,run2 为 9 行; +- ToolInvocation:run1 为 14 行,run2 为 8 行; +- mixed row check 为 0; +- `chat_session.message_pair_count = 2`,上下文连续性没有因隔离而丢失; +- focused tests、baseline diff、日志和数据库核验通过,未观察到 baseline drift。 + +这组证据同时验证了“该分开的确实分开”和“该连续的仍然连续”。 + +--- + +## 9. 后续演进验证了什么 + +系统后来从多角色 Agent 编排重构为单 Diagnosis ReAct Agent + Harness。执行结构发生了大变化,但 Session / Run 模型没有被替换,反而成为新架构的基础: + +- `ChatApplicationUseCase` 创建和结束 Run; +- `RunContext` 显式携带 `sessionId + runId`; +- ToolBoundary 校验 Tool 请求属于当前 Run; +- Redis canonical invocation 使用 `runId + toolCallId` 定位; +- EvidenceGuard 只接受当前 Run 的 READY invocation; +- AgentStep、ToolInvocation、TraceEvent 和 ReasoningAudit 都绑定 Run。 + +这说明当时解决的不是某一版代码的局部 bug,而是找到了稳定的领域边界。Agent 编排可以替换,Session 与 Run 的生命周期差异不会消失。 + +--- + +## 10. 可复用的设计判断 + +这次问题可以归纳为四条通用经验: + +1. **生命周期不同的对象,不应共享同一个聚合身份。** +2. **上下文复用不代表证据、状态和审计记录也可以复用。** +3. **兼容 fallback 必须可观察、可退出,不能静默猜测。** +4. **无法恢复的历史边界应明确降级,不能伪造精确性。** + +判断类似系统是否存在同类问题,可以直接问: + +- 一次请求是否有独立于 Session 的执行 ID? +- 所有 Step、Tool、Event、Feedback 是否都能精确归属一次执行? +- “查询最新”是否被误当成“查询指定执行”? +- 主表覆盖写、明细追加写是否使用了同一个过宽的关联键? +- 历史迁移是在保留不确定性,还是通过猜测制造精确性? + +如果这些问题没有明确答案,那么 Trace 即使看起来完整,也未必能作为可信审计证据。 + +--- + +## 11. 资料索引 + +- [ISS-010:同 session 多轮诊断 Trace 隔离](../../issues/archived/ISS-010-session-run-trace-isolation.md) +- [Session、Run 与 Trace 生命周期](../../architecture/session-trace-lifecycle.md) +- [当前 MVP 架构](../../architecture/current-mvp-architecture.md) +- [数据表索引](../../tables/README.md) +- [devflow brief](../../../devflow/projects/2026-07-10-session-run-trace-isolation/brief.md) +- [devflow decisions](../../../devflow/projects/2026-07-10-session-run-trace-isolation/decisions.md) +- [devflow acceptance](../../../devflow/projects/2026-07-10-session-run-trace-isolation/acceptance.md) +- [devflow evidence](../../../devflow/projects/2026-07-10-session-run-trace-isolation/evidence.md) diff --git a/mvp/engineering/harness/CONTEXT.md b/mvp/engineering/harness/CONTEXT.md new file mode 100644 index 0000000..769b68a --- /dev/null +++ b/mvp/engineering/harness/CONTEXT.md @@ -0,0 +1,378 @@ +# Harness Context:统一术语与命名边界 + +**更新日期**:2026-07-29 +**状态**:当前实现口径 +**适用范围**:`com.superbiz.agent.harness`、Chat SSE、Diagnosis Run 持久化与相关工程文档 + +> 本文是术语词典,不建议第一次接触 Harness 时顺序阅读。入门请从 [README.md](README.md) 开始,遇到名词歧义时再回到本文查询。 + +## 1. 为什么需要这份 Context + +当前系统同时存在 Run 状态、发布结果、Tool 状态、证据状态、收集状态、停止原因和 Fallback 原因。它们都使用了 `SUCCESS`、`ERROR`、`FAILED`、`READY` 等相近词汇,但回答的是不同问题。 + +如果把这些词排成一条“大状态机”,会产生错误理解,例如: + +- `NO_EVIDENCE` 被理解为 Tool 调用失败; +- `FALLBACK` 被理解为 Run 执行失败; +- `SATURATED` 被理解为预算耗尽; +- `READY` 被理解为证据足以支持根因; +- 数据库 `status=SUCCESS` 被理解为已经找到根因。 + +本文件是 Harness 工程文档的术语入口。阅读其他文章前,先以这里的定义区分身份、数据、状态和组件责任。代码与现行架构文档仍是最终事实来源;本文件不创建新的运行协议。 + +## 2. 一句话定义 Harness + +**Harness** 是包围非确定性模型执行的确定性控制边界:Agent 负责业务推理和 Draft,Harness 负责 Run 身份、生命周期、预算、取消、Tool 门禁、证据验真、停止控制、发布和审计。 + +Harness 不是: + +- 业务工作流引擎; +- Planner / Executor / Verifier / Composer 编排图; +- 框架 ReAct loop 的第二份实现; +- 判断业务根因的规则引擎; +- 用于存放所有 Agent 相关代码的泛化名称。 + +## 3. 三层范围:不要把 Harness、Core 和 Application 当成同义词 + +| 名称 | 定义 | 包含 | 不包含 | +|---|---|---|---| +| Chat Application | 一次 Chat 请求的应用用例 | Session/Run 创建、路由、分支执行、持久化、公开结果 | HTTP/SSE 连接本身、业务推理细节 | +| Diagnosis Harness | Diagnosis Agent 外部的确定性控制系统 | Core、Interceptor、ToolBoundary、Progress、Guard、Release、Audit | 根因推理和 ReAct 规划 | +| Harness Core | 最小运行控制内核 | RunContext、deadline、budget、cancel、lifecycle、retry policy | 路由、Tool backend、Guard、持久化、SSE | + +代码包 `com.superbiz.agent.harness` 同时包含 Chat Application 和 Diagnosis Harness 的实现,这是代码组织范围,不代表所有类都属于 Harness Core。 + +`DiagnosisHarnessCore` 目前也被 System Chat、Knowledge Query 和 Router 复用预算与生命周期能力。类名前缀保留了演进历史,概念上应理解为当前 Chat Run 的 Harness Core。 + +## 4. 身份术语 + +```mermaid +flowchart TB + S["Chat Session
sessionId,多轮容器"] -->|"1:N"| R1["Run
runId,一次请求"] + S -->|"1:N"| R2["Run
下一次请求"] + + R1 -->|"1:N"| AS["Agent Step
一次模型步骤"] + R1 -->|"1:N"| TC["Tool Call / Invocation
framework tool_call_id"] + R1 -->|"1:N"| TE["Trace Event
sequence_no"] + + TC --> CI["Canonical Invocation
Run 内短期 Tool 真相"] + AS --> TR["Diagnosis Trace
按 exact Run 聚合"] + TC --> TR + TE --> TR + + R1 -.->|"SUCCESS diagnosis only"| PT["PublishedResult
可投影为下一轮 PreviousTurn"] +``` + +图中的所有明细都必须绑定 exact runId。Session 只负责组织多轮,不能替代 Run 归属;Trace 是聚合视图,不能反过来成为执行身份。 + +### 4.1 Chat Session + +一次多轮对话容器,由 `sessionId` 标识。同一个 Session 可以包含多次 Run。 + +Session 用于: + +- 组织多轮请求; +- 查找安全的 PreviousTurn; +- 查询历史 Run。 + +Session 不代表一次执行,不拥有 Tool Call 或模型步骤的终态。 + +### 4.2 Run + +一次独立的 Chat Application 执行,由 `runId` 标识。每次请求创建新 Run,无论 intent 是 System Chat、Knowledge Query 还是 Diagnosis。 + +代码和表名中仍大量使用 `DiagnosisRun` / `diagnosis_run`,但当前 Chat Application 会为三种 intent 都创建该记录。因此文档中优先使用 **Run**;只有引用 Java 实体或数据库表时才写 `DiagnosisRun`。 + +### 4.3 Agent Step + +Diagnosis ReAct Agent 的一次模型步骤。多个 Agent Step 属于一个 Run,用于记录每轮模型调用的有界元数据和 Token。 + +Agent Step 不是 Run,也不是 retry attempt。ReAct 的下一轮模型调用是业务循环;retry 是同一技术操作的再次 attempt。 + +### 4.4 Tool Call / Tool Invocation + +- **Tool Call**:模型通过框架产生的调用请求,由 framework `tool_call_id` 标识。 +- **Tool Invocation**:该请求进入 ToolBoundary 后的一次实际执行记录。 +- **Tool Request Rejection**:在 progress、重复或停止门禁处被拒绝,backend 没有执行,不算 Tool Invocation。 + +Harness 不生成第二套 Tool Call ID。 + +### 4.5 Trace + +按 exact `sessionId + runId` 聚合的可回放观察视图,包含 Run、Agent Step、Tool metadata 和统一 Timeline。 + +Trace 是观察结果,不是新的执行上下文或状态所有者。`TraceEventStatus` 只描述单个事件,不能替代 RunState 或 ReleaseOutcome。 + +## 5. 核心执行术语 + +### 5.1 RunContext + +一次 Run 的显式执行上下文。结构不可变地携带: + +```text +sessionId +runId +deadline +RunCancellation +RunBudget +ModelCallLedger +HarnessRetryPolicies +RunLifecycle +DiagnosisProgressTracker +``` + +“结构不可变”表示 record 字段引用不变化;预算、取消、生命周期和进展通过各自线程安全句柄在 Run 内变化。 + +### 5.2 Run Lifecycle + +Run 的内存执行终态,由 `RunLifecycle` 所有,状态类型是 `RunState`。它采用 first-terminal-wins,后到达的成功、失败或取消不能覆盖第一个终态。 + +### 5.3 Run Cancellation + +请求停止 Run 的协作机制,由 `RunCancellationReason` 记录第一个原因。取消会阻止后续边界和迟到发布,但不承诺一定能立即物理中断已发送给 Provider 的同步请求。 + +### 5.4 Run Budget + +单 Run 的资源账本和门禁,包括模型调用、Tool 调用、单 Tool 次数、输入/输出/总 Token 和 Run bytes。 + +Budget 只回答“还能不能消耗资源”,不回答“继续诊断是否有价值”。后者属于 Information Gain 和 Collection State。 + +### 5.5 Retry Attempt + +同一个技术操作因允许的技术失败而再次执行。当前 Router 和 SemanticGuard 最多 2 次 attempt,Diagnosis Agent、Tool 和 EvidenceRepair 只有 1 次。 + +以下不是 retry: + +- ReAct Agent 的下一轮思考; +- 改用另一个 Tool; +- `NO_EVIDENCE` 后继续查询; +- 用户发起下一次 Run。 + +## 6. Agent 与输出术语 + +### 6.1 Diagnosis Agent + +唯一拥有业务 ReAct Tool loop 的 Agent,负责提出假设、选择 Tool、评价非空结果的信息增益并生成 `DiagnosisDraft`。 + +它不负责 Run 生命周期、Tool 授权、证据物理验真、SemanticGuard 或最终发布。 + +### 6.2 DiagnosisDraft + +Diagnosis Agent 的结构化草稿,是发布链输入,不是已经发布的报告。Draft 可以有结论,也可以 `conclusion=null`。 + +Draft 中的 Tool 引用和结论必须经过 Release Pipeline 后才能成为公开内容。 + +### 6.3 Agent Result + +当前 Tool 代码中的 `agent_result` 指 Tool-specific Projector 生成、存入 canonical invocation 的标准化有界结果。 + +它不是: + +- Diagnosis Agent 的最终 Draft; +- Chat Application 的最终结果; +- 直接进入模型上下文的完整内容。 + +更准确的理解是 **Canonical Projected Tool Result**。当前字段名因协议兼容保留。 + +### 6.4 Model Observation + +从 canonical `agent_result` 再次白名单投影后,真正作为 Tool Response 进入 Diagnosis Agent 上下文的内容。 + +预算、阈值、重复指纹、raw response 和完整 Harness 控制状态不进入 Model Observation。 + +### 6.5 PreviousTurn 与 PublishedResult + +- `PublishedResult`:只有 `DIAGNOSIS + ReleaseOutcome.SUCCESS` 才能持久化的安全诊断结果。 +- `PreviousTurn`:从 PublishedResult 生成的有界下一轮上下文。 + +Fallback、失败、取消、raw evidence 和完整历史不会进入 PreviousTurn。 + +`PublishedResult` 不等于当前请求直接返回的 `ChatApplicationResult`。 + +## 7. Tool 与证据术语 + +### 7.1 ToolBoundary + +所有业务 Tool 的统一执行边界,负责 Run/ID/授权/只读校验、预算、bytes、canonical 状态迁移和 metadata audit。 + +ToolBoundary 不判断信息增益或业务根因。 + +### 7.2 Canonical Invocation + +Redis TTL 内的完整 Tool 调用真相,包含 request、raw response、标准化 `agent_result`、调用状态和证据状态。 + +它用于当前 Run 的 EvidenceGuard 和 ProgressSnapshot,不是长期审计记录。 + +### 7.3 Durable Audit + +长期保存的有界元数据:Run/Tool identity、状态、耗时、bytes、模型步骤和 Token。它不保存 Prompt、完整 Tool 参数、raw response 或 canonical record。 + +### 7.4 Evidence + +Tool 在特定 scope 下返回、经过 Projector 标准化并能由当前 Run canonical record 验真的事实或负向观察。 + +`EVIDENCE_FOUND` 只代表存在候选内容,不代表它支持根因。 + +### 7.5 Negative Observation + +`READY + NO_EVIDENCE` 形成的限定范围事实,例如“在时间窗 T、服务 S、查询 Q 下没有匹配日志”。 + +它不能被解释为“故障不存在”或“系统健康”。Draft 中使用 `AnalysisKind.NEGATIVE_OBSERVATION` 表达这种分析。 + +### 7.6 VerifiedEvidenceSnapshot + +EvidenceGuard 从 canonical invocation 中投影出的已验真、最小证据集合,供 SemanticGuard 使用。它不包含 raw response。 + +### 7.7 ProgressSnapshot + +Tool loop 结束后,根据已完成 Tool Call identity 回读 canonical records 生成的有界过程视图,用于受控停止或无结论 Fallback。 + +VerifiedEvidenceSnapshot 面向“有结论报告的语义审查”;ProgressSnapshot 面向“没有可发布结论时说明已经检查了什么”,两者用途不同。 + +## 8. Guard 与发布术语 + +### 8.1 EvidenceGuard + +确定性引用验真器。检查 Draft 结构、analysis 引用闭包、当前 Run 所有权、READY 状态、EvidenceStatus 和 typed projection 自洽性。 + +不调用模型,不判断结论是否被证据支持。 + +### 8.2 EvidenceRepair + +一次性的模型修复步骤,只修结构和引用。修复前后 `SemanticDraftView` 必须保持用户可见语义一致,之后重新执行 EvidenceGuard。 + +它不是新的报告作者,也不是 retry Agent。 + +### 8.3 SemanticGuard + +隔离的单轮语义审查器,只判断 verified evidence 是否支持 Draft,输出 `SUPPORTED / UNSUPPORTED`。 + +它使用模型,但无 Tool、无记忆、无 ReAct loop、无报告改写权,因此文档中不要称它为第二个业务 Agent。 + +### 8.4 Release Pipeline + +从 DiagnosisDraft 或受控停止输入,到 `DiagnosisReleaseResult` 的安全决策链:EvidenceGuard、可选 Repair/Recheck、SemanticGuard 和 SafeFallback。 + +### 8.5 ReleaseOutcome + +应用层最终处理结果: + +- `SUCCESS`:发布安全正常内容; +- `FALLBACK`:请求已被安全处理,但没有发布正常诊断结论; +- `FAILED`:无法形成安全业务结果; +- `CANCELLED`:Run 被取消。 + +ReleaseOutcome 不等于 RunState。尤其 `FALLBACK` 不是 RunState。 + +### 8.6 SafeFallback 与 FallbackType + +SafeFallback 是确定性公开内容;FallbackType 解释为什么没有发布正常诊断结论,例如: + +- `EVIDENCE_VALIDATION_FAILED`; +- `SEMANTIC_UNSUPPORTED`; +- `SEMANTIC_UNAVAILABLE`; +- `INSUFFICIENT_EVIDENCE`; +- `MISSING_REQUIRED_CONTEXT`。 + +FallbackType 是 `ReleaseOutcome.FALLBACK` 的原因,不是新的生命周期状态。 + +## 9. 状态维度速查 + +| 类型 | 所有者 | 回答的问题 | 不能回答的问题 | +|---|---|---|---| +| `RunState` | RunLifecycle | Run 的内存执行是否终止、如何终止 | 发布了正常内容还是 Fallback | +| `RunCancellationReason` | RunCancellation | 谁首先请求取消、为什么 | 最终公开结果是什么 | +| `ChatApplicationStatus` | Application Observer | 当前向用户展示哪个处理阶段 | Run 是否已经终止 | +| `InvocationStatus` | Canonical Invocation | Tool 调用记录是否完成 | 是否找到候选证据 | +| `EvidenceStatus` | Tool Projector | 当前 scope 是否有候选证据 | 是否支持根因 | +| `InformationGain` | Harness/Diagnosis Agent | 结果是否推进当前诊断 | Tool 是否技术成功 | +| `DiagnosisCollectionState` | ProgressTracker | 是否允许继续调用证据 Tool | Run 是否终止 | +| `DiagnosisStopReason` | ProgressTracker | 为什么停止继续收集 | 对外发布什么内容 | +| `SemanticVerdict` | SemanticGuard | 已验真证据是否支持 Draft | Run 是否成功执行 | +| `ReleaseOutcome` | Application/Release | 对外处理结果属于成功、降级、失败还是取消 | Tool 或收集过程的内部状态 | +| `FallbackType` | SafeFallbackFactory | FALLBACK 的业务/安全原因 | 整个 Run 的执行终态 | +| `TraceEventStatus` | 单个 Trace Event | 某条事件的局部结果 | 全局生命周期 | +| SSE Session `State` | ChatSseSession | 连接能否继续发送事件 | Harness Run 的业务结果 | + +```mermaid +flowchart LR + subgraph Execution["执行控制维度"] + RS["RunState
RUNNING -> terminal"] + CS["CollectionState
COLLECTING / SATURATED"] + SR["StopReason
停止收集原因"] + end + + subgraph Tool["单次 Tool 维度"] + IS["InvocationStatus
PROJECTING / READY / ERROR"] + ES["EvidenceStatus
FOUND / NO_EVIDENCE / ERROR"] + IG["InformationGain
GAINED / NO_GAIN"] + end + + subgraph Validation["验证维度"] + EV["EvidenceGuardResult
valid / violations"] + SV["SemanticVerdict
SUPPORTED / UNSUPPORTED"] + end + + subgraph Publication["发布与观察维度"] + RO["ReleaseOutcome
SUCCESS / FALLBACK / FAILED / CANCELLED"] + FT["FallbackType
FALLBACK 原因"] + SSE["SSE Session State
连接发送状态"] + DB["diagnosis_run.status
持久化通用状态"] + end + + IS --> ES + ES --> IG --> CS + CS --> SR + ES --> EV --> SV + SR --> RO + SV --> RO + RS --> RO + RO --> FT + RO --> SSE + RO --> DB +``` + +箭头表示信息参与后续决策,不表示枚举之间一一转换。例如 `READY + EVIDENCE_FOUND` 仍可能得到 `NO_GAIN`,`RunState.SUCCESS` 也可能对应 `ReleaseOutcome.FALLBACK`。 + +完整状态转换和跨层映射见[Harness生命周期与状态.md](Harness生命周期与状态.md)。 + +## 10. 命名规则 + +后续代码和文档遵守以下用词: + +1. 说“Run 成功/失败/取消/超时/预算耗尽”时,明确写 `RunState`。 +2. 说“发布正常内容/Fallback/失败/取消”时,明确写 `ReleaseOutcome`。 +3. 不单独写“Tool 成功”,改写为 `InvocationStatus=READY`,并同时说明 EvidenceStatus。 +4. 不写“找到有效证据”,除非已经说明是候选内容、已验真事实还是足以支持结论。 +5. `NO_EVIDENCE` 必须附带 scope,不得写成全局否定。 +6. `SATURATED` 只用于 Collection State;预算耗尽使用 `BUDGET_LIMIT_REACHED` 或 `RunState.BUDGET_EXHAUSTED`。 +7. `Fallback` 只指安全发布降级,不用来泛指异常兜底代码。 +8. `Agent` 默认指 Diagnosis Agent;SemanticGuard 和 EvidenceRepair 分别称“语义审查器”和“引用修复步骤”。 +9. `agent_result` 引用字段名时保留原名,概念说明使用“canonical projected Tool result”。 +10. `status` 单独出现没有意义,必须注明所属类型或存储字段。 + +## 11. 已知命名债务 + +### 11.1 `DiagnosisRun` 名称大于实际诊断范围 + +当前 Chat Application 对三种 intent 都写入 `diagnosis_run`。文档统一称 Run;是否重命名实体和表属于单独的协议/迁移决策,本次不修改代码。 + +### 11.2 `SseOutcome` 当前未被运行时使用 + +`SseOutcome` 枚举存在,但当前 `ChatSseEvent.Done` 直接携带 `ReleaseOutcome`,并禁止公开 `CANCELLED`。因此当前 SSE 真理源是 `ReleaseOutcome`,不要再基于 `SseOutcome` 推导协议。 + +### 11.3 `FallbackType.BUDGET_EXHAUSTED` 不是当前诊断发布主路径 + +枚举值仍存在,但当前 Diagnosis Release 对预算受控停止的行为是:有已验真进展时发布 `INSUFFICIENT_EVIDENCE`,无安全进展时保持失败。文档不能仅因枚举存在就声称系统会发布 `BUDGET_EXHAUSTED` Fallback。 + +### 11.4 数据库 `status=SUCCESS` 不等于找到根因 + +`JpaChatRunStore` 将 `ReleaseOutcome.SUCCESS` 和 `FALLBACK` 都映射为数据库 `status=SUCCESS`,表示请求被正常处理。是否发布根因必须结合 `release_outcome`、`content_type` 和 FallbackType 判断。 + +### 11.5 RunState 没有直接作为独立字段持久化 + +当前持久化主记录保存通用 `status` 和 `release_outcome`,Trace 保存阶段事件;内存 `RunTermination.state/reason` 不是独立数据库字段。排查时不能只靠数据库 `status` 反推 TIMED_OUT 或 BUDGET_EXHAUSTED 的精确内部终态。 + +## 12. 如何使用本文 + +本文不是必读的第一章,而是遇到名词歧义时使用的词典。第一次接触 Harness,请先读 [README.md](README.md) 建立最小心智模型;需要理解某个状态或概念时,再回到本文对应章节查询。 diff --git a/mvp/engineering/harness/Harness-Tool双视图-从原始结果到可验证证据.md b/mvp/engineering/harness/Harness-Tool双视图-从原始结果到可验证证据.md new file mode 100644 index 0000000..c29f6fd --- /dev/null +++ b/mvp/engineering/harness/Harness-Tool双视图-从原始结果到可验证证据.md @@ -0,0 +1,280 @@ +# Harness Tool 双视图:从原始结果到可验证证据 + +**更新日期**:2026-07-29 +**主题**:ToolBoundary、Canonical Invocation、Harness Control View 与 Agent Observation +**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) +**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md) + +## 1. 要解决的不是 Tool 调用,而是 Tool 结果的所有权 + +Agent 调用 Tool 后,最直接的实现是把 backend 返回的 JSON 原样放进模型上下文。这在 Demo 中可以工作,但进入可验证的诊断系统后会出现一个根本矛盾:同一份结果需要同时服务推理、控制、验真和审计,而这些消费者需要的数据范围完全不同。 + +以日志查询为例: + +- Agent 需要少量匹配事件、模式、实际时间范围和是否截断; +- Harness 需要返回数量、规范化 scope、重复身份和客观证据状态; +- EvidenceGuard 需要证明这份结果确实来自当前 Run 的某次 READY 调用; +- 审计需要 Tool 名、调用 ID、状态、耗时和字节数; +- 安全边界又要求密码、Token、主机、IP、SQL 字面量和无限日志正文不能进入模型或长期审计。 + +如果只保留一份 JSON,只能在两个错误方向中选择:要么信息过多导致泄露和上下文膨胀,要么信息过少导致后续无法验真。 + +因此这项设计的核心不是“增加一个 Redis Store”,而是重新定义 Tool 数据所有权: + +> backend raw 属于 Harness;模型只能获得为推理目的生成的有界观察;长期审计只保留允许运营保存的元数据。 + +## 2. 三个消费者,三种数据责任 + +虽然实现中常称为“Tool 双视图”,完整的数据分层实际上包含三种用途: + +| 数据形态 | 消费者 | 解决的问题 | 生命周期 | +|---|---|---|---| +| Canonical Invocation | ToolBoundary、EvidenceGuard、ProgressProjector | 这次 Tool 真实执行了什么、属于哪个 Run、能否引用 | Redis 短 TTL | +| Harness Control View / Agent Observation | Harness / Diagnosis Agent | 是否重复、是否为空、模型下一步需要看到什么 | 当前 Run / 模型上下文 | +| Durable Audit | Trace、运营排障 | 何时调用、状态、耗时、大小、关联 Step | MySQL 长期 metadata | + +所谓“双视图”,特指 canonical 标准结果被进一步分成: + +1. Harness 使用的 Control View; +2. 模型使用的 Agent Observation。 + +Durable Audit 是第三个持久化面,但它不是 Tool 内容视图,不参与推理或证据验真。 + +```mermaid +flowchart LR + A["Typed Tool Request"] --> B["ToolBoundary"] + B --> C["Backend Raw Response"] + C --> D["Tool-specific Projector"] + D --> E["Canonical agent_result"] + + B --> F["Redis Canonical Invocation
request + raw + agent_result"] + E --> F + + E --> G["Harness Control View
count / status / scope"] + E --> H["Agent Observation
白名单、有界、脱敏"] + + B --> I["Durable Audit
identity / status / latency / bytes"] + G --> J["重复检测、NO_GAIN、停止"] + H --> K["Diagnosis Agent Context"] + F --> L["EvidenceGuard / ProgressSnapshot"] +``` + +## 3. 为什么不能让 Agent Observation 充当真相源 + +Agent Observation 是为了控制上下文而生成的投影,它可能: + +- 只保留前 N 条结果; +- 截断单条文本; +- 对敏感列和日志字段做脱敏; +- 把大量事件聚合成模式; +- 只暴露粗粒度相关度,不暴露检索轨迹和原始分数。 + +这意味着它适合帮助模型推理,却不适合作为“后台真实返回”的完整证明。如果 EvidenceGuard 反过来验证 Agent 自己收到的 observation,就相当于用被审查对象提供的摘要证明其自身真实性。 + +Canonical Invocation 解决的正是这个独立性问题。它以 exact `runId + tool_call_id` 建立记录,并保存: + +```text +tool_call_id +run_id +tool_name +request +raw_response +agent_result +invocation status +evidence status +error code +started_at / completed_at +``` + +EvidenceGuard 不信任 Draft 中的引用,也不从模型历史反推 Tool 结果,而是重新按当前 Run 构造 key,读取 canonical record 并验证状态和投影内容。 + +## 4. ToolBoundary 为什么必须是统一入口 + +RAG、日志和 MySQL 的业务执行方式不同,但以下控制规则完全相同: + +- Run 必须仍然 active; +- envelope 的 runId 必须等于当前 Run; +- `tool_call_id` 必须合法且不能重复; +- Tool 必须已授权并声明只读; +- 调用前必须预占 Tool 预算和 request bytes; +- raw response 和 agent result 必须分别检查容量; +- canonical 状态只能按合法路径迁移; +- 对 Agent 只返回稳定错误码; +- durable audit 失败不能改变已经得到的 Tool 结果。 + +如果把这些逻辑复制到三个 Adapter,任何新增 Tool 都可能漏掉其中一项。因此统一由 `ToolBoundary` 编排一次调用,具体 Adapter 只负责 typed request、backend 和 projector 的连接。 + +```mermaid +sequenceDiagram + participant I as Tool Interceptor + participant B as ToolBoundary + participant C as Harness Core + participant S as Canonical Store + participant T as Backend + participant P as Projector + participant A as Audit Sink + + I->>B: RunContext + ToolCallRequestEnvelope + B->>B: run / id / authorization / readonly / JSON preflight + B->>C: reserve Tool call + request bytes + B->>S: begin PROJECTING + B->>T: execute typed request + T-->>B: raw response + B->>C: reserve raw bytes + B->>P: project raw response + P-->>B: bounded agent_result + evidence_status + B->>C: reserve projection bytes + B->>S: mark READY + B-->>I: ToolBoundaryResult + B-->>A: best-effort metadata audit +``` + +错误发生时,已经创建的 canonical record 会尽力迁移到 ERROR;如果连 Store 都不可用,则向上只返回 `STORE_ERROR` 等稳定码,不把 Redis 或 backend 异常正文交给模型。 + +## 5. 两套状态为什么不能合并 + +Tool 调用同时有两个正交维度: + +| 维度 | 状态 | 回答的问题 | +|---|---|---| +| Invocation lifecycle | `PROJECTING / READY / ERROR` | 这次调用是否完成并形成了可引用记录 | +| Evidence semantics | `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | 客观结果中是否存在候选证据 | + +例如日志查询成功返回 0 条: + +```text +InvocationStatus = READY +EvidenceStatus = NO_EVIDENCE +``` + +它不是 `ERROR`。这条负向观察可以支持“在指定时间、服务和查询条件下没有匹配日志”,但不能支持“故障不存在”。 + +合法组合被类型约束为: + +| InvocationStatus | EvidenceStatus | 是否允许引用 | +|---|---|---:| +| `PROJECTING` | `null` | 否 | +| `READY` | `EVIDENCE_FOUND` | 是 | +| `READY` | `NO_EVIDENCE` | 是,但只能作为限定范围负向观察 | +| `ERROR` | `ERROR` | 否 | + +把两者合成一个 `SUCCESS / FAILED` 会丢失最重要的信息:技术成功但业务范围内没有证据。 + +## 6. 三类 Tool 如何投影 + +### 6.1 RAG + +`RagResultProjector` 从 backend 结果中提取有界 evidence block,稳定文档身份,限制 excerpt 数量和长度,并兼容 `relevanceLevel / relevance_level` 后统一为 `relevance_level`。 + +这里有一个刻意保留的区分: + +```text +evidence 非空 -> EVIDENCE_FOUND +relevance_level=REFERENCE -> 相关度一般 +``` + +`EVIDENCE_FOUND` 只说明存在候选内容,`REFERENCE` 也不自动表示 `NO_GAIN`。内容是否推进当前诊断假设,需要模型结合上下文判断。 + +### 6.2 Logs + +`QueryLogsResultProjector` 不只是截取前几条日志,它还会: + +- 脱敏 password、token、secret、API key; +- 脱敏 pod、host、PID、IP 和 SQL literal; +- 去掉堆栈尾部噪声; +- 对数字做模式归一化并聚合重复事件; +- 对事件做跨范围采样,而不是只保留开头; +- 保存实际时间、topic、query scope 和截断标记。 + +如果 backend 执行成功且日志数组为空,投影结果是 READY + NO_EVIDENCE。backend 明确返回失败,才是 Tool 执行或投影错误。 + +### 6.3 MySQL + +MySQL 在投影之前还有独立的只读安全链:`MysqlSqlValidator` 根据逻辑数据源、schema、table 和 column allowlist 生成 `MysqlQueryPlan`,`JdbcMysqlReadOnlyExecutor` 只执行该 Plan。 + +`MysqlResultProjector` 再负责: + +- 限制最大行数、单元格长度和总 bytes; +- 保持 number、boolean 和 null 类型; +- 对 password、token、secret、credential 等敏感列强制脱敏; +- 结果缩减时标记 `truncated=true`。 + +Validator 负责“能不能执行”,Projector 负责“模型能看到什么”,两者不能合并。 + +## 7. Control View 与 Model Observation 的字段边界 + +| 字段 | Harness | Agent | 原因 | +|---|---:|---:|---| +| `tool_call_id` | 是 | 是 | 引用和上一轮评价都需要 | +| 实际 scope | 是 | 是 | Harness 去重;模型理解负向观察边界 | +| 有界 evidence/events/rows | 是 | 是 | 模型推理所需事实 | +| `evidence_status` | 是 | 是 | 区分候选证据、空结果和错误 | +| `relevance_level` | 是 | 是 | 给模型粗粒度检索语义 | +| `truncated` | 是 | 是 | 防止模型误以为结果完整 | +| `returned_count` | 是 | 否 | Harness 统计,不必消耗模型上下文 | +| normalized scope / duplicate identity | 是 | 否 | 内部控制实现 | +| 连续 NO_GAIN、阈值和剩余预算 | 是 | 否 | 防止模型围绕限制博弈 | +| raw response / 检索轨迹 / 原始分数 | 是 | 否 | 敏感且体积不可控 | + +只有 Harness 必须改变模型行为时,才注入 `STOP_REQUIRED`、错误码或已检查范围等有限控制信息。 + +## 8. 存储决策:为什么是 Redis canonical + MySQL metadata + +### 8.1 不把完整 raw 长期写 MySQL + +完整 Tool 结果可能包含日志、SQL 查询结果和内部知识内容。长期保存会扩大泄露半径,也会让 JPA audit 成为第二个事实源。MySQL 只保存运营和对账需要的字段,可以长时间保留而不复制正文。 + +### 8.2 Canonical 读取不续期 + +Redis record 在创建时设置 TTL,读取或状态更新不恢复初始 TTL。原因是:如果一次历史查询就能续期,敏感 raw 可能因审计访问而永久存在。 + +状态更新保留当前剩余 TTL,代价是 read-TTL-write 存在小的并发窗口,但它比无界续期更符合数据治理目标。 + +### 8.3 raw 超限为什么不截断 + +raw 是内部事实。如果静默截断后仍标记 READY,系统无法区分“backend 只返回这些内容”和“Harness 丢掉了内容”。因此 raw 或完整 record 超限时调用进入 ERROR。 + +Agent projection 可以截断,因为它本来就是面向消费的摘要,但必须通过 `truncated=true` 明示不完整。 + +## 9. 真实问题如何改变设计 + +| 真实问题 | 暴露的错误假设 | 最终修正 | +|---|---|---| +| Tool raw 直接进入 Agent | backend 输出天然适合模型消费 | 增加 Tool-specific projector 和白名单 observation | +| Trace、raw、ContextPack、重复正文同时存在 | 多保存几份可以提高可观测性 | canonical、model view、metadata audit 明确分层 | +| Mock 日志 0 命中返回 `success=false` | 没数据等于调用失败 | 技术执行状态与 NO_EVIDENCE 分离 | +| RAG 的 `REFERENCE` 在投影中丢失 | 只要 evidence 非空就够了 | 保留 relevance_level,但不提升为根因证据 | +| 生产 ObjectMapper 未注册 Java Time 模块,Redis 全部 STORE_ERROR | 单元测试序列化环境等同生产 | 使用生产装配验证 canonical record,并增加 live E2E | +| backend 日志打印 raw/rewritten query | 排障信息可以直接进入普通日志 | 普通日志和 durable audit 只保留安全摘要 | + +## 10. 代价与边界 + +这项设计不是免费的: + +1. 每增加一种 Tool,都要同时定义 typed request/result、Adapter、Projector 和 EvidenceGuard 读取规则。 +2. Redis 在 TTL 内成为证据校验依赖;不可用时系统应 fail closed,而不是相信 Draft。 +3. Agent 看到的是有损投影,Projector 设计不当可能丢掉模型真正需要的诊断信号。 +4. durable audit 不能替代 canonical replay;TTL 到期后只能解释调用元数据,无法恢复完整正文。 + +但这些成本换来了明确的责任:Backend 决定原始事实,Projector 决定模型可见范围,Canonical Store 决定短期验真事实,Audit 决定长期允许保存什么。 + +## 11. 如何验证 + +| 验证内容 | 代表性测试或证据 | +|---|---| +| Run mismatch、未授权、非只读、重复 ID、预算和超限 | `ToolBoundaryTest` | +| PROJECTING / READY / ERROR 和 TTL 行为 | `CanonicalInvocationStoreTest` | +| RAG evidence identity、relevance 和 bytes | `RagResultProjectorTest` | +| 日志脱敏、模式聚合、空结果和采样 | `QueryLogsResultProjectorTest` | +| MySQL 行列/敏感字段/容量边界 | `MysqlResultProjectorTest` | +| typed Tool contract 不漂移 | 三类 `*ToolContractTest` | +| 生产 Redis serializer 与 ObjectMapper | single-react cleanup live E2E | + +单元测试只能证明数据变换和状态机;生产 Redis serializer、实际 Tool Calling ID 和 backend 返回格式仍必须通过 live E2E 验证。 + +## 12. 与后续机制的关系 + +Tool 双视图只解决“什么是可验证的 Tool 事实、模型允许看到什么”,并不解决: + +- 这些事实是否支持最终结论:见[Harness证据安全链-从引用真实到结论可发布.md](Harness证据安全链-从引用真实到结论可发布.md); +- Agent 是否应该继续查询:见[Harness信息增益停止-让无证据诊断正常收敛.md](Harness信息增益停止-让无证据诊断正常收敛.md)。 diff --git a/mvp/engineering/harness/Harness信息增益停止-让无证据诊断正常收敛.md b/mvp/engineering/harness/Harness信息增益停止-让无证据诊断正常收敛.md new file mode 100644 index 0000000..299a379 --- /dev/null +++ b/mvp/engineering/harness/Harness信息增益停止-让无证据诊断正常收敛.md @@ -0,0 +1,302 @@ +# Harness 信息增益停止:让无证据诊断正常收敛 + +**更新日期**:2026-07-29 +**主题**:Information Gain、Progress Tracker、STOP_REQUIRED 与过程型 Fallback +**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) +**上位设计**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md) + +## 1. 预算能限制损失,不能判断何时完成 + +在知识库只有通用说明、日志持续为空或查询条件不足时,ReAct Agent 容易出现一种看似合理的行为:不断修改关键词、时间范围或查询表达,再调用一次 Tool。 + +每一次调用单独看都可能合法,但整个 Run 没有获得新信息。旧系统只能等到模型次数、Tool 次数、Token 或 deadline 耗尽,再以 `BUDGET_EXHAUSTED` 或 `INTERNAL_FAILURE` 结束。 + +这暴露了两个被混淆的问题: + +```text +资源预算:这次 Run 最多允许消耗多少? +业务收敛:继续查询是否仍可能推进当前诊断? +``` + +预算是最后一道资源保护,不能承担正常停止策略。否则“当前证据不足”会被错误表达成“系统执行失败”。 + +信息增益停止契约的目标是:在硬预算之前识别连续无进展,使 Agent 停止调用 Tool,并把已经完成的有限检查发布为诚实、可验证的业务结果。 + +## 2. 先划清三方判断权 + +设计停止机制时最危险的做法,是让某一层承担它无法可靠完成的判断。 + +| 参与者 | 能可靠判断什么 | 不能判断什么 | +|---|---|---| +| Tool / Projector | 是否执行成功、结果是否为空、返回数量、实际 scope、是否截断 | 内容是否支持当前诊断假设 | +| Diagnosis Agent | 非空内容是否确认、排除或缩小当前假设 | 是否还能绕过预算、重复和饱和门禁 | +| Harness | scope 是否重复、连续 NO_GAIN、协议是否合规、是否允许继续 | 业务根因是什么 | +| Release | 已有进展可以发布成哪种安全结果 | 是否应该重新调用 Tool | + +最终原则是: + +> Tool 提供客观结果,模型判断语义价值,Harness 拥有最终停止权。 + +模型可以选择主动结束,但不能通过继续发 Tool Call 绕过 Harness 已经确定的饱和或预算状态。 + +## 3. 为什么信息增益只有两个值 + +当前契约只保留: + +```text +information_gain = GAINED | NO_GAIN +``` + +没有 `HIGH / MEDIUM / LOW`,也没有置信分数。停止控制只需要知道结果是否推进了当前诊断;引入更多等级会带来阈值解释、跨 Tool 标定和模型输出不稳定,却不一定改变最终动作。 + +判定标准是: + +| 结果 | Information Gain | 生产者 | +|---|---|---| +| Tool 执行失败 | 不产生 | 进入技术失败流程 | +| `evidence_status=NO_EVIDENCE` | `NO_GAIN` | Harness | +| 相同 `tool_name + normalized_scope` | `NO_GAIN` | Harness,并拒绝重复执行 | +| 成功非空,确认/排除/缩小假设 | `GAINED` | Diagnosis Agent | +| 成功非空,但只是通用知识、重复内容或无关事实 | `NO_GAIN` | Diagnosis Agent | + +RAG 的 `REFERENCE` 不能自动映射为 `NO_GAIN`。它只表示检索相关度一般;一段一般相关的资料可能仍排除一个假设,也可能完全无用,需要模型结合诊断上下文判断。 + +## 4. 为什么模型在下一次 Tool Call 中评价上一轮 + +模型只有在读到 Tool Observation 后,才能判断它是否有语义增益。但如果要求模型单独输出一条 progress 消息,就必须增加新的协议轮次或 Progress Judge。 + +当前设计利用模型已经要做的下一步行为: + +- 输出 DiagnosisDraft,表示主动结束; +- 发起下一次 Tool Call,表示希望继续。 + +当模型选择继续时,下一次 Tool Call Envelope 必须携带对上一轮的评价: + +```json +{ + "previous_observation": { + "tool_call_id": "call-123", + "information_gain": "NO_GAIN" + }, + "input": { + "query": "新的业务查询参数" + } +} +``` + +Interceptor 在调用业务 Tool 之前完成三件事: + +1. 校验 `previous_observation.tool_call_id` 是否正好是当前 pending 调用; +2. 应用 `GAINED / NO_GAIN`,更新连续计数和收集状态; +3. 剥离 `previous_observation`,只把 `input` 传给原业务 Tool。 + +这是 Agent-facing Tool Schema 的协议变化,但 RAG、日志和 MySQL 的业务 request 并没有被控制字段污染。 + +```mermaid +sequenceDiagram + participant M as Diagnosis Agent + participant I as Tool Interceptor + participant P as Progress Tracker + participant T as Business Tool + + M->>I: Tool Call #1 + input + I->>P: no pending observation + I->>T: business input + T-->>I: successful non-empty observation + I->>P: mark call #1 pending evaluation + I-->>M: bounded observation + + M->>I: Tool Call #2 + previous_observation(#1, NO_GAIN) + I->>P: validate ID and apply NO_GAIN + alt 仍为 COLLECTING + I->>T: strip control fields, execute input #2 + else 达到 SATURATED + I-->>M: STOP_REQUIRED + end +``` + +如果模型读完 observation 后直接输出 Draft,就不需要额外评价最后一轮。因为它已经通过实际行为表达“停止”,Harness 也不需要为收集一个统计字段强迫模型再调用 Tool。 + +## 5. ProgressTracker 保存什么,不保存什么 + +`DiagnosisProgressTracker` 是 `RunContext` 中的线程安全状态句柄,保存: + +- 已完成的 `tool_name + normalized_scope`; +- 已完成 Tool Call identity; +- 当前等待模型评价的 `pendingToolCallId`; +- 连续 `NO_GAIN` 次数; +- 连续 progress protocol violation 次数; +- `COLLECTING / SATURATED` 状态; +- stop reason; +- STOP_REQUIRED 是否已经交付。 + +它不保存 request、raw response、agent result 或模型 thought。完整 Tool 事实仍属于 Canonical Store。Tracker 只保存作出停止决策需要的最小 identity 和计数,避免出现第二份 Tool 真相。 + +结束时,`DiagnosisProgressProjector` 根据 completed identity 回读 canonical READY records,投影成有界 `ProgressSnapshot`。无法验证、不可读取或格式非法的记录不会被发布,只会形成安全 limitation。 + +## 6. 重复检测为什么只做参数级 + +Harness 使用 `ToolScopeNormalizer` 将业务输入转换成稳定 scope,再比较: + +```text +tool_name + normalized_scope +``` + +这可以识别字段顺序、格式差异下的完全相同查询,并在调用 backend 前拒绝重复执行。 + +首版没有做自然语言语义去重,例如以下两条 query 可能语义相同,但不会被代码证明为同一 scope: + +```text +“查询支付超时日志” +“查找支付请求 timeout 记录” +``` + +原因是语义去重需要 embedding、模型判断或跨 Tool 指纹,会引入新的不确定性和误杀风险。当前边界选择了可以确定性证明的参数重复;语义近似由模型的 `NO_GAIN` 义务约束。 + +## 7. 收集状态机 + +连续无增益阈值由配置控制,当前默认值为 2。`GAINED` 会清零连续计数,避免一次早期空查使后续有效取证被过早停止。 + +```mermaid +stateDiagram-v2 + [*] --> COLLECTING + COLLECTING --> COLLECTING: GAINED / consecutiveNoGain=0 + COLLECTING --> COLLECTING: NO_GAIN / 未达阈值 + COLLECTING --> SATURATED: NO_GAIN / 达到阈值 + SATURATED --> STOP_CHANCE: 交付一次 STOP_REQUIRED + STOP_CHANCE --> RELEASE: 模型输出 Draft + STOP_CHANCE --> TERMINATED: 模型再次请求 Tool +``` + +当某次 READY 结果是 NO_EVIDENCE 时,Harness 可以立即累计 `NO_GAIN`。当成功非空结果需要模型评价时,Tracker 设置 pending ID;上一轮未评价前,新的 Tool Call 不会被执行。 + +达到 `SATURATED` 后,Harness 给模型一次合法完成机会,而不是在 Tool response 中立刻抛出通用错误。STOP_REQUIRED 只交付一次;模型仍继续请求 Tool 时,`DiagnosisCollectionStoppedException` 将受控停止穿出框架 ReAct loop。 + +## 8. 协议错误为什么不能计为 NO_GAIN + +真实 E2E 曾出现 9 次 `INVALID_PROGRESS_PROTOCOL` Tool 请求拒绝和 13 轮 Diagnosis Agent 模型调用。模型没有正确回传上一轮评价,但这些拒绝也没有增加 NO_GAIN,最终持续空转。 + +一种看似简单的修复是把协议错误也计为 NO_GAIN。但这会混淆两个事实: + +- `NO_GAIN` 表示 Tool 结果没有推进诊断; +- 协议错误表示模型没有遵守控制契约,Tool 根本没有执行。 + +因此引入独立的协议错误计数和 `PROGRESS_PROTOCOL_VIOLATED`: + +1. 第一次错误返回可修正 observation,包含 violation type、缺失字段、期望的上一轮 ID 和允许值; +2. 连续错误达到配置阈值后进入 SATURATED; +3. 交付一次 `STOP_REQUIRED / PROGRESS_PROTOCOL_VIOLATED`; +4. 再次请求 Tool 时受控停止。 + +协议错误 Trace 使用 `TOOL_REQUEST_REJECTED`,不能记录成 `TOOL_INVOCATION`,因为 backend 从未被调用,Tool 预算和实际执行数也不应被污染。 + +## 9. 三种停止原因必须分开 + +| Stop Reason | 含义 | 是否等于技术失败 | +|---|---|---:| +| `INFORMATION_SATURATED` | 连续结果没有推进诊断 | 否 | +| `BUDGET_LIMIT_REACHED` | 达到模型、Tool、Token 或 bytes 预算边界 | 不一定;有安全进展时可发布过程 Fallback | +| `PROGRESS_PROTOCOL_VIOLATED` | 模型连续违反 progress Envelope | 协议失败;有安全进展时仍可保留过程价值 | + +信息饱和不能伪装成预算耗尽,否则无法判断阈值是否合理;预算耗尽也不能伪装成信息饱和,因为可能是在持续获得有效证据时资源不足。 + +这些 stop reason 是 Harness 内部控制语义,不直接作为第二套公开生命周期。最终仍由 Release 映射为 `SUCCESS / FALLBACK / FAILED / CANCELLED`。 + +## 10. 停止以后如何形成用户结果 + +停止本身不是答案。系统需要把已经完成的检查转换成可发布内容,又不能依赖预算耗尽后额外调用模型。 + +`DiagnosisProgressProjector` 从 canonical READY results 生成: + +- verified sources; +- observed facts,包括限定范围的空结果; +- 实际查询 scope; +- 投影失败、截断或不可读取形成的 limitations; +- stop reason。 + +Release 根据现有进展决定: + +```mermaid +flowchart TD + S["Agent 主动无结论
或 Harness 受控停止"] --> P["ProgressSnapshot"] + P --> Q{"存在已验真 observed facts?"} + Q -->|"是"| I["INSUFFICIENT_EVIDENCE
展示已检查内容和下一步"] + Q -->|"否"| M{"Draft 声明 missing_info?"} + M -->|"是"| C["MISSING_REQUIRED_CONTEXT"] + M -->|"否"| F["FAILED / fail closed"] +``` + +`conclusion=null` 是合法 Draft。它允许 Agent 在缺少企业、时间、服务或错误信息时零次调用 Tool,直接报告 `missing_info`,避免为了表现“已经排查”而执行无明确范围的查询。 + +## 11. 为什么没有引入更多控制字段 + +### 11.1 不使用 `new_count` + +“新记录数量”需要跨 RAG 文档、日志事件和数据库行建立稳定指纹,而且新数据不等于对当前假设有用。它增加了复杂度,却不能替代语义增益判断。 + +### 11.2 不使用 `next_action` + +模型发起 Tool Call已经表示继续,输出 Draft 已经表示结束。再要求 `CONTINUE / STOP` 只会形成一套可能与实际行为冲突的声明状态。 + +### 11.3 不增加 Progress Judge + +独立 Judge 会为每轮 Tool 结果增加模型调用、延迟和失败面。空结果和完全重复 scope 本可由代码判断;其他内容由正在做诊断的 Agent 评价即可。 + +### 11.4 不向模型公开剩余预算和阈值 + +模型只需要知道是否必须停止,不需要围绕“还剩几次”规划消耗。阈值、计数和预算属于 Harness Control View,只有 STOP_REQUIRED 等必要指令进入模型上下文。 + +## 12. Prompt 与硬门禁如何分工 + +Prompt 仍然需要告诉模型: + +- 不必须得出根因; +- `conclusion=null` 是合法完成; +- 缺少必要上下文时可以零 Tool 结束; +- 正确但不能推进假设的内容也是 NO_GAIN; +- 不要通过改写相似关键词重复查询; +- 收到 STOP_REQUIRED 后必须停止。 + +但 Prompt 只是帮助模型做出正确选择,不构成系统保证。重复 scope、pending evaluation、饱和状态、预算和一次性 STOP_REQUIRED 都由代码门禁执行。 + +## 13. 真实问题如何改变设计 + +| 真实现象 | 被证伪的假设 | 设计修正 | +|---|---|---| +| 空日志、通用知识仍不断改写查询 | 模型会自然意识到没有进展 | 明确信息增益义务和 Harness 饱和状态 | +| 最终以 BUDGET_EXHAUSTED 结束 | 硬预算可以充当正常停止 | 预算与信息饱和分离 | +| `REFERENCE` 非空结果持续触发查询 | 非空候选就是有价值证据 | 检索相关度与诊断增益分离 | +| 相同 scope 被反复执行 | Prompt 足以禁止重复 | Harness 参数级去重并在 backend 前拒绝 | +| 9 次协议拒绝仍消耗 13 轮模型 | 错误 observation 会让模型自修复 | 可修正反馈 + 独立协议阈值 + STOP_REQUIRED | +| 非法 Draft 使已完成检查丢失 | 只有合法最终 Draft 才有用户价值 | 已验真 ProgressSnapshot 可形成过程型 Fallback | +| 缺少企业/时间仍被迫调用 Tool | Tool 调用次数大于零才算诊断 | 允许零 Tool、missing context 合法结束 | + +## 14. 代价与当前边界 + +1. 模型侧 Tool Schema 增加了 `previous_observation + input`,这是明确的 Agent-facing 协议变化。 +2. 首版只能确定性识别参数相同的重复 scope,不能阻止所有自然语言近义改写。 +3. 默认连续 NO_GAIN 阈值 2 是工程起点,需要依靠固定评测集校准;太小会过早停止,太大会增加空转。 +4. 最后一轮非空 Tool 结果如果模型直接输出 Draft,Tracker 不强制收集其 information gain;这是减少无意义协议轮次的主动取舍。 +5. ProgressSnapshot 依赖 canonical record 仍在 TTL 内且可解析,无法验真的进展不会被发布。 +6. 受控预算停止只有在已有安全进展时才能转为 Fallback;没有可验证内容仍然 fail closed。 + +## 15. 如何验证 + +| 需要证明 | 代表性测试或 E2E | +|---|---| +| NO_EVIDENCE 自动累计 NO_GAIN,GAINED 清零 | `DiagnosisProgressTrackerTest` | +| 重复 scope 在 backend 前被拒绝 | `HarnessToolInterceptorTest`、`ToolScopeNormalizerTest` | +| pending evaluation 的缺失、乱序和意外回传被拒绝 | Interceptor protocol focused cases | +| 连续协议错误达到阈值并只交付一次 STOP_REQUIRED | Tracker + Interceptor tests | +| 模型主动停止、饱和停止和预算停止都能进入 Release | `DiagnosisAgentUseCaseTest`、`DiagnosisReleaseUseCaseTest` | +| 非法 Draft 只有在存在安全进展时降级 | `DiagnosisChatExecutorTest` | +| Tool 拒绝与实际 Tool 执行分开审计 | exact-run Trace | +| 未知 Query 不再以通用内部错误结束 | ISS-016 named SSE E2E | + +其中一条 live E2E 曾准确暴露“协议拒绝不累计 NO_GAIN”的盲区。这说明停止机制不能只验证最终 SSE,还要核对模型轮次、Tool 实际执行数、Tool 拒绝数、Token 和 Timeline 序列。 + +## 16. 与另外两项设计的关系 + +信息增益依赖 [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)提供客观 evidence status、scope 和有界 observation;停止后的 ProgressSnapshot 和最终 Fallback 依赖[证据安全链](Harness证据安全链-从引用真实到结论可发布.md)确保只发布当前 Run 可验证的事实。 + +三者组合后,Harness 才能完整回答:模型看到了什么、为什么继续或停止、最终哪些内容可以发布。 diff --git a/mvp/engineering/harness/Harness生命周期与状态.md b/mvp/engineering/harness/Harness生命周期与状态.md new file mode 100644 index 0000000..751ce92 --- /dev/null +++ b/mvp/engineering/harness/Harness生命周期与状态.md @@ -0,0 +1,440 @@ +# Harness 生命周期与状态 + +**更新日期**:2026-07-29 +**状态**:当前实现口径 +**术语前置**:[CONTEXT.md](CONTEXT.md) + +## 1. 先澄清:系统不存在一条包含所有状态的总状态机 + +当前 Harness 有多组正交状态: + +- RunState 控制内存执行终态; +- ChatApplicationStatus 表示用户可见处理阶段; +- InvocationStatus 表示单次 Tool invocation 生命周期; +- EvidenceStatus 表示 Tool 客观结果; +- CollectionState 表示能否继续收集证据; +- StopReason 表示停止收集的内部原因; +- SemanticVerdict 表示结论支持度; +- ReleaseOutcome 表示最终发布结果; +- SSE Session State 表示连接能否继续发送。 + +它们在一次请求中并行演进,只在少数边界发生映射。生命周期文档的目标不是把它们合并,而是说明谁先发生、由谁拥有、在哪里汇合。 + +## 2. 一次 Run 的主时间线 + +```mermaid +sequenceDiagram + participant C as Client / SSE + participant A as ChatApplicationUseCase + participant H as Harness Core + participant R as Intent Router + participant D as Diagnosis Runtime + participant L as Release Pipeline + participant P as Persistence / Trace + + C->>A: query + optional sessionId + A->>A: 读取 routing history / PreviousTurn + A->>H: startRun(sessionId) + H-->>A: RUNNING RunContext + A->>P: diagnosis_run=RUNNING + RUN_STARTED + A-->>C: metadata(sessionId, runId) + + A->>R: route(query, bounded history) + R-->>A: IntentType + + alt SYSTEM_CHAT + A->>A: 单轮受控模型回答 + else KNOWLEDGE_QUERY + A->>A: 一次 RAG + 单轮答案 + else DIAGNOSIS + A->>D: Agent ReAct / Tool / Progress + D-->>A: Draft 或 Controlled Stop + A->>L: release(Draft/Progress/StopReason) + L-->>A: SUCCESS Draft 或 SafeFallback + end + + alt 正常完成 + A->>H: completeSuccess(预算 Fallback 特例除外) + A->>P: 持久化 release outcome / safe content / usage + A->>P: RUN_FINISHED + A-->>C: content + done + else 失败 + A->>H: completeFailure 或读取既有终态 + A->>P: 持久化 FAILED/CANCELLED + A->>P: RUN_FINISHED + A-->>C: failure + done(FAILED) + end +``` + +创建顺序很重要:Application 先读取会话上下文,再创建 RunContext、持久化 RUNNING、记录 RUN_STARTED,之后才进入路由和执行。SSE 的 metadata 在 `onStarted` 中发布 exact sessionId/runId,后续结果必须匹配这组身份。 + +## 3. Run 生命周期 + +### 3.1 状态 + +```mermaid +stateDiagram-v2 + [*] --> RUNNING + RUNNING --> SUCCESS: 正常路径完成 + RUNNING --> FAILED: 不可恢复内部失败 + RUNNING --> CANCELLED: 客户端断开或用户取消 + RUNNING --> TIMED_OUT: deadline 到达 + RUNNING --> BUDGET_EXHAUSTED: 任一硬预算超限 + + SUCCESS --> [*] + FAILED --> [*] + CANCELLED --> [*] + TIMED_OUT --> [*] + BUDGET_EXHAUSTED --> [*] +``` + +`RunLifecycle.finish` 使用原子 compare-and-set:只有从“尚无 termination”到某个终态的第一次转换成功。所有终态都不可再次转换。 + +### 3.2 状态含义 + +| RunState | 含义 | 常见来源 | +|---|---|---| +| `RUNNING` | 尚未产生 RunTermination | `startRun` 后默认状态 | +| `SUCCESS` | Application 已形成正常可持久化结果 | 正常内容,或非预算类 SafeFallback | +| `FAILED` | 不可恢复执行失败 | 路由不可用、模型失败、无安全进展的非法 Draft 等 | +| `CANCELLED` | 协作取消已成为第一个终态 | 客户端断开、用户请求 | +| `TIMED_OUT` | `checkActive` 发现 deadline 已到 | 模型/Tool/Guard 边界前后检查 | +| `BUDGET_EXHAUSTED` | 模型、Tool、Token 或 bytes 预算超限 | `DiagnosisHarnessCore.exhaustBudget` | + +### 3.3 RunState 与 ReleaseOutcome 不一一对应 + +最重要的反例是 Fallback: + +- 证据不足、缺少上下文、语义不支持等正常降级,RunState 最终为 `SUCCESS`,ReleaseOutcome 为 `FALLBACK`; +- 预算耗尽后已经有安全 ProgressSnapshot,RunState 保持 `BUDGET_EXHAUSTED`,但 ReleaseOutcome 可以为 `FALLBACK`; +- 超时或预算耗尽且无法形成安全内容时,RunState 分别保持 `TIMED_OUT / BUDGET_EXHAUSTED`,ReleaseOutcome 映射为 `FAILED`。 + +所以不能从 ReleaseOutcome 反推出精确 RunState,也不能把 `FALLBACK` 当作 RunState。 + +## 4. 取消生命周期 + +`RunCancellation` 使用 first-reason-wins。取消原因包括: + +| Reason | RunState 映射 | +|---|---| +| `CLIENT_DISCONNECTED` | `CANCELLED` | +| `USER_REQUESTED` | `CANCELLED` | +| `DEADLINE_EXCEEDED` | `TIMED_OUT` | +| `BUDGET_EXHAUSTED` | `BUDGET_EXHAUSTED` | +| `INTERNAL_FAILURE` | `FAILED` | + +生命周期和取消句柄互相配合:取消回调尝试完成 RunLifecycle;deadline 和预算路径也会先确定终态,再触发取消阻止后续工作。 + +取消语义是协作式的: + +1. 后续模型、Tool、Guard 边界调用 `checkActive` 时立即失败; +2. SSE 断开后不再发送 content; +3. first-terminal-wins 阻止迟到成功覆盖 CANCELLED/TIMED_OUT; +4. 已经进入 Provider 的同步调用是否物理停止,取决于底层客户端,Harness 不作虚假保证。 + +## 5. Application 阶段不是生命周期状态 + +`ChatApplicationStatus` 用于 SSE status 事件: + +```text +ROUTING +SYSTEM_RESPONDING +KNOWLEDGE_SEARCHING +KNOWLEDGE_ANSWERING +DIAGNOSIS_RUNNING +SAFETY_VALIDATING +``` + +这些值只是用户可见的处理阶段: + +- 不是严格完备的状态机; +- 不表示终态; +- 不持有取消或预算; +- 不保证每个 Run 都经过全部阶段。 + +例如 Diagnosis Run 通常经过 `ROUTING -> DIAGNOSIS_RUNNING -> SAFETY_VALIDATING`,System Chat 则经过 `ROUTING -> SYSTEM_RESPONDING`。 + +## 6. Diagnosis Agent 执行生命周期 + +`DiagnosisAgentExecution` 只有两种合法形态,不额外定义一套枚举: + +| 形态 | 字段 | 含义 | +|---|---|---| +| Completed | `draft != null, stopReason=null` | Agent 输出严格合法 DiagnosisDraft | +| Controlled Stop | `draft=null, stopReason != null` | Tool loop 因信息饱和、预算或协议错误受控停止 | + +其他情况通过异常表达: + +- 空 Draft; +- 非法 JSON; +- Schema 不合法; +- 模型调用失败; +- RunAborted。 + +Draft 合同失败不是 DiagnosisStopReason。非法 Draft 会被丢弃;只有异常携带的 ProgressSnapshot 已包含可验真 facts,Application 才允许 Release 生成过程型 Fallback。 + +## 7. 单次 Tool Invocation 生命周期 + +### 7.1 状态转换 + +```mermaid +stateDiagram-v2 + [*] --> PREFLIGHT + PREFLIGHT --> REJECTED: invalid run/id/auth/readonly/request/budget/store + PREFLIGHT --> PROJECTING: canonical begin 成功 + PROJECTING --> READY: backend + projection + store 成功 + PROJECTING --> ERROR: execution/projection/size/budget/store error + READY --> [*] + ERROR --> [*] + REJECTED --> [*] +``` + +`PREFLIGHT` 和 `REJECTED` 是本文用于解释流程的阶段,不是 `InvocationStatus` 枚举值。canonical record 只有: + +- `PROJECTING`:已创建,尚未形成可引用结果; +- `READY`:执行和投影完成,可以被当前 Run 引用; +- `ERROR`:调用失败,不可引用。 + +部分 preflight 失败发生在 canonical begin 之前,因此可能只有 `ToolBoundaryResult.ERROR` 和审计事件,没有 canonical ERROR record。 + +### 7.2 EvidenceStatus 是另一条轴 + +| InvocationStatus | EvidenceStatus | 语义 | +|---|---|---| +| `PROJECTING` | `null` | 未完成 | +| `READY` | `EVIDENCE_FOUND` | 技术完成,存在候选内容 | +| `READY` | `NO_EVIDENCE` | 技术完成,当前 scope 为空 | +| `ERROR` | `ERROR` | 技术失败 | + +`READY + EVIDENCE_FOUND` 仍不表示证据足以支持结论;后续还要经过 InformationGain、EvidenceGuard 和 SemanticGuard。 + +## 8. 信息收集生命周期 + +### 8.1 Collection State + +```mermaid +stateDiagram-v2 + [*] --> COLLECTING + COLLECTING --> COLLECTING: GAINED / 清零 no-gain + COLLECTING --> COLLECTING: NO_GAIN / 未达阈值 + COLLECTING --> SATURATED: 连续 NO_GAIN 达阈值 + COLLECTING --> SATURATED: 连续 progress protocol violation 达阈值 + SATURATED --> SATURATED: 终态,不再允许新 evidence Tool +``` + +`SATURATED` 只表示证据收集不再允许继续。它不是整个 Run 的终态,Agent 仍有一次机会输出 Draft,之后进入 Release。 + +预算超限不会把 CollectionState 改成 SATURATED。它将 RunState 置为 `BUDGET_EXHAUSTED`,并把内部 DiagnosisStopReason 标记为 `BUDGET_LIMIT_REACHED`。 + +### 8.2 InformationGain + +- `NO_EVIDENCE` 和完全重复的 `tool_name + normalized_scope` 由 Harness 产生 `NO_GAIN`; +- 其他成功非空结果由 Diagnosis Agent 评价 `GAINED / NO_GAIN`; +- `GAINED` 清零连续 NO_GAIN; +- 技术失败和 progress 协议错误不产生 InformationGain。 + +### 8.3 StopReason + +| DiagnosisStopReason | 触发条件 | 对 CollectionState 的影响 | +|---|---|---| +| `INFORMATION_SATURATED` | 连续 NO_GAIN 达阈值 | 进入 SATURATED | +| `PROGRESS_PROTOCOL_VIOLATED` | 连续协议错误达阈值 | 进入 SATURATED | +| `BUDGET_LIMIT_REACHED` | RunBudget 超限 | 不要求进入 SATURATED;Run 已终止 | + +STOP_REQUIRED 是否已经交付由独立 boolean 记录,它不是第四种 CollectionState。一次指令交付后仍请求 Tool,会抛出受控停止异常。 + +## 9. Guard 与 Release 生命周期 + +```mermaid +flowchart TD + I["DiagnosisAgentExecution"] --> D{"有合法 Draft?"} + D -->|"否,受控停止"| PS["验证 ProgressSnapshot"] + D -->|"是,无 conclusion"| NC["验证已有 Tool references"] + D -->|"是,有 conclusion"| EG["EvidenceGuard initial"] + + EG --> EV{"evidence valid?"} + EV -->|"否"| ER["EvidenceRepair once"] + ER --> RE["EvidenceGuard recheck"] + EV -->|"是"| SG["SemanticGuard"] + RE -->|"valid"| SG + RE -->|"invalid"| EF["EVIDENCE_VALIDATION_FAILED"] + + SG -->|"SUPPORTED"| SU["ReleaseOutcome.SUCCESS"] + SG -->|"UNSUPPORTED"| SF["SEMANTIC_UNSUPPORTED"] + SG -->|"unavailable"| UF["SEMANTIC_UNAVAILABLE"] + + NC -->|"有 progress"| IF["INSUFFICIENT_EVIDENCE"] + NC -->|"无 progress,有 missing_info"| MF["MISSING_REQUIRED_CONTEXT"] + NC -->|"两者都无"| FAIL["FAILED"] + PS -->|"有 verified facts"| IF + PS -->|"无 verified facts"| FAIL + + EF --> FB["ReleaseOutcome.FALLBACK"] + SF --> FB + UF --> FB + IF --> FB + MF --> FB +``` + +`DiagnosisReleaseResult` 只表达 `SUCCESS` 或 `FALLBACK`。真正不可恢复的 `FAILED / CANCELLED` 由 Chat Application 异常路径映射。 + +## 10. RunState、ReleaseOutcome 和 FallbackType 映射 + +| 场景 | RunState | ReleaseOutcome | FallbackType / 内容 | +|---|---|---|---| +| 有结论且 Guards 通过 | `SUCCESS` | `SUCCESS` | Diagnosis report | +| 缺少必要上下文,零 Tool 合法结束 | `SUCCESS` | `FALLBACK` | `MISSING_REQUIRED_CONTEXT` | +| 有限检查后证据不足 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` | +| 信息饱和后有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` | +| 协议停止后有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` | +| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` | +| SemanticGuard 判定不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` | +| SemanticGuard 技术不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` | +| 预算耗尽但有安全进展 | `BUDGET_EXHAUSTED` | `FALLBACK` | 当前发布 `INSUFFICIENT_EVIDENCE` | +| 超时,无法形成安全内容 | `TIMED_OUT` | `FAILED` | failure | +| 预算耗尽且无安全进展 | `BUDGET_EXHAUSTED` | `FAILED` | failure | +| 不可恢复内部失败 | `FAILED` | `FAILED` | failure | +| 客户端断开 | `CANCELLED` | `CANCELLED` | 不公开 CANCELLED done | + +这张表解释了为什么不能只问“最后 status 是什么”。必须先确定是在问内存执行、发布结果还是 Fallback 原因。 + +## 11. SSE 连接生命周期 + +`ChatSseSession` 有自己的连接状态机: + +```mermaid +stateDiagram-v2 + [*] --> NEW + NEW --> OPEN: onStarted / metadata + NEW --> DISCONNECTED: 客户端提前断开 + OPEN --> OPEN: status events + OPEN --> TERMINAL: content + done + OPEN --> TERMINAL: failure + done(FAILED) + OPEN --> DISCONNECTED: send failure / disconnect + TERMINAL --> [*] + DISCONNECTED --> [*] +``` + +公开事件顺序是: + +```text +metadata -> status* -> content | failure -> done +``` + +边界规则: + +- content 最多发送一次; +- result 的 sessionId/runId 必须匹配 metadata; +- TERMINAL 或 DISCONNECTED 后拒绝迟到内容; +- `ReleaseOutcome.CANCELLED` 不作为公开 done outcome;客户端已经断开时没有可靠发送目标; +- 当前 `SseOutcome` 枚举未参与运行时协议,`ChatSseEvent.Done` 使用 `ReleaseOutcome`。 + +## 12. 持久化生命周期 + +### 12.1 创建 + +RunContext 创建后,Application 写入: + +```text +diagnosis_run.status = RUNNING +session_id / run_id / query +``` + +之后记录 intent。 + +### 12.2 完成映射 + +`JpaChatRunStore` 根据 ReleaseOutcome 映射数据库通用 status: + +| ReleaseOutcome | diagnosis_run.status | +|---|---| +| `SUCCESS` | `SUCCESS` | +| `FALLBACK` | `SUCCESS` | +| `FAILED` | `FAILED` | +| `CANCELLED` | `CANCELLED` | + +这里的 `status=SUCCESS` 表示请求已被正常处理并形成安全内容,不表示一定有诊断 conclusion。 + +同时保存: + +- `release_outcome`; +- safe answer JSON; +- 从 safe content 提取的 conclusion; +- duration、Token、Agent step 数和实际 Tool call 数; +- 仅在 `DIAGNOSIS + SUCCESS` 时保存 `published_result`。 + +Fallback 不进入下一轮 PreviousTurn。 + +### 12.3 当前可观测缺口 + +内存 `RunTermination.state/reason` 当前没有独立字段直接持久化。`RUN_FINISHED` 主要记录 ReleaseOutcome 和预算对账,精确的 TIMED_OUT/BUDGET_EXHAUSTED 原因需要结合异常路径和其他 Trace 事件判断。 + +因此数据库 `status`、ReleaseOutcome 和 Trace 都是必要观察面,任何一个都不是完整替代品。 + +## 13. Trace Timeline 如何对应生命周期 + +典型 Diagnosis SUCCESS: + +```text +RUN_STARTED +ROUTING_ATTEMPT +ROUTING_DECISION +AGENT_MODEL_STEP +TOOL_INVOCATION / TOOL_PROGRESS ... +EVIDENCE_GUARD_INITIAL +SEMANTIC_GUARD_ATTEMPT +SEMANTIC_GUARD_DECISION +RELEASE_DECISION SUCCESS +RUN_FINISHED SUCCESS +``` + +典型信息不足 Fallback: + +```text +RUN_STARTED +ROUTING_ATTEMPT +ROUTING_DECISION +AGENT_MODEL_STEP +TOOL_INVOCATION / TOOL_PROGRESS ... +COLLECTION_STOP(可选) +EVIDENCE_GUARD_INITIAL(无结论引用检查) +RELEASE_DECISION FALLBACK +RUN_FINISHED FALLBACK +``` + +`TracePhase` 和 `TraceEventStatus` 用来组织 Timeline。它们描述事件发生在哪一阶段、该事件结果如何,不构成新的 Run 生命周期。 + +## 14. 并发和迟到结果规则 + +一次 Run 的终止安全依赖三层协作: + +1. `RunLifecycle`:first-terminal-wins,终态不可覆盖; +2. `DiagnosisHarnessCore.checkActive`:每个关键边界阻止终止后的新工作; +3. `ChatSseSession`:TERMINAL/DISCONNECTED 后拒绝迟到 content。 + +这能保证逻辑上的“取消后不再发布”。它不等于强制终止底层线程或 Provider 计算;底层调用返回后仍要经过 active 和 SSE state 检查,迟到结果才会被丢弃。 + +## 15. 排障时应该先看哪个状态 + +| 问题 | 首先看 | 然后看 | +|---|---|---| +| 用户为什么收到 Fallback | `release_outcome + FallbackType` | RELEASE/EVIDENCE/SEMANTIC Trace | +| Agent 为什么停止调用 Tool | `DiagnosisStopReason` | TOOL_PROGRESS、TOOL_REQUEST_REJECTED、COLLECTION_STOP | +| Tool 为什么没有证据 | `InvocationStatus + EvidenceStatus` | canonical record 和 Tool audit error code | +| Run 是超时还是预算耗尽 | 内存 termination(运行中)或相关 Trace/异常 | budget usage、collection stop、失败码 | +| 数据库为什么 status=SUCCESS 但没有结论 | `release_outcome` | answer 的 content type / FallbackType | +| 为什么没有 SSE done | `ChatSseSession.State` | client disconnect/send failure 和 Run cancellation | +| 为什么下一轮没有上一轮上下文 | 是否 `DIAGNOSIS + ReleaseOutcome.SUCCESS + published_result` | PublishedResultPolicy | + +## 16. 生命周期不变量 + +1. 一个 Run 只能有一个 RunTermination。 +2. 一个公开 SSE 最多有一次 content/failure 和一次 done。 +3. Tool Invocation 只有 READY 才能被引用。 +4. READY 必须同时拥有 `EVIDENCE_FOUND` 或 `NO_EVIDENCE`。 +5. `NO_EVIDENCE` 不能被提升为全局否定。 +6. SATURATED 后不再执行新的 evidence Tool。 +7. StopReason 不直接决定公开内容,必须经过 Release。 +8. 有 conclusion 才执行完整 EvidenceGuard/Repair/SemanticGuard 链。 +9. FALLBACK 是正常发布结果,不等于 RunState.FAILED。 +10. 数据库 status、ReleaseOutcome 和 RunState 不能互相替代。 diff --git a/mvp/engineering/harness/Harness组件全景-职责-设计原因与边界.md b/mvp/engineering/harness/Harness组件全景-职责-设计原因与边界.md new file mode 100644 index 0000000..b0ee54d --- /dev/null +++ b/mvp/engineering/harness/Harness组件全景-职责-设计原因与边界.md @@ -0,0 +1,374 @@ +# Harness 组件全景:职责、设计原因与边界 + +**更新日期**:2026-07-29 +**适用代码**:`src/main/java/com/superbiz/agent/harness` +**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) +**配套主文**:[Harness设计-非确定性Agent的确定性控制边界.md](Harness设计-非确定性Agent的确定性控制边界.md) + +> 本文是完整组件参考手册,不建议第一次接触 Harness 时顺序阅读。入门请先读 [Harness 阅读入口](README.md),需要逐组理解组件时使用[组件渐进式导读](components/README.md)。 + +## 1. 这份文档怎样定义“全部组件” + +当前 `harness` 目录包含 10 个一级职责域、189 个 Java 源文件。它们并不都是独立运行的“服务”: + +- **执行组件**拥有行为,例如 Core、Interceptor、Boundary、Guard、Release、Executor; +- **端口与适配器**隔离框架、Redis、JPA、JDBC 和业务 Tool; +- **状态与契约类型**固定跨组件语言,防止字符串协议漂移; +- **Limits、Prompt 和异常类型**把边界配置与失败语义显式化。 + +因此本篇先解释 10 个职责域为什么存在,再列出每个生产类型。判断某个类应该放在哪里时,只问三个问题:它拥有什么状态、它能作出什么决定、它绝不能决定什么。 + +## 2. 组件地图 + +| 职责域 | 文件数 | 解决的问题 | 核心组件 | +|---|---:|---|---| +| `application` | 34 | 谁创建 Run、路由请求、持久化和映射公开结果 | `ChatApplicationUseCase`、三个 Executor、`ChatRunStore` | +| `core` | 14 | deadline、取消、预算和唯一终态由谁拥有 | `DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunLifecycle` | +| `agent` | 17 | 如何把框架 ReAct 接入 Harness,而不复制 ReAct | `DiagnosisAgentUseCase`、Factory、Model/Tool Interceptor | +| `progress` | 14 | 如何识别无增益、重复和协议空转 | `DiagnosisProgressTracker`、Projector、scope normalizer | +| `tool` | 49 | Tool 如何安全执行、保存真相并只暴露必要内容 | `ToolBoundary`、Adapters、Projectors、Canonical Store、MySQL sandbox | +| `guard` | 15 | 如何分开验证引用真实性和结论支持度 | `EvidenceGuard`、`SemanticGuard`、`GuardModelCall` | +| `release` | 6 | 谁拥有最终 SUCCESS / FALLBACK 决策 | `DiagnosisReleaseUseCase`、`EvidenceRepair`、`SafeFallbackFactory` | +| `retry` | 8 | 哪些失败允许重试、attempt 如何可见 | `HarnessRetryExecutor`、Policies、Failure taxonomy | +| `audit` | 17 | 如何重放决策而不复制敏感正文 | Trace、Tool audit、Model ledger、Agent hook | +| `contract` | 15 | 跨层公开语言如何保持类型化 | Draft、PublishedResult、Fallback、状态枚举 | + +```mermaid +flowchart LR + APP["application
Run 与公开用例"] --> CORE["core
执行不变量"] + APP --> AGENT["agent
ReAct 接入"] + AGENT --> PROGRESS["progress
收敛控制"] + AGENT --> TOOL["tool
证据边界"] + APP --> RELEASE["release
唯一发布"] + RELEASE --> GUARD["guard
真实性与支持度"] + CORE --> RETRY["retry
显式 attempt"] + CORE -.-> AUDIT["audit
可观测账本"] + AGENT -.-> AUDIT + TOOL -.-> AUDIT + RELEASE -.-> AUDIT + CONTRACT["contract
类型化语言"] -.-> APP + CONTRACT -.-> AGENT + CONTRACT -.-> TOOL + CONTRACT -.-> GUARD + CONTRACT -.-> RELEASE +``` + +## 3. Application:Run 的应用所有者 + +### 为什么需要 + +Core 只知道一次 Run 是否活跃,并不知道 HTTP、SSE、意图路由、数据库持久化和上一轮上下文。若这些职责塞进 Core,Harness 会变成业务工作流引擎;若散落在 Controller,则每个入口都可能产生不同的终态和 Fallback。 + +### 核心组件 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `ChatApplicationUseCase` | 为一次请求建立唯一应用事务边界 | 解析 session、读取历史、创建 Run、路由、执行分支、落终态和安全输出 | 不做诊断推理,不自行构造诊断 Fallback | +| `IntentRouter` | 路由也会消耗模型、超时并返回非法 JSON | 在有界输入、timeout 和显式 retry 下输出唯一 `IntentType` | 不执行业务 Tool,不生成最终回答 | +| `SystemChatExecutor` | 系统问答不需要 ReAct,但仍必须受模型预算控制 | 单轮回答产品能力和闲聊 | 不声称查询了实时数据 | +| `KnowledgeQueryExecutor` | 知识问答需要一次 RAG 和一次受控生成,但不需要完整诊断链 | 调用知识 Tool、校验 Tool result、生成带来源答案 | 首版不进入 SemanticGuard,不执行多 Tool 诊断 | +| `DiagnosisChatExecutor` | Agent 执行和安全发布需要一个明确接合点 | 执行 Agent、处理合法停止/非法 Draft、调用 Release、映射公开内容 | 不重复 Guard 或 Release 决策 | +| `ChatRunStore` / `JpaChatRunStore` | 内存 Run 状态与长期数据库状态职责不同 | 保存 Run 开始、intent、结果、预算摘要;读取安全上一轮 | 不保存 canonical raw;只允许安全发布结果进入 PreviousTurn | +| `PublishedResultPolicy` | 直接复用上一轮完整结果会让上下文无限增长并传播失败内容 | 生成可持久化 PublishedResult 和有界 PreviousTurn | Fallback、失败、raw evidence 不进入下一轮 | + +### 类型清单 + +| 类型 | 分类与功能 | +|---|---| +| `ChatApplicationRequest`、`ChatApplicationResult` | 应用入口和出口 DTO;固定 session、run、intent、outcome 和内容类型 | +| `ChatApplicationContent`、`ChatContentType` | 公开内容的 sealed/typed 边界,避免任意对象直接发给 SSE | +| `DiagnosisContent`、`KnowledgeContent`、`SystemChatContent`、`FallbackContent` | 四种公开内容载体;分别包装安全诊断、知识答案、系统回答和 Fallback | +| `ChatApplicationStatus`、`ChatApplicationObserver` | 向 SSE 报告有界阶段,不泄露模型内部步骤 | +| `ChatRunControl` | 只向入口暴露 exact session/run 和客户端断开取消能力 | +| `ChatApplicationException`、`ChatFailureCode` | 把内部异常映射为稳定、可公开的失败语义 | +| `IntentRouting`、`SystemChatOperation`、`KnowledgeQueryOperation`、`DiagnosisOperation` | 四个应用端口;使主用例不依赖具体模型或执行器 | +| `DiagnosisExecutionResult` | Diagnosis 分支的内部返回,携带 outcome、content、published result 和预算已处理标记 | +| `IntentRouterInput`、`IntentRouterLimits`、`IntentRouterPrompt`、`IntentRoutingException` | 路由输入、边界、Prompt 和失败类型 | +| `KnowledgeQueryLimits`、`SingleTurnExecutorLimits` | 知识与单轮模型路径的输入、输出、timeout 上限 | +| `ChatRunStore`、`RoutingHistory` | 持久化端口及最小路由历史 | +| `PreviousTurnLimits`、`PublishedResultPolicy` | 安全历史的字段/大小限制与投影策略 | + +## 4. Core:每个 Run 的执行不变量 + +### 为什么需要 + +模型、Tool、Guard 和 Application 都要检查取消、deadline 和预算。如果每层各自维护计数或终态,会出现多个真相源;如果依赖 ThreadLocal,则异步线程无法可靠继承。 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `DiagnosisHarnessCore` | 所有执行边界需要同一套 active / budget / terminal 规则 | 创建 RunContext,执行模型/Tool/Token/bytes 门禁,处理取消和终态 | 不持久化,不调用 Agent/Tool,不维护全局 Run Map | +| `RunContext` | Run 身份和状态句柄必须一起显式传播 | 固定 sessionId、runId、deadline 及 per-run handles | record 结构不可变,不代表内部计数不变化 | +| `RunLifecycle` | 成功、失败、取消可能竞态到达 | 原子 `compareAndSet` 实现 first-terminal-wins | 不映射公开 ReleaseOutcome | +| `RunCancellation` | 取消原因和回调只能被第一个请求确定 | first-reason-wins,并通知 lifecycle/资源回调 | 不承诺强杀同步 Provider 请求 | +| `RunBudget` | 多维预算必须原子地先检查再计数 | 模型、Tool、单 Tool、输入/输出/总 Token 和 bytes 计量 | 不判断信息是否有价值 | +| `RunCapacityCounter` | bytes 可能由并发边界累计 | CAS 方式维护 Run 总容量 | 不负责字段级截断策略 | + +### 类型清单 + +| 类型 | 分类与功能 | +|---|---| +| `RunBudgetLimits`、`RunBudgetUsage` | 预算配置和值快照;区分限制与已使用量 | +| `BudgetKind`、`BudgetExceededException` | 明确指出耗尽的是模型、Tool、Token 还是 bytes | +| `RunState`、`RunTermination` | 内存执行状态与不可变终止快照 | +| `RunCancellationReason` | 客户端断开、用户请求、deadline、预算和内部失败的取消分类 | +| `RunAbortedException` | 将已经确定的 RunTermination 穿过深层调用栈,不丢失终态 | + +## 5. Agent:框架 ReAct 与 Harness 的接合层 + +### 为什么需要 + +业务需要框架原生 Tool Calling 和 ReAct loop,但框架默认并不知道项目的 RunContext、预算、审计、Tool 双视图和停止协议。接合层的目标是“拦截边界”,不是重新实现 Agent 循环。 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `DiagnosisAgentFactory` | 每个 Run 的 interceptor 和 metadata 不同 | 为当前 Run 创建 ReactAgent,注册 Tool callback、Prompt、Hook 和 interceptor | 不缓存跨 Run Agent 状态 | +| `DiagnosisAgentUseCase` | 框架输入输出是字符串,业务要求严格 Draft 和 bytes 边界 | 序列化输入、显式注入 Run metadata、调用 Agent、严格解析 Draft、映射受控停止 | 不执行证据和语义校验 | +| `HarnessModelInterceptor` | 每一轮 ReAct 模型调用都必须进入预算和 Token 账本 | 调用前 reserve,调用后记录 Provider usage 并再次检查 active | 不重试模型 | +| `HarnessToolInterceptor` | 模型 Tool Call 中混有 Harness 进展协议和业务参数 | 校验 Envelope、上一轮增益、重复/饱和、调用 Tool、投影 observation、交付 STOP_REQUIRED | 不执行 backend,不复制 ToolBoundary 预算 | +| `HarnessEvidenceTools` | Tool schema 必须由服务端原生注册,且业务 Tool 可选启用 | 注册 RAG/log/MySQL callback,严格解析通用 Envelope,桥接 Adapter | Prompt 不写死 Tool schema;未配置 MySQL 时不暴露死 Tool | +| `ToolResultViewProjector` | canonical agent_result 仍含 Harness 控制字段 | 生成 Control View 和白名单 Model Observation | 不读取 raw response,不判断根因 | + +### 类型清单 + +| 类型 | 分类与功能 | +|---|---| +| `DiagnosisAgentInput`、`DiagnosisAgentExecution` | Agent 用例输入,以及 Draft/ProgressSnapshot/stop reason 的执行结果 | +| `DiagnosisAgentLimits` | query、previous turn、总输入和 Draft 的 UTF-8 bytes 上限 | +| `DiagnosisAgentPrompt`、`DiagnosisDraftOutputSchema` | 最小职责 Prompt 与严格结构化输出 Schema | +| `EvidenceToolInvoker` | Agent 层到具体 Adapter 的函数端口 | +| `ParsedAgentToolCall` | 解包后的 previous observation、typed business input 和 JSON 参数 | +| `ToolControlView` | Harness 消费的 evidence status、count、scope 等控制视图 | +| `DiagnosisAgentLimitException` | Agent 输入或输出越界 | +| `DiagnosisAgentOutputException` | 空、非法 JSON、Schema 不合格 Draft,并可携带安全 ProgressSnapshot | +| `DiagnosisCollectionStoppedException` | STOP_REQUIRED 后仍请求 Tool 时,把受控停止穿出框架 loop | + +## 6. Progress:从资源上限到正常收敛 + +### 为什么需要 + +预算只能阻止无限消耗,不能识别“连续查询没有推进诊断”。Progress 子系统只保存 Run 内最小控制状态,不复制完整证据。 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `DiagnosisProgressTracker` | 连续无增益、待评价调用和协议错误需要线程安全单一所有者 | 记录 completed scope、pending evaluation、NO_GAIN、协议错误、饱和和一次停止指令 | 不保存 raw/agent_result,不判断非空内容的业务价值 | +| `ToolScopeNormalizer` | 字段顺序或无关格式不应绕过重复检测 | 将各 Tool typed input 规范化为稳定 scope | 首版不做自然语言语义去重 | +| `DiagnosisProgressProjector` | 受控停止或非法 Draft 后仍需安全说明已检查内容 | 按 Tracker identity 回读 canonical READY 记录,生成有界事实、来源和限制 | 无法验真的记录直接排除,不输出 raw | +| `DiagnosisProgressProjection` | Agent 用例不应依赖具体 Redis projector | 定义 Run 到安全快照的端口,并提供 empty 实现 | 不决定 Fallback 类型 | + +### 类型清单 + +| 类型 | 分类与功能 | +|---|---| +| `InformationGain` | 仅 `GAINED / NO_GAIN`,避免引入含混质量等级 | +| `DiagnosisCollectionState` | `COLLECTING / SATURATED`,只描述信息收集状态 | +| `DiagnosisStopReason` | 区分信息饱和、预算限制和进展协议错误 | +| `PreviousObservation` | 模型在下一次 Tool Call 回传上一轮 `tool_call_id + information_gain` | +| `CompletedToolCall`、`ToolScopeIdentity` | 保存已完成调用的 identity 和规范化 scope,不保存 payload | +| `DiagnosisProgressSnapshotState` | Tracker 的内部控制快照,含计数、pending ID 和停止指令状态 | +| `DiagnosisProgressSnapshot` | Release 可消费的安全快照,含 verified sources、observed facts 和 limitations | +| `ProgressProtocolViolationType`、`ProgressProtocolViolationException` | 缺字段、乱序评价、意外评价和非法 Envelope 的稳定分类 | + +## 7. Tool:执行、真相、投影与后端安全 + +Tool 是文件最多的职责域,但可以按四层理解。 + +### 7.1 Boundary:所有 Tool 共用的确定性入口 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `ToolBoundary` | 每个 Adapter 自己实现授权、预算、store 和 audit 会产生漂移 | 统一 preflight、active、read-only、Tool budget、bytes、canonical 状态迁移和 audit | 不理解 Tool 业务内容;不做信息增益判断 | +| `ToolCallRequestEnvelope` | 调用必须同时证明 Run、ID、Tool、参数、授权和只读意图 | Boundary 的内部调用信封 | 不等同于模型侧 progress Envelope | +| `ToolExecutor` | Boundary 不依赖具体 backend | raw 执行函数端口 | 不投影结果 | +| `ToolResultProjector` | raw 到 canonical agent_result 的逻辑因 Tool 而异 | 标准化、限制、计算客观 evidence status | 不判断结论支持度 | +| `ToolBoundaryResult` | 只允许 READY 或 ERROR 离开 Boundary | 向上返回 ID、状态、agent result、evidence status 或稳定错误码 | PROJECTING 不对外暴露 | +| `ProjectedToolResult` | projector 同时返回有界 agent result 与客观状态 | Boundary/store 的中间值 | 不是最终 Model Observation | +| `ToolBoundaryErrorCode` | 不能把内部异常正文交给 Agent | 固定非法 ID、Run mismatch、未授权、非只读、超限、执行/投影/store 等错误 | 不包含敏感原因 | + +### 7.2 Canonical Store:TTL 内的完整 Tool 真相 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `CanonicalInvocationStore` | Guard 需要独立于 Agent 上下文读取原始调用真相 | 定义 begin/find/markReady/markError 状态端口 | 不负责长期审计 | +| `RedisCanonicalInvocationStore` | 完整 request/raw 有敏感性和容量,适合短期 TTL 存储 | 原子创建,保持剩余 TTL 的状态更新,读取不续期 | 只有该 Adapter 访问 Redis | +| `CanonicalToolInvocation` | request、raw、agent_result 和两个状态必须形成合法组合 | 封装 PROJECTING -> READY/ERROR 转换及 Run 可引用判断 | ERROR/PROJECTING 不可被 EvidenceGuard 引用 | +| `CanonicalInvocationLimits` | Redis record、raw 候选和 agent result 需要独立硬限制 | 统一 UTF-8 bytes 校验 | 不执行截断 | +| `ToolCallKeyFactory` | Key 要隔离 Run 且保留框架 ID | 校验安全 segment,生成 prefix/runId/toolCallId | 不生成或改写 tool_call_id | +| `DuplicateInvocationException`、`InvocationStateException`、`CanonicalStoreException`、`ResultTooLargeException` | Store 失败需要可分类而不是字符串猜测 | 区分重复、非法迁移、基础设施错误和容量错误 | 最终对 Agent 仍映射为安全错误码 | + +### 7.3 Adapter 与 Projector:隔离业务后端 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `RagToolAdapter` | 检索实现会演进,但 Agent schema 和 Boundary 不应随之变化 | 解析 RAG request,经 Boundary 调 backend 和 `RagResultProjector` | 不把检索轨迹直接给模型 | +| `QueryLogsToolAdapter` | 现有日志 Tool 的返回和时间语义需要规范化 | 校验范围、桥接 backend、投影日志事件 | 0 命中是 `NO_EVIDENCE`,不是技术失败 | +| `MysqlToolAdapter` | LLM 生成 SQL 必须经过授权和只读沙箱 | 解析 request、先校验 SQL,再经 Boundary 执行和投影 | 未配置数据源时 Tool 不注册 | +| `RagResultProjector` | 上游 evidence、score、relevance 表达不稳定 | 输出有界证据,并保留粗粒度 `relevance_level` | 非空/REFERENCE 不等于支持根因 | +| `QueryLogsResultProjector` | raw 日志不可直接进入上下文 | 生成有界 events、pattern、scope 和截断标记 | 不泄露无限日志正文 | +| `MysqlResultProjector` | JDBC rows 和元数据需要稳定 Agent contract | 限制行列、单元格和 bytes,输出 rows/columns/scope | 不执行 SQL 安全判断 | +| `ToolProjectionLimits`、`MysqlToolLimits` | 各投影边界必须集中、可测试 | 配置证据数、文本、行列和结果上限 | 不改变 Run 总 bytes 预算 | + +### 7.4 MySQL 只读沙箱 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `MysqlSqlValidator` | `readOnly=true` 声明不能证明 SQL 安全 | 解析并限制单条 SELECT、数据源、schema/table/column、limit 等 | 不执行查询 | +| `MysqlDataSourceDefinition` | 连接存在不代表 Agent 可访问所有表列 | 保存逻辑数据源和 allowlist | 不携带密码 | +| `MysqlQueryPlan` | 校验后的内容不应在 Executor 再解析原始请求 | 固定已批准数据源、SQL 和限制 | 只能由 Validator 产生 | +| `MysqlReadOnlyExecutor` / `JdbcMysqlReadOnlyExecutor` | JDBC 细节与 Harness Boundary 解耦 | 在只读连接、timeout 和 row limit 下执行 plan | 不接收未经验证的 request | +| `MysqlRawResult` | JDBC 原始但结构化的执行结果 | 携带 columns、rows、truncated、duration | 仍需 Projector 后才能进入 canonical agent_result | +| `MysqlSecurityException` | 安全拒绝必须与基础设施错误区分 | 表达非法 SQL、未授权对象等 | 不向 Agent泄露详细策略 | + +### 7.5 Tool contract 完整清单 + +| 类型 | 设计原因与功能 | +|---|---| +| `AgentToolContracts` | Tool 名、描述和服务端注册契约的唯一常量源,防止 Prompt/代码漂移 | +| `RagToolCall`、`QueryLogsToolCall`、`MysqlToolCall` | 模型侧统一 Envelope:`previous_observation + input` | +| `RagToolRequest`、`QueryLogsRequest`、`MysqlToolRequest` | 业务 Tool 的 typed input;Interceptor 解包后仍保持原业务协议 | +| `RagToolResult`、`QueryLogsToolResult`、`MysqlToolResult` | canonical agent-facing 标准结果,不等同于 raw backend response | +| `RagEvidence`、`SourceDocument` | 有界知识证据及文档身份 | +| `RagRelevanceLevel` | `PRECISE / HIGHLY_RELEVANT / REFERENCE` 等客观检索相关度,不是诊断置信度 | +| `LogQueryScope`、`LogEvent`、`LogPattern` | 日志查询实际范围、事件和聚合模式 | +| `LogSourceKind`、`LogTopic` | 限制日志来源和主题的可选集合 | +| `ToolContractCollections` | 对 contract 集合做 defensive copy 和空值规范化 | + +## 8. Guard:把两个验证命题分开 + +### 8.1 Evidence Guard + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `EvidenceGuard` | 模型不能被信任去验证自己引用的 ID 和 Run 归属 | 严格验证 Draft、analysis ID、canonical READY、当前 Run 所有权、evidence status 和引用闭包 | 不调用模型,不判断结论语义是否成立 | +| `EvidenceGuardResult` | 校验只能是 verified snapshot 或 violations | 阻止半有效结果继续发布 | 不包含原始 Draft 修复逻辑 | +| `VerifiedEvidenceSnapshot` | SemanticGuard 只能看到已验真的证据投影 | 汇总 verified analyses 和 sources | 不包含 raw Tool payload | +| `VerifiedAnalysisEvidence`、`VerifiedEvidence` | 保持 analysis 到证据的归属关系 | 提供来源、scope、excerpt 等安全证据 | 不提升为业务结论 | +| `EvidenceViolation`、`EvidenceViolationCode` | Repair 和 Fallback 需要稳定失败原因 | 标识缺 analysis、未知调用、Run mismatch、非 READY、引用不闭合等 | 不保存内部异常栈 | + +### 8.2 Semantic Guard 与模型调用边界 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `SemanticGuard` | 引用真实仍可能无法支持结论 | 用隔离单轮调用输出 `SUPPORTED / UNSUPPORTED`,严格解析并有限 retry | 无 Tool、无记忆、不访问 Redis、不改写 Draft | +| `GuardModelCall` | Router、单轮回答、Repair、Semantic 都需一致的模型预算、timeout 和 bytes 控制 | 在线程池中执行单轮 ChatModel,记录 usage,超时取消 future | 不决定业务 retry policy | +| `SemanticDraftView` | Guard/Repair 只应比较用户可见语义 | 从 Draft 提取稳定语义视图,并判断修复前后是否一致 | 不包含引用实现细节 | +| `SemanticGuardInput`、`SemanticGuardDecision` | 固定 Guard 输入和带 verdict 的输出 | 只传 query、Draft view 和 verified snapshot | 不传 Prompt 历史或 raw Tool response | +| `SemanticGuardLimits`、`SemanticGuardPrompt` | 输入、输出、单次/总 timeout 和判定职责可测试 | 限定一次语义审查的成本与 Prompt | 不暴露给 Diagnosis Agent | +| `GuardModelCallException` | 单轮模型失败要按 timeout、transport、parse、schema 分类 | 为 Harness retry 提供类型化信号 | 不直接映射用户内容 | + +## 9. Release:唯一公开决策点 + +### 为什么需要 + +如果 Agent、Guard、Application 都可以各自构造结果,同一个失败会出现不同用户语义,迟到内容也可能绕过安全校验。Release 必须集中回答一个问题:当前 Run 有哪些内容可以公开? + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `DiagnosisReleaseUseCase` | Draft、停止、Guard 和 Repair 的组合分支必须只有一个所有者 | 处理有结论、无结论、受控停止、非法 Draft;协调 Guard/Repair/Semantic;返回成功或 Fallback | 不持久化、不发送 SSE、不写新结论 | +| `EvidenceRepair` | 引用或结构小错不应总是丢掉语义正确的 Draft | 单轮修复引用,严格解析,并用 `SemanticDraftView` 保证用户语义不变 | 只 attempt 一次;不新增事实、不改结论 | +| `SafeFallbackFactory` | 失败文案若交给模型生成会再次引入幻觉 | 确定性构造 evidence failed、semantic unsupported/unavailable、insufficient evidence、missing context | 只使用已验真事实和有界问题码 | +| `DiagnosisReleaseResult` | 下游不能同时收到 Draft 和 Fallback | 类型化承载 outcome、Draft、verified evidence 或 SafeFallback | 不等同于 RunState | +| `EvidenceRepairLimits`、`EvidenceRepairPrompt` | Repair 的成本与职责必须比 Diagnosis Agent 更窄 | 限制输入/输出/timeout,固定只修引用的指令 | 不允许 Tool Calling | + +## 10. Retry:显式、类型化、可审计的 attempt + +### 为什么需要 + +重试会改变成本、延迟和副作用,必须是调用者的有意识决策。统一 Executor 负责循环,但具体组件持有自己的 Policy。 + +| 类型 | 设计原因与功能 | +|---|---| +| `HarnessRetryExecutor` | 在每个 attempt 前检查 Run active,统一记录成功/失败,并绝不吞掉取消和预算耗尽 | +| `HarnessRetryPolicies` | 集中定义 Router/SemanticGuard 最多两次,其余一次的严格策略 | +| `RetryPolicy` | `maxAttempts + retryable failures` 的不可变值,避免布尔 `retry=true` | +| `RetryFailure` | timeout、transport、parse、schema、invalid output、cancel、budget 等稳定分类 | +| `RetryAttempt` | 记录 attempt 序号、是否成功和失败类型,供 Trace 使用 | +| `RetryOperation`、`RetryFailureClassifier` | 将执行和异常分类作为端口注入,Executor 不依赖具体模型组件 | +| `RetryExecutionException` | 重试终止时保留最终 attempt 和失败分类 | + +## 11. Audit:记录控制事实,而不是复制业务正文 + +### 为什么需要 + +诊断系统需要回答“为什么停止、调用了什么、Token 是否对账、发布为何降级”,但普通观察面不应长期保存 Prompt、SQL、日志 query、raw response 或 reasoning。 + +| 组件 | 为什么设计 | 作用 | 明确边界 | +|---|---|---|---| +| `DiagnosisTraceRecorder` / `JpaDiagnosisTraceRecorder` | 所有阶段需要同一 exact-run timeline | 按 Run 分配 sequence,追加安全事件;失败 best-effort | 不改变业务结果,不保存敏感正文 | +| `TraceAuditEvents` | 各组件手写 details 容易字段漂移或泄露 | 集中构造 run/routing/model/tool/progress/guard/release 事件 | 只接受有界、安全字段 | +| `DiagnosisTraceAuditEvent` | Recorder 与业务组件解耦 | 统一事件 identity、phase、type、status、details | 不是领域事件总线 | +| `ToolInvocationAuditSink` / `JpaToolInvocationAuditSink` | canonical raw 不能长期保存,但调用元数据要留存 | 保存 exact Run、Tool ID、状态、耗时、bytes 和有界 enrichments | 不保存完整 request/raw/agent result | +| `RagLookupAuditEnricher` | RAG 需要有限的质量诊断字段 | 从受限输入提取 step/query/relevance 等安全摘要 | 不打印完整 rewritten/raw query | +| `HarnessAgentAuditHook` | 框架每轮 Agent 模型步骤需关联数据库 step 和 Provider reasoning 可用性 | 写 AgentStep metadata,并把 reasoning/assistant text 放独立受限存储 | reasoning 不进普通 Timeline、Guard 或下一轮上下文 | +| `AgentStepAuditTracker` | Tool audit 需要关联当前 Agent step,但不能使用 ThreadLocal | 按 runId 显式绑定/查询/清理 stepId | 不保存 Step 实体 | +| `ModelCallAuditor` / `ModelCallLedger` | 所有模型入口都要按组件、轮次对账 Token | begin call、记录 Provider usage、写 MODEL_TOKEN_USAGE、生成 Run reconciliation | Usage 缺失时标 unavailable,不估算 | +| `RunConclusionExtractor` | Trace/DB 常需直接读取安全发布结论 | 从 public JSON 提取 conclusion | 不读取 Provider reasoning | + +### Audit 类型清单 + +| 类型 | 分类与功能 | +|---|---| +| `ModelCallComponent` | Router、System、Knowledge、Diagnosis、Repair、Semantic 的统一组件枚举,并映射 Trace phase | +| `ToolInvocationAuditEvent` | durable Tool metadata 事件 | +| `TracePhase`、`TraceEventType`、`TraceEventStatus` | Timeline 的阶段、事件和结果词汇表 | + +## 12. Contract:跨组件唯一语言 + +### 为什么需要 + +Harness 横跨模型 JSON、Java 对象、Redis、JPA 和 SSE。若状态以自由字符串在各层重复定义,`SUCCESS`、`READY`、`EVIDENCE_FOUND` 很容易被混为一谈。Contract 包将不同维度保持正交。 + +| 类型 | 为什么存在与表达什么 | +|---|---| +| `DiagnosisDraft` | Diagnosis Agent 唯一结构化输出;analysis 绑定 Tool Call 引用,允许 `conclusion=null` | +| `KnowledgeAnswerDraft` | Knowledge Query 单轮模型输出,答案项绑定文档来源 | +| `PublishedResult` | 只有成功、安全的诊断结果可持久化为下一轮候选 | +| `PreviousTurn` | PublishedResult 的有界历史投影,不是完整会话记录 | +| `SafeFallback` | 确定性公开降级结构,包含 verified sources、observed facts、limitations、next steps 和 validation issues | +| `SourceDocument` | 知识回答中的文档身份与来源 | +| `IntentType` | `SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS` 路由结果 | +| `InvocationStatus` | Tool invocation 生命周期:`PROJECTING / READY / ERROR` | +| `EvidenceStatus` | Tool 客观结果:`EVIDENCE_FOUND / NO_EVIDENCE / ERROR` | +| `SemanticVerdict` | 最终推论支持度:`SUPPORTED / UNSUPPORTED` | +| `ReleaseOutcome` | Harness 发布结果:`SUCCESS / FALLBACK / FAILED / CANCELLED` | +| `SseOutcome` | 当前未被运行时消费的遗留枚举;公开 `done` 实际使用 `ReleaseOutcome`,不要把它作为 SSE 协议真理源 | +| `FallbackType` | evidence validation、semantic、insufficient evidence、missing context 等降级原因 | +| `AnalysisKind` | 区分正向证据分析与限定范围的负向观察 | +| `ContractCollections` | 对公共 contract 集合做 defensive copy、去空和不可变处理 | + +## 13. 配置装配:组件如何真正连起来 + +`HarnessChatConfiguration` 不在 `harness` 包内,但它是运行时组件图的 Composition Root: + +- 构造两个有界线程池:Chat worker 与 Harness model executor; +- 从 `ChatHarnessProperties` 创建 Run、预算、停止阈值和各模型调用 limits; +- 装配 Redis canonical store、ToolBoundary、三类 Adapter/Projector; +- 仅在存在有效逻辑数据源时注册 `query_mysql`; +- 为每个 Run 创建带 Model/Tool interceptor 和 Audit Hook 的 Agent; +- 装配 Guard、Repair、Release、Router、三个执行分支和顶层 Application UseCase。 + +它存在的原因是让所有限制和替换点在一个地方可见。组件内部不应自行读取 Spring 配置或寻找全局 Bean,否则 focused test 很难证明其边界。 + +## 14. 用调用链快速定位组件 + +| 想回答的问题 | 首先阅读 | 接着阅读 | +|---|---|---| +| 一次请求如何创建并结束 Run | `ChatApplicationUseCase` | `DiagnosisHarnessCore`、`JpaChatRunStore` | +| ReAct 每轮如何计预算 | `DiagnosisAgentFactory` | `HarnessModelInterceptor`、`ModelCallAuditor` | +| Tool 为什么被拒绝 | `HarnessToolInterceptor` | `DiagnosisProgressTracker`、`ToolBoundary` | +| Tool 结果为何没有原样给模型 | `ToolBoundary` | 具体 ResultProjector、`ToolResultViewProjector` | +| 一条 evidence_ref 如何验真 | `EvidenceGuard` | `CanonicalToolInvocation`、具体 ToolResult contract | +| 为什么最终是 FALLBACK | `DiagnosisReleaseUseCase` | `SafeFallbackFactory`、Trace 的 RELEASE 事件 | +| 为什么诊断停止继续查 | `DiagnosisProgressTracker` | Tool progress / rejection / collection stop Trace | +| Token 为什么对不上 | `ModelCallAuditor` | `ModelCallLedger`、`RUN_FINISHED` reconciliation | + +## 15. 组件边界自检 + +未来新增能力时,可以用下面的问题判断放置位置: + +1. 它是在判断业务根因吗?应留在 Diagnosis Agent,而不是 Core/Tool/Guard。 +2. 它能由代码机械证明吗?应放在 Boundary、Tracker 或 EvidenceGuard。 +3. 它需要完整 Tool 真相吗?读取 canonical store,不要从 Agent observation 反推。 +4. 它要决定公开内容吗?只能进入 Release,不要在 Application 或 Guard 私自构造。 +5. 它是短期验真数据还是长期运营元数据?前者 canonical,后者 metadata audit。 +6. 它会再次调用模型吗?必须进入 Core budget、ModelCallAuditor、timeout 和显式 retry policy。 +7. 它引入了新的状态吗?先确认是否只是 error code、stop reason 或 fallback reason,避免再造全局生命周期。 diff --git a/mvp/engineering/harness/Harness设计-非确定性Agent的确定性控制边界.md b/mvp/engineering/harness/Harness设计-非确定性Agent的确定性控制边界.md new file mode 100644 index 0000000..441eef1 --- /dev/null +++ b/mvp/engineering/harness/Harness设计-非确定性Agent的确定性控制边界.md @@ -0,0 +1,503 @@ +# Harness 设计:非确定性 Agent 的确定性控制边界 + +**更新日期**:2026-07-29 +**适用范围**:当前 MVP Chat / Diagnosis Harness +**代码基线**:`com.superbiz.agent.harness` +**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) +**组件索引**:[Harness组件全景-职责-设计原因与边界.md](Harness组件全景-职责-设计原因与边界.md) + +## 1. 结论先行 + +这个系统真正需要解决的,不是“怎样让模型多调用几个 Tool”,而是: + +> 当业务推理由非确定性模型完成时,怎样保证每一次执行仍然有身份、有边界、有停止条件、有证据闭包,并且只发布系统能够负责的内容。 + +当前 Harness 给出的答案是职责分治: + +- Diagnosis Agent 负责提出假设、选择 Tool、理解结果和撰写 Draft。 +- Harness 负责所有必须确定的事情:Run 生命周期、预算、取消、Tool 授权、结果隔离、停止、验真、发布和审计。 +- Tool 只报告客观执行结果,不宣称业务根因。 +- Guard 不重新做诊断,只回答受限的验证问题。 +- Release 是唯一对外发布决策点,只能发布原始安全 Draft 或确定性 SafeFallback。 + +这不是一个新的工作流引擎。Harness 不复制 ReAct 循环,不维护 Planner / Executor / Composer 图,也不替模型判断根因。它包围 ReAct 的不确定部分,在所有外部副作用和最终发布点建立确定性门禁。 + +## 2. 根问题:模型能推理,但系统必须能够负责 + +### 2.1 旧架构把“推理角色”当成了“安全边界” + +旧方案使用 Planner、Executor、Verifier、Composer 等多个 Agent 串联。表面上每个角色各司其职,实际上每个角色都拥有一部分 Prompt、模型调用、Schema 转换、重试和 Fallback 逻辑。结果是: + +1. 一次诊断的控制权分散在多个模型角色和业务 Service 中。 +2. 同一种错误可能被不同层重试、改写或吞掉。 +3. Verifier 既检查引用,又判断语义,还可能重写报告,成为第二个诊断者。 +4. Tool 原始结果、Agent 上下文和长期审计没有明确的数据边界。 +5. Run 身份依赖 ThreadLocal 传播,跨线程后无法证明一次调用属于哪个 Run。 + +```mermaid +flowchart LR + U["用户请求"] --> P["Planner Agent"] + P --> E["Executor Agent"] + E --> T["Tool / raw result"] + E --> V["Verifier Agent"] + V --> C["Composer Agent"] + C --> O["公开结果"] + + P -.-> X1["独立 Prompt / retry / schema"] + E -.-> X2["独立 Prompt / retry / fallback"] + V -.-> X3["验真、语义判断、改写混合"] + C -.-> X4["再次生成用户事实"] + T -.-> X5["raw、trace、正文边界不清"] + + H["ThreadLocal context"] -.-> P + H -.-> E + H -.-> V + + classDef risk fill:#fff1f0,stroke:#cf1322,color:#5c0011; + class X1,X2,X3,X4,X5,H risk; +``` + +问题并不在于“Agent 数量多”本身,而在于控制职责没有单一所有者。只要预算、重试、证据和发布仍分散,多 Agent 换成 Graph 也不会自然变得可靠。 + +### 2.2 单 Agent 仍然不等于受控系统 + +把多 Agent 合并成一个 ReAct Agent,只消除了重复推理链,并没有自动解决以下问题: + +- 模型可能无限改写相似查询; +- SDK 可能在 Harness 之外自动重试; +- Tool 可能返回过大、敏感或不可引用的原始数据; +- 模型可以引用其他 Run 或不存在的 Tool Call; +- 证据引用真实,不代表结论被证据支持; +- 客户端断开后,迟到的模型结果仍可能覆盖终态; +- “没有足够证据”可能被错误地发布为技术失败。 + +所以重构的核心不是“单 Agent”,而是“单 Agent + 确定性 Harness”。 + +## 3. 设计约束与不变量 + +以下不变量是组件拆分的依据,不是实现后的总结。 + +| 不变量 | 系统含义 | 由谁保证 | +|---|---|---| +| 一个 Run 只有一个终态 | 成功、失败、取消、超时、预算耗尽不能相互覆盖 | `RunLifecycle` 的 first-terminal-wins | +| 所有边界显式携带 Run 身份 | 不依赖线程绑定的隐式上下文 | `RunContext` 和 framework `tool_call_id` | +| 所有模型与 Tool 消耗可计量 | SDK 隐式 retry 不能绕过预算 | Core、Model/Tool interceptor、Harness retry | +| Tool raw 不直接进入模型 | 内部数据、体积和控制字段不能污染上下文 | ToolBoundary、canonical store、双视图投影 | +| 只有当前 Run 的 READY 调用可被引用 | 防止伪造、串 Run 和引用失败调用 | EvidenceGuard | +| 引用真实性与结论支持度分开 | 确定性规则和语义判断不互相冒充 | EvidenceGuard、SemanticGuard | +| Guard 不写报告 | 防止验证器成为第二个业务 Agent | Release Policy | +| 停止权属于 Harness | Prompt 建议不能替代系统保证 | ProgressTracker、预算和 interceptor | +| 没有根因是合法业务结果 | 证据不足不应伪装成内部故障 | `conclusion=null`、ProgressSnapshot、SafeFallback | +| 普通审计不保存敏感正文 | 可观测性不能以泄露 Prompt、raw、reasoning 为代价 | metadata-only audit、reasoning 独立存储 | + +## 4. 当前总体架构 + +```mermaid +flowchart TB + subgraph Entry["应用入口与 Run 所有权"] + APP["ChatApplicationUseCase"] + ROUTER["IntentRouter"] + EXEC["DiagnosisChatExecutor"] + STORE["ChatRunStore"] + end + + subgraph Harness["确定性 Harness 边界"] + CORE["Core
context / lifecycle / budget / cancel"] + MI["Model Interceptor
调用预算与 Token"] + TI["Tool Interceptor
协议、重复、停止"] + TB["ToolBoundary
授权、执行、canonical"] + PROGRESS["ProgressTracker
信息增益与饱和"] + EG["EvidenceGuard
引用真实性"] + SG["SemanticGuard
结论支持度"] + RELEASE["Release Policy
SUCCESS / FALLBACK"] + AUDIT["Audit / Trace
metadata-only"] + end + + subgraph Nondeterministic["非确定性区域"] + AGENT["Diagnosis ReAct Agent"] + MODEL["Chat Model"] + end + + subgraph Evidence["证据执行与存储"] + ADAPTER["RAG / Logs / MySQL Adapter"] + BACKEND["Evidence Backend"] + CANON["Redis Canonical Invocation"] + META["MySQL Durable Metadata"] + end + + APP --> CORE + APP --> ROUTER + ROUTER --> MODEL + APP --> EXEC + EXEC --> AGENT + AGENT --> MI --> MODEL + AGENT --> TI + TI --> PROGRESS + TI --> TB --> ADAPTER --> BACKEND + TB --> CANON + TB --> META + TB --> TI --> AGENT + EXEC --> EG --> SG --> RELEASE + PROGRESS --> RELEASE + RELEASE --> APP --> STORE + CORE -.-> MI + CORE -.-> TI + CORE -.-> TB + CORE -.-> EG + CORE -.-> SG + APP -.-> AUDIT + MI -.-> AUDIT + TI -.-> AUDIT + TB -.-> AUDIT + EG -.-> AUDIT + SG -.-> AUDIT + RELEASE -.-> AUDIT +``` + +图中的关键边界有三条: + +1. **Agent 之前**:Application 创建 Run,Core 固定身份、预算和终止语义。 +2. **Tool 前后**:Interceptor 管控制协议,ToolBoundary 管执行与真相,Projector 管模型可见内容。 +3. **发布之前**:EvidenceGuard 验引用,SemanticGuard 验推论,Release 决定唯一公开内容。 + +## 5. 关键决策及设计推导 + +### 5.1 决策一:只保留一个拥有 Tool loop 的 Diagnosis Agent + +**问题**:多角色 Agent 把同一份业务上下文在多个模型之间传递,每一跳都可能增加信息损失、重试和幻觉面。 + +**候选方案**: + +| 方案 | 优点 | 主要问题 | +|---|---|---| +| 保留 Planner / Executor / Verifier / Composer | 角色概念直观 | 控制权分散;多套 Prompt 和重试;Verifier/Composer 会产生新事实 | +| 外层自建 ReAct / StateGraph | 流程显式 | 与框架 ReAct 重复;状态和异常路径翻倍 | +| 单 ReAct Agent + Harness | 推理上下文连续;控制边界集中 | 对 Harness 的契约和门禁设计要求更高 | + +**决策**:Diagnosis Agent 是唯一业务推理者和 Draft 作者,使用框架 `ReactAgent` 自己完成 Thought / Action / Observation。项目不在外层复制循环。 + +**代价**:单 Agent 并不能通过“角色互审”获得表面冗余,因此必须把可机械验证的安全要求下沉到 Guard,把语义审查收缩成隔离的单轮判断。 + +### 5.2 决策二:RunContext 显式传播,结构不可变、状态句柄线程安全 + +**问题**:旧 ThreadLocal 可以在同步调用中工作,但异步 Tool、线程池和取消回调跨线程后,调用方无法证明读到的是当前 Run 的上下文。 + +**容易走错的方案**:把所有字段都做成不可变值对象。这样 deadline 和 identity 很干净,但预算、取消和终态不得不在外部另建全局 Map,反而形成第二真相源。 + +**决策**:`RunContext` 本身是 record,固定 `sessionId / runId / deadline` 和各状态句柄引用;预算、取消、生命周期、模型账本和进展 Tracker 在各自线程安全对象内变化。 + +```mermaid +flowchart LR + RC["RunContext
结构不可变"] --> ID["sessionId / runId / deadline"] + RC --> C["RunCancellation
first-reason-wins"] + RC --> B["RunBudget
同步复合计数"] + RC --> L["RunLifecycle
first-terminal-wins"] + RC --> M["ModelCallLedger
组件/轮次账本"] + RC --> P["ProgressTracker
Run 内进展"] +``` + +**为什么不是全局 Run Registry**:Harness 只控制当前调用,不承担 Run 查询和持久化;持久化真相仍由 `diagnosis_run` 所有,避免 Core 演变成工作流引擎。 + +### 5.3 决策三:关闭 SDK 隐式 retry,由 Harness 按失败类型拥有 retry + +**问题**:Spring AI 默认 `maxAttempts=10`。如果 SDK 在模型边界内部自动重试,Harness 看到的一次调用可能对应多个 Provider attempt,预算、延迟、Trace 和取消都失真。 + +**决策**:底层 SDK retry 设为 1;仅在 Harness 中使用类型化策略: + +- IntentRouter:超时、传输、非法输出最多 2 次 attempt; +- SemanticGuard:超时、传输、解析或 Schema 问题最多 2 次 attempt; +- Diagnosis Agent、业务 Tool、EvidenceRepair:1 次,不自动重试; +- 取消、预算耗尽、`NO_EVIDENCE`、业务拒绝:从不重试。 + +**设计理由**:重试不是通用容错开关。只有调用者知道一次失败是否幂等、是否还在 deadline 内、是否应该再次消耗预算。 + +**代价**:Provider 瞬时抖动更容易直接暴露,但真实 attempt 终于可计算、可审计,且不会把业务无证据误判为技术重试条件。 + +### 5.4 决策四:Tool 使用 canonical truth 与 Agent projection 双视图 + +**问题**:同一份 Tool 返回同时服务三个目标,而三个目标互相冲突: + +- 证据验真需要完整、稳定、按 Run 归属的记录; +- Agent 只需要完成下一步推理的最小内容; +- 长期审计应保留身份和耗时,但不能长期保存敏感 raw。 + +**决策**:把 Tool 数据分成三层,而不是让一个 JSON 到处流转。 + +```mermaid +flowchart LR + REQ["framework tool_call_id
typed request"] --> B["ToolBoundary"] + B --> RAW["backend raw response"] + RAW --> CAN["Redis canonical invocation
request + raw + agent_result
短 TTL"] + RAW --> PRJ["Tool-specific projector"] + PRJ --> CTRL["Harness control view"] + PRJ --> OBS["Agent observation
白名单、有界"] + B --> META["Durable audit
identity / status / latency / bytes"] + CTRL --> STOP["重复、NO_GAIN、停止控制"] + OBS --> AGENT["Diagnosis Agent"] + CAN --> GUARD["EvidenceGuard"] +``` + +**关键取舍**: + +- 使用框架 `tool_call_id`,Harness 不生成第二套 ID; +- `PROJECTING / READY / ERROR` 表达调用生命周期; +- `EVIDENCE_FOUND / NO_EVIDENCE / ERROR` 表达客观结果语义; +- raw 超限直接失败,不静默截断;Agent projection 可以有界截断,但必须标记 `truncated`; +- Redis 是 TTL 内完整 Tool 真相源,MySQL 只做长期 metadata audit; +- Redis 读取不续期,避免一次历史读取无限延长敏感 raw 生命周期。 + +**代价**:同一次 Tool 调用需要 projector、store 和 audit 三套表达。但它们不是重复数据模型,而是分别回答“真实发生了什么”“模型允许看到什么”“长期允许保留什么”。 + +### 5.5 决策五:EvidenceGuard、SemanticGuard 和 Release 三段分工 + +**问题**:“引用是真实的”和“结论被引用支持”是两个不同命题。只做前者会放过牵强推论;都交给模型则无法确定性防止伪造 ID、跨 Run 引用或修改报告。 + +**决策**: + +1. `EvidenceGuard` 使用纯代码检查 Draft 结构、analysis ID、Tool Call 当前 Run 所有权、READY 状态、evidence status 和引用闭包。 +2. 首次引用失败时,`EvidenceRepair` 只允许修复结构和引用;修复前后用户可见语义必须一致,然后重新执行 EvidenceGuard。 +3. `SemanticGuard` 只接收 query、完整 Draft 和 verified snapshot,进行无 Tool、无记忆的单轮 `SUPPORTED / UNSUPPORTED` 判断。 +4. `DiagnosisReleaseUseCase` 是唯一发布决策点。Guard 无权改写最终报告。 + +```mermaid +sequenceDiagram + participant A as Diagnosis Agent + participant R as Release UseCase + participant E as EvidenceGuard + participant C as Canonical Store + participant X as EvidenceRepair + participant S as SemanticGuard + participant P as Public Result + + A->>R: DiagnosisDraft + R->>E: validate draft references + E->>C: read exact run/tool_call_id + C-->>E: READY agent_result + alt 引用或结构错误 + E-->>R: violations + R->>X: 原 Draft + violations + X-->>R: 语义不变的修复 Draft + R->>E: revalidate once + end + alt 证据闭包有效 + E-->>R: verified snapshot + R->>S: query + draft + snapshot + S-->>R: SUPPORTED / UNSUPPORTED + end + alt SUPPORTED + R-->>P: 原始安全 Draft + else 任一门禁失败 + R-->>P: deterministic SafeFallback + end +``` + +**为什么 Repair 不能修正文义**:一旦 Repair 可以修改结论,它就成为新的报告作者;此时最终内容不再是 Diagnosis Agent 的 Draft,也无法证明修复只解决了引用问题。 + +**为什么 SemanticGuard 不是第二个 Agent**:它没有 Tool、历史记忆或 ReAct loop,只回答一个受约束的二值审查问题,不能探索新事实或生成新结论。 + +### 5.6 决策六:资源预算和信息增益是两套停止机制 + +**问题**:预算只能回答“还能不能花资源”,不能回答“继续查询还有没有价值”。知识库只有通用资料、日志持续为空时,Agent 可以在预算范围内不断换关键词,最终以 `BUDGET_EXHAUSTED` 结束。用户得到的是技术失败,而系统实际已经知道“当前范围证据不足”。 + +**决策**: + +- Tool 负责客观事实:是否执行成功、是否为空、实际 scope; +- Harness 机械判定空结果和完全重复的 `tool_name + normalized_scope` 为 `NO_GAIN`; +- 其他成功非空结果由模型在下一次 Tool Call Envelope 中声明 `GAINED / NO_GAIN`; +- `DiagnosisProgressTracker` 维护连续 `NO_GAIN`,达到阈值后进入 `SATURATED`; +- 连续 Envelope 协议错误使用独立计数和 `PROGRESS_PROTOCOL_VIOLATED`,不伪装成无信息增益; +- STOP_REQUIRED 只交付一次,再次请求 Tool 直接受控终止; +- `INFORMATION_SATURATED` 与 `BUDGET_LIMIT_REACHED` 始终分开记录。 + +```mermaid +stateDiagram-v2 + [*] --> COLLECTING + COLLECTING --> COLLECTING: GAINED / 清零 NO_GAIN + COLLECTING --> COLLECTING: NO_GAIN / 未达阈值 + COLLECTING --> SATURATED: 连续 NO_GAIN 达阈值 + COLLECTING --> SATURATED: 连续协议错误达阈值 + COLLECTING --> STOPPED: 硬预算到达 + SATURATED --> DRAFT_CHANCE: 单次 STOP_REQUIRED + DRAFT_CHANCE --> STOPPED: 再次请求 Tool + DRAFT_CHANCE --> RELEASE: 输出合法 Draft + STOPPED --> RELEASE: ProgressSnapshot +``` + +**为什么不用 `new_count`**:跨 RAG、日志和数据库建立统一内容指纹成本高,而且“新记录”不等于“对假设有价值”。 + +**为什么不用独立 Progress Judge**:它会增加模型成本和新的失败点,还会把简单的空结果、重复 scope 判断模型化。 + +**首版边界**:重复检测只比较规范化参数,不承诺识别自然语言语义等价查询。 + +### 5.7 决策七:`conclusion=null` 是合法完成,不是模型失败 + +**问题**:如果成功的唯一含义是“必须给出根因”,Agent 在缺少企业、时间范围或错误信息时只能继续盲查,或者编造结论。 + +**决策**:DiagnosisDraft 允许 `conclusion=null`: + +- 缺少开始查询所需信息:`MISSING_REQUIRED_CONTEXT`; +- 已完成有限检查但证据不足:`INSUFFICIENT_EVIDENCE`; +- 有结论:才进入完整 EvidenceGuard、Repair、SemanticGuard 链。 + +最终公开生命周期仍只有 `SUCCESS / FALLBACK / FAILED / CANCELLED`。证据不足是 `FALLBACK` 的原因,不再引入一套与 Run 终态平行的诊断状态机。 + +如果模型输出非法 Draft,系统会丢弃非法正文;只有当前 Run 已存在可验真的 ProgressSnapshot,才允许降级成过程型 Fallback,否则保持 fail closed。 + +### 5.8 决策八:可观测性记录决策证据,不复制敏感上下文 + +**问题**:为了调试 Agent,最直接的做法是保存 Prompt、模型正文、Tool arguments 和 raw response。但这会让普通 Trace 变成敏感数据仓库,也会造成多份事实副本。 + +**决策**: + +- `diagnosis_trace_event` 只保存追加式、按 exact Run 排序的事件和有界 metadata; +- `ToolInvocation` 长期保存 Tool 身份、状态、耗时和字节数,不保存完整请求/响应; +- `AgentStep` 保存步骤元数据和 Token,不保存 Prompt、Tool payload; +- Provider reasoning 与 assistant text 放入独立受限审计表和接口; +- Trace 写入失败不得改变业务结果; +- Model usage 按组件和轮次入账,Run 结束做 Token 对账; +- Tool 请求被 Harness 拒绝时记录 `TOOL_REQUEST_REJECTED`,不能伪装成一次真实 `TOOL_INVOCATION`。 + +**尚未完成的治理**:Reasoning 的访问控制、保留期限和加密仍由 ISS-015 跟踪。已有隔离不代表完整合规闭环。 + +## 6. 一次诊断的完整控制流程 + +```mermaid +sequenceDiagram + participant U as Client + participant App as Chat Application + participant Core as Harness Core + participant Agent as Diagnosis Agent + participant TI as Tool Interceptor + participant TB as ToolBoundary + participant Store as Canonical Store + participant Guard as Guards + participant Release as Release + + U->>App: query + optional sessionId + App->>Core: startRun(sessionId) + Core-->>App: RunContext(runId, deadline, handles) + App->>Agent: query + bounded safe previousTurn + + loop ReAct 由框架拥有 + Agent->>TI: Tool Call Envelope + TI->>TI: 应用上一轮信息增益、检查重复/饱和/协议 + alt 门禁允许 + TI->>TB: framework id + business input + RunContext + TB->>TB: active / auth / readonly / budget / size + TB->>Store: PROJECTING -> READY or ERROR + TB-->>TI: bounded canonical agent_result + TI-->>Agent: whitelist observation + else 信息饱和或协议停止 + TI-->>Agent: one-shot STOP_REQUIRED + end + end + + Agent-->>App: DiagnosisDraft 或受控停止 + App->>Release: Draft + ProgressSnapshot + stop reason + alt 有结论 + Release->>Guard: evidence truth + semantic support + Guard-->>Release: verified / unsupported + else 无结论或受控停止 + Release->>Release: 构造确定性 Fallback + end + Release-->>App: original safe Draft or SafeFallback + App->>Core: first terminal wins + App-->>U: content/failure + done +``` + +## 7. 真实问题如何反向修正设计 + +这些不是零散 Bug 清单。每个问题都暴露了一个原设计假设不成立,并促成了边界调整。 + +| 现场问题 | 被证伪的假设 | 设计修正 | 固化位置 | +|---|---|---|---| +| Spring AI 默认 10 次 retry | 一次 Harness 模型调用等于一次 Provider attempt | SDK retry=1,retry 所有权上移 | Core / Retry / 配置测试 | +| ThreadLocal 跨异步边界不稳定 | 同线程上下文足以表示 Run 所有权 | 显式 `RunContext` 贯穿调用链 | Core / framework metadata | +| Tool raw 直接进入 Agent | Tool 返回可同时服务推理、验真和审计 | canonical / control / observation 三层拆分 | ToolBoundary / Projector / Store | +| 0 条日志被表达为 `success=false` | 空结果等于技术失败 | 技术执行与 `NO_EVIDENCE` 分离 | Tool contract / Projector | +| `REFERENCE` 被当作诊断证据 | 非空候选等于支持结论 | 保留相关度,语义增益交给模型 | RAG projector / Progress | +| Redis canonical 全部 `STORE_ERROR` | 单测 ObjectMapper 与生产配置行为一致 | 使用生产 ObjectMapper 能力并增加 live E2E | Store wiring / E2E | +| raw/rewritten query 出现在日志 | 可观测性可以直接打印检索输入 | 普通日志和 Trace 仅保存安全 metadata | Audit boundary | +| 空查反复改写直到预算耗尽 | 硬预算可以承担正常收敛 | 引入信息增益和饱和停止 | ProgressTracker / Interceptor | +| 9 次协议拒绝仍消耗 13 轮模型 | 协议错误会被模型自然修正 | 独立协议错误阈值和一次 STOP_REQUIRED | Progress protocol | +| 非法 Draft 导致已完成检查全部丢失 | Draft 失败意味着整个 Run 没有安全价值 | 仅在已有验真进展时发布过程型 Fallback | ProgressSnapshot / Release | + +## 8. 关键决策总表 + +| 决策 | 选择 | 放弃的方案 | 获得的能力 | 付出的代价 | +|---|---|---|---|---| +| 推理拓扑 | 单 Diagnosis ReAct Agent | 多 Agent Graph、外层 ReAct | 上下文连续、唯一 Draft 作者 | Harness 门禁必须完整 | +| Run 上下文 | 显式 record + 状态句柄 | ThreadLocal、全局 Registry | 异步可证明、状态所有权清楚 | 参数需要显式传递 | +| Tool ID | framework `tool_call_id` | Harness 二次生成 ID | 引用链唯一 | 依赖框架 ID 契约 | +| Tool 真相 | Redis canonical,短 TTL | JPA 保存完整 raw | 可验真且限制敏感数据寿命 | Redis 可用性成为验证依赖 | +| Agent 输入 | 白名单 projection | raw / 完整 canonical 直传 | 上下文有界、减少泄露 | 需为每类 Tool 维护 projector | +| 长期审计 | metadata-only | 永久保存请求和响应 | 降低泄露与重复真相 | 深度回放受 TTL 限制 | +| 证据安全 | 确定性 EvidenceGuard + 隔离 SemanticGuard | 单一 Verifier Agent | 分清真实性和支持度 | 两段门禁增加延迟 | +| 修复 | 只修引用且语义必须不变 | Guard 重写报告 | 保持唯一作者 | 部分报告只能 Fallback | +| 停止 | 预算 + 信息增益双机制 | 只靠 Prompt 或硬上限 | 正常无证据收敛 | 首版只能做参数级重复判断 | +| 无结论 | 合法 Draft + SafeFallback | 强制根因 | 避免盲查和编造 | 调用方需理解 FALLBACK 是业务结果 | +| 发布 | 唯一 Release Policy | 各层自行 fallback | 对外语义一致 | Release 成为关键集中组件 | + +## 9. Harness 明确不做什么 + +边界是否清晰,既看它做什么,也看它拒绝做什么: + +- 不判断业务根因; +- 不实现 Planner / Executor / Verifier / Composer 角色图; +- 不在框架外复制 ReAct while-loop; +- 不让 Tool 声称结果是否支持诊断结论; +- 不把检索分数直接提升为结论可信度; +- 不依赖 Prompt 作为唯一预算或停止机制; +- 不自动重试 Diagnosis Agent 和业务 Tool; +- 不允许 Guard 生成新事实或改写结论; +- 不把 Redis canonical 变成永久审计库; +- 不承诺同步 Provider 调用一定能被立即物理中断; +- 不在首版做自然语言语义去重或跨 Tool 内容指纹。 + +这些非目标是在防止 Harness 再次长成一套不可维护的业务编排系统。 + +## 10. 如何验证设计成立 + +验证重点不是某个类是否被调用,而是上述不变量能否在失败和竞态下保持: + +| 验证层 | 需要证明的事实 | 代表性测试/证据 | +|---|---|---| +| Core 单测 | deadline、取消、预算、first-terminal-wins、异步显式 context | `RunContextTest`、`RunBudgetTest`、`DiagnosisHarnessCoreTest` | +| Tool 单测 | cross-run、重复 ID、只读、大小、状态迁移、投影 | `ToolBoundaryTest`、`CanonicalInvocationStoreTest`、各 Projector test | +| Agent loop | framework ID、Envelope、STOP_REQUIRED、非法 Draft | `DiagnosisAgentUseCaseTest`、`HarnessToolInterceptorTest` | +| Guard / Release | 引用闭包、Repair 语义不变、Unsupported Fallback | `EvidenceGuardTest`、`SemanticGuardTest`、`DiagnosisReleaseUseCaseTest` | +| Audit | metadata 边界、Token 对账、拒绝与执行分离 | audit 包 focused tests、exact-run Trace | +| Live E2E | 生产序列化、Redis、数据库、模型和 SSE 的组合契约 | `devflow/projects/2026-07-22-single-react-cleanup-e2e/`、ISS-016 evidence | + +单元测试能证明局部状态机,不能证明生产 `ObjectMapper`、Redis serializer、Provider Tool Calling 和 SSE 串联正确。因此 canonical store 的 Java Time 问题、零日志语义和协议空转都必须依靠 live E2E 反证,而不能只看 mock tests。 + +## 11. 当前边界与后续治理 + +当前设计已经建立可运行的确定性边界,但仍有明确限制: + +1. 重复检测是 `tool_name + normalized_scope` 的参数等价,不识别语义近似查询。 +2. Redis canonical 受 TTL 约束;TTL 过后只能依赖 metadata audit,不能重建完整证据正文。 +3. 同步模型请求的取消主要阻止后续边界和迟到发布,不承诺 Provider 已执行计算立即停止。 +4. SemanticGuard 仍是模型判断,只是被限制在无 Tool、无记忆、二值输出的最小范围内。 +5. Reasoning 已与普通 Trace 隔离,但访问控制、保留期限和加密仍需完成。 +6. 信息增益阈值默认为 2,仍需通过固定评测集持续校准,不能通过线上直觉随意调大。 + +## 12. 可复用的设计原则 + +这套 Harness 最值得复用的不是某个 Java 类,而是以下判断顺序: + +1. 先列出系统必须保证的确定性不变量,再决定组件。 +2. 将推理权留给模型,将授权、预算、归属、验真和发布权留给代码。 +3. 不让一个数据表示同时承担内部真相、模型上下文和长期审计。 +4. 不用资源耗尽代替业务收敛,也不用 Prompt 代替硬门禁。 +5. 不让验证器成为第二个作者;验证失败时降级,而不是偷偷改写。 +6. 把“没有足够证据”设计成一等业务结果,系统才不需要用幻觉换取成功率。 + +## 13. 代码与文档入口 + +- 完整组件说明:[Harness组件全景-职责-设计原因与边界.md](Harness组件全景-职责-设计原因与边界.md) +- 当前质量门禁:[harness-quality-gates.md](../../architecture/harness-quality-gates.md) +- Diagnosis Agent 架构:[agent-orchestration.md](../../architecture/agent-orchestration.md) +- 信息增益停止:[diagnosis-information-gain-stop-architecture.md](../../architecture/diagnosis-information-gain-stop-architecture.md) +- 核心重构 Issue:[ISS-014-single-react-agent-harness-aci-ptk-refactor.md](../../issues/archived/ISS-014-single-react-agent-harness-aci-ptk-refactor.md) +- 信息增益 Issue:[ISS-016-diagnosis-information-gain-stop-contract.md](../../issues/active/ISS-016-diagnosis-information-gain-stop-contract.md) diff --git a/mvp/engineering/harness/Harness证据安全链-从引用真实到结论可发布.md b/mvp/engineering/harness/Harness证据安全链-从引用真实到结论可发布.md new file mode 100644 index 0000000..de57dd9 --- /dev/null +++ b/mvp/engineering/harness/Harness证据安全链-从引用真实到结论可发布.md @@ -0,0 +1,276 @@ +# Harness 证据安全链:从引用真实到结论可发布 + +**更新日期**:2026-07-29 +**主题**:EvidenceGuard、EvidenceRepair、SemanticGuard 与 Diagnosis Release +**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md) +**前置阅读**:[Harness-Tool双视图-从原始结果到可验证证据.md](Harness-Tool双视图-从原始结果到可验证证据.md) + +## 1. 证据存在,不代表结论成立 + +诊断报告至少可能出现三类不同错误: + +1. **伪造引用**:Draft 引用了不存在、失败或属于其他 Run 的 Tool Call。 +2. **引用断裂**:analysis 有证据,但 conclusion、action plan 或 recommendation 没有绑定对应 analysis。 +3. **牵强推论**:引用和结构都真实,但证据并不足以支持所写根因。 + +例如,系统确实查到了一条支付超时日志,但报告把根因写成“数据库连接池耗尽”。此时 Tool Call 真实、日志真实、引用格式也正确,结论仍然不成立。 + +因此“报告是否安全”不能由一个笼统的 Verifier 回答。它至少包含两个不同命题: + +```text +P1:引用的证据是否真实、属于当前 Run,并形成结构闭包? +P2:这些真实证据是否足以支持报告中的结论? +``` + +P1 可以由代码确定性证明;P2 是语义判断。把两者都交给模型,会让本可机械验证的身份和状态也变成概率结果;全部交给规则,则规则代码会逐渐变成另一套业务诊断引擎。 + +## 2. 设计目标:验证链不能产生新事实 + +证据安全链遵守四个约束: + +- Diagnosis Agent 是唯一报告作者; +- EvidenceGuard 只做确定性验真; +- SemanticGuard 只做受限语义审查; +- Release 是唯一公开决策点。 + +任何 Guard 都不能调用业务 Tool、探索新证据或重写最终报告。否则验证链会变成第二条隐式 Agent 链,重新引入多 Agent 架构的问题。 + +```mermaid +flowchart LR + D["DiagnosisDraft
唯一业务 Draft"] --> R["DiagnosisReleaseUseCase
发布编排所有者"] + R --> E["EvidenceGuard
确定性引用验真"] + E -->|"invalid"| X["EvidenceRepair
只修结构/引用"] + X --> E2["EvidenceGuard Recheck"] + E -->|"valid"| V["VerifiedEvidenceSnapshot"] + E2 -->|"valid"| V + V --> S["SemanticGuard
SUPPORTED / UNSUPPORTED"] + S -->|"SUPPORTED"| OK["发布原始或等义修复 Draft"] + E -->|"repair 失败"| F["SafeFallback"] + E2 -->|"invalid"| F + S -->|"UNSUPPORTED / unavailable"| F +``` + +## 3. 第一层:EvidenceGuard 证明引用真实性 + +`EvidenceGuard` 不调用模型。它从当前 `RunContext` 出发,对 Draft 进行两阶段检查。 + +### 3.1 Draft 结构与引用闭包 + +首先检查报告自身结构: + +- Draft、analysis、analysis ID、kind 和正文是否存在; +- analysis ID 是否重复; +- 每条有结论分析是否绑定 Tool Call; +- conclusion、action plan 和 recommendation 是否绑定已存在的 analysis ID; +- limitations 是否存在并声明报告范围。 + +引用链的目标不是让 JSON 看起来完整,而是形成下面的闭包: + +```text +Conclusion / Action / Recommendation + | + v + Analysis Item + | + v + framework tool_call_id + | + v + Canonical READY Invocation +``` + +只要其中一跳缺失,公开报告就不能证明其来源。 + +### 3.2 Canonical Invocation 验真 + +对每个 `tool_call_id`,Guard 使用当前 `runId` 重新生成 canonical key,然后检查: + +1. ID 格式是否合法; +2. canonical record 是否存在; +3. record 内 ID 是否与引用一致; +4. record 是否属于当前 Run; +5. invocation 是否 READY; +6. analysis kind 是否接受该 evidence status; +7. Tool 类型是否受支持; +8. `agent_result` 是否能严格解析为对应 typed result; +9. typed result 中的 ID、状态、count 和集合是否自洽。 + +因此,即使模型猜中了另一个 Run 的 Tool Call ID,也无法通过当前 Run key 和 ownership 检查。 + +### 3.3 正向证据和负向观察不能混用 + +`AnalysisKind` 与 `EvidenceStatus` 的匹配是关键约束。 + +一条 READY + NO_EVIDENCE 日志结果,只能支持如下陈述: + +> 在企业 A、时间窗 T、服务 S、查询条件 Q 下没有匹配事件。 + +它不能支持: + +> 系统没有发生故障。 + +EvidenceGuard 会把空结果投影为带 source 和 scope 的负向观察,而不是把它提升为支持任意根因的正向证据。 + +## 4. VerifiedEvidenceSnapshot 为什么是必要中间产物 + +EvidenceGuard 验证通过后不会把 canonical raw 直接交给 SemanticGuard,而是生成 `VerifiedEvidenceSnapshot`。它保存: + +- analysis ID、正文和 kind; +- 已验证证据的 source type、source、scope、timestamp、excerpt; +- Tool-specific 的有限结构化 values; +- 去重后的 verified sources。 + +Snapshot 的作用是形成一条新的最小信任边界:SemanticGuard 不需要访问 Redis,也不能看到 request、raw response 或内部错误。它只判断“这组已经验真的事实是否支持 Draft”。 + +这也避免了语义审查阶段重新解释 backend 私有格式。 + +## 5. 第二层:EvidenceRepair 只允许修引用 + +### 5.1 为什么需要 Repair + +模型可能生成业务语义正确、但引用结构存在局部问题的 Draft,例如: + +- conclusion 漏写 `based_on_analysis_ids`; +- analysis 引用了错误的 Tool Call ID; +- 输出字段结构不符合严格 Schema。 + +如果所有结构错误都直接 Fallback,会丢掉部分可修复结果。但让 Repair 自由重写又会产生更严重的问题:最终结论不再来自 Diagnosis Agent,Repair 事实上成为第二个报告作者。 + +### 5.2 Repair 的不变量 + +`EvidenceRepair` 只获得: + +- 原始 query; +- 原始 Draft; +- EvidenceGuard 给出的 violations。 + +它没有 Tool,也不能获取新证据。修复输出必须再次严格解析,并满足: + +```text +SemanticDraftView(original) + == +SemanticDraftView(repaired) +``` + +即用户可见的 analysis、conclusion、action plan、recommendations 和 limitations 语义不变,只允许修复引用和结构。之后必须重新执行 EvidenceGuard,不能因为“已经 Repair”就跳过验真。 + +当前策略只给 EvidenceRepair 一次 attempt。Repair 失败、改变语义或复检仍不通过,直接进入 `EVIDENCE_VALIDATION_FAILED` Fallback。 + +## 6. 第三层:SemanticGuard 判断结论支持度 + +EvidenceGuard 证明的是“证据是真的”,SemanticGuard 判断的是“推论是否成立”。为了不让它变成第二个 Agent,系统主动削减了它的能力: + +| 约束 | 目的 | +|---|---| +| 无 Tool | 不能自行寻找新事实 | +| 无会话记忆 | 不能引入当前输入之外的信息 | +| 单轮调用 | 不形成新的 ReAct loop | +| 输入固定 | 只接收 query、Draft 和 VerifiedEvidenceSnapshot | +| 输出严格 | 只能返回 `verdict + reason` | +| 二值 verdict | 只允许 `SUPPORTED / UNSUPPORTED` | +| 无发布权限 | 不能改写或直接返回用户报告 | + +技术超时、传输失败、解析失败和 Schema 非法可以按完全相同输入进行一次显式重试。仍失败时不假设“可能支持”,而是 fail closed,发布 `SEMANTIC_UNAVAILABLE` Fallback。 + +这里需要准确描述“确定性边界”:SemanticGuard 的语义判断仍然是概率性的;确定的是它的权限、输入、输出、成本、重试次数和失败后的发布行为。 + +## 7. Release 为什么必须是唯一发布入口 + +如果 Application、EvidenceGuard 和 SemanticGuard 都能构造公开结果,同一种失败会出现多个含义,甚至可能绕过前置校验。`DiagnosisReleaseUseCase` 集中处理以下分支: + +| 输入情况 | 验证路径 | 发布结果 | +|---|---|---| +| Draft 有 conclusion | EvidenceGuard -> 可选 Repair/Recheck -> SemanticGuard | 原 Draft/等义修复 Draft,或安全 Fallback | +| Draft 无 conclusion,有已验真进展 | 只验证其中已有 Tool 引用 | `INSUFFICIENT_EVIDENCE` | +| Draft 无 conclusion,无进展但声明缺失信息 | 检查引用边界 | `MISSING_REQUIRED_CONTEXT` | +| Harness 受控停止,有已验真进展 | ProgressSnapshot | `INSUFFICIENT_EVIDENCE` | +| Draft 非法,但有已验真进展 | 丢弃非法 Draft,使用 ProgressSnapshot | `INSUFFICIENT_EVIDENCE` | +| Draft 非法且没有安全进展 | 无可发布事实 | FAILED,保持 fail closed | + +有结论时,只有 `SUPPORTED` 可以发布 Draft。发布的是 Diagnosis Agent 原始 Draft,或者经证明用户可见语义相同的 Repair Draft,不让 SemanticGuard生成“更好的答案”。 + +## 8. `conclusion=null` 为什么不进入完整语义审查 + +当模型明确表示无法确认根因时,不存在需要验证的根因结论。继续调用 EvidenceRepair 或 SemanticGuard,不仅浪费 Token,还可能让验证模型反向补出一个原本不存在的结论。 + +因此无结论路径只做必要的引用真实性检查,然后根据安全进展决定: + +```text +有 verified progress + -> INSUFFICIENT_EVIDENCE + +没有 progress,但有 limitations.missing_info + -> MISSING_REQUIRED_CONTEXT + +两者都没有 + -> 不是合法业务结果,fail closed +``` + +这项设计把“没有找到根因”从模型失败中分离出来,但没有降低证据要求。 + +## 9. SafeFallback 不是通用错误文案 + +SafeFallback 是结构化、确定性的发布结果。它可以包含: + +- `verified_sources`:通过当前 Run 验真的来源; +- `observed_facts`:有界、去重后的事实或负向观察; +- `limitations`:为什么不能确认根因; +- `next_steps`:下一步需要补充的信息或检查; +- `validation_issues`:稳定违规码和目标字段; +- `failure_stage`:失败发生在收集、证据还是语义阶段。 + +它不能包含 Prompt、原始 Draft、raw Tool payload、内部异常或模型 reasoning。Fallback 不是“把错误吞掉”,而是只发布系统已经能够证明的部分。 + +## 10. 为什么不采用其他方案 + +### 10.1 单个 Verifier Agent + +优点是实现表面简单,缺点是把 ID、Run ownership、Schema 和业务支持度混在同一次模型判断中。本来可以 100% 用代码拒绝的跨 Run 引用,也会变成概率审查。 + +### 10.2 Guard 自动改写报告 + +可以提高表面成功率,但破坏唯一作者原则。Guard 为了“修好”结论,往往会引入新解释;此时必须重新验证新内容,最终形成循环。 + +### 10.3 只做引用存在检查 + +能阻止伪造 ID,却无法阻止“真实证据 + 错误根因”。这正是 SemanticGuard 存在的原因。 + +### 10.4 语义失败直接技术报错 + +丢失了已经验真的事实,也把“结论不受支持”错误表达为基础设施故障。SafeFallback 可以保留过程价值,同时拒绝发布根因。 + +## 11. 真实问题如何改变设计 + +| 问题 | 暴露的设计缺陷 | 修正 | +|---|---|---| +| 旧 Verifier 同时验引用、判断语义和改写答案 | 验证职责没有边界 | EvidenceGuard、SemanticGuard、Release 三段拆分 | +| Tool 结果面向开发者,含 raw 和重复正文 | Guard 与 Agent 没有独立事实面 | 先建立 canonical truth 和 verified snapshot | +| `NO_EVIDENCE` 被当成一般失败或正向证据 | 负向观察没有范围约束 | AnalysisKind 与 EvidenceStatus 匹配校验 | +| Repair 可能改变结论 | 修复者变成新的报告作者 | SemanticDraftView 前后等义检查 | +| 非法 Draft 使已完成 Tool 检查全部丢失 | Draft 是唯一可发布价值来源 | 只在 ProgressSnapshot 已验真时允许过程型 Fallback | +| SemanticGuard 技术失败时行为不一致 | 各层自行决定降级 | Release 统一生成 `SEMANTIC_UNAVAILABLE` | + +## 12. 代价与剩余风险 + +1. SemanticGuard 增加一次模型调用和延迟;这是语义安全与成本之间的明确取舍。 +2. Semantic verdict 不是形式化证明,仍可能误判;当前设计限制的是权限和失败影响,而不是宣称模型绝对正确。 +3. EvidenceGuard 需要理解每种 Tool 的 typed result;新增 Tool 必须同步增加验证规则。 +4. Repair 的语义等价由结构化视图定义,无法证明两个自然语言文本在所有解释下完全等价,因此 Repair 能力被刻意限制。 +5. Canonical record TTL 到期后无法重新完成完整验真,所以公开决策必须在当前 Run 内完成。 + +## 13. 如何验证 + +| 需要证明 | 代表性验证 | +|---|---| +| 缺失、重复、未知 analysis 引用被拒绝 | `EvidenceGuardTest` | +| cross-run、非 READY、ID/status 不一致被拒绝 | `EvidenceGuardTest` + canonical fixtures | +| NO_EVIDENCE 只能形成限定负向观察 | Evidence kind focused cases | +| Repair 不得改变用户可见语义 | `DiagnosisReleaseUseCaseTest`、Repair tests | +| Semantic 非法输出只有限重试并安全降级 | `SemanticGuardTest` | +| Guard 不改写报告,Release 只发布原 Draft 或 Fallback | `DiagnosisReleaseUseCaseTest` | +| 无结论、非法 Draft、受控停止正确映射 | `DiagnosisChatExecutorTest`、Release focused cases | +| exact-run Trace 能解释每一道门禁 | live E2E Timeline | + +## 14. 与前后链路的关系 + +证据安全链依赖 [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)提供独立 canonical truth;它解决的是“哪些内容能够发布”,不负责判断 Agent 是否还应该继续取证。正常收敛由[Harness信息增益停止-让无证据诊断正常收敛.md](Harness信息增益停止-让无证据诊断正常收敛.md)负责。 diff --git a/mvp/engineering/harness/README.md b/mvp/engineering/harness/README.md new file mode 100644 index 0000000..408f970 --- /dev/null +++ b/mvp/engineering/harness/README.md @@ -0,0 +1,77 @@ +# Harness:先从一次诊断请求开始 + +这不是 Harness 的完整说明,而是一页入门导读。 + +第一次阅读时,不需要记组件名,不需要看状态枚举,也不需要理解所有边界。先回答一个问题:**为什么 Diagnosis Agent 外面还需要一层 Harness?** + +## 1. 如果只有 Agent,会发生什么 + +假设用户问: + +> 为什么支付服务在 10:00 到 10:10 大量超时? + +Diagnosis Agent 会自己决定查什么、调用哪些 Tool、什么时候停止,并根据返回结果写出结论。这里有四个不能只靠 Prompt 解决的问题: + +- 它可能查询过多,耗尽时间和 Token; +- Tool 原始结果可能包含敏感或超大内容,不能直接交给模型; +- 它引用了某条日志,不代表这条日志真的来自本次查询; +- 它写出了一个看似合理的根因,不代表证据足以支持这个根因。 + +这些都不是“诊断能力”问题,而是**执行是否受控、结果是否可信**的问题。Harness 就是为此存在的。 + +## 2. Harness 在一次请求中做了什么 + +```mermaid +flowchart LR + Q["用户提出诊断问题"] --> R["给本次执行建立 Run
限制时间和资源"] + R --> A["Agent 分析问题
选择 Tool"] + A --> T["Harness 执行 Tool
保存事实,只给 Agent 安全视图"] + T --> A + A --> D["Agent 写出诊断草稿"] + D --> G["Harness 检查
引用是否真实、证据是否支持结论"] + G --> P["发布报告
或安全地说明证据不足"] +``` + +顺着这条线看,Harness 只做三类事情: + +1. **执行前设边界**:为本次请求建立身份、deadline、预算和取消能力。 +2. **执行中管事实**:所有 Tool 经过统一入口,完整事实由系统保管,模型只看到安全、有限的内容。 +3. **发布前做验证**:先验证引用,再判断证据是否支持结论;不满足条件就发布 Fallback,而不是让未经验证的结论出去。 + +Agent 仍然负责“问题的根因是什么”。Harness 不替 Agent 推理,它负责的是:**让这次推理有边界、有证据、能停止。** + +## 3. 先建立这个最小心智模型 + +```mermaid +flowchart TB + A["Diagnosis Agent
负责业务推理"] + H["Harness
负责确定性控制"] + U["用户最终看到的结果"] + + A -->|"提出 Tool 请求和诊断草稿"| H + H -->|"返回受控的 Tool 观察"| A + H -->|"验证通过后发布,失败则 Fallback"| U +``` + +到这里,第一次阅读就可以停下。只要能说清下面这句话,就已经抓住了主干: + +> Agent 决定如何诊断,Harness 决定这次诊断可以消耗什么、可以看到什么、何时必须停止,以及什么结果允许发布。 + +## 4. 需要时再往下读 + +不要按目录顺序阅读。遇到具体问题时,只进入对应文档: + +| 当你想知道 | 再阅读 | +|---|---| +| 想逐步认识 Harness 的各组组件 | [组件渐进式导读](components/README.md) | +| 为什么选这种控制边界,而不是工作流编排 | [Harness 主设计](Harness设计-非确定性Agent的确定性控制边界.md) | +| 一次请求从开始到结束经历什么 | [生命周期与状态](Harness生命周期与状态.md) | +| Tool 为什么要保存一份事实、给模型另一份视图 | [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md) | +| 如何阻止“引用是真的,但结论是错的” | [证据安全链](Harness证据安全链-从引用真实到结论可发布.md) | +| Agent 为什么不会无限查询 | [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md) | +| 某个类属于哪里、负责什么 | [组件全景](Harness组件全景-职责-设计原因与边界.md) | +| 某个名词或状态是什么意思 | [Context 词典](CONTEXT.md) | + +如果准备系统学习,建议先读组件渐进式导读的四篇文章。每次只读一篇,读到“先记住这些”就可以停止。 + +“组件全景”和“Context”都是参考手册,不需要从头读,也不需要背。 diff --git a/mvp/engineering/harness/components/01-运行控制-让一次请求有边界.md b/mvp/engineering/harness/components/01-运行控制-让一次请求有边界.md new file mode 100644 index 0000000..0f0adf2 --- /dev/null +++ b/mvp/engineering/harness/components/01-运行控制-让一次请求有边界.md @@ -0,0 +1,121 @@ +# 01 运行控制:让一次请求有边界 + +这一篇只回答一个问题:**一次诊断请求由谁负责到底?** + +## 1. 先看一个具体问题 + +用户发起支付超时诊断。请求开始后,Router 调了一次模型,Diagnosis Agent 又调用多轮模型和 Tool,SemanticGuard 最后还要调用一次模型。与此同时,客户端可能断开,某次调用可能超时,两个线程也可能同时报告成功和失败。 + +如果没有统一的运行控制,每个组件都会有自己的理解: + +- Controller 认为连接断开了,后台 Agent 却还在继续查询; +- Agent 认为还可以调用 Tool,Run 的总预算其实已经耗尽; +- 超时线程先写入失败,迟到的模型结果又把它覆盖为成功; +- SDK 在内部偷偷重试,系统无法解释多出来的延迟和 Token; +- 线上只看到最终失败,却不知道请求在哪一步、因为什么停止。 + +所以运行控制不是一个计时器,而是由几个职责不同的组件共同完成。 + +## 2. 四组组件怎样协作 + +```mermaid +sequenceDiagram + participant APP as Application + participant CORE as Core / RunContext + participant WORK as Agent、Tool、Guard + participant RETRY as Retry + participant AUDIT as Audit + + APP->>CORE: 创建 RunContext + CORE-->>APP: runId、deadline、budget、cancel、lifecycle + APP->>WORK: 显式传入 RunContext + WORK->>CORE: 每次消耗前检查 active 和预算 + WORK->>RETRY: 仅允许策略声明的技术重试 + RETRY->>AUDIT: 记录每个 attempt + WORK->>AUDIT: 记录模型、Tool 和阶段元数据 + APP->>CORE: 尝试写入最终终态 + CORE-->>APP: first-terminal-wins + APP->>AUDIT: 记录公开结果和预算对账 +``` + +第一次阅读可以把它们理解为: + +- **Application 是负责人**:组织一次请求从创建到公开结果。 +- **Core 是执行规则**:统一管理身份、deadline、预算、取消和唯一终态。 +- **Retry 是重做规则**:明确什么失败允许再试一次。 +- **Audit 是运行账本**:记录发生过什么,但不保存敏感正文。 + +## 3. Application:负责人,而不是推理者 + +Application 负责建立一次请求的应用边界:读取安全历史、创建 Run、判断 intent、调用对应执行分支、持久化结果,并把安全内容交给 SSE。 + +设计它的原因,是这些步骤必须由一个地方协调。如果散落在 Controller、Agent 和 Guard 中,会出现多个结果出口和不同的失败语义。 + +它不负责判断支付超时的根因,也不负责自己拼一份诊断 Fallback。它只负责把正确的执行组件按顺序接起来,并确保结果经过统一出口。 + +主要代码入口:`ChatApplicationUseCase`、`DiagnosisChatExecutor`、`KnowledgeQueryExecutor`、`SystemChatExecutor`、`ChatRunStore`。 + +## 4. Core:一次 Run 的共同规则 + +Core 创建 `RunContext`。这个上下文显式携带: + +```text +这是谁的请求:sessionId + runId +最晚执行到何时:deadline +还能消耗多少:RunBudget +是否要求停止:RunCancellation +最终如何结束:RunLifecycle +模型消耗如何对账:ModelCallLedger +信息收集是否仍有价值:DiagnosisProgressTracker +``` + +这里最重要的决策是**显式传递 RunContext**,而不是使用 ThreadLocal 或让每个组件自己查询全局状态。原因是模型和 Tool 可能跨线程运行;隐式上下文很容易丢失、串线,也很难在测试中证明归属关系。 + +`RunLifecycle` 使用 first-terminal-wins:第一个成功写入的终态不可被迟到结果覆盖。它解决的是并发一致性,不是公开结果的业务含义。 + +主要代码入口:`DiagnosisHarnessCore`、`RunContext`、`RunBudget`、`RunCancellation`、`RunLifecycle`。 + +## 5. Retry:不能让“再试一次”藏起来 + +重试会增加成本和延迟,也可能重复副作用,因此不能由 SDK、HTTP Client 和各组件各自决定。当前策略是: + +- Router 和 SemanticGuard 的特定技术失败最多尝试两次; +- Diagnosis Agent、业务 Tool 和 EvidenceRepair 不做隐藏自动重试; +- 取消、预算耗尽和协议错误不能被重试吞掉。 + +这里没有设计通用重试 DSL。首版只需要不可变的 `RetryPolicy`、稳定的失败分类和统一的 `HarnessRetryExecutor`,复杂配置系统反而会掩盖真实执行路径。 + +## 6. Audit:留下解释,而不是留下全部内容 + +线上排障需要知道:调用了哪个组件、用了多少 Token、Tool 是否完成、为什么停止、为什么发布 Fallback。但这不等于长期保存 Prompt、SQL、日志原文、Tool raw response 和模型 reasoning。 + +因此 Audit 长期保留的是有界元数据,完整 Tool 真相只在 canonical store 中短期存在。这个决策同时满足可回放和最小暴露原则。 + +主要代码入口:`DiagnosisTraceRecorder`、`TraceAuditEvents`、`ToolInvocationAuditSink`、`HarnessAgentAuditHook`、`ModelCallAuditor`。 + +## 7. 为什么没有合并成一个 RunManager + +把上述职责都放进一个 `RunManager` 看起来更简单,但它会同时知道 HTTP、路由、预算、模型、Tool、数据库和 SSE,最后变成新的业务工作流引擎。 + +当前拆分依据不是“代码越细越好”,而是决策权不同: + +| 组件 | 它拥有的决定 | 它不能决定 | +|---|---|---| +| Application | 请求走哪条应用分支、何时持久化和返回 | 业务根因是否成立 | +| Core | 是否仍可执行、资源是否允许、哪个终态生效 | 返回正常报告还是 Fallback | +| Retry | 某类技术失败是否允许下一 attempt | 修改业务结果 | +| Audit | 记录哪些安全元数据 | 影响执行结果 | + +## 8. 先记住这些 + +第一次阅读只需记住: + +1. Application 对一次请求负责,Core 对执行不变量负责。 +2. RunContext 必须显式传播,所有模型和 Tool 共用同一预算与取消信号。 +3. 第一个终态获胜,迟到结果不能翻案。 +4. Retry 必须显式、分类、可审计。 +5. Audit 记录控制事实,不长期复制敏感正文。 + +下一篇:[02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md)。 + +需要查完整状态转换时,阅读[生命周期与状态](../Harness生命周期与状态.md);需要查全部类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。 diff --git a/mvp/engineering/harness/components/02-Agent接入与收敛-让推理可以工作也可以停止.md b/mvp/engineering/harness/components/02-Agent接入与收敛-让推理可以工作也可以停止.md new file mode 100644 index 0000000..c181438 --- /dev/null +++ b/mvp/engineering/harness/components/02-Agent接入与收敛-让推理可以工作也可以停止.md @@ -0,0 +1,148 @@ +# 02 Agent 接入与收敛:让推理可以工作,也可以停止 + +这一篇只回答一个问题:**怎样保留 Agent 的自主推理,同时阻止它重复查询或无限空转?** + +## 1. 先看一个具体问题 + +Diagnosis Agent 第一次查询 10:00 到 10:10 的支付日志,没有发现异常;第二次换了关键词,仍然没有新信息;第三次又提交了与第一次等价的查询。 + +仅设置“最多调用 10 次 Tool”只能限制最坏损失,却回答不了: + +- 上一次结果有没有推进诊断? +- 这次查询是否已经做过? +- 连续多少次没有新信息后应该停止? +- Agent 已经收到停止要求,为什么还能继续调用 Tool? +- 停止后,怎样安全地向用户说明已经检查过什么? + +这就是 Agent 接入层和 Progress 组件共同解决的问题。 + +## 2. 一轮 ReAct 怎样经过 Harness + +```mermaid +sequenceDiagram + participant A as Diagnosis Agent + participant MI as Model Interceptor + participant TI as Tool Interceptor + participant P as Progress Tracker + participant T as Tool Boundary + + A->>MI: 发起一轮模型调用 + MI->>MI: 检查 Run、预留预算、记录 Token + A->>TI: 请求调用 Tool + TI->>P: 评价上一轮是否有信息增益 + P-->>TI: 可继续 / 重复 / 已饱和 / 协议错误 + alt 允许继续 + TI->>T: 执行业务 Tool + T-->>TI: Control View + Model Observation + TI->>P: 登记已完成调用和 scope + TI-->>A: 只返回 Model Observation + else 必须停止 + TI-->>A: STOP_REQUIRED + end +``` + +第一次阅读可以把它们理解为: + +- **Agent 接入层是检查站**:把框架原生 ReAct loop 接入预算、Tool 和审计边界。 +- **Progress Tracker 是收敛记录器**:判断是否重复、是否连续无增益、是否必须停止。 + +## 3. 为什么复用框架 ReAct,而不是自己重写循环 + +Diagnosis Agent 需要模型原生 Tool Calling 和多轮 ReAct。项目没有再写一套 `while` 循环,而是通过 Factory、Model Interceptor、Tool Interceptor 和 Audit Hook 接入框架。 + +这样做的决策依据是: + +- ReAct 的业务推理由框架和模型负责; +- 预算、Tool 授权、事实保存和停止协议由 Harness 负责; +- 两者通过明确边界连接,不互相复制实现。 + +如果 Harness 自己维护另一套 ReAct 状态机,就会同时出现“框架认为的下一步”和“Harness 认为的下一步”,调试时很难确定谁才是真理源。 + +主要代码入口:`DiagnosisAgentFactory`、`DiagnosisAgentUseCase`、`HarnessModelInterceptor`、`HarnessToolInterceptor`、`HarnessEvidenceTools`。 + +## 4. Model Interceptor:每一轮模型调用都必须记账 + +一个 Diagnosis Agent 执行不等于一次模型调用。ReAct 可能经历多轮思考和 Tool 返回,因此每一轮都要: + +1. 检查 Run 是否仍然 active; +2. 在调用前预留模型预算; +3. 调用后记录 Provider 返回的 Token usage; +4. 再次检查取消或迟到结果。 + +Interceptor 不负责重试模型,也不判断输出是否支持根因。它只确保框架内部的每轮调用无法绕过 Harness。 + +## 5. Tool Interceptor:先检查进展,再允许查询 + +模型提交的 Tool Call 不只是业务参数,还携带对上一轮结果的评价。Tool Interceptor 会依次检查: + +- previous observation 是否完整、顺序是否正确; +- 上一轮是 `GAINED` 还是 `NO_GAIN`; +- 当前 Tool scope 是否已经完成过; +- 收集状态是否已经 `SATURATED`; +- 通过检查后,才把业务请求交给 ToolBoundary。 + +它不执行后端查询,也不再次扣 Tool 预算。实际执行属于 ToolBoundary;收敛状态属于 ProgressTracker。Interceptor 只是两者与 ReAct 框架之间的接合点。 + +## 6. 为什么“有结果”不等于“有信息增益” + +Tool 可以客观判断是否返回候选内容,却不知道这些内容是否推进了当前假设。例如同一条超时日志再次出现: + +```text +InvocationStatus = READY +EvidenceStatus = EVIDENCE_FOUND +InformationGain = NO_GAIN +``` + +前三个状态回答不同问题:查询是否完成、是否有候选内容、内容是否推进当前诊断。把它们合成一个 `SUCCESS` 会让系统无法正常收敛。 + +当前由 Agent 在**下一次 Tool Call** 中评价上一轮的信息增益。这样评价发生在它真正做出下一步行动时,Harness 也能机械检查调用顺序,而不需要再增加一个 Progress Judge 模型。 + +## 7. Progress Tracker 保存什么 + +Tracker 只保存控制所需的最小状态: + +- 已完成的 `tool_call_id` 和规范化 scope; +- 哪个结果仍等待 Agent 评价; +- 连续 `NO_GAIN` 次数; +- 收集状态和停止原因; +- 停止指令是否已经交付。 + +它不保存 raw response,也不判断日志是否证明支付线程池耗尽。业务价值判断仍由 Diagnosis Agent 负责,事实内容仍由 canonical store 负责。 + +主要代码入口:`DiagnosisProgressTracker`、`ToolScopeNormalizer`、`DiagnosisProgressProjector`。 + +## 8. 为什么停止不直接等于失败 + +连续无增益后停止,是一次正常的受控收敛,不是技术异常。Harness 会从已经完成的 canonical Tool 记录中投影 `ProgressSnapshot`,只保留可验真的检查范围、观察事实和限制,再形成安全 Fallback。 + +```mermaid +flowchart LR + N["连续 NO_GAIN 或重复查询"] --> S["CollectionState = SATURATED"] + S --> X["拒绝新的证据 Tool"] + X --> P["生成安全 ProgressSnapshot"] + P --> F["发布证据不足的 Fallback"] +``` + +因此,“没有找到足够证据”可以正常结束;只有违反进展协议、预算耗尽且没有安全进展等情况,才可能升级为失败。 + +## 9. 为什么没有采用其他方案 + +| 方案 | 没有采用的原因 | +|---|---| +| 只设 Tool 次数上限 | 只能止损,不能识别查询已经没有价值 | +| Harness 根据结果条数判断增益 | 条数是客观统计,不代表是否推进业务假设 | +| 增加 Progress Judge Agent | 增加模型成本和新的非确定性判断点 | +| 用自然语言相似度判断重复 | 首版难以稳定解释误判,当前只做 typed 参数级 scope 规范化 | +| 把剩余预算告诉模型 | 容易让模型围绕阈值博弈,硬限制应由 Harness 保持 | + +## 10. 先记住这些 + +1. 框架负责 ReAct loop,Harness 通过 Interceptor 接入控制能力。 +2. 预算回答“还能不能查”,信息增益回答“继续查有没有价值”。 +3. Tool 是否有结果与结果是否推进诊断是两件事。 +4. ProgressTracker 只保存控制状态,不保存完整证据。 +5. 信息饱和可以正常发布 Fallback,不等于 Run 执行失败。 + +下一篇:[03 Tool 事实边界](03-Tool事实边界-让模型看到必要信息而系统保留真相.md)。 + +需要深入停止协议时,阅读[信息增益停止专题](../Harness信息增益停止-让无证据诊断正常收敛.md)。 diff --git a/mvp/engineering/harness/components/03-Tool事实边界-让模型看到必要信息而系统保留真相.md b/mvp/engineering/harness/components/03-Tool事实边界-让模型看到必要信息而系统保留真相.md new file mode 100644 index 0000000..3a7b322 --- /dev/null +++ b/mvp/engineering/harness/components/03-Tool事实边界-让模型看到必要信息而系统保留真相.md @@ -0,0 +1,135 @@ +# 03 Tool 事实边界:让模型看到必要信息,系统保留真相 + +这一篇只回答一个问题:**Tool 返回的数据应该由谁保管,模型究竟可以看到多少?** + +## 1. 先看一个具体问题 + +Agent 查询支付日志,后端一次返回了几千行内容,其中包含重复堆栈、敏感字段和远超上下文窗口的正文。 + +直接把原始结果塞回模型会产生几个问题: + +- 敏感数据进入模型上下文; +- 大结果挤占 Token,真正关键的证据反而被淹没; +- 模型后续引用经过裁剪的文本,系统无法证明它对应哪次真实调用; +- 若只保存裁剪结果,EvidenceGuard 又失去了独立验真的事实来源; +- 若把完整 raw 长期写入数据库,泄露面和存储成本都会扩大。 + +所以 Tool 设计的核心不是“怎样调用后端”,而是**谁拥有原始事实,以及不同消费者应该看到哪一层数据**。 + +## 2. 一次 Tool 调用的数据怎样变化 + +```mermaid +flowchart LR + REQ["Typed Tool Request"] --> B["ToolBoundary
权限、只读、预算、大小检查"] + B --> RAW["Backend Raw Result"] + RAW --> P["Tool-specific Projector"] + P --> C["Canonical Invocation
短期保存 request、raw、agent_result 和状态"] + C --> CV["Control View
供 Harness 判断状态和进展"] + C --> MO["Model Observation
供 Agent 推理的白名单内容"] + C --> EG["EvidenceGuard
按当前 Run 独立验真"] + B -.-> AU["Durable Audit
长期只留有界元数据"] +``` + +第一次阅读只需区分三份内容: + +- **Canonical Invocation**:系统在当前 Run 内短期保管的完整调用真相。 +- **Control View**:Harness 用来判断状态、scope 和证据数量的控制字段。 +- **Model Observation**:模型真正看到的安全、有限内容。 + +## 3. ToolBoundary:所有 Tool 的统一入口 + +RAG、日志和 MySQL 后端完全不同,但它们都必须遵守相同的不变量: + +1. Run 仍然 active,调用 ID 与当前 Run 匹配; +2. Tool 已授权,请求声明和实际行为保持只读; +3. 调用前预算允许,结果大小没有越界; +4. canonical 状态只能从 `PROJECTING` 进入 `READY` 或 `ERROR`; +5. 长期 Audit 不保存完整请求和 raw response。 + +如果每个 Adapter 自己实现这些规则,三种 Tool 很快会出现不同的错误码、预算顺序和存储行为。ToolBoundary 集中处理共同规则,Adapter 只处理业务协议。 + +ToolBoundary 不判断信息增益,也不判断某条日志是否支持根因。它只保证调用合法、状态可信、结果有界。 + +主要代码入口:`ToolBoundary`、`ToolCallRequestEnvelope`、`ToolBoundaryResult`、`ToolBoundaryErrorCode`。 + +## 4. Projector:把后端结果变成稳定事实 + +不同后端的 raw 数据不能直接成为 Agent 契约: + +- RAG 需要限制证据数量和摘录长度,并保留文档身份; +- Logs 需要限制事件、聚合 pattern,明确实际时间范围和是否截断; +- MySQL 需要限制行、列、单元格和总 bytes,并保留查询 scope。 + +每种 Tool 使用自己的 `ToolResultProjector`。Projector 负责标准化和计算客观 `EvidenceStatus`,但它不调用模型,也不判断这些事实是否足以支持最终结论。 + +这是刻意保留的业务差异。强行做一个“万能 JSON Cleaner”,往往会丢掉每类证据真正需要的身份和范围信息。 + +## 5. Canonical Store:为什么要保留独立真相 + +Agent Observation 是为推理优化的,它会被裁剪和白名单投影,不能反过来充当验真依据。Canonical Store 保存当前调用的 request、raw、标准化 `agent_result`、状态和时间,使 EvidenceGuard 能绕开 Agent 上下文独立读取事实。 + +当前选择 Redis TTL,是因为完整调用记录: + +- 只在当前 Run 的验证阶段需要; +- 可能包含敏感内容,不应永久保存; +- 需要按 `runId + tool_call_id` 快速定位; +- 容量必须有硬上限,读取不能自动续期。 + +raw 超限时选择失败,而不是悄悄截断。因为一旦截断,canonical record 就不再代表后端真实返回,后续验真会建立在不完整事实之上。 + +主要代码入口:`CanonicalInvocationStore`、`RedisCanonicalInvocationStore`、`CanonicalToolInvocation`、`CanonicalInvocationLimits`。 + +## 6. Model Observation:模型只拿完成任务所需的内容 + +即使 canonical `agent_result` 已经标准化,其中仍可能包含 Harness 控制字段。`ToolResultViewProjector` 会进一步拆成: + +```text +Control View:status、evidence count、scope、截断状态等 +Model Observation:有界证据正文、可读来源和下一步推理所需字段 +``` + +模型不会看到 raw response、Redis key、预算阈值、重复指纹和完整 Harness 状态。这样既减少上下文噪声,也避免模型利用或复述内部控制信息。 + +## 7. MySQL 为什么还需要单独的只读沙箱 + +`readOnly=true` 只是请求声明,不能证明模型生成的 SQL 安全。MySQL Tool 在进入真实执行前还要经过: + +- JSqlParser AST 解析; +- 单条 SELECT 和保守语法子集限制; +- 数据源、schema、table、column 精确 allowlist; +- 禁止投影通配符和元数据探测; +- 只读账号、timeout、LIMIT 与最大行数。 + +Validator 产生已经批准的 `MysqlQueryPlan`,Executor 只接受这个 plan,不重新信任原始字符串。这是纵深防御:任何一层都不能单独被当作完整授权。 + +## 8. 为什么短期真相和长期审计要分开 + +```mermaid +flowchart TB + C["Canonical Store
完整、敏感、短期"] -->|"用于"| V["当前 Run 验真"] + A["Durable Audit
有界、元数据、长期"] -->|"用于"| O["排障、统计、成本对账"] +``` + +把两者合并会走向两个极端:要么长期复制所有敏感正文,要么为了安全只存元数据,导致当前 Run 无法验真。分开后,每种存储只承担自己的责任。 + +## 9. 为什么没有采用其他方案 + +| 方案 | 没有采用的原因 | +|---|---| +| raw 直接返回 Agent | 泄露、超限、噪声和无法独立验真 | +| 只保存 Agent Observation | 投影内容不是完整事实,Guard 会依赖模型看到的版本 | +| 完整 raw 永久写 MySQL | 扩大敏感数据暴露面和长期存储成本 | +| raw 超限后静默截断 | canonical truth 会变成不完整真相 | +| 所有 Tool 共用万能 Projector | 无法保持日志、RAG、表格各自的身份和 scope 语义 | + +## 10. 先记住这些 + +1. ToolBoundary 统一执行规则,Adapter 处理具体后端。 +2. Canonical Invocation 是系统真相,Model Observation 是模型视图。 +3. Agent 看到的内容不能反过来成为 EvidenceGuard 的事实来源。 +4. 完整真相短期保存,长期 Audit 只留有界元数据。 +5. Tool 找到候选内容,不代表它支持最终根因。 + +下一篇:[04 验证与发布](04-验证与发布-让未经证明的结论无法越过出口.md)。 + +需要深入字段和存储取舍时,阅读[Tool 双视图专题](../Harness-Tool双视图-从原始结果到可验证证据.md)。 diff --git a/mvp/engineering/harness/components/04-验证与发布-让未经证明的结论无法越过出口.md b/mvp/engineering/harness/components/04-验证与发布-让未经证明的结论无法越过出口.md new file mode 100644 index 0000000..15e4ed9 --- /dev/null +++ b/mvp/engineering/harness/components/04-验证与发布-让未经证明的结论无法越过出口.md @@ -0,0 +1,162 @@ +# 04 验证与发布:让未经证明的结论无法越过出口 + +这一篇只回答一个问题:**Agent 写完诊断草稿以后,为什么还不能直接返回给用户?** + +## 1. 先看一个具体问题 + +Agent 在报告中写道: + +> 10:03 出现数据库连接池耗尽,因此支付接口大量超时。 + +它同时引用了一次日志 Tool Call。系统即使确认这个调用真实存在、属于当前 Run,而且日志中确实出现“connection timeout”,仍然不能直接发布上述结论。 + +因为这里至少有三个不同问题: + +1. Agent 引用的 Tool Call 是不是真的? +2. 真实日志是否足以证明“数据库连接池耗尽导致支付超时”? +3. 验证失败后,系统最终应该向用户发布什么? + +它们分别属于 EvidenceGuard、SemanticGuard 和 Release。 + +## 2. 草稿怎样通过发布链 + +```mermaid +flowchart LR + D["DiagnosisDraft
Agent 写出的草稿"] --> E["EvidenceGuard
机械验证引用和归属"] + E -->|"结构或引用可修复"| R["EvidenceRepair
只修引用,不改语义"] + R --> E + E -->|"得到已验真证据"| S["SemanticGuard
判断证据是否支持报告"] + S -->|"SUPPORTED"| P["Release SUCCESS
发布原始安全 Draft"] + E -->|"无法验真"| F["SafeFallback"] + S -->|"UNSUPPORTED 或不可用"| F + F --> O["Release FALLBACK"] +``` + +第一次阅读只需记住: + +- **EvidenceGuard 验真**:证明引用确实来自当前 Run 的已完成 Tool 调用。 +- **SemanticGuard 验义**:判断这些真实证据是否支持报告中的结论。 +- **Release 决定出口**:只发布验证通过的原 Draft,或者发布确定性的 SafeFallback。 + +## 3. EvidenceGuard:代码能证明的事交给代码 + +EvidenceGuard 会检查: + +- Draft 结构和 analysis ID 是否合法; +- 所有结论分析是否形成完整引用闭包; +- `tool_call_id` 是否属于当前 Run; +- canonical invocation 是否为 `READY`; +- `EvidenceStatus` 与引用类型是否一致; +- 引用的 evidence 是否确实存在于标准化 Tool 结果中。 + +这些都是确定性问题,不需要模型判断。让模型验证自己的引用,会让同一个非确定性来源同时当作者和裁判。 + +验证通过后,EvidenceGuard 生成 `VerifiedEvidenceSnapshot`。它只包含 SemanticGuard 所需的已验真证据、来源和 scope,不包含 Tool raw response。 + +主要代码入口:`EvidenceGuard`、`EvidenceGuardResult`、`VerifiedEvidenceSnapshot`、`EvidenceViolation`。 + +## 4. EvidenceRepair:为什么允许修,又为什么只能修一次 + +有些 Draft 的业务语义可能没有问题,只是引用结构出错,例如漏写一个 analysis ID。直接丢弃会浪费一次昂贵诊断,因此系统允许一次 EvidenceRepair。 + +但 Repair 不是第二个报告作者。它必须满足: + +```text +修复前用户可见语义 == 修复后用户可见语义 +``` + +它只能修结构和引用,不能新增事实、改变结论或补写建议;修复后还必须重新经过 EvidenceGuard。若语义视图发生变化,Repair 立即失败。 + +只允许一次,是为了避免形成“修复失败再修复”的隐式 Agent loop,也让成本和执行路径保持可解释。 + +## 5. SemanticGuard:引用真实不等于推论成立 + +一条真实的 `connection timeout` 日志,可能来自下游网络问题,也可能只是故障结果,不能自动证明数据库连接池耗尽。 + +这类支持关系无法完全用规则判断,因此 SemanticGuard 使用一次隔离的模型调用,只接收: + +- 用户原始问题; +- Draft 的用户可见语义; +- EvidenceGuard 产生的 verified snapshot。 + +它没有 Tool、没有记忆、没有 ReAct loop、不访问 Redis,也不能改写报告。输出只有 `SUPPORTED / UNSUPPORTED` 和审计原因。 + +这个设计没有追求一个看似精确的置信度分数。首版真正需要的是发布门禁,而未经校准的 0.73 并不能形成比二元判定更可靠的协议。 + +主要代码入口:`SemanticGuard`、`SemanticGuardInput`、`SemanticGuardDecision`、`GuardModelCall`。 + +## 6. Release:为什么必须只有一个出口 + +如果 Agent、Guard、Application 都能各自构造最终结果,会出现: + +- 同一验证失败被不同层解释成不同文案; +- 某个分支忘记经过 SemanticGuard; +- 迟到的 Draft 绕过已经确定的取消或失败; +- Fallback 混入未经验证的 Agent 内容。 + +`DiagnosisReleaseUseCase` 因此拥有唯一发布决策。它协调 EvidenceGuard、可选 Repair、SemanticGuard 和 SafeFallbackFactory,但自己不写新根因。 + +```mermaid +flowchart TB + V{"可以安全发布正常报告吗?"} + V -->|"引用真实且语义受支持"| OK["ReleaseOutcome.SUCCESS
发布原 Draft"] + V -->|"证据不足或验证不通过"| FB["ReleaseOutcome.FALLBACK
发布 SafeFallback"] + V -->|"无法形成安全业务结果"| ER["ReleaseOutcome.FAILED"] +``` + +这里必须区分: + +```text +RunState.SUCCESS + ReleaseOutcome.FALLBACK +``` + +这表示系统完整、安全地处理了请求,但证据不足以发布正常诊断结论。Fallback 是安全产品结果,不等于执行失败。 + +## 7. SafeFallback:不是让另一个模型重新回答 + +验证失败后再次让模型“写得保守一点”,仍然可能产生新事实。SafeFallback 因此由代码确定性构造,只允许使用: + +- 已验真的来源和检查范围; +- 可以安全表达的观察事实; +- 当前证据的限制; +- 面向补充数据的下一步建议; +- 稳定、可公开的问题类型。 + +它不会包含 Agent 原 Draft 的未验证结论、SemanticGuard 内部原因、Provider 错误或异常栈。 + +主要代码入口:`DiagnosisReleaseUseCase`、`EvidenceRepair`、`SafeFallbackFactory`、`DiagnosisReleaseResult`。 + +## 8. Contract:为什么状态和结果必须类型化 + +发布链横跨模型 JSON、Java、Redis、数据库和 SSE。如果各层都使用自由字符串 `SUCCESS`,很快就无法区分: + +- Tool 调用完成; +- Tool 找到候选证据; +- 语义审查通过; +- Run 执行成功; +- 最终发布正常报告。 + +Contract 包用不同类型保持这些问题正交,例如 `InvocationStatus`、`EvidenceStatus`、`SemanticVerdict`、`RunState` 和 `ReleaseOutcome`。类型多不是为了复杂,而是为了阻止一个含糊的 `status` 穿过整个系统。 + +## 9. 为什么没有采用其他方案 + +| 方案 | 没有采用的原因 | +|---|---| +| 只检查 Tool Call ID 存在 | 只能证明引用存在,不能证明归属、状态和内容一致 | +| 一个 Verifier Agent 同时验引用和语义 | 混合确定性与非确定性判断,失败后难以定位责任 | +| Guard 自动改写报告 | Guard 会变成第二个作者,并可能引入未验证事实 | +| SemanticGuard 使用 Tool 再查证 | 会形成第二条诊断链,预算和证据归属变复杂 | +| 验证失败直接抛技术错误 | 证据不足是正常业务结果,用户仍需要安全说明 | +| 用置信度阈值发布 | 未校准分数不能充当可靠安全协议 | + +## 10. 先记住这些 + +1. Draft 是候选结果,不是已发布报告。 +2. EvidenceGuard 用代码证明引用真实,SemanticGuard 判断证据是否支持语义。 +3. Repair 只能修引用,不能改变用户可见语义,而且只尝试一次。 +4. Release 是唯一出口,只发布原 Draft 或确定性 SafeFallback。 +5. `FALLBACK` 可以对应一次正常完成的 Run。 + +四篇组件导读到这里结束。需要回看整体路径时返回[组件学习地图](README.md)。 + +需要深入验证规则时,阅读[证据安全链专题](../Harness证据安全链-从引用真实到结论可发布.md);需要查所有 contract 类型时,阅读[组件全景](../Harness组件全景-职责-设计原因与边界.md)。 diff --git a/mvp/engineering/harness/components/README.md b/mvp/engineering/harness/components/README.md new file mode 100644 index 0000000..9ef19da --- /dev/null +++ b/mvp/engineering/harness/components/README.md @@ -0,0 +1,22 @@ +# Harness 组件:从一次请求逐步认识 + +这里不按照 Java 包逐个介绍组件,而是沿着一次诊断请求,分四步回答四个问题。 + +```mermaid +flowchart LR + Q["一个诊断请求来了"] --> C1["01 运行控制
怎样保证这次执行受控"] + C1 --> C2["02 Agent 接入与收敛
怎样让 Agent 工作但不空转"] + C2 --> C3["03 Tool 事实边界
怎样安全地取得事实"] + C3 --> C4["04 验证与发布
怎样决定结果能否交给用户"] +``` + +| 顺序 | 先回答的问题 | 涉及的职责域 | +|---|---|---| +| [01 运行控制](01-运行控制-让一次请求有边界.md) | 谁创建 Run,谁限制资源,谁记录它如何结束? | Application、Core、Retry、Audit | +| [02 Agent 接入与收敛](02-Agent接入与收敛-让推理可以工作也可以停止.md) | 怎样使用框架 ReAct,同时阻止重复查询和无增益空转? | Agent、Progress | +| [03 Tool 事实边界](03-Tool事实边界-让模型看到必要信息而系统保留真相.md) | Tool 原始结果由谁保存,模型究竟能看到什么? | Tool | +| [04 验证与发布](04-验证与发布-让未经证明的结论无法越过出口.md) | 引用真实是否等于结论成立,最终由谁决定发布? | Guard、Release、Contract | + +建议一次只读一篇。每篇读到“先记住这些”就可以停下;类名只在最后用于定位代码。 + +需要查全部生产类型时,再使用[组件全景参考手册](../Harness组件全景-职责-设计原因与边界.md)。