# Harness 异常处理:Loop 内 vs Loop 外 **日期**:2026-07-30 **范围**:诊断路径(`intent=DIAGNOSIS`)上 ReactAgent ReAct 循环内外的异常、受控停止、降级与终态 **读者**:已读 E2E 全流程 / 运行控制,需要把「失败时系统到底怎么走」串成一张图 **关联文档**: - [Harness 失败图谱](./Harness失败图谱-异常-停止-降级与终态.md)(失败类型总览) - [Harness 生命周期与状态](./Harness生命周期与状态.md)(RunState / CollectionState) - [信息增益停止](./Harness信息增益停止-让无证据诊断正常收敛.md)(SATURATED / STOP_REQUIRED) --- ## 0. 先记住一张总图 ```mermaid 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. 状态有三层,不要混 异常处理会同时碰到三套状态,职责不同: ```mermaid flowchart LR subgraph core["Core 执行面"] RS[RunState
RUNNING / SUCCESS / FAILED
CANCELLED / TIMED_OUT / BUDGET_EXHAUSTED] end subgraph prog["Progress 收集面"] CS[CollectionState
COLLECTING / SATURATED] SR[DiagnosisStopReason] end subgraph out["对外发布面"] RO[ReleaseOutcome
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. 架构位置:异常在哪一层被「接住」 ```mermaid flowchart TB subgraph L0["L0 协议"] CTRL[ChatController / SSE] end subgraph L1["L1 Application"] APP[ChatApplicationUseCase
统一 catch → ChatFailureCode] DEX[DiagnosisChatExecutor
recoverInvalidDraft] end subgraph L2["L2 Agent 用例"] DAU[DiagnosisAgentUseCase
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 正常成功路径(对照) ```mermaid 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` ```text beforeModelCall / handler.call / checkActive → BudgetExceededException → RunAbortedException(已终态、超时、取消) → 其它 RuntimeException(供应商错误等) ``` ```mermaid 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` ```mermaid 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
或连续违规后 stop] PARSE --> DUP{重复 scope?} DUP -->|是| DUPR[DUPLICATE_SCOPE observation] DUP -->|否| INV[ToolBoundary.invoke] INV -->|READY| OBS[投影 modelObservation 回注] INV -->|BUDGET_EXHAUSTED 等 error| ERR[error observation
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` 对预算的处理(**吞异常 → 错误码**,不抛给框架): ```text catch (RunAbortedException | BudgetExceededException) → ToolBoundaryResult.error(BUDGET_EXHAUSTED | RUN_INACTIVE) ``` --- ## 4. Loop 边界:`controlledExecution` **位置**:`DiagnosisAgentUseCase`,包住 `agent.call(...)`。 **职责**:把「Harness 约定的受控停止」从异常栈(含 cause 链)捞出来,转成 **`DiagnosisAgentExecution.stopped`**;其余失败交给外层。 ```mermaid 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
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` → 包装执行失败异常 | **结果形态**: ```text completed(draft, progress) // 有合法草稿 stopped(progress, stopReason) // 无草稿,仅有进度快照 ``` --- ## 5. Loop 外:坏 Draft 与 `recoverInvalidDraft` **时机**:`agent.call` **已经返回**(或解析阶段),最终文本 **不是**合法 `DiagnosisDraft`。 **位置**:`DiagnosisChatExecutor` catch `DiagnosisAgentOutputException`。 ```mermaid 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 形态分叉 ```mermaid flowchart TD EX[DiagnosisAgentExecution] --> D{draft == null?} D -->|是 stopped| CS[releaseControlledStop
需有 observedFacts] D -->|否| C{conclusion == null?} C -->|是| NC[releaseNoConclusion
部分 Evidence + progress] C -->|否| RC[releaseConclusion
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`: ```mermaid 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. 两条主恢复路径对比(必记) ```mermaid 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
前提 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. 阅读检查清单 1. 这个失败是 **还在 loop 里**,还是 **已经穿出 agent.call**? 2. 是 **资源/取消**(Core),还是 **收集收敛**(Progress),还是 **输出契约**(Draft Kind)? 3. 最终有没有 **`observedFacts`**?有才能谈 FALLBACK。 4. `RunState` 与 `ReleaseOutcome` 是否一致理解(预算耗尽 ≠ 一定 FAILED)? 5. Tool 错误是 **observation** 还是 **异常穿出**?不要默认「有 error 就崩 run」。 --- ## 12. 一句话总结 > **Loop 内:能安全回注的变成 observation,必须停收集或硬预算的穿出异常。** > **Loop 边界:controlledExecution 把受控停止收成 stopped。** > **Loop 外:坏 draft 用 recoverInvalidDraft,只认 observedFacts 做降级。** > **全程 fail closed:没有可验证事实,就不发「看起来友好」的假成功。**