错误分层处理


文档摘要

错误分层处理 本节摘要:前面几节多次提到「错误分层」,这一节正面讲清它。Agent 在运行中会遇到各种各样的错误:网络抖动、服务端限流、认证失效、上下文超限、工具执行失败、用户取消,甚至 Agent 自己陷入死循环。这些错误的性质、严重程度、处理方式各不相同。Grok Build 的做法是分层:sampler 只管「可重试的传输/协议错误」,shell 翻译并处理「语义错误」(上下文超限、认证失效),工具层处理「工具执行失败」,而框架级保护(doom-loop 检测)则防止最坏情况。本节把这些层次讲透,让你看清 Agent 如何在错误面前既鲁棒又不失控。 一、为什么错误处理要分层 先理解为什么不能把所有错误堆在一起处理。

错误分层处理

本节摘要:前面几节多次提到「错误分层」,这一节正面讲清它。Agent 在运行中会遇到各种各样的错误:网络抖动、服务端限流、认证失效、上下文超限、工具执行失败、用户取消,甚至 Agent 自己陷入死循环。这些错误的性质、严重程度、处理方式各不相同。Grok Build 的做法是分层:sampler 只管「可重试的传输/协议错误」,shell 翻译并处理「语义错误」(上下文超限、认证失效),工具层处理「工具执行失败」,而框架级保护(doom-loop 检测)则防止最坏情况。本节把这些层次讲透,让你看清 Agent 如何在错误面前既鲁棒又不失控。

一、为什么错误处理要分层

先理解为什么不能把所有错误堆在一起处理。

设想一种朴素做法:循环里调一次模型,失败了就「重试」,不管什么错都重试。这会有几个问题:

  • 认证失效也重试:access_token 过期了,重试多少次都是 401,无意义地消耗请求
  • 上下文超限也重试:请求太大,重试还是太大,陷入死循环
  • 网络错误与逻辑错误混为一谈:无法针对性恢复
  • 重试策略无处施展:不同错误需要不同的退避策略,混在一起无法细化

正确的做法是按错误性质分层,让每一层只处理它最了解的那类错误:

  • sampler 最了解网络与协议,所以它处理「可重试的传输错误」(网络抖动、限流、空响应)
  • shell 最了解会话语义,所以它处理「语义错误」(认证失效、上下文超限)
  • 工具层 最了解具体工具,所以它处理「工具执行失败」
  • 框架保护 兜底,防止 Agent 失控(死循环检测)

这种分层让每层的错误处理聚焦、清晰,也方便针对性优化。

二、错误的全景分类

把 Agent 可能遇到的错误按性质分类:

错误 ├── 传输/协议层(sampler 处理,可重试) │ ├── 网络抖动(连接中断、超时) │ ├── 服务端限流(429) │ ├── 服务端临时错误(5xx) │ ├── 空响应(流建立但没内容) │ └── 空闲超时(流建立后长时间无数据) │ ├── 语义层(shell 处理,需特定恢复动作) │ ├── 认证失效(401,需刷新 token) │ ├── 上下文超限(请求太大,需压缩) │ └── 序列化错误(请求格式问题) │ ├── 工具层(tool_bridge 处理) │ ├── 工具鉴权被拒(权限不足) │ ├── 工具执行失败(命令返回非零、文件不存在等) │ └── 工具超时 │ ├── 用户动作 │ ├── 用户取消(Cancel) │ └── 用户关停(Shutdown) │ └── 框架级保护 ├── 达到最大轮数(--max-turns) ├── doom-loop 检测(疑似死循环) └── 致命错误(不可恢复)

这个分类是理解错误分层的基础。下面按层看每层怎么处理自己的错误。

三、sampler 层:可重试的传输错误

sampler 是最底层,离网络最近。它处理的错误是「这次请求失败,但换一次可能成功」的传输/协议错误。

典型错误

  • 网络中断或超时
  • 服务端限流(429 Too Many Requests)
  • 服务端临时错误(5xx)
  • 空响应(连接成功但没收到内容)
  • 空闲超时(连接成功但长时间无数据)

处理方式:重试

sampler 维护一个 RetryPolicy,定义了最大重试次数、退避策略(指数退避等)。遇到上述错误时:

遇到可重试错误: if 已重试次数 < 最大次数: 按 RetryPolicy 计算 backoff 时间 发出 Retrying 事件(让桥/UI 知道) 等待 backoff 重新发起请求 else: 发出 Failed 事件(用尽重试)

关键点:

  • 重试在 sampler 内部完成:桥与循环看到的是 Retrying 事件(可选)或最终的 Completed/Failed,不需要自己处理重试。
  • 退避策略避免风暴:指数退避让重试间隔递增,避免在服务端过载时雪上加霜。
  • 重试次数有上限:不会无限重试,用尽后承认失败。
  • 取消优先于重试:如果在 backoff 期间用户取消,CancellationToken 会让重试中止。

关键概念:sampler 的重试是「对调用方透明」的——调用方(桥)只看到最终结果,不感知中间的重试。这让上层的代码简洁,重试策略的调整也局限在 sampler 内。

四、shell 层:语义错误的翻译与处理

shell 层处理的错误是「不是传输问题,而是会话语义问题」。这类错误重试无意义,需要特定的恢复动作。第 05 节讲的桥,把这类错误从 Failed 事件里识别出来,翻译成两种 outcome。

认证失效(401 → RefreshAuthAndResubmit)

错误表现:服务端返回 401,说明 access_token 过期或无效。

为什么不能简单重试:重试还是 401,因为 token 还是过期的。必须先刷新认证。

shell 的处理:

桥翻译出 RefreshAuthAndResubmit → 循环: 1. 触发认证刷新 - 用 refresh_token 换新的 access_token(后台静默) - 或触发重新登录流程(若 refresh 也失效) 2. 更新 SamplerConfig 的认证信息 3. backoff 一会儿(避免刷新失败时风暴) 4. continue,用新认证重新组装请求重试

这种处理让用户几乎无感——token 过期了,Agent 自动刷新继续干活,只有 refresh_token 也失效时才提示重新登录。

上下文超限(→ CompactAndResubmit)

错误表现:服务端返回特定错误,说明请求的 token 数超过模型上下文窗口。

为什么不能简单重试:请求还是那么大,重试还是超限。必须先压缩。

shell 的处理:

桥翻译出 CompactAndResubmit → 循环: 1. 触发上下文压缩 - 选择要压缩的历史段(通常是较早的对话) - 调用模型把它们总结成摘要(这本身是一次模型调用) - 用摘要替换原始消息 2. 重新组装请求(现在更短了) 3. continue,用压缩后的请求重试

注意压缩本身也是一次模型调用——这是个有趣的递归:为了让主请求能成功,先发一个压缩请求。第 7 章会详谈压缩的具体策略(full-replace、intra、inter 等)。

设计警示:压缩重试也要有上限。如果压缩后还是超限(理论上不应发生,但服务端窗口可能变化),不能无限压缩重试。框架会有保护机制,达到上限后承认失败。

序列化错误与其他

请求格式问题(序列化错误)、不支持的参数等,通常不可重试,直接作为致命错误上浮,让循环结束本轮并通知用户。

五、工具层:工具执行失败

工具执行也可能失败,但这与模型调用是不同的错误源。工具失败的处理在 tool_bridge 与具体工具内部:

典型工具错误

  • 鉴权被拒(权限规则 deny,或 PreToolUse hook 拒绝)
  • 命令返回非零退出码(bash 工具跑的命令失败)
  • 文件不存在、路径无效
  • 工具执行超时
  • 工具内部异常

处理方式:结果回填,而非中断循环

关键区别:工具失败通常不中断 Agent 循环。失败的返回值作为「工具结果消息」塞回历史,模型看到后自行判断:

模型: 调 bash(cargo build) 工具: 执行失败,退出码 1,stderr 是编译错误 → 不中断循环,把"失败 + 错误输出"作为 tool_result 塞回历史 下一轮模型: 看到编译错误,决定怎么修复

这种「失败也是信息」的设计,让 Agent 能像真人一样从错误中学习——跑测试失败不是结束,而是给模型的反馈,模型据此调整策略。

例外:鉴权被拒

如果用户在权限询问时选择「拒绝」,工具不执行,返回一个「被用户拒绝」的结果。模型看到后通常会换个思路或询问用户。这也不中断循环(除非用户明确要取消整个任务)。

六、用户动作:取消与关停

用户的主动动作也是「错误源」之一,但有专门的语义:

取消(Cancel)

用户在 Agent 跑的过程中按了取消(如 Ctrl+C 或 UI 的取消按钮):

  • 桥收到 Cancelled 事件(来自 sampler,因为它响应了 CancellationToken)
  • 当前正在进行的模型调用与工具执行被中止
  • 循环退出,但会话不结束——用户可以发新的 prompt

关停(Shutdown)

用户彻底退出会话:

  • SessionActor 收到 Shutdown 命令
  • 清理资源(关闭 MCP 连接、flush 记忆、保存会话)
  • 进程退出(或会话结束)

取消与关停是「协作式」的——框架发信号,各组件响应,而不是强杀。这保证了清理的完整性(如会话状态正确保存)。

七、框架级保护:兜底防线

最后是框架级的保护机制,防止 Agent 失控:

最大轮数(--max-turns)

循环有最大轮数限制(可通过 --max-turns 配置)。达到上限后,无论模型是否还想继续,循环强制结束。这防止模型陷入「无限调工具」的状态。

doom-loop 检测

**doom-loop(死循环)**是 Agent 最危险的状态:模型反复调用同样的工具、得到同样的结果、再调用同样的工具,陷入死循环。这会无意义地消耗大量 API 调用与 token。

Grok Build 有 doom-loop 检测机制(在 SamplerConfig 里有 DoomLoopRecoverySettings):

典型检测逻辑(简化): 维护最近的工具调用序列 如果检测到高度重复的模式(如连续 N 次调用同样的工具+类似参数): 判定为疑似 doom-loop 触发恢复策略: - 注入一条系统提醒,提示模型"你可能陷入了重复,尝试换个思路" - 或强制结束当前轮,让用户介入

这种检测是「框架替模型踩刹车」——模型自己意识不到陷入死循环(它只是在忠实地执行「调工具→看结果→再调」),框架从外部观察到重复模式后干预。

致命错误

真正不可恢复的错误(如序列化错误、关键资源不可用),直接结束本轮,通知用户。这类错误应该少见,出现时通常需要用户介入(改配置、重启等)。

八、错误分层的价值

把这套分层放在一起看,它的价值在于:

价值一:每种错误有最合适的处理者

网络错误给最懂网络的 sampler,语义错误给最懂会话的 shell,工具错误给最懂工具的 tool_bridge。各司其职,处理最精准。

价值二:可恢复与不可恢复清晰区分

可恢复错误(网络、认证、上下文)用重试或特定恢复动作处理;不可恢复错误(致命、达上限)才中断。这让 Agent 对常见波动具有韧性,不会动不动就失败。

价值三:重试策略集中

sampler 内的重试策略、shell 的恢复动作、框架的保护机制,各自集中在一处。调整任何一层的策略,不影响其他层。

价值四:用户体验良好

绝大多数错误用户无感(自动重试、自动刷新认证、自动压缩),只有真正需要介入时才通知用户。这让 Agent 用起来像「一个能扛事的同事」,而不是「动不动就报错的半成品」。

九、把错误分层与循环联系起来

最后,用一张图把错误处理与第 03 节的循环联系起来:

这张图把错误处理的各个分支与循环的主干连在一起。建议结合第 03 节的循环骨架反复对照,直到你能在脑海里完整地「演练」一次 Agent 在各种情况下的行为。

本节要点回顾

  1. 错误按性质分层:传输(sampler)、语义(shell)、工具(tool_bridge)、用户动作、框架保护,各层处理自己最了解的错误。
  2. sampler 处理可重试传输错误:网络、限流、5xx、空响应、空闲超时,按 RetryPolicy 退避重试,对调用方透明。
  3. shell 处理语义错误:401→刷新认证重试(RefreshAuthAndResubmit)、上下文超限→压缩重试(CompactAndResubmit)。
  4. 工具失败不中断循环:失败结果作为 tool_result 塞回历史,模型据此调整;鉴权被拒也回填而非中断。
  5. 取消与关停是协作式:框架发信号,各组件响应清理,保证状态完整保存。
  6. 框架级保护兜底:最大轮数防无限循环、doom-loop 检测防重复模式、致命错误结束本轮。
  7. 价值:每种错误有最合适处理者、可恢复与不可恢复清晰区分、重试策略集中、用户体验良好。

至此,第三章全部完成。你已经从 Actor 模型、运行入口、核心循环、ChatStateActor、sampler 桥接到错误分层,完整理解了 shell 层的内部。下一章,我们将跨过桥,深入 sampler 层的内部——看清那个把 HTTP chunk 流变成统一事件的采样器,究竟是如何工作的。


发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U