24 KiB
Harness 失败图谱:异常、停止、降级与终态如何对应
更新日期:2026-07-30
主题:失败分类、停止决策、安全降级、Run 终态与发布结果
术语与状态:CONTEXT.md · Harness生命周期与状态.md
在 Harness 中,“没有找到根因”“Tool 报错”“证据不够”“超时”和“客户端断开”不是同一种失败。如果把它们都压成一个 FAILED,用户无法知道系统是否完成了有效检查,开发者也无法判断应该重试、换 Tool、发布 Fallback,还是立即终止。
这篇文章不从状态枚举开始,而是用三个问题建立一张失败图谱:
- 问题发生后,这次 Run 还能继续吗?
- 已经产生的内容中,有没有可验证的安全事实?
- 最终能公开正常报告、安全说明,还是只能失败?
第一次阅读只看第 1、2、8、10 和 13 节即可。先掌握判断方法和几个典型场景,不需要记住全部状态。
1. 先记住:Harness 的失败处理不是“捕获异常”
假设一次支付超时诊断中连续发生这些事情:
日志 Tool 查询成功,但指定时间段没有记录
RAG Tool 第一次调用网络超时
Agent 换了一个查询范围,找到一条可验证的配置事实
Agent 据此声称“数据库连接池耗尽”
EvidenceGuard 发现报告引用的证据并不支持这个结论
如果只看局部,既出现了空结果、技术异常、有效事实,又出现了发布门禁失败。系统不能把其中任何一个事件直接等同于整次 Run 的终态。
Harness 真正要做的是分层决策:
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 结果使用两个正交状态:
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 的支付日志为空,不代表全天没有超时,也不代表日志系统之外没有故障。
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; - 已完成有限检查,但证据只够描述现象,不够支持根因。
这些场景不应该伪装成系统异常。只要请求被正常处理并形成安全说明,就可以是:
RunState.SUCCESS + ReleaseOutcome.FALLBACK
为什么不把 NO_EVIDENCE 设计成异常
如果空结果抛异常,系统会产生三个问题:
- Agent 无法区分“查询为空”和“查询服务不可用”;
- 监控会把正常业务分布统计成技术故障;
- Release 无法向用户解释已经检查的范围。
因此放弃了“Tool 无数据即失败”的简化方案,代价是状态维度增加,但换来了可解释的控制语义。
3. 第二类:技术异常可能可以重试
并非所有技术异常都应立即结束 Run。对于瞬时、明确、可能恢复的 Provider 故障,Harness 可以执行有限的显式重试。
当前重试策略遵循两个原则:
只有稳定分类为可恢复的技术失败才重试
所有 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 | 不是无限修复循环 |
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。
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
这里不能建立一条简单映射:
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 并不表示用户一定能看到这份报告。发布前还要经过两类门禁:
EvidenceGuard:引用是否真实、是否属于当前 Run、是否来自 READY invocation
SemanticGuard:这些真实证据是否支持用户可见结论
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 会把证据收集状态切换为:
CollectionState.SATURATED
可能的停止原因包括:
INFORMATION_SATURATED
PROGRESS_PROTOCOL_VIOLATED
BUDGET_LIMIT_REACHED
SATURATED 只表示不再允许新的 evidence Tool,不是 Run 终态。Harness 仍然要给 Agent 一次机会输出 Draft,或者根据 canonical records 投影 ProgressSnapshot。
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 的终态只有:
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,但发布结果取决于是否已有安全进展:
有 ProgressSnapshot -> ReleaseOutcome.FALLBACK
没有安全进展 -> ReleaseOutcome.FAILED
这说明 RunState 和 ReleaseOutcome 不能一一对应。
客户端断开
客户端断开意味着公开通道已经不存在,Run 转为 CANCELLED。取消是协作式的:已经发出的同步 Provider 调用可能无法被物理中止,但返回后必须经过 active check 和 SSE state 检查,迟到结果不能再发布。
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;
- 推荐补充的上下文或下一步检查。
它不能包含未经支持的根因结论。
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 没有一条能容纳全部语义的总状态机。一次请求结束后,至少要区分四个观察面:
flowchart LR
R["RunState<br/>执行为什么停止"] --> X["一次请求"]
O["ReleaseOutcome<br/>最终公开什么"] --> X
D["diagnosis_run.status<br/>请求是否被安全处理"] --> X
S["SSE 事件<br/>客户端实际收到什么"] --> X
RunState:执行为什么停止
它由 Harness Core 拥有,表达正常完成、内部失败、取消、超时或预算耗尽。
ReleaseOutcome:最终公开什么
SUCCESS / FALLBACK / FAILED / CANCELLED
它由 Release 决定,表达正常诊断报告、安全降级、失败或取消。
数据库 status:请求是否被安全处理
JpaChatRunStore 当前映射为:
| ReleaseOutcome | diagnosis_run.status |
|---|---|
SUCCESS |
SUCCESS |
FALLBACK |
SUCCESS |
FAILED |
FAILED |
CANCELLED |
CANCELLED |
因此:
status=SUCCESS + release_outcome=FALLBACK
表示请求被正常、安全地处理并发布了降级内容,不表示系统找到了根因。
只有 DIAGNOSIS + ReleaseOutcome.SUCCESS + published_result 才能进入下一轮 PreviousTurn。Fallback 不进入后续上下文,避免把“证据不足”当作已确认结论继续传播。
SSE:客户端实际收到什么
公开事件顺序是:
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. 排障时按什么顺序看
面对“用户为什么没有看到诊断结论”,不要先搜索所有异常日志。按发布结果向前追踪更容易定位:
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
推荐顺序:
- 看 SSE 是
content、failure,还是连接提前断开。 - 看
release_outcome和FallbackType,确认是正常报告、降级还是失败。 - 看 Run termination,区分正常完成、超时、预算耗尽和取消。
- 看 Release、EvidenceGuard、SemanticGuard Trace,确认原报告为什么不能发布。
- 看 Tool 的
InvocationStatus + EvidenceStatus,不要只看一个success。 - 最后查看 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. 当前边界与代价
- 多套正交状态提高了准确性,也增加了学习成本,必须通过 Context、映射表和 Trace 查询规范维持统一口径。
- 内存中的
RunTermination.state/reason当前没有完整独立持久化;精确判断TIMED_OUT / BUDGET_EXHAUSTED仍需结合 Trace、异常路径和预算记录。 - 协作式取消能阻止迟到发布,但不保证立即终止已经发出的同步 Provider 计算。
- Tool ERROR 后是否继续由 Agent 在 Harness 门禁内选择,因此模型可能做出次优恢复动作;硬预算和停止协议负责限制损失。
- 有安全进展才能 Fallback 的策略会让部分请求直接失败,但这是避免发布未验证内容的主动取舍。
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 单内容、终态和断开规则。
继续阅读: