17 KiB
Harness 异常处理:Loop 内 vs Loop 外
日期:2026-07-30
范围:诊断路径(intent=DIAGNOSIS)上 ReactAgent ReAct 循环内外的异常、受控停止、降级与终态
读者:已读 E2E 全流程 / 运行控制,需要把「失败时系统到底怎么走」串成一张图
关联文档:
- Harness 失败图谱(失败类型总览)
- Harness 生命周期与状态(RunState / CollectionState)
- 信息增益停止(SATURATED / STOP_REQUIRED)
0. 先记住一张总图
flowchart TB
subgraph app["Application 编排"]
UC[ChatApplicationUseCase]
EX[DiagnosisChatExecutor]
end
subgraph agent["Agent 用例"]
UC2[DiagnosisAgentUseCase]
CE[controlledExecution]
RF[recoverInvalidDraft]
end
subgraph loop["ReactAgent ReAct Loop"]
MI[HarnessModelInterceptor]
LLM[ChatModel]
TI[HarnessToolInterceptor]
TB[ToolBoundary]
end
subgraph release["Release"]
REL[DiagnosisReleaseUseCase]
FB[SafeFallback / FALLBACK]
OK[SUCCESS 报告]
end
UC --> EX
EX --> UC2
UC2 -->|agent.call| loop
MI --> LLM
LLM -->|tool_call| TI
TI --> TB
TB -->|observation| LLM
loop -->|受控异常穿出| CE
CE -->|stopped draft=null| REL
loop -->|正常返回坏 draft| UC2
UC2 -->|DiagnosisAgentOutputException| RF
RF -->|有 observedFacts| REL
REL --> FB
REL --> OK
CE -->|取消/超时再抛| UC
RF -->|无 facts 再抛| UC
一句话:
| 区域 | 异常怎么处理 |
|---|---|
| Loop 内 | 多数变成 tool/model 侧 observation 或拦截;少数 受控异常穿出 loop |
| Loop 边界 | controlledExecution:受控停止 → stopped;不可控 → 包装/再抛 |
| Loop 外(Executor) | recoverInvalidDraft:坏 draft + 有事实 → FALLBACK;否则失败 |
| Release | 有 draft 走 Guard;无 draft / 非法 draft 走专用降级 |
| Application | 未消化异常 → ChatFailureCode + Run 终态落库 |
1. 状态有三层,不要混
异常处理会同时碰到三套状态,职责不同:
flowchart LR
subgraph core["Core 执行面"]
RS[RunState<br/>RUNNING / SUCCESS / FAILED<br/>CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED]
end
subgraph prog["Progress 收集面"]
CS[CollectionState<br/>COLLECTING / SATURATED]
SR[DiagnosisStopReason]
end
subgraph out["对外发布面"]
RO[ReleaseOutcome<br/>SUCCESS / FALLBACK / FAILED / CANCELLED]
FT[FallbackType]
CF[ChatFailureCode]
end
RS -.->|"预算耗尽可并存"| RO
SR -.->|"有 facts 常映射"| FT
RO -.->|"失败粗码"| CF
| 层 | 枚举 | 回答的问题 |
|---|---|---|
| Core | RunState |
这次 Run 技术上还能不能继续? |
| Progress | DiagnosisStopReason / CollectionState |
证据收集为何停、是否已饱和? |
| 发布 | ReleaseOutcome / FallbackType |
用户看到报告、降级还是失败? |
| 协议失败 | ChatFailureCode |
SSE failure 的粗粒度原因? |
典型组合:
| 场景 | RunState | StopReason | ReleaseOutcome |
|---|---|---|---|
| 正常成功 | SUCCESS | — | SUCCESS |
| 信息饱和且有事实 | 常仍可 SUCCESS 收尾* | INFORMATION_SATURATED | FALLBACK / INSUFFICIENT_EVIDENCE |
| 预算耗尽且有事实 | BUDGET_EXHAUSTED | BUDGET_LIMIT_REACHED | FALLBACK / INSUFFICIENT_EVIDENCE |
| 预算耗尽且无事实 | BUDGET_EXHAUSTED | BUDGET_LIMIT_REACHED | FAILED |
| 客户端断开 | CANCELLED | — | CANCELLED / 可能无 done |
*饱和后若 Agent 合法写完 draft 并过 Guard,也可能 SUCCESS;若 stopped 无 draft 则走 FALLBACK。
2. 架构位置:异常在哪一层被「接住」
flowchart TB
subgraph L0["L0 协议"]
CTRL[ChatController / SSE]
end
subgraph L1["L1 Application"]
APP[ChatApplicationUseCase<br/>统一 catch → ChatFailureCode]
DEX[DiagnosisChatExecutor<br/>recoverInvalidDraft]
end
subgraph L2["L2 Agent 用例"]
DAU[DiagnosisAgentUseCase<br/>controlledExecution]
end
subgraph L3["L3 框架 Loop"]
RA[ReactAgent.call]
end
subgraph L4["L4 Interceptor / Boundary"]
MI[ModelInterceptor]
TI[ToolInterceptor]
TB[ToolBoundary]
CORE[DiagnosisHarnessCore]
end
CTRL --> APP --> DEX --> DAU --> RA
RA --> MI --> CORE
RA --> TI --> TB --> CORE
MI -.->|BudgetExceeded / RunAborted 上抛| DAU
TI -.->|多数 error observation 留在 loop| RA
TI -.->|CollectionStopped 上抛| DAU
DAU -.->|stopped| DEX
DAU -.->|Draft 契约异常| DEX
DEX -.->|未恢复| APP
3. Loop 内:一次 ReAct 轮次里发生什么
3.1 正常成功路径(对照)
sequenceDiagram
participant DAU as DiagnosisAgentUseCase
participant RA as ReactAgent
participant MI as ModelInterceptor
participant LLM as ChatModel
participant TI as ToolInterceptor
participant TB as ToolBoundary
DAU->>RA: agent.call(input, config)
RA->>MI: interceptModel
MI->>MI: beforeModelCall 预算闸
MI->>LLM: handler.call
LLM-->>MI: tool_call
MI-->>RA: ModelResponse
RA->>TI: interceptToolCall
TI->>TI: 协议/重复/饱和检查
TI->>TB: invoke
TB-->>TI: READY + agentResult
TI-->>RA: 投影 observation
RA->>MI: interceptModel 第 2 轮
MI->>LLM: 写 Draft
LLM-->>MI: 文本 JSON
MI-->>RA: ModelResponse
RA-->>DAU: AssistantMessage
DAU->>DAU: 解析 DiagnosisDraft
DAU-->>DAU: completed(draft, progress)
3.2 Loop 内:Model 路径异常(会穿出 loop)
触发点:HarnessModelInterceptor.interceptModel
beforeModelCall / handler.call / checkActive
→ BudgetExceededException
→ RunAbortedException(已终态、超时、取消)
→ 其它 RuntimeException(供应商错误等)
sequenceDiagram
participant RA as ReactAgent
participant MI as ModelInterceptor
participant Core as DiagnosisHarnessCore
participant DAU as DiagnosisAgentUseCase
RA->>MI: interceptModel(第 N 次模型)
MI->>Core: beforeModelCall
Core-->>MI: throw BudgetExceeded / RunAborted
Note over MI: 不吞异常,不转 observation
MI-->>RA: 异常上抛
RA-->>DAU: agent.call 失败
DAU->>DAU: controlledExecution(e)
| 异常 | Loop 内是否消化 | 穿出后 |
|---|---|---|
BudgetExceededException |
否 | → stopped(BUDGET_LIMIT_REACHED) |
RunAbortedException(BUDGET_EXHAUSTED) |
否 | → 同上 |
RunAbortedException(CANCELLED/TIMED_OUT/…) |
否 | controlledExecution 再抛 → Application |
| 其它未识别 | 否 | → DiagnosisAgentOutputException(EXECUTION_FAILED) |
3.3 Loop 内:Tool 路径(多数不穿出)
触发点:HarnessToolInterceptor.interceptToolCall + ToolBoundary
flowchart TD
TC[收到 tool_call] --> SUP{是否证据工具?}
SUP -->|否| H[handler.call 旁路]
SUP -->|是| SAT{已 SATURATED 且 stop 已交付?}
SAT -->|是| EX[throw DiagnosisCollectionStoppedException]
SAT -->|否| PARSE[解析 Envelope / previous_observation]
PARSE -->|协议违规| REP[可修复 error observation<br/>或连续违规后 stop]
PARSE --> DUP{重复 scope?}
DUP -->|是| DUPR[DUPLICATE_SCOPE observation]
DUP -->|否| INV[ToolBoundary.invoke]
INV -->|READY| OBS[投影 modelObservation 回注]
INV -->|BUDGET_EXHAUSTED 等 error| ERR[error observation<br/>markBudgetLimitReached]
EX --> OUT[穿出 ReactAgent loop]
OBS --> LOOP[留在 loop,模型继续]
ERR --> LOOP
REP --> LOOP
DUPR --> LOOP
设计选择:
| 情况 | 策略 | 原因 |
|---|---|---|
| 工具执行失败、投影失败、结果过大 | error observation 留在 loop | 给模型一次感知/改写机会,不立刻整 run 崩 |
| 预算在 ToolBoundary 触顶 | 先 error observation + progress 标记预算 | 本轮不强制撕开 loop;下一轮 model 的 beforeModelCall 会硬闸 |
| 信息饱和后仍要工具 | DiagnosisCollectionStoppedException 穿出 |
收集已无价值,禁止空转 |
| 协议违规(未达阈值) | repairable error observation | 要求模型补 previous_observation 等 |
ToolBoundary 对预算的处理(吞异常 → 错误码,不抛给框架):
catch (RunAbortedException | BudgetExceededException)
→ ToolBoundaryResult.error(BUDGET_EXHAUSTED | RUN_INACTIVE)
4. Loop 边界:controlledExecution
位置:DiagnosisAgentUseCase,包住 agent.call(...)。
职责:把「Harness 约定的受控停止」从异常栈(含 cause 链)捞出来,转成 DiagnosisAgentExecution.stopped;其余失败交给外层。
flowchart TD
E[catch Exception from agent.call] --> F1{cause 含 CollectionStopped?}
F1 -->|是| S1[stopped progress + stopReason]
F1 -->|否| F2{cause 含 RunAborted?}
F2 -->|是且 BUDGET_EXHAUSTED| S2[markBudgetLimitReached<br/>stopped BUDGET_LIMIT_REACHED]
F2 -->|是且其它终态| R1[再抛 RunAborted]
F2 -->|否| F3{BudgetExceeded 或 lifecycle 已预算耗尽?}
F3 -->|是| S2
F3 -->|否| N[return null]
N --> W[包装 DiagnosisAgentOutputException EXECUTION_FAILED]
S1 --> RET[正常 return 给 Executor]
S2 --> RET
| 输入信号 | 输出 |
|---|---|
DiagnosisCollectionStoppedException |
stopped(stopReason),draft=null |
| 预算耗尽类 | stopped(BUDGET_LIMIT_REACHED) |
取消 / 超时类 RunAborted |
不转 stopped,再抛 |
| 未知 | null → 包装执行失败异常 |
结果形态:
completed(draft, progress) // 有合法草稿
stopped(progress, stopReason) // 无草稿,仅有进度快照
5. Loop 外:坏 Draft 与 recoverInvalidDraft
时机:agent.call 已经返回(或解析阶段),最终文本 不是合法 DiagnosisDraft。
位置:DiagnosisChatExecutor catch DiagnosisAgentOutputException。
flowchart TD
A[DiagnosisAgentOutputException] --> K{isDraftContractFailure?}
K -->|否 EXECUTION_FAILED| P1[再抛 → Application FAILED]
K -->|是 EMPTY/INVALID_JSON/SCHEMA| H{hasObservedFacts?}
H --> AUD[审计 agentDraftInvalid]
AUD --> H2{hasObservedFacts?}
H2 -->|否| P1
H2 -->|是| SSE[status SAFETY_VALIDATING]
SSE --> RID[releaseInvalidDraft progress]
RID --> FB[FALLBACK INSUFFICIENT_EVIDENCE]
要点(与常见误解对照):
| 误解 | 实际 |
|---|---|
| recover 里再验 draft 完不完整 | draft 合法性已在 Agent 用例判定;此处只看 异常 Kind |
| hasProgress = 有过 tool_call | = progress.observedFacts 非空(可发布观察事实) |
| 降级会带上坏 JSON | 丢弃非法正文,只根据 progress 生成 SafeFallback |
专用发布:DiagnosisReleaseUseCase.releaseInvalidDraft
- 不再跑 Evidence/Semantic Guard(没有可校验 draft)
- 要求
hasObservedFacts(),否则IllegalStateException - 产出
FallbackType.INSUFFICIENT_EVIDENCE
6. Release:按 execution 形态分叉
flowchart TD
EX[DiagnosisAgentExecution] --> D{draft == null?}
D -->|是 stopped| CS[releaseControlledStop<br/>需有 observedFacts]
D -->|否| C{conclusion == null?}
C -->|是| NC[releaseNoConclusion<br/>部分 Evidence + progress]
C -->|否| RC[releaseConclusion<br/>Evidence → 可选 Repair → Semantic]
CS --> FB[FALLBACK]
NC --> FB
RC -->|SUPPORTED| OK[SUCCESS]
RC -->|其它| FB
| 入口 | 典型来源 |
|---|---|
releaseControlledStop |
controlledExecution → stopped |
releaseInvalidDraft |
recoverInvalidDraft(loop 结束坏 draft) |
releaseConclusion |
正常 completed + 有结论 |
releaseNoConclusion |
draft 合法但无 conclusion |
7. Application 统一失败出口
未被转成 FALLBACK / SUCCESS 的异常,进入 ChatApplicationUseCase:
sequenceDiagram
participant EX as DiagnosisChatExecutor
participant APP as ChatApplicationUseCase
participant Core as DiagnosisHarnessCore
participant Store as ChatRunStore
participant SSE as ChatSseSession
EX-->>APP: 抛 ChatApplicationException 或 RuntimeException
APP->>Core: terminalOutcome / completeFailure
APP->>Store: finish(terminal, ...)
APP->>APP: safeFailure → ChatFailureCode
APP-->>SSE: fail(code, 安全文案)
常见映射直觉:
| 情况 | ChatFailureCode 倾向 |
|---|---|
| 取消 | RUN_CANCELLED |
| 路由失败 | ROUTING_UNAVAILABLE |
| 落库失败 | RUN_PERSISTENCE_FAILED |
| 其它 | INTERNAL_FAILURE |
8. 端到端对照表(按场景)
| # | 场景 | 发生位置 | 关键信号 | 第一处理点 | 用户侧 |
|---|---|---|---|---|---|
| 1 | 第 N 次模型前预算没了 | Loop 内 Model | BudgetExceeded |
controlledExecution → stopped | 有 facts→FALLBACK;无→FAILED |
| 2 | 工具执行失败 | Loop 内 Tool | TOOL_EXECUTION_ERROR observation |
留在 loop | 模型可能改写或再试 |
| 3 | 工具触顶预算 | Loop 内 ToolBoundary | error + markBudget | 常留在 loop,下轮 model 硬闸 | 同预算结局 |
| 4 | 饱和后仍要工具 | Loop 内 Tool | DiagnosisCollectionStopped |
controlledExecution → stopped | 有 facts→FALLBACK |
| 5 | 取消/超时 | Core → 任意 checkActive | RunAborted |
controlledExecution 再抛 | CANCELLED / FAILED |
| 6 | 模型返回烂 JSON | Loop 外解析 | INVALID_JSON 等 |
recoverInvalidDraft | 有 facts→FALLBACK;无→FAILED |
| 7 | 执行崩溃未识别 | Loop 边界 | EXECUTION_FAILED |
recover 不恢复,上抛 | FAILED |
| 8 | Guard 引用失败 | Loop 外 Release | Evidence 违规 | repair 或 FALLBACK | FALLBACK EVIDENCE_* |
| 9 | 语义不支持 | Loop 外 Release | UNSUPPORTED | FALLBACK | FALLBACK SEMANTIC_* |
9. 两条主恢复路径对比(必记)
flowchart LR
subgraph mid["Loop 中途打断"]
A1[Interceptor / Core 异常] --> B1[controlledExecution]
B1 --> C1[stopped draft=null]
C1 --> D1[releaseControlledStop]
end
subgraph end["Loop 正常结束但输出坏"]
A2[解析 Draft 失败] --> B2[DiagnosisAgentOutputException]
B2 --> C2[recoverInvalidDraft]
C2 --> D2[releaseInvalidDraft]
end
D1 --> E[INSUFFICIENT_EVIDENCE FALLBACK<br/>前提 hasObservedFacts]
D2 --> E
controlledExecution |
recoverInvalidDraft |
|
|---|---|---|
| 时机 | loop 中途 | loop 结束后 |
| 输入 | Throwable cause 链 |
DiagnosisAgentOutputException |
| 成功产物 | Execution.stopped |
直接 DiagnosisExecutionResult(FALLBACK) |
| 发布入口 | releaseControlledStop |
releaseInvalidDraft |
| 共同点 | 都依赖 可发布 observedFacts;都 fail closed |
10. 代码索引
| 组件 | 路径 |
|---|---|
| Model 拦截 | harness/agent/HarnessModelInterceptor.java |
| Tool 拦截 | harness/agent/HarnessToolInterceptor.java |
| 工具边界 | harness/tool/boundary/ToolBoundary.java |
| 受控停止转换 | harness/agent/DiagnosisAgentUseCase#controlledExecution |
| 坏 Draft 恢复 | harness/application/executor/DiagnosisChatExecutor#recoverInvalidDraft |
| 非法 draft 发布 | harness/release/DiagnosisReleaseUseCase#releaseInvalidDraft |
| 受控停止发布 | DiagnosisReleaseUseCase#releaseControlledStop |
| 应用失败出口 | harness/application/ChatApplicationUseCase |
| 错误码注释 | ChatFailureCode / ReleaseOutcome / FallbackType / RunState / DiagnosisStopReason / ToolBoundaryErrorCode 等 |
11. 阅读检查清单
- 这个失败是 还在 loop 里,还是 已经穿出 agent.call?
- 是 资源/取消(Core),还是 收集收敛(Progress),还是 输出契约(Draft Kind)?
- 最终有没有
observedFacts?有才能谈 FALLBACK。 RunState与ReleaseOutcome是否一致理解(预算耗尽 ≠ 一定 FAILED)?- Tool 错误是 observation 还是 异常穿出?不要默认「有 error 就崩 run」。
12. 一句话总结
Loop 内:能安全回注的变成 observation,必须停收集或硬预算的穿出异常。
Loop 边界:controlledExecution 把受控停止收成 stopped。
Loop 外:坏 draft 用 recoverInvalidDraft,只认 observedFacts 做降级。
全程 fail closed:没有可验证事实,就不发「看起来友好」的假成功。