docs(mvp): add harness design and progressive guides

This commit is contained in:
zhuyongxin
2026-07-29 19:04:44 +08:00
parent 584639fa2a
commit 3a7eee8af4
15 changed files with 3550 additions and 0 deletions
+17
View File
@@ -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 |
架构对照:
@@ -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<br/>query A / steps A / tools A"] --> SID["同一个 sessionId"]
R2["Round 2<br/>query B / steps B / tools B"] --> SID
SID --> Main["diagnosis_session<br/>只剩 query B / answer B"]
SID --> Steps["agent_step<br/>steps A + steps B"]
SID --> Tools["tool_invocation<br/>tools A + tools B"]
Main --> Trace["混合 Trace"]
Steps --> Trace
Tools --> Trace
Trace --> Error["无法回答:<br/>哪组证据支持了哪次回答?"]
```
这个错误会沿数据链继续放大:
| 消费方 | 错误结果 |
|---|---|
| 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<br/>sessionId:多轮对话目录"]
Session --> Run1["diagnosis_run<br/>runId 1:一次执行"]
Session --> Run2["diagnosis_run<br/>runId 2:另一次执行"]
Run2 --> Step["agent_step<br/>模型步骤 metadata"]
Run2 --> Tool["tool_invocation<br/>Tool 审计 metadata"]
Run2 --> Event["diagnosis_trace_event<br/>生命周期 Timeline"]
Run2 --> Reasoning["agent_reasoning_audit<br/>受限原文"]
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)
+378
View File
@@ -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<br/>sessionId,多轮容器"] -->|"1:N"| R1["Run<br/>runId,一次请求"]
S -->|"1:N"| R2["Run<br/>下一次请求"]
R1 -->|"1:N"| AS["Agent Step<br/>一次模型步骤"]
R1 -->|"1:N"| TC["Tool Call / Invocation<br/>framework tool_call_id"]
R1 -->|"1:N"| TE["Trace Event<br/>sequence_no"]
TC --> CI["Canonical Invocation<br/>Run 内短期 Tool 真相"]
AS --> TR["Diagnosis Trace<br/>按 exact Run 聚合"]
TC --> TR
TE --> TR
R1 -.->|"SUCCESS diagnosis only"| PT["PublishedResult<br/>可投影为下一轮 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<br/>RUNNING -> terminal"]
CS["CollectionState<br/>COLLECTING / SATURATED"]
SR["StopReason<br/>停止收集原因"]
end
subgraph Tool["单次 Tool 维度"]
IS["InvocationStatus<br/>PROJECTING / READY / ERROR"]
ES["EvidenceStatus<br/>FOUND / NO_EVIDENCE / ERROR"]
IG["InformationGain<br/>GAINED / NO_GAIN"]
end
subgraph Validation["验证维度"]
EV["EvidenceGuardResult<br/>valid / violations"]
SV["SemanticVerdict<br/>SUPPORTED / UNSUPPORTED"]
end
subgraph Publication["发布与观察维度"]
RO["ReleaseOutcome<br/>SUCCESS / FALLBACK / FAILED / CANCELLED"]
FT["FallbackType<br/>FALLBACK 原因"]
SSE["SSE Session State<br/>连接发送状态"]
DB["diagnosis_run.status<br/>持久化通用状态"]
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) 建立最小心智模型;需要理解某个状态或概念时,再回到本文对应章节查询。
@@ -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<br/>request + raw + agent_result"]
E --> F
E --> G["Harness Control View<br/>count / status / scope"]
E --> H["Agent Observation<br/>白名单、有界、脱敏"]
B --> I["Durable Audit<br/>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)。
@@ -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 主动无结论<br/>或 Harness 受控停止"] --> P["ProgressSnapshot"]
P --> Q{"存在已验真 observed facts?"}
Q -->|"是"| I["INSUFFICIENT_EVIDENCE<br/>展示已检查内容和下一步"]
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 才能完整回答:模型看到了什么、为什么继续或停止、最终哪些内容可以发布。
@@ -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 不能互相替代。
@@ -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<br/>Run 与公开用例"] --> CORE["core<br/>执行不变量"]
APP --> AGENT["agent<br/>ReAct 接入"]
AGENT --> PROGRESS["progress<br/>收敛控制"]
AGENT --> TOOL["tool<br/>证据边界"]
APP --> RELEASE["release<br/>唯一发布"]
RELEASE --> GUARD["guard<br/>真实性与支持度"]
CORE --> RETRY["retry<br/>显式 attempt"]
CORE -.-> AUDIT["audit<br/>可观测账本"]
AGENT -.-> AUDIT
TOOL -.-> AUDIT
RELEASE -.-> AUDIT
CONTRACT["contract<br/>类型化语言"] -.-> 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,避免再造全局生命周期。
@@ -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<br/>context / lifecycle / budget / cancel"]
MI["Model Interceptor<br/>调用预算与 Token"]
TI["Tool Interceptor<br/>协议、重复、停止"]
TB["ToolBoundary<br/>授权、执行、canonical"]
PROGRESS["ProgressTracker<br/>信息增益与饱和"]
EG["EvidenceGuard<br/>引用真实性"]
SG["SemanticGuard<br/>结论支持度"]
RELEASE["Release Policy<br/>SUCCESS / FALLBACK"]
AUDIT["Audit / Trace<br/>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<br/>结构不可变"] --> ID["sessionId / runId / deadline"]
RC --> C["RunCancellation<br/>first-reason-wins"]
RC --> B["RunBudget<br/>同步复合计数"]
RC --> L["RunLifecycle<br/>first-terminal-wins"]
RC --> M["ModelCallLedger<br/>组件/轮次账本"]
RC --> P["ProgressTracker<br/>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<br/>typed request"] --> B["ToolBoundary"]
B --> RAW["backend raw response"]
RAW --> CAN["Redis canonical invocation<br/>request + raw + agent_result<br/>短 TTL"]
RAW --> PRJ["Tool-specific projector"]
PRJ --> CTRL["Harness control view"]
PRJ --> OBS["Agent observation<br/>白名单、有界"]
B --> META["Durable audit<br/>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)
@@ -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<br/>唯一业务 Draft"] --> R["DiagnosisReleaseUseCase<br/>发布编排所有者"]
R --> E["EvidenceGuard<br/>确定性引用验真"]
E -->|"invalid"| X["EvidenceRepair<br/>只修结构/引用"]
X --> E2["EvidenceGuard Recheck"]
E -->|"valid"| V["VerifiedEvidenceSnapshot"]
E2 -->|"valid"| V
V --> S["SemanticGuard<br/>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)负责。
+77
View File
@@ -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<br/>限制时间和资源"]
R --> A["Agent 分析问题<br/>选择 Tool"]
A --> T["Harness 执行 Tool<br/>保存事实,只给 Agent 安全视图"]
T --> A
A --> D["Agent 写出诊断草稿"]
D --> G["Harness 检查<br/>引用是否真实、证据是否支持结论"]
G --> P["发布报告<br/>或安全地说明证据不足"]
```
顺着这条线看,Harness 只做三类事情:
1. **执行前设边界**:为本次请求建立身份、deadline、预算和取消能力。
2. **执行中管事实**:所有 Tool 经过统一入口,完整事实由系统保管,模型只看到安全、有限的内容。
3. **发布前做验证**:先验证引用,再判断证据是否支持结论;不满足条件就发布 Fallback,而不是让未经验证的结论出去。
Agent 仍然负责“问题的根因是什么”。Harness 不替 Agent 推理,它负责的是:**让这次推理有边界、有证据、能停止。**
## 3. 先建立这个最小心智模型
```mermaid
flowchart TB
A["Diagnosis Agent<br/>负责业务推理"]
H["Harness<br/>负责确定性控制"]
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”都是参考手册,不需要从头读,也不需要背。
@@ -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)。
@@ -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)。
@@ -0,0 +1,135 @@
# 03 Tool 事实边界:让模型看到必要信息,系统保留真相
这一篇只回答一个问题:**Tool 返回的数据应该由谁保管,模型究竟可以看到多少?**
## 1. 先看一个具体问题
Agent 查询支付日志,后端一次返回了几千行内容,其中包含重复堆栈、敏感字段和远超上下文窗口的正文。
直接把原始结果塞回模型会产生几个问题:
- 敏感数据进入模型上下文;
- 大结果挤占 Token,真正关键的证据反而被淹没;
- 模型后续引用经过裁剪的文本,系统无法证明它对应哪次真实调用;
- 若只保存裁剪结果,EvidenceGuard 又失去了独立验真的事实来源;
- 若把完整 raw 长期写入数据库,泄露面和存储成本都会扩大。
所以 Tool 设计的核心不是“怎样调用后端”,而是**谁拥有原始事实,以及不同消费者应该看到哪一层数据**。
## 2. 一次 Tool 调用的数据怎样变化
```mermaid
flowchart LR
REQ["Typed Tool Request"] --> B["ToolBoundary<br/>权限、只读、预算、大小检查"]
B --> RAW["Backend Raw Result"]
RAW --> P["Tool-specific Projector"]
P --> C["Canonical Invocation<br/>短期保存 request、raw、agent_result 和状态"]
C --> CV["Control View<br/>供 Harness 判断状态和进展"]
C --> MO["Model Observation<br/>供 Agent 推理的白名单内容"]
C --> EG["EvidenceGuard<br/>按当前 Run 独立验真"]
B -.-> AU["Durable Audit<br/>长期只留有界元数据"]
```
第一次阅读只需区分三份内容:
- **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<br/>完整、敏感、短期"] -->|"用于"| V["当前 Run 验真"]
A["Durable Audit<br/>有界、元数据、长期"] -->|"用于"| 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)。
@@ -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<br/>Agent 写出的草稿"] --> E["EvidenceGuard<br/>机械验证引用和归属"]
E -->|"结构或引用可修复"| R["EvidenceRepair<br/>只修引用,不改语义"]
R --> E
E -->|"得到已验真证据"| S["SemanticGuard<br/>判断证据是否支持报告"]
S -->|"SUPPORTED"| P["Release SUCCESS<br/>发布原始安全 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<br/>发布原 Draft"]
V -->|"证据不足或验证不通过"| FB["ReleaseOutcome.FALLBACK<br/>发布 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)。
@@ -0,0 +1,22 @@
# Harness 组件:从一次请求逐步认识
这里不按照 Java 包逐个介绍组件,而是沿着一次诊断请求,分四步回答四个问题。
```mermaid
flowchart LR
Q["一个诊断请求来了"] --> C1["01 运行控制<br/>怎样保证这次执行受控"]
C1 --> C2["02 Agent 接入与收敛<br/>怎样让 Agent 工作但不空转"]
C2 --> C3["03 Tool 事实边界<br/>怎样安全地取得事实"]
C3 --> C4["04 验证与发布<br/>怎样决定结果能否交给用户"]
```
| 顺序 | 先回答的问题 | 涉及的职责域 |
|---|---|---|
| [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)。