docs(mvp): add remaining harness audit guides and engineering index
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
# Harness 失败图谱:异常、停止、降级与终态如何对应
|
||||
|
||||
**更新日期**:2026-07-30
|
||||
|
||||
**主题**:失败分类、停止决策、安全降级、Run 终态与发布结果
|
||||
|
||||
**术语与状态**:[CONTEXT.md](CONTEXT.md) · [Harness生命周期与状态.md](Harness生命周期与状态.md)
|
||||
|
||||
在 Harness 中,“没有找到根因”“Tool 报错”“证据不够”“超时”和“客户端断开”不是同一种失败。如果把它们都压成一个 `FAILED`,用户无法知道系统是否完成了有效检查,开发者也无法判断应该重试、换 Tool、发布 Fallback,还是立即终止。
|
||||
|
||||
这篇文章不从状态枚举开始,而是用三个问题建立一张失败图谱:
|
||||
|
||||
1. 问题发生后,这次 Run 还能继续吗?
|
||||
2. 已经产生的内容中,有没有可验证的安全事实?
|
||||
3. 最终能公开正常报告、安全说明,还是只能失败?
|
||||
|
||||
第一次阅读只看第 1、2、8、10 和 13 节即可。先掌握判断方法和几个典型场景,不需要记住全部状态。
|
||||
|
||||
## 1. 先记住:Harness 的失败处理不是“捕获异常”
|
||||
|
||||
假设一次支付超时诊断中连续发生这些事情:
|
||||
|
||||
```text
|
||||
日志 Tool 查询成功,但指定时间段没有记录
|
||||
RAG Tool 第一次调用网络超时
|
||||
Agent 换了一个查询范围,找到一条可验证的配置事实
|
||||
Agent 据此声称“数据库连接池耗尽”
|
||||
EvidenceGuard 发现报告引用的证据并不支持这个结论
|
||||
```
|
||||
|
||||
如果只看局部,既出现了空结果、技术异常、有效事实,又出现了发布门禁失败。系统不能把其中任何一个事件直接等同于整次 Run 的终态。
|
||||
|
||||
Harness 真正要做的是分层决策:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
X["某个问题发生"] --> Q1{"Run 是否仍可继续?"}
|
||||
Q1 -->|"可以"| C["继续诊断或改查其他 Tool"]
|
||||
Q1 -->|"不可以"| Q2{"是否已有可验证的安全进展?"}
|
||||
C --> Q3{"最终报告能否通过发布门禁?"}
|
||||
Q3 -->|"可以"| S["ReleaseOutcome.SUCCESS<br/>发布诊断报告"]
|
||||
Q3 -->|"不可以,但有安全说明"| F["ReleaseOutcome.FALLBACK<br/>发布 SafeFallback"]
|
||||
Q3 -->|"没有任何安全内容"| E["ReleaseOutcome.FAILED<br/>发布 failure"]
|
||||
Q2 -->|"有"| F
|
||||
Q2 -->|"无"| E
|
||||
```
|
||||
|
||||
这张图背后的核心决策是:
|
||||
|
||||
> 局部事件决定下一步动作,只有 Run Lifecycle 和 Release 才能决定整次请求如何结束、什么可以公开。
|
||||
|
||||
因此,失败处理不是一个全局 `try/catch`,而是执行控制、安全事实和发布策略共同完成的结果。
|
||||
|
||||
## 2. 第一类:没有答案,但系统没有坏
|
||||
|
||||
最容易被误判为失败的场景,是 Tool 正常执行却没有返回证据。
|
||||
|
||||
当前 Tool 结果使用两个正交状态:
|
||||
|
||||
```text
|
||||
InvocationStatus:PROJECTING / READY / ERROR
|
||||
EvidenceStatus:EVIDENCE_FOUND / NO_EVIDENCE / ERROR
|
||||
```
|
||||
|
||||
它们回答不同问题:
|
||||
|
||||
| 组合 | 含义 | 是否是技术失败 |
|
||||
|---|---|---:|
|
||||
| `READY + EVIDENCE_FOUND` | Tool 成功,当前 scope 有候选证据 | 否 |
|
||||
| `READY + NO_EVIDENCE` | Tool 成功,当前 scope 没有候选证据 | 否 |
|
||||
| `ERROR + ERROR` | Tool 执行、投影或 canonical 保存失败 | 是 |
|
||||
|
||||
`READY + NO_EVIDENCE` 只能证明“本次查询范围为空”,不能证明“整个系统不存在该问题”。例如查询 10:00 至 10:10 的支付日志为空,不代表全天没有超时,也不代表日志系统之外没有故障。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
T["执行 Tool"] --> I{"InvocationStatus"}
|
||||
I -->|"ERROR"| X["技术失败路径"]
|
||||
I -->|"READY"| E{"EvidenceStatus"}
|
||||
E -->|"EVIDENCE_FOUND"| G["交给 Agent 判断信息增益"]
|
||||
E -->|"NO_EVIDENCE"| N["记录限定 scope 的空事实<br/>累计 NO_GAIN"]
|
||||
N --> Q{"还值得继续查询吗?"}
|
||||
Q -->|"值得"| R["调整假设或 scope"]
|
||||
Q -->|"连续无增益"| P["CollectionState.SATURATED"]
|
||||
```
|
||||
|
||||
类似的正常无结果还包括:
|
||||
|
||||
- 用户没有提供企业、服务、时间范围等必要上下文;
|
||||
- 多次查询都合法,但连续没有推进诊断;
|
||||
- Agent 遵循协议主动输出 `conclusion=null`;
|
||||
- 已完成有限检查,但证据只够描述现象,不够支持根因。
|
||||
|
||||
这些场景不应该伪装成系统异常。只要请求被正常处理并形成安全说明,就可以是:
|
||||
|
||||
```text
|
||||
RunState.SUCCESS + ReleaseOutcome.FALLBACK
|
||||
```
|
||||
|
||||
### 为什么不把 NO_EVIDENCE 设计成异常
|
||||
|
||||
如果空结果抛异常,系统会产生三个问题:
|
||||
|
||||
- Agent 无法区分“查询为空”和“查询服务不可用”;
|
||||
- 监控会把正常业务分布统计成技术故障;
|
||||
- Release 无法向用户解释已经检查的范围。
|
||||
|
||||
因此放弃了“Tool 无数据即失败”的简化方案,代价是状态维度增加,但换来了可解释的控制语义。
|
||||
|
||||
## 3. 第二类:技术异常可能可以重试
|
||||
|
||||
并非所有技术异常都应立即结束 Run。对于瞬时、明确、可能恢复的 Provider 故障,Harness 可以执行有限的显式重试。
|
||||
|
||||
当前重试策略遵循两个原则:
|
||||
|
||||
```text
|
||||
只有稳定分类为可恢复的技术失败才重试
|
||||
所有 attempt 都由 Harness 计数、计费并记录
|
||||
```
|
||||
|
||||
典型策略如下:
|
||||
|
||||
| 组件 | 可重试场景 | 最大 attempt | 不重试场景 |
|
||||
|---|---|---:|---|
|
||||
| Intent Router | timeout、transport、非法输出 | 2 | 已得到合法路由结果 |
|
||||
| SemanticGuard | timeout、transport、parse/schema failure | 2 | `UNSUPPORTED` |
|
||||
| Diagnosis Agent | 不执行隐藏 retry | 1 个受控业务循环 | 正常下一轮 ReAct 不是 retry |
|
||||
| Tool | 不执行隐藏 retry | 每个 Tool Call 一次 | Agent 可以基于结果选择其他 Tool |
|
||||
| EvidenceRepair | 一次显式修复机会 | 1 | 不是无限修复循环 |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as Harness
|
||||
participant P as Router / Semantic Provider
|
||||
participant B as Budget & Trace
|
||||
|
||||
H->>B: reserve attempt #1
|
||||
H->>P: request #1
|
||||
P-->>H: timeout / transport / invalid output
|
||||
H->>B: record classified failure
|
||||
H->>B: reserve attempt #2
|
||||
H->>P: request #2
|
||||
alt 得到合法结果
|
||||
P-->>H: valid result
|
||||
H->>B: record success
|
||||
else 再次技术失败
|
||||
P-->>H: unavailable
|
||||
H->>B: record final failure
|
||||
H-->>H: 进入 Fallback 或 FAILED 决策
|
||||
end
|
||||
```
|
||||
|
||||
### 为什么不使用 SDK 的默认隐藏重试
|
||||
|
||||
隐藏重试会让系统无法准确回答:
|
||||
|
||||
- 这次请求实际调用了 Provider 几次;
|
||||
- Token、deadline 和 attempt 消耗在哪里;
|
||||
- Trace 中的一次调用为什么延迟异常;
|
||||
- 客户端取消后是否还在后台继续重试。
|
||||
|
||||
所以重试必须是 Harness 的控制行为,而不是各组件自行决定。代价是需要维护失败分类和 attempt 协议,但预算与审计仍然闭合。
|
||||
|
||||
### `UNSUPPORTED` 为什么不重试
|
||||
|
||||
SemanticGuard 返回 `UNSUPPORTED`,表示它成功完成了判断,只是证据不支持报告。这是业务结果,不是技术不可用。重试同一份报告只是在要求模型重新投票,既不能创造新证据,也会削弱门禁的一致性。
|
||||
|
||||
## 4. 第三类:一个 Tool 失败,不等于整个 Run 失败
|
||||
|
||||
Diagnosis Agent 通常拥有多个只读 Tool。某个 Tool 出现 `ERROR` 后,Agent 可能仍然可以:
|
||||
|
||||
- 改查另一个数据源;
|
||||
- 缩小或调整查询范围;
|
||||
- 使用已经取得的其他 canonical 事实;
|
||||
- 明确说明某个数据源不可用,并结束为 Fallback。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
E["单个 Tool ERROR"] --> A{"Run 仍 active 且预算允许?"}
|
||||
A -->|"否"| T["进入终止决策"]
|
||||
A -->|"是"| O{"是否还有合法替代动作?"}
|
||||
O -->|"换 Tool / 换 scope"| C["Agent 继续诊断"]
|
||||
O -->|"没有"| P{"已有安全进展?"}
|
||||
P -->|"有"| F["ReleaseOutcome.FALLBACK"]
|
||||
P -->|"无"| X["ReleaseOutcome.FAILED"]
|
||||
C --> D{"最终是否形成可发布报告?"}
|
||||
D -->|"是"| S["ReleaseOutcome.SUCCESS"]
|
||||
D -->|"否"| P
|
||||
```
|
||||
|
||||
这里不能建立一条简单映射:
|
||||
|
||||
```text
|
||||
Tool ERROR -> RunState.FAILED
|
||||
```
|
||||
|
||||
真正必须终止的情况,是错误破坏了 Harness 的安全前提,或者已经没有合法的恢复路径。例如:
|
||||
|
||||
- canonical store 无法保存或回读 Tool 真相;
|
||||
- Tool raw 或 Agent result 超过硬 bytes 上限;
|
||||
- 路由经过允许的 attempts 后仍不可用;
|
||||
- Agent 输出非法 Draft,且不存在可验证的 ProgressSnapshot;
|
||||
- Harness 自身出现无法分类、无法形成安全响应的内部错误。
|
||||
|
||||
### 为什么不让 Tool 自己决定 Run 失败
|
||||
|
||||
Tool 只知道一次 invocation 是否成功,不知道整个诊断还拥有多少预算、其他 Tool 是否可用、是否已有安全事实,也不知道最终发布策略。让 Tool 抛出全局终止异常,会把局部职责扩大成 Run 决策权。
|
||||
|
||||
当前设计的代价是 Agent 和 Application 必须处理结构化 Tool 错误,而不是依赖异常一路冒泡;收益是局部故障不会无条件摧毁整次诊断。
|
||||
|
||||
## 5. 第四类:执行成功,发布仍可能被拒绝
|
||||
|
||||
Agent 完成 Draft 并不表示用户一定能看到这份报告。发布前还要经过两类门禁:
|
||||
|
||||
```text
|
||||
EvidenceGuard:引用是否真实、是否属于当前 Run、是否来自 READY invocation
|
||||
SemanticGuard:这些真实证据是否支持用户可见结论
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D["DiagnosisDraft"] --> E{"EvidenceGuard"}
|
||||
E -->|"通过"| S{"SemanticGuard"}
|
||||
E -->|"失败"| EF["EVIDENCE_VALIDATION_FAILED<br/>SafeFallback"]
|
||||
S -->|"SUPPORTED"| R["发布 Diagnosis Report"]
|
||||
S -->|"UNSUPPORTED"| SU["SEMANTIC_UNSUPPORTED<br/>SafeFallback"]
|
||||
S -->|"技术不可用"| SA["SEMANTIC_UNAVAILABLE<br/>SafeFallback"]
|
||||
```
|
||||
|
||||
对应的典型结果是:
|
||||
|
||||
| 发布门禁结果 | RunState | ReleaseOutcome | FallbackType |
|
||||
|---|---|---|---|
|
||||
| 两层门禁通过 | `SUCCESS` | `SUCCESS` | 无 |
|
||||
| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` |
|
||||
| SemanticGuard 判断不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` |
|
||||
| SemanticGuard 技术不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` |
|
||||
|
||||
这里最反直觉的一点是:Guard 拒绝发布原报告,通常仍然是 `RunState.SUCCESS`。因为 Harness 成功执行了安全策略,并向用户发布了诚实的降级结果;失败的是“原报告获得发布资格”,不是“控制系统无法完成请求”。
|
||||
|
||||
### 为什么 Guard 不能直接改写结论
|
||||
|
||||
项目放弃了让 Verifier 或 SemanticGuard 顺手生成“更正确答案”的方案。Guard 没有业务 Tool、完整 ReAct 上下文和重新取证能力,改写报告会让审查者同时成为证据生产者。
|
||||
|
||||
因此 Guard 只能批准或拒绝,Release 只能发布原报告或确定性 SafeFallback。代价是部分“看起来只差一点”的报告也会降级,但发布责任保持清晰。
|
||||
|
||||
## 6. 第五类:停止收集,不等于 Run 已终止
|
||||
|
||||
当连续查询没有信息增益,或 Agent 持续违反 progress 协议时,Harness 会把证据收集状态切换为:
|
||||
|
||||
```text
|
||||
CollectionState.SATURATED
|
||||
```
|
||||
|
||||
可能的停止原因包括:
|
||||
|
||||
```text
|
||||
INFORMATION_SATURATED
|
||||
PROGRESS_PROTOCOL_VIOLATED
|
||||
BUDGET_LIMIT_REACHED
|
||||
```
|
||||
|
||||
`SATURATED` 只表示不再允许新的 evidence Tool,不是 Run 终态。Harness 仍然要给 Agent 一次机会输出 Draft,或者根据 canonical records 投影 `ProgressSnapshot`。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> COLLECTING
|
||||
COLLECTING --> COLLECTING: GAINED
|
||||
COLLECTING --> COLLECTING: NO_GAIN / 未达阈值
|
||||
COLLECTING --> SATURATED: 连续 NO_GAIN 或协议违规
|
||||
SATURATED --> DRAFTING: 交付一次 STOP_REQUIRED
|
||||
DRAFTING --> RELEASE: Agent 正常结束
|
||||
DRAFTING --> TERMINATED: Agent 再次请求 Tool
|
||||
RELEASE --> [*]
|
||||
TERMINATED --> [*]
|
||||
```
|
||||
|
||||
这项区分解决了一个早期问题:系统过去只能依靠预算把空转“撞停”,最终把证据不足表达成 `BUDGET_EXHAUSTED` 或内部失败。引入 Collection 状态后,业务收敛可以发生在硬资源终止之前。
|
||||
|
||||
## 7. 第六类:超时、预算和取消是三种不同终止
|
||||
|
||||
Run 的终态只有:
|
||||
|
||||
```text
|
||||
RUNNING
|
||||
SUCCESS
|
||||
FAILED
|
||||
CANCELLED
|
||||
TIMED_OUT
|
||||
BUDGET_EXHAUSTED
|
||||
```
|
||||
|
||||
`RunLifecycle` 使用 first-terminal-wins:第一个成功设置的终态不可被后来返回的 Provider、Tool 或异步回调覆盖。
|
||||
|
||||
### Deadline 到期
|
||||
|
||||
deadline 回答“这次 Run 还能否继续占用时间”。到期后终态是 `TIMED_OUT`。如果没有可发布的安全内容,Release 为 `FAILED`;当前实现不会为了美化结果在超时后继续调用模型生成说明。
|
||||
|
||||
### Budget 耗尽
|
||||
|
||||
预算可能限制模型 attempt、Tool 调用、Token 或 bytes。终态是 `BUDGET_EXHAUSTED`,但发布结果取决于是否已有安全进展:
|
||||
|
||||
```text
|
||||
有 ProgressSnapshot -> ReleaseOutcome.FALLBACK
|
||||
没有安全进展 -> ReleaseOutcome.FAILED
|
||||
```
|
||||
|
||||
这说明 `RunState` 和 `ReleaseOutcome` 不能一一对应。
|
||||
|
||||
### 客户端断开
|
||||
|
||||
客户端断开意味着公开通道已经不存在,Run 转为 `CANCELLED`。取消是协作式的:已经发出的同步 Provider 调用可能无法被物理中止,但返回后必须经过 active check 和 SSE state 检查,迟到结果不能再发布。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant S as ChatSseSession
|
||||
participant H as Harness Core
|
||||
participant P as Provider / Tool
|
||||
|
||||
H->>P: 已发出的同步调用
|
||||
C--xS: 断开连接
|
||||
S->>H: cancel Run
|
||||
H->>H: first terminal = CANCELLED
|
||||
P-->>H: 迟到结果
|
||||
H->>H: checkActive 拒绝继续
|
||||
H-->>S: 不发送 content / done
|
||||
S->>S: DISCONNECTED 拒绝迟到事件
|
||||
```
|
||||
|
||||
客户端断开时不可靠地发送 `done(CANCELLED)`,因为连接已经不可用。取消事实应由服务端 Trace 和 Run 状态观察,而不是假设客户端还能收到终止事件。
|
||||
|
||||
## 8. “安全进展”决定 FALLBACK 还是 FAILED
|
||||
|
||||
停止之后,系统不能把 Agent 的未验证草稿直接当作降级内容。所谓安全进展,必须来自当前 Run 的 canonical READY records,并经过有界投影。
|
||||
|
||||
`ProgressSnapshot` 可以包含:
|
||||
|
||||
- 已实际执行的查询 scope;
|
||||
- 可验证的 observed facts;
|
||||
- 限定范围内的 `NO_EVIDENCE`;
|
||||
- 数据源不可用、结果截断或投影失败等 limitations;
|
||||
- 推荐补充的上下文或下一步检查。
|
||||
|
||||
它不能包含未经支持的根因结论。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
T["Run 无法继续或 Agent 无结论"] --> P["从 canonical records<br/>构建 ProgressSnapshot"]
|
||||
P --> V{"存在可验证的 observed facts?"}
|
||||
V -->|"有"| F["FALLBACK<br/>说明已检查内容、限制和下一步"]
|
||||
V -->|"没有"| M{"是否明确缺少必要上下文?"}
|
||||
M -->|"是"| C["FALLBACK<br/>MISSING_REQUIRED_CONTEXT"]
|
||||
M -->|"否"| E["FAILED<br/>不公开未验证内容"]
|
||||
```
|
||||
|
||||
因此,下面两次预算耗尽可以有不同结果:
|
||||
|
||||
| 场景 | RunState | ReleaseOutcome | 用户看到什么 |
|
||||
|---|---|---|---|
|
||||
| 已验证支付服务在目标时段无日志,随后预算耗尽 | `BUDGET_EXHAUSTED` | `FALLBACK` | 已检查范围、空结果限制和下一步 |
|
||||
| 第一次模型调用前预算检查失败,没有任何安全事实 | `BUDGET_EXHAUSTED` | `FAILED` | failure |
|
||||
|
||||
### 为什么不为所有失败生成一段“友好回答”
|
||||
|
||||
如果失败后再调用模型组织解释,会继续消耗已经耗尽的预算,也可能根据异常文本编造业务结论。确定性模板虽然表达能力有限,但不会把未知包装成答案。
|
||||
|
||||
这项设计选择了 fail closed:有安全事实才降级,没有就失败。代价是用户体验不总是“自然语言很完整”,但不会为了完整感牺牲可信度。
|
||||
|
||||
## 9. 同一个结果,要从四个视图理解
|
||||
|
||||
Harness 没有一条能容纳全部语义的总状态机。一次请求结束后,至少要区分四个观察面:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
R["RunState<br/>执行为什么停止"] --> X["一次请求"]
|
||||
O["ReleaseOutcome<br/>最终公开什么"] --> X
|
||||
D["diagnosis_run.status<br/>请求是否被安全处理"] --> X
|
||||
S["SSE 事件<br/>客户端实际收到什么"] --> X
|
||||
```
|
||||
|
||||
### RunState:执行为什么停止
|
||||
|
||||
它由 Harness Core 拥有,表达正常完成、内部失败、取消、超时或预算耗尽。
|
||||
|
||||
### ReleaseOutcome:最终公开什么
|
||||
|
||||
```text
|
||||
SUCCESS / FALLBACK / FAILED / CANCELLED
|
||||
```
|
||||
|
||||
它由 Release 决定,表达正常诊断报告、安全降级、失败或取消。
|
||||
|
||||
### 数据库 status:请求是否被安全处理
|
||||
|
||||
`JpaChatRunStore` 当前映射为:
|
||||
|
||||
| ReleaseOutcome | diagnosis_run.status |
|
||||
|---|---|
|
||||
| `SUCCESS` | `SUCCESS` |
|
||||
| `FALLBACK` | `SUCCESS` |
|
||||
| `FAILED` | `FAILED` |
|
||||
| `CANCELLED` | `CANCELLED` |
|
||||
|
||||
因此:
|
||||
|
||||
```text
|
||||
status=SUCCESS + release_outcome=FALLBACK
|
||||
```
|
||||
|
||||
表示请求被正常、安全地处理并发布了降级内容,不表示系统找到了根因。
|
||||
|
||||
只有 `DIAGNOSIS + ReleaseOutcome.SUCCESS + published_result` 才能进入下一轮 `PreviousTurn`。Fallback 不进入后续上下文,避免把“证据不足”当作已确认结论继续传播。
|
||||
|
||||
### SSE:客户端实际收到什么
|
||||
|
||||
公开事件顺序是:
|
||||
|
||||
```text
|
||||
metadata -> status* -> content | failure -> done
|
||||
```
|
||||
|
||||
- `content` 最多一次;
|
||||
- `content` 与 `failure` 互斥;
|
||||
- `TERMINAL / DISCONNECTED` 后拒绝迟到结果;
|
||||
- 当前 `done` 使用 `ReleaseOutcome`,不是另一套遗留终态。
|
||||
|
||||
四个视图回答不同问题,排障时不能拿数据库 `SUCCESS` 推断用户收到了一份成功诊断报告。
|
||||
|
||||
## 10. 典型场景总表
|
||||
|
||||
| 场景 | RunState | ReleaseOutcome | 公开内容或 FallbackType |
|
||||
|---|---|---|---|
|
||||
| EvidenceGuard、SemanticGuard 全部通过 | `SUCCESS` | `SUCCESS` | Diagnosis report |
|
||||
| 缺少企业、服务、时间等必要上下文 | `SUCCESS` | `FALLBACK` | `MISSING_REQUIRED_CONTEXT` |
|
||||
| 有限检查后仍然证据不足 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
|
||||
| 信息饱和且有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
|
||||
| Progress 协议停止且有安全进展 | `SUCCESS` | `FALLBACK` | `INSUFFICIENT_EVIDENCE` |
|
||||
| EvidenceGuard 最终失败 | `SUCCESS` | `FALLBACK` | `EVIDENCE_VALIDATION_FAILED` |
|
||||
| SemanticGuard 判断不支持 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNSUPPORTED` |
|
||||
| SemanticGuard attempts 后不可用 | `SUCCESS` | `FALLBACK` | `SEMANTIC_UNAVAILABLE` |
|
||||
| 预算耗尽但有安全进展 | `BUDGET_EXHAUSTED` | `FALLBACK` | 当前发布为 `INSUFFICIENT_EVIDENCE` |
|
||||
| 超时且没有安全内容 | `TIMED_OUT` | `FAILED` | failure |
|
||||
| 预算耗尽且没有安全进展 | `BUDGET_EXHAUSTED` | `FAILED` | failure |
|
||||
| 不可恢复内部失败 | `FAILED` | `FAILED` | failure |
|
||||
| 客户端断开 | `CANCELLED` | `CANCELLED` | 不可靠发送 `done` |
|
||||
|
||||
这张表不是一个双向转换规则。例如看到 `ReleaseOutcome.FALLBACK`,不能单独推断 Run 是正常完成还是预算耗尽;仍要结合 Run termination 和 Trace。
|
||||
|
||||
## 11. 排障时按什么顺序看
|
||||
|
||||
面对“用户为什么没有看到诊断结论”,不要先搜索所有异常日志。按发布结果向前追踪更容易定位:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["用户实际收到的 SSE"] --> O["release_outcome<br/>fallback_type"]
|
||||
O --> R["Run termination<br/>deadline / budget / cancel"]
|
||||
O --> G["Evidence / Semantic<br/>Release Trace"]
|
||||
R --> T["Tool Invocation<br/>Progress / canonical record"]
|
||||
G --> T
|
||||
```
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 看 SSE 是 `content`、`failure`,还是连接提前断开。
|
||||
2. 看 `release_outcome` 和 `FallbackType`,确认是正常报告、降级还是失败。
|
||||
3. 看 Run termination,区分正常完成、超时、预算耗尽和取消。
|
||||
4. 看 Release、EvidenceGuard、SemanticGuard Trace,确认原报告为什么不能发布。
|
||||
5. 看 Tool 的 `InvocationStatus + EvidenceStatus`,不要只看一个 `success`。
|
||||
6. 最后查看 canonical record、scope、bytes、budget usage 和 progress stop reason。
|
||||
|
||||
这个顺序从“用户看到什么”追到“内部为什么这样决定”,比从第一条异常开始阅读整条 Timeline 更容易建立因果关系。
|
||||
|
||||
## 12. 这套设计放弃了哪些更简单的方案
|
||||
|
||||
### 一个 `status` 表示一切
|
||||
|
||||
放弃原因:`SUCCESS` 无法同时表达 Tool 执行、Run 终止、报告发布和数据库处理结果。单状态简单,但必然丢失原因。
|
||||
|
||||
### 任意异常直接终止整个 Run
|
||||
|
||||
放弃原因:局部 Tool 故障仍可能有替代路径;空结果也不是异常。这样做会降低系统可用性并掩盖有限检查的价值。
|
||||
|
||||
### 所有错误都自动重试
|
||||
|
||||
放弃原因:业务拒绝、容量上限、非法输入和证据不支持不会因为重试自动恢复;隐藏重试还会破坏预算和审计。
|
||||
|
||||
### Agent 自己决定何时降级
|
||||
|
||||
放弃原因:Agent 无法读取 canonical truth,也不能验证自己的引用。让它同时生成结论和批准发布,会形成自证循环。
|
||||
|
||||
### 失败后继续调用模型润色 Fallback
|
||||
|
||||
放弃原因:终止后继续消耗资源,且无法保证新文本只包含已验证事实。当前使用确定性 Release Policy 和安全模板。
|
||||
|
||||
### 取消时强制等待所有底层调用结束
|
||||
|
||||
放弃原因:同步 Provider 未必支持真正中断,等待会延长资源占用。当前采用协作式取消、active check、first-terminal-wins 和 SSE 状态拒绝迟到发布。
|
||||
|
||||
## 13. 面试时如何讲这套失败设计
|
||||
|
||||
可以用下面这段话概括:
|
||||
|
||||
> 我们没有把 Harness 的失败处理设计成一个全局异常捕获器,因为 Agent 系统里“没有证据、单个 Tool 失败、证据不支持结论、预算耗尽和客户端取消”代表完全不同的控制语义。系统先判断 Run 是否还能继续,再从当前 Run 的 canonical records 判断是否已有可验证进展,最后由唯一的 Release Policy 决定发布正常报告、SafeFallback 还是 failure。Tool 使用 InvocationStatus 和 EvidenceStatus 区分技术失败与正常空结果;RunState 解释执行为什么停止,ReleaseOutcome 解释用户最终看到什么,两者不做一一映射。可恢复的 Provider 故障只允许 Harness 做有限、可审计的显式重试,Guard 拒绝发布通常降级而不是把整次请求标成失败,超时、预算和取消则通过 first-terminal-wins 与 SSE 状态阻止迟到结果。这样做的目标不是让每次诊断都成功,而是保证任何结束方式都可解释、可审计,并且不会把未经验证的内容发布给用户。
|
||||
|
||||
这段回答要表达的不是“系统定义了很多状态”,而是:**每个状态都对应不同的决策权和失败责任。**
|
||||
|
||||
## 14. 当前边界与代价
|
||||
|
||||
1. 多套正交状态提高了准确性,也增加了学习成本,必须通过 Context、映射表和 Trace 查询规范维持统一口径。
|
||||
2. 内存中的 `RunTermination.state/reason` 当前没有完整独立持久化;精确判断 `TIMED_OUT / BUDGET_EXHAUSTED` 仍需结合 Trace、异常路径和预算记录。
|
||||
3. 协作式取消能阻止迟到发布,但不保证立即终止已经发出的同步 Provider 计算。
|
||||
4. Tool ERROR 后是否继续由 Agent 在 Harness 门禁内选择,因此模型可能做出次优恢复动作;硬预算和停止协议负责限制损失。
|
||||
5. 有安全进展才能 Fallback 的策略会让部分请求直接失败,但这是避免发布未验证内容的主动取舍。
|
||||
6. `FallbackType` 枚举仍保留 `BUDGET_EXHAUSTED`,但当前预算受控停止实际统一发布 `INSUFFICIENT_EVIDENCE`;这是已知命名债务。未来若要区分资源不足与业务证据不足,需要先明确对外语义和兼容策略,不能只替换当前映射。
|
||||
|
||||
## 15. 事实来源与延伸阅读
|
||||
|
||||
本文对应的主要实现边界:
|
||||
|
||||
- `ChatApplicationUseCase`:Application 生命周期、Router、Run 终止和 Release 协作;
|
||||
- `DiagnosisHarnessCore`:Run active check、预算、终态与控制边界;
|
||||
- `DiagnosisAgentUseCase`:Tool loop、受控停止和 Draft 形成;
|
||||
- `DiagnosisReleaseUseCase`:EvidenceGuard、SemanticGuard 与唯一发布策略;
|
||||
- `JpaChatRunStore`:ReleaseOutcome 到数据库 status 的映射;
|
||||
- `ChatSseSession`:SSE 单内容、终态和断开规则。
|
||||
|
||||
继续阅读:
|
||||
|
||||
- [Harness 入门](README.md)
|
||||
- [支付超时诊断案例](案例-从一次支付超时诊断看Harness如何控制Agent.md)
|
||||
- [Harness 生命周期与状态](Harness生命周期与状态.md)
|
||||
- [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md)
|
||||
- [证据安全链](Harness证据安全链-从引用真实到结论可发布.md)
|
||||
- [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)
|
||||
@@ -0,0 +1,371 @@
|
||||
# Harness 设计演进:从多 Agent 编排到确定性控制边界
|
||||
|
||||
Harness 不是一开始就被完整设计出来的。它来自几轮真实重构:系统先拆出多个 Agent 角色,又增加 Gatekeeper 保证证据真实性,随后用 StateGraph 显式管理状态和分支,最终才发现一个更根本的问题:**外层系统正在重复实现 Agent 本身已经具备的 ReAct 生命周期。**
|
||||
|
||||
这篇文章不按提交逐条记流水账,而是追踪每一次设计变化背后的问题:当时为什么这样做、它解决了什么、为什么后来仍然不够,以及哪些思想最终保留了下来。
|
||||
|
||||
第一次阅读只看第 1、2、6、8 和 10 节即可。先建立演进主线,再理解最终边界和可复用经验。
|
||||
|
||||
## 1. 先看完整演进路线
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
M["多 Agent 分工<br/>Planner / Executor / Verifier / Composer"]
|
||||
G["Gatekeeper<br/>在模型审查前机械验真"]
|
||||
S["StateGraph<br/>显式状态、条件边和终态"]
|
||||
H["Single ReAct + Harness<br/>推理与控制分离"]
|
||||
P["Progress Control<br/>从预算止损到正常收敛"]
|
||||
|
||||
M -->|"证据引用可能伪造"| G
|
||||
G -->|"状态藏在 Service、Hook 和 ThreadLocal"| S
|
||||
S -->|"显式了编排,但仍重复 ReAct"| H
|
||||
H -->|"预算能止损,不能判断继续是否有价值"| P
|
||||
```
|
||||
|
||||
这几次变化不是简单地“旧方案错、新方案对”。每一阶段都解决了当时最明显的问题,同时也让下一个更深层的问题暴露出来。
|
||||
|
||||
| 阶段 | 当时解决的核心问题 | 后来暴露的核心问题 |
|
||||
|---|---|---|
|
||||
| 多 Agent | 复杂诊断如何分工 | 一次 ReAct 被拆成多个模型角色和协议 |
|
||||
| Gatekeeper | 如何阻止伪造证据引用 | 验真依赖旧输出结构、Trace 和隐式上下文 |
|
||||
| StateGraph | 如何显式表达状态、重试和 Fallback | Graph 仍在编排多个重复的推理角色 |
|
||||
| Single ReAct + Harness | 如何分离业务推理与确定性控制 | Agent 仍可能在证据不足时空转 |
|
||||
| Progress Control | 如何让无证据诊断正常停止 | 阈值和信息增益仍需持续校准 |
|
||||
|
||||
## 2. 第一阶段:把复杂诊断拆成多个 Agent
|
||||
|
||||
项目早期采用过 Supervisor 和 Sequential 两类多 Agent 编排。Chat 诊断最终形成了一条固定链路:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q["用户问题"] --> P["Planner<br/>制定排查计划"]
|
||||
P --> E["Executor<br/>调用 Tool 收集证据"]
|
||||
E --> G["Gatekeeper<br/>检查证据引用"]
|
||||
G --> V["Verifier<br/>判断 Claim 是否可信"]
|
||||
V --> C["Composer<br/>组织最终回答"]
|
||||
```
|
||||
|
||||
这个方案有合理动机:复杂诊断既要规划、执行,又要验证和表达,把职责拆开比让一个 Prompt 包办所有事情更容易理解。
|
||||
|
||||
它也确实建立了几项重要能力:
|
||||
|
||||
- Planner 不直接编造执行结果;
|
||||
- Executor 专注 Tool 调用和微观事实;
|
||||
- Verifier 不再负责重新检索;
|
||||
- Composer 只能表达经过允许的 Claim;
|
||||
- 每个角色都有自己的结构化输出和测试入口。
|
||||
|
||||
问题出在拆分粒度。Planner、Executor、Verifier 和 Composer 看似是不同业务岗位,实际上刚好覆盖了一次完整 ReAct:
|
||||
|
||||
```text
|
||||
思考 -> Planner
|
||||
行动观察 -> Executor + Tool
|
||||
自我检查 -> Verifier
|
||||
最终回答 -> Composer
|
||||
```
|
||||
|
||||
Agent 框架本来已经支持“思考、调用 Tool、读取 Observation、继续推理、生成答案”。外层再把它拆成四个 Agent 后,系统必须额外维护:
|
||||
|
||||
- 四套 Prompt 和输出 Schema;
|
||||
- Agent 间的 JSON 转换;
|
||||
- 证据和上下文的重复搬运;
|
||||
- PASS、LOW_CONFID、REJECT 与重试分支;
|
||||
- 每个角色各自的 Token、timeout 和错误语义;
|
||||
- Composer 是否严格遵守 Verifier 输出的新风险。
|
||||
|
||||
第一阶段真正留下的经验不是“多 Agent 一定不好”,而是:
|
||||
|
||||
> 只有当角色拥有不同数据权限、不同工具或真正独立的业务目标时,拆成多个 Agent 才可能值得。仅仅把一次 ReAct 的内部步骤外置成多个角色,会放大协议成本。
|
||||
|
||||
## 3. 第二阶段:Gatekeeper 把确定性验真从模型中拿出来
|
||||
|
||||
多 Agent 链路很快遇到另一个问题:Verifier 可以判断一段 evidence excerpt 看起来是否支持 Claim,却不能证明 `source_invocation_id`、`raw_path` 和 excerpt 真正对应某次 Tool 调用。
|
||||
|
||||
如果仍然让模型检查这些字段,就会出现“模型验证模型”的循环。于是系统在 Executor 和 Verifier 之间加入 `ExecutorGatekeeperService`:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
E["Executor 输出引用"] --> G["Gatekeeper<br/>查 Tool Invocation 和 raw path"]
|
||||
G -->|"真实"| V["Verifier<br/>判断语义支持关系"]
|
||||
G -->|"伪造或错配"| R["拒绝进入 Verifier"]
|
||||
```
|
||||
|
||||
这是演进中一个非常重要、并且最终被保留的决策:
|
||||
|
||||
```text
|
||||
代码能够机械证明的事实,不交给模型判断。
|
||||
```
|
||||
|
||||
Gatekeeper 能拒绝伪造 invocation、错误 raw path 和不匹配的 evidence excerpt,使“引用真实”和“语义成立”第一次成为两个独立问题。
|
||||
|
||||
但旧 Gatekeeper 仍然耦合在多 Agent 协议上:
|
||||
|
||||
- 它读取 Executor 特定的 `executor_evidence_v2`;
|
||||
- 引用协议包含 `source_invocation_id + raw_path + excerpt`;
|
||||
- Verifier 输入依赖 Hook 组装;
|
||||
- Tool Trace 和 Agent 上下文通过 ThreadLocal 等隐式状态关联;
|
||||
- 它只能保护旧 Executor 到 Verifier 的这一段链路。
|
||||
|
||||
后来的 `EvidenceGuard` 不是凭空出现的。它继承了 Gatekeeper 的核心思想,但把真理源改为当前 Run 的 canonical Tool Invocation,并把验证对象改为最终 `DiagnosisDraft`。
|
||||
|
||||
## 4. 第三阶段:StateGraph 让隐式编排变得可见
|
||||
|
||||
随着重试、低置信分支、Fallback、Run Trace 和多个 Agent 输出不断增加,旧 `ChatService + SequentialAgent + Hook + ThreadLocal` 很难回答一个简单问题:**当前诊断到底处于哪个状态,下一步为什么走这条分支?**
|
||||
|
||||
2026-07-17 到 2026-07-20,项目完成了一轮 StateGraph 改造。它将节点、条件边、共享状态和终态显式化,并切换公开 Chat 诊断入口。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Planner Node"] --> E["Executor Node"]
|
||||
E --> G["Gatekeeper Node"]
|
||||
G --> V["Verifier Node"]
|
||||
V -->|"PASS"| C["Composer Node"]
|
||||
V -->|"补证据"| RP["Evidence Retry Prepare"]
|
||||
RP --> P
|
||||
V -->|"拒绝"| F["Fallback Node"]
|
||||
```
|
||||
|
||||
StateGraph 解决了几个真实问题:
|
||||
|
||||
- 分支不再隐藏在大段 Service `if/else` 中;
|
||||
- Graph State 显式携带 Run 级数据;
|
||||
- Node 和条件边可以独立测试;
|
||||
- Fallback 和 retry 路径可以画出来并验证;
|
||||
- 旧 `VerifierContextHolder`、部分 Hook 和 ThreadLocal 状态得以清理;
|
||||
- `ChatService` 从直接拥有全部诊断细节转为调用 Graph Runtime。
|
||||
|
||||
因此,StateGraph 不是一次无效重构。它提高了旧多 Agent 架构的可见性和可测试性。
|
||||
|
||||
但它解决的是“怎样更清楚地编排这些角色”,没有重新质疑“这些角色是否都应该存在”。结果是:
|
||||
|
||||
- Planner、Executor、Verifier、Composer 仍然各自调用模型;
|
||||
- Graph State 继续搬运多个角色的结构化上下文;
|
||||
- Node Adapter、Result Mapper、条件边和业务协议形成第二套控制结构;
|
||||
- 外层 Graph 决定何时计划、执行、补证据和回答,而框架内 Agent 也在做相似的 ReAct 控制;
|
||||
- 状态机显式了复杂度,却没有消除复杂度。
|
||||
|
||||
这一阶段带来的关键认识是:
|
||||
|
||||
> 显式状态机能够治理复杂流程,但不能证明流程本身有必要。如果核心流程只是一个 Agent 的自然 ReAct,Graph 可能只是把重复实现变得更整齐。
|
||||
|
||||
## 5. 转折点:问题不是编排写得不好,而是重复实现了 ReAct
|
||||
|
||||
ISS-014 对旧架构做了一个更根本的判断:Planner、Executor、Verifier、Composer 并不是四个真正独立的业务主体,而是把一个完整 ReAct 生命周期拆到了 Agent 外部。
|
||||
|
||||
这使设计问题从:
|
||||
|
||||
```text
|
||||
怎样把多 Agent 编排得更清楚?
|
||||
```
|
||||
|
||||
转变为:
|
||||
|
||||
```text
|
||||
哪些决定必须由模型做,哪些约束必须由代码拥有?
|
||||
```
|
||||
|
||||
这个问题带来了新的职责划分:
|
||||
|
||||
| 决定 | 所有者 |
|
||||
|---|---|
|
||||
| 提出故障假设、选择 Tool、解释证据、写 Draft | Diagnosis Agent |
|
||||
| Run 身份、deadline、预算、取消、retry、唯一终态 | Harness Core |
|
||||
| Tool 权限、只读、bytes、canonical truth | ToolBoundary |
|
||||
| 引用是否真实 | EvidenceGuard |
|
||||
| 真实证据是否支持报告 | 隔离的 SemanticGuard |
|
||||
| 发布原 Draft 还是 SafeFallback | Release |
|
||||
|
||||
这不是把所有能力重新塞回一个“大 Agent”。相反,它按**判断性质**而不是按“岗位名称”拆分:
|
||||
|
||||
- 非确定性的业务推理留给 Agent;
|
||||
- 可以机械证明的控制规则交给代码;
|
||||
- 必须使用模型的语义审查被隔离成单轮、无 Tool、无记忆的 Guard。
|
||||
|
||||
## 6. 第四阶段:一个 Diagnosis Agent,加一层 Harness
|
||||
|
||||
最终架构只保留一个拥有业务 Tool loop 的 Diagnosis Agent:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
U["用户问题"] --> APP["Chat Application"]
|
||||
APP --> H["Harness Core<br/>Run、预算、取消、终态"]
|
||||
H --> A["Diagnosis ReAct Agent<br/>假设、Tool、Observation、Draft"]
|
||||
A --> TB["ToolBoundary<br/>执行与 canonical truth"]
|
||||
TB --> A
|
||||
A --> EG["EvidenceGuard<br/>确定性引用验真"]
|
||||
EG --> SG["SemanticGuard<br/>隔离语义审查"]
|
||||
SG --> REL["Release<br/>SUCCESS 或 SafeFallback"]
|
||||
```
|
||||
|
||||
这里做了几项明确取舍。
|
||||
|
||||
### 不再保留业务 StateGraph
|
||||
|
||||
项目不再用外层 Graph 编排 Planner、Executor、Verifier 和 Composer。底层 ReactAgent 框架内部是否使用 Graph 属于框架实现细节,不再成为项目业务协议。
|
||||
|
||||
### 不自己重写 ReAct loop
|
||||
|
||||
Diagnosis Agent 使用框架原生 Tool Calling 和 ReAct。Harness 通过 Model/Tool Interceptor、Hook 和显式 RunContext 接入,不维护第二套 `while` 循环。
|
||||
|
||||
### 不让单 Agent 获得全部权力
|
||||
|
||||
Agent 合并的是业务推理职责,不是安全职责。它不能管理预算、读取 canonical raw、验证自己引用、调用 SemanticGuard 或决定最终发布。
|
||||
|
||||
### SemanticGuard 不是第二个业务 Agent
|
||||
|
||||
它只接收原始问题、Draft 的用户可见语义和 verified evidence,输出 `SUPPORTED / UNSUPPORTED`。它无 Tool、无记忆、不回调主 Agent,也不能改写报告。
|
||||
|
||||
### 迁移按边界而不是按页面完成
|
||||
|
||||
实施顺序先冻结 Contract,再建立 RunContext/Retry Core、Tool Boundary、各类投影、单 Diagnosis Agent、Evidence/Semantic Guard,最后切换 Application/SSE 并删除旧架构。这避免了“先切入口,再补安全边界”的过渡风险。
|
||||
|
||||
## 7. Harness 建成后,问题继续暴露
|
||||
|
||||
单 Agent + Harness 解决了外层重复编排,但真实运行又暴露了几类更细的问题。
|
||||
|
||||
### Tool 成功不等于有证据
|
||||
|
||||
旧代码常用一个 `success` 表达所有含义。后来拆为:
|
||||
|
||||
```text
|
||||
InvocationStatus:Tool 调用是否完成
|
||||
EvidenceStatus:当前 scope 是否返回候选证据
|
||||
SemanticVerdict:证据是否支持报告
|
||||
ReleaseOutcome:最终向用户发布什么
|
||||
```
|
||||
|
||||
状态变多不是为了复杂,而是为了避免 `SUCCESS` 在四层中表达四种不同意思。
|
||||
|
||||
### Agent Observation 不能充当真理源
|
||||
|
||||
Agent 需要的是有界、清洗后的 Tool 结果;EvidenceGuard 需要的是独立、可回读的调用真相;长期 Audit 又不能复制全部敏感 raw。于是形成 canonical truth、Model Observation 和 metadata audit 三层数据责任。
|
||||
|
||||
### 引用真实不等于结论成立
|
||||
|
||||
Gatekeeper 思想被升级为 EvidenceGuard,但仅验引用仍不足以拦截“真实日志被过度解释”。因此保留隔离的 SemanticGuard,并由 Release 掌握唯一出口。
|
||||
|
||||
### 隐藏 retry 会破坏预算和审计
|
||||
|
||||
SDK、HTTP Client 和各模型组件各自重试会让 Token、延迟和 attempt 无法解释。最终只允许 Harness 根据稳定失败分类执行显式 retry;正常 ReAct 下一轮和重新调用 Tool 都不叫 retry。
|
||||
|
||||
## 8. 第五阶段:预算能止损,但不能让诊断正常完成
|
||||
|
||||
单 Agent 运行后又出现一个问题:当知识库未知、日志为空或查询条件不足时,预算只能限制最大调用次数,不能判断继续搜索是否还有价值。
|
||||
|
||||
模型可能不断改写查询,直到:
|
||||
|
||||
```text
|
||||
BUDGET_EXHAUSTED
|
||||
或 INTERNAL_FAILURE
|
||||
```
|
||||
|
||||
用户最终只看到通用错误,却不知道系统已经检查了什么、为什么没有结论。
|
||||
|
||||
于是 Harness 增加 ProgressTracker 和信息增益停止协议:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
T["Tool Result"] --> I{"是否推进当前诊断?"}
|
||||
I -->|"GAINED"| C["继续收集"]
|
||||
I -->|"NO_GAIN"| N["连续无增益计数"]
|
||||
N -->|"未达阈值"| C
|
||||
N -->|"达到阈值"| S["SATURATED / STOP_REQUIRED"]
|
||||
S --> P["ProgressSnapshot"]
|
||||
P --> F["INSUFFICIENT_EVIDENCE Fallback"]
|
||||
```
|
||||
|
||||
这次演进补上了资源控制与任务完成之间的差距:
|
||||
|
||||
- Budget 回答“还能不能继续消耗”;
|
||||
- Information Gain 回答“继续查询是否推进诊断”;
|
||||
- ProgressSnapshot 回答“没有结论时,哪些已检查事实仍可安全告诉用户”。
|
||||
|
||||
最重要的行为变化是:**证据不足成为合法完成,而不是只能撞到预算后失败。**
|
||||
|
||||
## 9. 哪些设计被放弃,哪些思想被保留
|
||||
|
||||
| 曾经的设计 | 最终处理 | 保留下来的思想 |
|
||||
|---|---|---|
|
||||
| Supervisor 调度多个诊断角色 | Chat 主链不再使用 | 复杂任务需要清晰职责边界 |
|
||||
| Planner / Executor / Verifier / Composer | 合并业务推理到一个 Diagnosis Agent | 规划、执行、审查、表达仍需明确责任,只是不必都是 Agent |
|
||||
| Executor Gatekeeper | 旧实现删除 | 确定性验真先于语义审查,演化为 EvidenceGuard |
|
||||
| SequentialAgent | 删除 | 固定业务步骤必须可测试、可观测 |
|
||||
| 业务 StateGraph | 删除 | 状态和终态必须显式,转化为 typed contracts、RunLifecycle 和 Release |
|
||||
| ThreadLocal 上下文 | 删除 | exact Run 归属仍必须传播,改为显式 RunContext |
|
||||
| PASS / LOW_CONFID / REJECT + 补证据循环 | 删除 | 不支持的结论不能发布,改为二元语义门禁和确定性 Fallback |
|
||||
| Tool raw 直接参与上下文与审计 | 分层 | Tool 结果必须可追溯,但不同消费者使用不同视图 |
|
||||
|
||||
好的重构通常不是把过去全部推翻,而是把有效思想从不合适的实现形式中提取出来。
|
||||
|
||||
## 10. 这段演进真正说明了什么
|
||||
|
||||
Harness 最终形成,不是因为团队一开始就知道所有组件,而是逐步回答了四个问题:
|
||||
|
||||
1. **业务推理应该由谁负责?** 一个完整的 Diagnosis ReAct Agent。
|
||||
2. **哪些约束不能依赖 Prompt?** 身份、预算、取消、权限、容量、验真和唯一发布。
|
||||
3. **哪些模型判断必须隔离?** 证据是否支持用户可见报告的 SemanticGuard。
|
||||
4. **证据不足怎样成为正常结果?** ProgressTracker、ProgressSnapshot 和 SafeFallback。
|
||||
|
||||
最终边界可以浓缩为:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B["需要理解业务语义和提出假设"] --> A["交给 Diagnosis Agent"]
|
||||
M["能够由代码机械证明"] --> H["交给 Harness"]
|
||||
S["必须使用模型但不能拥有业务循环"] --> G["交给隔离 Guard"]
|
||||
O["决定什么可以公开"] --> R["只交给 Release"]
|
||||
```
|
||||
|
||||
这也是本项目对 Agent 系统最核心的工程判断:
|
||||
|
||||
> 不要围绕模型的“角色感”设计系统,而要围绕决策权、真理源和失败责任设计边界。
|
||||
|
||||
## 11. 这套演进的代价和未完成问题
|
||||
|
||||
当前方案不是没有代价:
|
||||
|
||||
- Harness 类型和状态较多,需要统一 Context 防止误读;
|
||||
- canonical store 引入 Redis TTL、容量和访问控制成本;
|
||||
- EvidenceGuard 与 SemanticGuard 增加发布延迟;
|
||||
- 信息增益依赖模型对非空结果的二元评价,仍可能误判;
|
||||
- `NO_GAIN` 阈值、Token 和 timeout 需要根据 Trace 持续校准;
|
||||
- 当前 Mock 日志和未配置的业务 MySQL 数据源限制了真实诊断覆盖面。
|
||||
|
||||
但这些复杂度与旧多 Agent/Graph 的复杂度性质不同:旧复杂度主要用于搬运推理过程,当前复杂度主要用于保护身份、资源、事实和发布边界。前者会随角色数增长,后者围绕稳定的不变量增长。
|
||||
|
||||
## 12. 面试时如何讲这段演进
|
||||
|
||||
可以用下面这段话概括:
|
||||
|
||||
> 项目最初把复杂诊断拆成 Planner、Executor、Verifier 和 Composer,并通过 Gatekeeper 验证证据引用;为了治理 Service、Hook 和 ThreadLocal 中的隐式分支,又引入 StateGraph 显式管理状态和终态。Graph 提高了可测试性,但没有解决根问题:外层仍在重复框架已有的 ReAct 生命周期,并产生多套 Prompt、Schema、重试和上下文搬运。后来我们按决策性质重新划分责任,只保留一个拥有 Tool loop 的 Diagnosis Agent,把 Run、预算、取消、Tool 真相、证据验真和发布权放进确定性 Harness,语义审查则隔离成无 Tool、无记忆的单轮 Guard。最后又通过信息增益协议,让证据不足从预算失败变成可解释的正常 Fallback。
|
||||
|
||||
这段回答的重点不是“我们用了哪些框架”,而是展示:系统怎样从症状修补逐步走到责任边界重构。
|
||||
|
||||
## 13. 事实来源与延伸阅读
|
||||
|
||||
关键演进节点可由 Git 提交确认:
|
||||
|
||||
| 日期 | 代表提交 | 含义 |
|
||||
|---|---|---|
|
||||
| 2026-07-03 | `f01866c` | Chat 切换 Sequential Agent |
|
||||
| 2026-07-08 | `c5e496e`、`1b31e78`、`6015bcb` | Gatekeeper、Verifier、Composer 完成 |
|
||||
| 2026-07-17 | `581daff`、`99e490f` | StateGraph 设计冻结并切换 Chat |
|
||||
| 2026-07-20 | `190013c` | StateGraph 清理与验收完成 |
|
||||
| 2026-07-21 | `58c3910` 至 `f8809cb` | Single ReAct、Harness、Tool、Guard 和 Application 分阶段落地 |
|
||||
| 2026-07-22 | `8ee7cc0` | 删除旧 Agent 架构 |
|
||||
| 2026-07-27 | `d045218`、`38f781b` | 信息增益停止与协议修复完成 |
|
||||
|
||||
主要历史资料:
|
||||
|
||||
- `mvp/architecture/archive/2026-07-22-legacy/agent-orchestration.md`;
|
||||
- `mvp/issues/archived/ISS-014-single-react-agent-harness-aci-ptk-refactor.md`;
|
||||
- `devflow/projects/2026-07-21-single-react-design-freeze/decisions.md`;
|
||||
- `openspec/changes/archive/2026-07-27-diagnosis-information-gain-stop-contract/`。
|
||||
|
||||
继续阅读:
|
||||
|
||||
- [Harness 入门](README.md)
|
||||
- [支付超时诊断案例](案例-从一次支付超时诊断看Harness如何控制Agent.md)
|
||||
- [Harness 主设计](Harness设计-非确定性Agent的确定性控制边界.md)
|
||||
- [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md)
|
||||
- [组件渐进式导读](components/README.md)
|
||||
@@ -0,0 +1,304 @@
|
||||
# Harness 面试速查:用一张图讲清设计
|
||||
|
||||
这篇文章是 Harness 系列的收尾,不增加新的组件和状态。它把现有设计压缩成一套可在面试中逐层展开的叙事:先用一句话定义,再用一张图说明边界,最后根据追问进入事实、停止、发布和失败设计。
|
||||
|
||||
如果只剩 10 分钟,阅读第 1、2、4 和 8 节即可。
|
||||
|
||||
## 1. 30 秒回答:什么是 Harness
|
||||
|
||||
> Harness 是包围非确定性 Agent 的确定性控制边界。Diagnosis Agent 负责提出假设、选择 Tool、解释观察并生成 Draft;Harness 负责一次 Run 的身份、deadline、预算、取消、Tool 权限和事实保管,发布前再验证引用真实性与结论支持度。它不保证 Agent 每次都找到根因,但保证执行过程有边界、失败能够收敛,并且只有可验证的内容能够发布。
|
||||
|
||||
这段回答包含三个重点:
|
||||
|
||||
```text
|
||||
Agent 负责业务推理
|
||||
Harness 负责确定性约束
|
||||
Release 决定什么可以公开
|
||||
```
|
||||
|
||||
不要一开始列 10 个职责域。面试官追问“具体怎么做”时,再沿下面的总图展开。
|
||||
|
||||
## 2. 一张图讲清完整设计
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["用户问题"] --> APP["Chat Application<br/>创建 Run、路由、持久化、SSE"]
|
||||
|
||||
subgraph CONTROL["一、运行控制"]
|
||||
CORE["Harness Core<br/>identity / deadline / budget<br/>cancel / lifecycle / retry"]
|
||||
PROGRESS["Progress Control<br/>重复、信息增益、停止"]
|
||||
end
|
||||
|
||||
subgraph REASONING["二、业务推理"]
|
||||
AGENT["Diagnosis ReAct Agent<br/>假设、选 Tool、解释 Observation、写 Draft"]
|
||||
end
|
||||
|
||||
subgraph TRUTH["三、事实边界"]
|
||||
TB["ToolBoundary<br/>授权、只读、容量、状态迁移"]
|
||||
CAN["Canonical Invocation<br/>当前 Run 的短期完整真相"]
|
||||
OBS["Model Observation<br/>模型可见的有界投影"]
|
||||
AUDIT["Metadata Audit<br/>长期可观测账本"]
|
||||
end
|
||||
|
||||
subgraph PUBLICATION["四、验证发布"]
|
||||
EG["EvidenceGuard<br/>引用是否真实"]
|
||||
SG["SemanticGuard<br/>证据是否支持结论"]
|
||||
REL["Release<br/>原报告或 SafeFallback"]
|
||||
end
|
||||
|
||||
APP --> CORE
|
||||
APP --> AGENT
|
||||
CORE -.->|"RunContext 控制句柄"| AGENT
|
||||
CORE -.->|"active / budget 门禁"| TB
|
||||
AGENT --> PROGRESS
|
||||
AGENT -->|"Tool Call"| TB
|
||||
TB --> CAN
|
||||
CAN --> OBS --> AGENT
|
||||
TB -.-> AUDIT
|
||||
AGENT -->|"DiagnosisDraft"| REL
|
||||
REL --> EG
|
||||
CAN --> EG --> SG --> REL
|
||||
PROGRESS -->|"无结论或受控停止"| REL
|
||||
REL --> APP --> U
|
||||
```
|
||||
|
||||
这张图不表示 Harness 替 Agent 安排固定步骤。Agent 自己决定查什么、何时形成 Draft;Harness 只在每个边界回答:
|
||||
|
||||
- 这一步是否属于当前 active Run;
|
||||
- 是否仍有时间、预算和调用权限;
|
||||
- Tool 事实应该保存在哪里,模型可以看到多少;
|
||||
- Agent 的引用能否从当前 Run 的真实调用中验出;
|
||||
- 现有证据是否足以支持对用户公开的结论;
|
||||
- 无法继续时,应该发布安全说明还是失败。
|
||||
|
||||
## 3. 一次请求怎样穿过 Harness
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Client
|
||||
participant APP as Chat Application
|
||||
participant CORE as Harness Core
|
||||
participant A as Diagnosis Agent
|
||||
participant I as Tool Interceptor
|
||||
participant T as ToolBoundary
|
||||
participant C as Canonical Store
|
||||
participant R as Release Pipeline
|
||||
|
||||
U->>APP: 支付服务为什么超时?
|
||||
APP->>CORE: startRun(sessionId)
|
||||
CORE-->>APP: RunContext(runId, deadline, handles)
|
||||
APP->>A: query + bounded PreviousTurn
|
||||
|
||||
loop 框架原生 ReAct
|
||||
A->>I: Tool Call Envelope
|
||||
I->>I: progress / duplicate / saturation
|
||||
I->>T: business input + RunContext
|
||||
T->>T: active / auth / readonly / budget / bytes
|
||||
T->>C: PROJECTING -> READY or ERROR
|
||||
T-->>I: canonical projected result
|
||||
I-->>A: 有界 Model Observation
|
||||
end
|
||||
|
||||
A-->>R: DiagnosisDraft
|
||||
R->>C: 回读当前 Run 的 Tool 真相
|
||||
R->>R: EvidenceGuard + SemanticGuard
|
||||
alt 验证通过
|
||||
R-->>APP: 原始安全报告 / SUCCESS
|
||||
else 证据不足或门禁失败
|
||||
R-->>APP: SafeFallback / FALLBACK
|
||||
end
|
||||
APP->>CORE: first terminal wins
|
||||
APP-->>U: content or failure + done
|
||||
```
|
||||
|
||||
讲这条链路时只需要抓住四个时间点:
|
||||
|
||||
1. Agent 运行前先建立 Run 边界。
|
||||
2. Tool 调用经过 Harness,但 Tool 选择仍由 Agent 决定。
|
||||
3. Agent 只看到有界观察,完整事实由系统独立保管。
|
||||
4. Draft 必须经过唯一发布出口,不能直接发送给用户。
|
||||
|
||||
## 4. 三个最核心的设计决策
|
||||
|
||||
### 决策一:按决策权拆分,而不是按角色拆分
|
||||
|
||||
项目早期使用过 Planner、Executor、Verifier、Composer 和 StateGraph。它提高了职责可见性,但也把一次自然 ReAct 拆成多个模型调用、Prompt、Schema 和状态搬运。
|
||||
|
||||
最终选择是:
|
||||
|
||||
| 决策 | 所有者 |
|
||||
|---|---|
|
||||
| 提出假设、选择 Tool、解释证据、撰写 Draft | Diagnosis Agent |
|
||||
| 身份、预算、取消、权限、容量、唯一终态 | Harness |
|
||||
| 引用能否被代码机械证明 | EvidenceGuard |
|
||||
| 真实证据是否支持用户可见结论 | 隔离的 SemanticGuard |
|
||||
| 发布正常报告还是 SafeFallback | Release |
|
||||
|
||||
这里不是把多个 Agent 粗暴合并成一个“大 Agent”。业务推理合并了,安全权力反而被拆得更清楚:Agent 没有 canonical raw 读取权、不能验证自己的引用,也没有最终发布权。
|
||||
|
||||
放弃的方案:在外层继续编排多 Agent 或重新实现一套 ReAct loop。
|
||||
|
||||
获得的能力:唯一业务上下文、唯一 Draft 作者、控制规则可测试。
|
||||
|
||||
付出的代价:Harness 契约和门禁必须完整,不能再依赖角色之间“互相提醒”。
|
||||
|
||||
### 决策二:系统事实、模型观察和长期审计不能共用一份数据
|
||||
|
||||
同一份 Tool 结果要服务三个互相冲突的目标:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
RAW["Tool backend raw result"] --> TB["ToolBoundary + Projector"]
|
||||
TB --> CAN["Canonical truth<br/>当前 Run、短 TTL、可验真"]
|
||||
CAN --> OBS["Model Observation<br/>白名单、有界、服务推理"]
|
||||
CAN --> GUARD["EvidenceGuard<br/>独立回读、验证引用"]
|
||||
TB -.-> META["Metadata Audit<br/>身份、状态、耗时、bytes"]
|
||||
```
|
||||
|
||||
如果 raw 直接给模型,上下文、敏感数据和 prompt injection 风险不可控;如果只保存裁剪后的 Observation,EvidenceGuard 无法独立证明 Agent 引用了真实结果;如果把完整 raw 永久写入审计库,又会制造敏感数据副本。
|
||||
|
||||
因此当前设计将数据责任拆开:
|
||||
|
||||
- Redis canonical invocation 保存当前 Run 的短期完整 Tool 真相;
|
||||
- Model Observation 只包含 Agent 下一步推理所需字段;
|
||||
- 长期 Audit 只保存 identity、状态、耗时、Token 和 bytes 等元数据;
|
||||
- EvidenceGuard 从 canonical store 验证物理真实性;
|
||||
- SemanticGuard 只在 verified evidence 上判断语义支持关系。
|
||||
|
||||
放弃的方案:一份 Tool JSON 在 Agent、Guard 和数据库之间直接流转。
|
||||
|
||||
获得的能力:模型不能靠自己看到的内容完成自证,长期审计也不必复制全部敏感正文。
|
||||
|
||||
付出的代价:每类 Tool 都需要 projector、canonical contract 和容量策略,Redis TTL 也成为验证可用性的边界。
|
||||
|
||||
### 决策三:把“如何结束”设计成一等能力
|
||||
|
||||
Agent 系统最常见的问题不只是错误,而是无法正常结束:空日志、通用知识和相似查询都可能让模型持续尝试,最后撞上预算。
|
||||
|
||||
当前 Harness 使用两套不同机制:
|
||||
|
||||
```text
|
||||
Budget:还能不能继续消耗资源
|
||||
Information Gain:继续查询是否推进诊断
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
X["一次 Tool 结果或执行问题"] --> C{"Run 还能继续?"}
|
||||
C -->|"可以"| G{"结果是否推进诊断?"}
|
||||
G -->|"GAINED"| N["继续 ReAct"]
|
||||
G -->|"连续 NO_GAIN"| S["SATURATED<br/>停止新增 Tool"]
|
||||
C -->|"不可以"| P{"已有可验证进展?"}
|
||||
S --> P
|
||||
P -->|"有"| F["SafeFallback<br/>已检查内容、限制和下一步"]
|
||||
P -->|"无"| E["FAILED<br/>不发布未验证内容"]
|
||||
N --> D{"最终 Draft 可发布?"}
|
||||
D -->|"是"| OK["SUCCESS"]
|
||||
D -->|"否"| F
|
||||
```
|
||||
|
||||
`READY + NO_EVIDENCE` 表示查询成功但当前 scope 为空,不是技术异常;`conclusion=null` 是合法 Draft,不是模型失败;`SATURATED` 只停止收集,不是 Run 终态;`FALLBACK` 表示请求被安全处理但没有正常报告,也不等于 `RunState.FAILED`。
|
||||
|
||||
放弃的方案:强制 Agent 必须给出根因,或者只依靠硬预算和全局异常结束。
|
||||
|
||||
获得的能力:证据不足可以成为诚实、可解释的产品结果,局部 Tool 故障也不必立即摧毁整个 Run。
|
||||
|
||||
付出的代价:RunState、Tool 状态、CollectionState 和 ReleaseOutcome 必须保持正交,排障不能只看一个 `status`。
|
||||
|
||||
## 5. 这套设计最难的地方是什么
|
||||
|
||||
面试中不要把难点回答成“接入了 Spring AI”或“写了很多 Interceptor”。真正困难的是确定责任边界,并让这些边界在异常和竞态下仍成立。
|
||||
|
||||
| 难点 | 核心问题 | 当前答案 |
|
||||
|---|---|---|
|
||||
| Run 归属 | 异步调用和多轮 Session 中,事实到底属于哪次执行 | 显式 `RunContext` 和 exact `runId` |
|
||||
| 事实可信 | Agent 引用的 Tool 内容如何独立验真 | canonical invocation + EvidenceGuard |
|
||||
| 结论可信 | 引用真实但推论牵强怎么办 | 隔离的 SemanticGuard |
|
||||
| 正常收敛 | 没有证据时如何避免反复查询 | Information Gain + SATURATED + ProgressSnapshot |
|
||||
| 失败一致 | 超时、预算、Tool ERROR 和断开如何对应结果 | first-terminal-wins + 唯一 Release Policy |
|
||||
| 可观测性 | 如何回放决策又不永久保存敏感正文 | metadata audit + 短期 canonical truth |
|
||||
|
||||
如果面试官只允许选一个,回答“事实可信”最能代表这套设计:它要求系统同时解决 Tool 身份、Run 归属、数据视图、证据引用和最终发布,而不是只调一个模型接口。
|
||||
|
||||
## 6. 用真实案例讲 2 分钟
|
||||
|
||||
可以使用支付超时案例:
|
||||
|
||||
> 用户要求诊断支付服务超时。Application 先创建独立 Run,并为 Router、Agent、Tool 和 Guard 共享同一套 deadline、预算和取消能力。Diagnosis Agent 自主调用知识库和日志 Tool;ToolBoundary 执行调用并把完整事实保存为当前 Run 的 canonical invocation,只把有界 Observation 返回给 Agent。Agent 根据两个 Tool 结果生成 Draft,但 Draft 没有直接发给用户。EvidenceGuard 回读 canonical store 后发现引用无法完成真实性校验,因此 Release 没有继续让模型润色或猜测,而是发布 `EVIDENCE_VALIDATION_FAILED` SafeFallback。最终数据库记录请求处理成功、ReleaseOutcome 为 FALLBACK,SSE 也完整结束,但未经验证的根因没有离开系统。
|
||||
|
||||
这个案例的价值不在于“最终失败了”,而在于证明:
|
||||
|
||||
```text
|
||||
Tool READY != 引用已验真
|
||||
引用已验真 != 结论被支持
|
||||
Agent 生成 Draft != 报告允许发布
|
||||
```
|
||||
|
||||
完整数据和过程见[支付超时诊断案例](案例-从一次支付超时诊断看Harness如何控制Agent.md)。
|
||||
|
||||
## 7. 常见追问怎样展开
|
||||
|
||||
| 面试官追问 | 回答主线 | 深入阅读 |
|
||||
|---|---|---|
|
||||
| 为什么不继续使用多 Agent? | 多角色重复实现 ReAct;改为按决策权拆分 | [设计演进](Harness设计演进-从多Agent编排到确定性控制边界.md) |
|
||||
| 为什么 Harness 不是工作流引擎? | Agent 选择下一步,Harness 只检查边界和发布资格 | [Harness 主设计](Harness设计-非确定性Agent的确定性控制边界.md) |
|
||||
| 全部组件有哪些? | 先讲四组,再按需展开 10 个职责域 | [组件全景](Harness组件全景-职责-设计原因与边界.md) |
|
||||
| Tool 结果为什么不直接给模型? | 真相、观察和长期审计有不同数据责任 | [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md) |
|
||||
| 如何防止 Agent 编造证据? | framework Tool ID、canonical store、EvidenceGuard | [证据安全链](Harness证据安全链-从引用真实到结论可发布.md) |
|
||||
| EvidenceGuard 已经通过,为什么还要 SemanticGuard? | 引用真实不等于结论被支持 | [证据安全链](Harness证据安全链-从引用真实到结论可发布.md) |
|
||||
| Agent 为什么不会无限调用 Tool? | 预算止损,信息增益负责正常收敛 | [信息增益停止](Harness信息增益停止-让无证据诊断正常收敛.md) |
|
||||
| Tool 报错是不是 Run 就失败? | 局部失败先看是否可继续及是否已有安全进展 | [失败图谱](Harness失败图谱-异常-停止-降级与终态.md) |
|
||||
| FALLBACK 算成功还是失败? | RunState、ReleaseOutcome、数据库和 SSE 是不同视图 | [生命周期与状态](Harness生命周期与状态.md) |
|
||||
| 如何验证不是纸面设计? | focused tests 证明不变量,live E2E 验证组合契约 | [支付超时诊断案例](案例-从一次支付超时诊断看Harness如何控制Agent.md) |
|
||||
| 当前还有什么限制? | 语义去重、TTL、同步取消、SemanticGuard 不确定性、Reasoning 治理 | [Harness 主设计](Harness设计-非确定性Agent的确定性控制边界.md) |
|
||||
|
||||
## 8. 面试中最容易讲错的六件事
|
||||
|
||||
### 不要说:Harness 负责安排 Agent 的执行步骤
|
||||
|
||||
应说:ReAct Agent 自己选择 Tool 和下一步,Harness 负责运行边界、事实边界和发布边界。
|
||||
|
||||
### 不要说:SemanticGuard 是第二个诊断 Agent
|
||||
|
||||
应说:它是无 Tool、无记忆、单轮二值判断的隔离审查器,不能探索事实或改写报告。
|
||||
|
||||
### 不要说:Tool 返回 SUCCESS 就找到了证据
|
||||
|
||||
应说:`InvocationStatus=READY` 只表示调用完成,还要结合 `EvidenceStatus`;存在候选证据也不代表支持结论。
|
||||
|
||||
### 不要说:FALLBACK 就是 Run 失败
|
||||
|
||||
应说:Fallback 是安全发布结果。典型证据不足场景可以是 `RunState.SUCCESS + ReleaseOutcome.FALLBACK`。
|
||||
|
||||
### 不要说:Redis 是完整的长期审计库
|
||||
|
||||
应说:Redis canonical store 保存当前 Run 的短期完整 Tool 真相;长期审计只保存有界元数据。
|
||||
|
||||
### 不要说:取消能立刻杀死所有模型调用
|
||||
|
||||
应说:当前是协作式取消;first-terminal-wins、active check 和 SSE 状态保证迟到结果不能发布,但同步 Provider 计算未必立即停止。
|
||||
|
||||
## 9. 当前设计的代价和边界
|
||||
|
||||
一套可信的面试叙事不能只讲收益,还要主动说明代价:
|
||||
|
||||
1. 正交状态较多,必须用统一 Context 防止 `SUCCESS / READY / FALLBACK` 被混读。
|
||||
2. canonical store、Projector 和 Guard 增加了实现复杂度与发布延迟。
|
||||
3. Redis TTL 过期后只能保留元数据回放,不能恢复完整 Tool 正文。
|
||||
4. 自然语言近义查询目前不能被确定性去重,只能依赖 Agent 的信息增益义务。
|
||||
5. SemanticGuard 仍是模型判断,只是被收缩到最小、隔离、无 Tool 的范围。
|
||||
6. 协作式取消保护逻辑终态和发布,不等于强制终止 Provider 计算。
|
||||
7. 信息增益阈值和各类预算仍需依靠固定评测集持续校准。
|
||||
|
||||
主动说出这些边界,会让设计从“组件介绍”变成可讨论的工程决策。
|
||||
|
||||
## 10. 最后只记住四句话
|
||||
|
||||
```text
|
||||
Agent 决定如何诊断,Harness 决定诊断必须遵守什么边界。
|
||||
系统保管 Tool 真相,模型只读取完成下一步所需的有界观察。
|
||||
引用真实与结论成立是两个问题,必须由不同门禁处理。
|
||||
Harness 不保证每次找到答案,但保证任何结束方式都诚实、可审计、不会越权发布。
|
||||
```
|
||||
|
||||
到这里,Harness 文档的主线已经闭合。需要回忆某个细节时,通过第 7 节进入专题即可,不需要重新从组件清单开始阅读。
|
||||
@@ -0,0 +1,326 @@
|
||||
# 从一次支付超时诊断看 Harness 如何控制 Agent
|
||||
|
||||
这篇文章不从组件清单开始,而是跟随一次真实请求,看 Agent 如何完成诊断,以及 Harness 在每个关键节点控制了什么。
|
||||
|
||||
先说最终结果:这次请求正常执行了两轮 Agent、调用了两个 Tool,两个 Tool 都返回了候选证据,但最终没有发布诊断结论,而是安全地返回了 Fallback。
|
||||
|
||||
这不是一次“什么都没做成”的失败。恰恰相反,它展示了 Harness 最重要的价值:**即使 Agent 已经写出答案,只要证据无法完成验真,答案就不能越过发布出口。**
|
||||
|
||||
第一次阅读只看第 1、2、8、9 和 13 节即可:先知道问题和主流程,再看为什么被挡下、用户最终收到什么。其余章节用于展开每一步的组件设计。
|
||||
|
||||
## 1. 这次诊断从什么问题开始
|
||||
|
||||
用户请求是:
|
||||
|
||||
> 支付接口最近出现超时。在给出结论前必须实际调用 `lookup_knowledge` 和 `query_logs` 各一次,日志范围使用 APPLICATION 并查询 `payment-service error slow database`;随后结束诊断,证据不足时明确说明缺口。
|
||||
|
||||
这是一个很典型的 AIOps 问题。用户看到的是“支付超时”,但可能原因很多:
|
||||
|
||||
- 数据库连接池耗尽;
|
||||
- 下游服务响应过慢;
|
||||
- Redis 或网络超时;
|
||||
- JVM、线程池或 CPU 资源异常;
|
||||
- 只是历史知识中的相似案例,并非当前生产故障。
|
||||
|
||||
如果只有 Agent,它可以查询资料、阅读日志并写出一个听起来合理的解释。但系统还必须回答:查询是否属于本次请求、结果能否被引用、引用是否支持结论,以及证据不足时应该怎样结束。
|
||||
|
||||
## 2. 先看完整故事
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q["用户报告支付超时"] --> R["创建 Run<br/>建立身份、预算和取消"]
|
||||
R --> I["Router 判定为 DIAGNOSIS"]
|
||||
I --> A1["Agent 第 1 轮<br/>选择知识库和日志 Tool"]
|
||||
A1 --> T["ToolBoundary<br/>执行、投影并保存调用真相"]
|
||||
T --> A2["Agent 第 2 轮<br/>根据观察结果生成 Draft"]
|
||||
A2 --> E["EvidenceGuard<br/>验证引用真实性"]
|
||||
E -->|"本次未通过"| F["SafeFallback<br/>不发布未经验证的根因"]
|
||||
F --> U["SSE content + done FALLBACK"]
|
||||
```
|
||||
|
||||
沿着这条线,可以把双方职责简单分开:
|
||||
|
||||
| 阶段 | Agent 在做什么 | Harness 在控制什么 |
|
||||
|---|---|---|
|
||||
| 请求开始 | 尚未参与 | 创建 Run,固定身份、deadline、预算和取消 |
|
||||
| 意图路由 | 尚未诊断 | 限制 Router 输入、调用次数和重试 |
|
||||
| 选择 Tool | 提出查询计划 | 检查 Run、进展协议、重复 scope 和 Tool 权限 |
|
||||
| Tool 返回 | 阅读有界观察 | 保存 canonical truth,限制 bytes,只投影必要字段 |
|
||||
| 生成 Draft | 写出候选报告 | 限制输出结构和大小,Draft 尚不可发布 |
|
||||
| 验证发布 | 不再拥有决定权 | 验引用、验语义,决定报告或 Fallback |
|
||||
| 请求结束 | 执行结束 | 固化终态、持久化结果、记录 Trace 和 Token |
|
||||
|
||||
下面逐步展开。
|
||||
|
||||
## 3. 第一步:Harness 先创建 Run
|
||||
|
||||
请求进入 `ChatApplicationUseCase` 后,系统不会立即调用 Agent,而是先创建本次执行的 `RunContext`。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
S["sessionId<br/>多轮对话容器"] --> R["runId<br/>本次独立执行"]
|
||||
R --> D["deadline"]
|
||||
R --> B["RunBudget"]
|
||||
R --> C["RunCancellation"]
|
||||
R --> L["RunLifecycle"]
|
||||
R --> P["ProgressTracker"]
|
||||
```
|
||||
|
||||
为什么不能只使用 sessionId?因为同一个会话可以连续提出多个问题。Tool Call、Agent Step、Evidence 和最终结果都必须属于某一个精确 Run,否则上一轮日志可能被下一轮报告误引用。
|
||||
|
||||
为什么要在 Agent 之前创建预算和取消能力?因为 Router、Agent、Tool 和 SemanticGuard 都会消耗时间或资源。只有共享同一个 RunContext,系统才能统一回答“还能不能继续执行”。
|
||||
|
||||
这一阶段最重要的不是创建了几个对象,而是确定了一条规则:
|
||||
|
||||
> 后续任何模型调用、Tool 调用和发布动作,都必须证明自己仍属于这个 active Run。
|
||||
|
||||
## 4. 第二步:Router 只决定走哪条路
|
||||
|
||||
用户问题先经过 Intent Router。它只判断请求属于:
|
||||
|
||||
```text
|
||||
SYSTEM_CHAT
|
||||
KNOWLEDGE_QUERY
|
||||
DIAGNOSIS
|
||||
```
|
||||
|
||||
本次结果是 `DIAGNOSIS`,于是 `ChatApplicationUseCase` 将同一个 RunContext 交给 `DiagnosisChatExecutor`。
|
||||
|
||||
Router 不读取完整历史,不调用业务 Tool,也不尝试回答支付超时的根因。这样设计是为了避免“路由”在不知不觉中变成一个简化版诊断 Agent。
|
||||
|
||||
即便只是路由,Harness 仍然控制它的输入大小、timeout、Token、显式 retry 和 Trace。因为一次隐藏的模型重试,同样会造成成本和延迟无法解释。
|
||||
|
||||
## 5. 第三步:Agent 决定查什么,Harness 决定能不能查
|
||||
|
||||
Diagnosis Agent 第 1 轮读取用户问题和服务端注册的 Tool schema,随后发出两个 Tool Call:
|
||||
|
||||
```text
|
||||
lookup_knowledge
|
||||
query_logs
|
||||
```
|
||||
|
||||
这里仍然由 Agent 负责业务判断:它认为需要先查排障知识,再查看应用日志。Harness 不会用固定工作流替它安排“先 RAG、再日志”。
|
||||
|
||||
但 Agent 发出 Tool Call 不等于后端会立即执行。调用先经过 `HarnessToolInterceptor`:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
TC["Agent Tool Call"] --> A{"Run 仍 active?"}
|
||||
A --> P{"上一轮进展协议正确?"}
|
||||
P --> D{"scope 是否重复或已饱和?"}
|
||||
D --> B{"ToolBoundary 权限与预算允许?"}
|
||||
B -->|"全部通过"| X["执行 Tool backend"]
|
||||
A -->|"否"| STOP["拒绝调用"]
|
||||
P -->|"否"| STOP
|
||||
D -->|"否"| STOP
|
||||
B -->|"否"| STOP
|
||||
```
|
||||
|
||||
这就是 Harness 与工作流引擎的区别:
|
||||
|
||||
- 工作流引擎决定下一步必须调用什么;
|
||||
- Harness 不替 Agent 选下一步,只检查这一步是否满足执行条件。
|
||||
|
||||
## 6. 第四步:Tool 返回的不是一份数据,而是三种视图
|
||||
|
||||
两个 Tool 都通过 `ToolBoundary`。它统一完成 Run 归属、Tool 授权、只读约束、调用预算、结果 bytes、canonical 状态和长期审计检查。
|
||||
|
||||
后端原始结果不会直接返回给 Agent,而是形成三种用途不同的视图:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
RAW["Tool Raw Result"] --> C["Canonical Invocation<br/>当前 Run 的短期完整真相"]
|
||||
C --> CV["Control View<br/>状态、证据数量、scope"]
|
||||
C --> MO["Model Observation<br/>Agent 可见的有界内容"]
|
||||
C --> EG["EvidenceGuard<br/>独立验真来源"]
|
||||
C -.-> AU["Durable Audit<br/>长期只保存元数据"]
|
||||
```
|
||||
|
||||
为什么要分开?
|
||||
|
||||
- Agent 需要的是能继续推理的少量事实,不需要 Redis key、预算阈值和全部 raw;
|
||||
- EvidenceGuard 需要独立于 Agent 上下文读取真实调用记录;
|
||||
- 线上审计需要状态、耗时和 bytes,但不应该永久复制日志正文。
|
||||
|
||||
本次真实 Run 中:
|
||||
|
||||
| Tool | InvocationStatus | EvidenceStatus | 说明 |
|
||||
|---|---|---|---|
|
||||
| `lookup_knowledge` | `READY` | `EVIDENCE_FOUND` | 找到了候选排障知识 |
|
||||
| `query_logs` | `READY` | `EVIDENCE_FOUND` | 返回了候选日志内容,但数据源明确为 Mock |
|
||||
|
||||
这两个结果只能证明“查询成功并返回候选内容”,不能证明“已经找到支付超时的生产根因”。尤其 Mock 日志不能被包装成生产环境事实。
|
||||
|
||||
## 7. 第五步:Agent 写出 Draft,但 Draft 还不是报告
|
||||
|
||||
Tool Observation 回到 ReAct 上下文后,Agent 进入第 2 轮模型调用并生成结构化 `DiagnosisDraft`。
|
||||
|
||||
真实 Trace 中可以看到:
|
||||
|
||||
```text
|
||||
Agent Step 0:has_text=false,发出 lookup_knowledge 和 query_logs
|
||||
Agent Step 1:has_text=true,不再调用 Tool
|
||||
```
|
||||
|
||||
到这一步,Agent 的职责已经完成:它收集了观察结果,并根据这些内容写出候选结论、分析和限制。
|
||||
|
||||
但 Harness 把它称为 Draft,而不是 Report。原因是模型输出还没有证明:
|
||||
|
||||
- 引用的 `tool_call_id` 属于当前 Run;
|
||||
- 引用对应的调用已经 `READY`;
|
||||
- 引用内容确实存在于 canonical result;
|
||||
- 真实证据足以支持用户可见结论。
|
||||
|
||||
如果此时直接通过 SSE 返回,前面所有 canonical store 和 Guard 设计都失去了意义。
|
||||
|
||||
## 8. 第六步:EvidenceGuard 挡住了这次发布
|
||||
|
||||
Release Pipeline 首先调用 EvidenceGuard。它不调用模型,而是通过代码检查 Draft 的结构、引用闭包、Run 归属、Tool 状态和 evidence 内容。
|
||||
|
||||
本次结果没有通过 EvidenceGuard。公开 SSE 没有泄露具体内部违规字段,只给出了稳定结果:
|
||||
|
||||
```text
|
||||
FallbackType = EVIDENCE_VALIDATION_FAILED
|
||||
message = 当前证据无法完成真实性校验,无法确认根因
|
||||
verified_sources = []
|
||||
```
|
||||
|
||||
这意味着系统不能把 Agent Draft 中的引用提升为已验真证据。既然第一层确定性验证都没有通过,后续 SemanticGuard 就没有可信输入,也不应继续讨论结论是否“语义上合理”。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D["DiagnosisDraft"] --> E{"EvidenceGuard"}
|
||||
E -->|"通过"| S["SemanticGuard<br/>检查证据是否支持结论"]
|
||||
E -->|"本次未通过"| F["EVIDENCE_VALIDATION_FAILED"]
|
||||
S -->|"SUPPORTED"| OK["发布原 Draft"]
|
||||
S -->|"UNSUPPORTED"| SF["语义不支持 Fallback"]
|
||||
```
|
||||
|
||||
这里最值得注意的是:两个 Tool 都成功了,Agent 也成功输出了 Draft,但 Release 仍然拒绝发布。
|
||||
|
||||
```text
|
||||
Tool READY
|
||||
不等于 EvidenceGuard 通过
|
||||
EvidenceGuard 通过
|
||||
不等于 SemanticGuard SUPPORTED
|
||||
SemanticGuard SUPPORTED
|
||||
才可能发布正常诊断报告
|
||||
```
|
||||
|
||||
## 9. 第七步:Fallback 是安全结果,不是技术失败
|
||||
|
||||
EvidenceGuard 未通过后,`SafeFallbackFactory` 由代码构造公开内容。它没有让另一个模型“重新写得保守一点”,也没有复用 Draft 中未经验证的根因。
|
||||
|
||||
用户最终收到:
|
||||
|
||||
```json
|
||||
{
|
||||
"content_type": "SAFE_FALLBACK",
|
||||
"fallback": {
|
||||
"type": "EVIDENCE_VALIDATION_FAILED",
|
||||
"conclusion": null,
|
||||
"message": "当前证据无法完成真实性校验,无法确认根因",
|
||||
"verified_sources": [],
|
||||
"limitations": ["证据引用校验未通过"],
|
||||
"next_steps": ["重新收集当前诊断范围内的证据后再发起诊断"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
SSE 顺序完整结束:
|
||||
|
||||
```text
|
||||
metadata
|
||||
-> status ROUTING
|
||||
-> status DIAGNOSIS_RUNNING
|
||||
-> status SAFETY_VALIDATING
|
||||
-> content SAFE_FALLBACK
|
||||
-> done FALLBACK
|
||||
```
|
||||
|
||||
数据库记录则是:
|
||||
|
||||
```text
|
||||
diagnosis_run.status = SUCCESS
|
||||
diagnosis_run.release_outcome = FALLBACK
|
||||
```
|
||||
|
||||
两者并不矛盾:`status=SUCCESS` 表示请求被系统正常、安全地处理完毕;`release_outcome=FALLBACK` 表示没有正常诊断结论可以发布。
|
||||
|
||||
## 10. 这次 Run 最终留下了什么
|
||||
|
||||
| 项目 | 真实结果 |
|
||||
|---|---|
|
||||
| sessionId | `mvp-demo-payment-timeout-stage7-20260722-1741` |
|
||||
| runId | `363f481c-33b8-42e7-8699-428a6ec61806` |
|
||||
| 总耗时 | 35,996 ms |
|
||||
| 总 Token | 11,764 |
|
||||
| Agent Step | 2 |
|
||||
| Tool Invocation | 2 |
|
||||
| Tool 结果 | 两次均 `READY / EVIDENCE_FOUND` |
|
||||
| 最终内容 | `SAFE_FALLBACK` |
|
||||
| ReleaseOutcome | `FALLBACK` |
|
||||
| FallbackType | `EVIDENCE_VALIDATION_FAILED` |
|
||||
|
||||
长期审计保留的是 Run、Agent Step、Tool 状态、耗时和 Token 等有界信息。模型 Thought 保持为空,完整 Tool raw 也不会因为排障方便就永久进入普通 Trace。
|
||||
|
||||
因此这次 Run 虽然没有根因结论,却仍然可以回答:谁执行了什么、用了多少资源、在哪一层停止、为什么没有发布以及用户最终收到了什么。
|
||||
|
||||
## 11. 如果验证通过,后续会发生什么
|
||||
|
||||
本次真实路径在 EvidenceGuard 结束。为了理解完整设计,可以继续看通过分支:
|
||||
|
||||
1. EvidenceGuard 生成 `VerifiedEvidenceSnapshot`;
|
||||
2. SemanticGuard 只接收用户问题、Draft 语义和已验真证据;
|
||||
3. SemanticGuard 输出 `SUPPORTED / UNSUPPORTED`,不能改写报告;
|
||||
4. 只有 `SUPPORTED` 才发布 Agent 原 Draft;
|
||||
5. `UNSUPPORTED` 或 Guard 不可用时发布对应 SafeFallback。
|
||||
|
||||
这条分支不是本次支付超时 Run 的真实结果,因此这里只说明当前代码协议,不把它写成已经发生的事实。
|
||||
|
||||
## 12. 这次案例体现了哪些设计决策
|
||||
|
||||
### 决策一:Agent 拥有推理权,不拥有发布权
|
||||
|
||||
Agent 可以选择 Tool、解释 Observation 和撰写 Draft,但不能决定 Draft 是否直接交给用户。否则模型既是作者又是最终审批者。
|
||||
|
||||
### 决策二:Tool 成功与结论成立必须分层
|
||||
|
||||
`READY + EVIDENCE_FOUND` 只描述一次 Tool 调用。EvidenceGuard 和 SemanticGuard 分别处理引用真实性与结论支持度,避免一个含糊的 `SUCCESS` 贯穿全链路。
|
||||
|
||||
### 决策三:系统保留事实,模型只拿观察
|
||||
|
||||
Canonical Invocation 为当前 Run 保存完整真相;Agent Observation 只服务推理。Guard 不依赖模型看到的裁剪版本进行自证。
|
||||
|
||||
### 决策四:证据不足也应正常结束
|
||||
|
||||
不是所有诊断都能找到根因。Fallback 把“不能安全下结论”转换成稳定产品行为,而不是抛出技术异常或让模型猜一个答案。
|
||||
|
||||
### 决策五:审计记录控制事实,不复制思维过程
|
||||
|
||||
系统需要可回放,但不需要永久存储 Chain of Thought、完整 Prompt 和所有 raw payload。可观测性本身也必须有数据边界。
|
||||
|
||||
## 13. 用一句话复述这次案例
|
||||
|
||||
> 用户提出支付超时问题后,Harness 为请求建立独立 Run,允许 Diagnosis Agent 自主调用知识库和日志 Tool,并将完整调用事实保存在系统边界内;Agent 根据有界 Observation 写出 Draft 后,EvidenceGuard 发现引用无法完成真实性校验,Release 因此拒绝发布根因,转而返回确定性的 SafeFallback。整个请求正常结束、过程可回放,但未经验证的结论没有离开系统。
|
||||
|
||||
理解这句话,就理解了 Harness 的核心:**它不保证 Agent 每次都找到答案,但保证系统只对能够证明的答案负责。**
|
||||
|
||||
## 14. 事实来源与延伸阅读
|
||||
|
||||
本文案例数据来自:
|
||||
|
||||
- `mvp/demo/requests/payment-timeout-chat.json`;
|
||||
- `mvp/demo/output/stage7-20260722-1741/chat-sse.txt`;
|
||||
- `mvp/demo/output/stage7-20260722-1741/trace-response.json`;
|
||||
- `devflow/projects/2026-07-22-single-react-cleanup-e2e/evidence.md`。
|
||||
|
||||
继续理解具体机制:
|
||||
|
||||
- [Harness 入门](README.md)
|
||||
- [组件渐进式导读](components/README.md)
|
||||
- [Tool 双视图](Harness-Tool双视图-从原始结果到可验证证据.md)
|
||||
- [证据安全链](Harness证据安全链-从引用真实到结论可发布.md)
|
||||
- [生命周期与状态](Harness生命周期与状态.md)
|
||||
|
||||
需要查看另一条真实 `SUCCESS` 路径及详细 Token、RAG 和 Trace 数据,阅读[一次诊断全流程 E2E 导读](../diagnosis/一次诊断全流程-E2E导读.md)。
|
||||
Reference in New Issue
Block a user