Files
SuperBizAgent-java/mvp/engineering/harness/Harness失败图谱-异常-停止-降级与终态.md
T

24 KiB
Raw Blame History

Harness 失败图谱:异常、停止、降级与终态如何对应

更新日期:2026-07-30

主题:失败分类、停止决策、安全降级、Run 终态与发布结果

术语与状态:CONTEXT.md · Harness生命周期与状态.md

在 Harness 中,“没有找到根因”“Tool 报错”“证据不够”“超时”和“客户端断开”不是同一种失败。如果把它们都压成一个 FAILED,用户无法知道系统是否完成了有效检查,开发者也无法判断应该重试、换 Tool、发布 Fallback,还是立即终止。

这篇文章不从状态枚举开始,而是用三个问题建立一张失败图谱:

  1. 问题发生后,这次 Run 还能继续吗?
  2. 已经产生的内容中,有没有可验证的安全事实?
  3. 最终能公开正常报告、安全说明,还是只能失败?

第一次阅读只看第 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

推荐顺序:

  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 单内容、终态和断开规则。

继续阅读: