错误分层处理 本节摘要:前面几节多次提到「错误分层」,这一节正面讲清它。Agent 在运行中会遇到各种各样的错误:网络抖动、服务端限流、认证失效、上下文超限、工具执行失败、用户取消,甚至 Agent 自己陷入死循环。这些错误的性质、严重程度、处理方式各不相同。Grok Build 的做法是分层:sampler 只管「可重试的传输/协议错误」,shell 翻译并处理「语义错误」(上下文超限、认证失效),工具层处理「工具执行失败」,而框架级保护(doom-loop 检测)则防止最坏情况。本节把这些层次讲透,让你看清 Agent 如何在错误面前既鲁棒又不失控。 一、为什么错误处理要分层 先理解为什么不能把所有错误堆在一起处理。
本节摘要:前面几节多次提到「错误分层」,这一节正面讲清它。Agent 在运行中会遇到各种各样的错误:网络抖动、服务端限流、认证失效、上下文超限、工具执行失败、用户取消,甚至 Agent 自己陷入死循环。这些错误的性质、严重程度、处理方式各不相同。Grok Build 的做法是分层:sampler 只管「可重试的传输/协议错误」,shell 翻译并处理「语义错误」(上下文超限、认证失效),工具层处理「工具执行失败」,而框架级保护(doom-loop 检测)则防止最坏情况。本节把这些层次讲透,让你看清 Agent 如何在错误面前既鲁棒又不失控。
先理解为什么不能把所有错误堆在一起处理。
设想一种朴素做法:循环里调一次模型,失败了就「重试」,不管什么错都重试。这会有几个问题:
正确的做法是按错误性质分层,让每一层只处理它最了解的那类错误:
这种分层让每层的错误处理聚焦、清晰,也方便针对性优化。
把 Agent 可能遇到的错误按性质分类:
错误 ├── 传输/协议层(sampler 处理,可重试) │ ├── 网络抖动(连接中断、超时) │ ├── 服务端限流(429) │ ├── 服务端临时错误(5xx) │ ├── 空响应(流建立但没内容) │ └── 空闲超时(流建立后长时间无数据) │ ├── 语义层(shell 处理,需特定恢复动作) │ ├── 认证失效(401,需刷新 token) │ ├── 上下文超限(请求太大,需压缩) │ └── 序列化错误(请求格式问题) │ ├── 工具层(tool_bridge 处理) │ ├── 工具鉴权被拒(权限不足) │ ├── 工具执行失败(命令返回非零、文件不存在等) │ └── 工具超时 │ ├── 用户动作 │ ├── 用户取消(Cancel) │ └── 用户关停(Shutdown) │ └── 框架级保护 ├── 达到最大轮数(--max-turns) ├── doom-loop 检测(疑似死循环) └── 致命错误(不可恢复)
这个分类是理解错误分层的基础。下面按层看每层怎么处理自己的错误。
sampler 是最底层,离网络最近。它处理的错误是「这次请求失败,但换一次可能成功」的传输/协议错误。
典型错误
处理方式:重试
sampler 维护一个 RetryPolicy,定义了最大重试次数、退避策略(指数退避等)。遇到上述错误时:
遇到可重试错误: if 已重试次数 < 最大次数: 按 RetryPolicy 计算 backoff 时间 发出 Retrying 事件(让桥/UI 知道) 等待 backoff 重新发起请求 else: 发出 Failed 事件(用尽重试)
关键点:
关键概念:sampler 的重试是「对调用方透明」的——调用方(桥)只看到最终结果,不感知中间的重试。这让上层的代码简洁,重试策略的调整也局限在 sampler 内。
shell 层处理的错误是「不是传输问题,而是会话语义问题」。这类错误重试无意义,需要特定的恢复动作。第 05 节讲的桥,把这类错误从 Failed 事件里识别出来,翻译成两种 outcome。
错误表现:服务端返回 401,说明 access_token 过期或无效。
为什么不能简单重试:重试还是 401,因为 token 还是过期的。必须先刷新认证。
shell 的处理:
桥翻译出 RefreshAuthAndResubmit → 循环: 1. 触发认证刷新 - 用 refresh_token 换新的 access_token(后台静默) - 或触发重新登录流程(若 refresh 也失效) 2. 更新 SamplerConfig 的认证信息 3. backoff 一会儿(避免刷新失败时风暴) 4. continue,用新认证重新组装请求重试
这种处理让用户几乎无感——token 过期了,Agent 自动刷新继续干活,只有 refresh_token 也失效时才提示重新登录。
错误表现:服务端返回特定错误,说明请求的 token 数超过模型上下文窗口。
为什么不能简单重试:请求还是那么大,重试还是超限。必须先压缩。
shell 的处理:
桥翻译出 CompactAndResubmit → 循环: 1. 触发上下文压缩 - 选择要压缩的历史段(通常是较早的对话) - 调用模型把它们总结成摘要(这本身是一次模型调用) - 用摘要替换原始消息 2. 重新组装请求(现在更短了) 3. continue,用压缩后的请求重试
注意压缩本身也是一次模型调用——这是个有趣的递归:为了让主请求能成功,先发一个压缩请求。第 7 章会详谈压缩的具体策略(full-replace、intra、inter 等)。
设计警示:压缩重试也要有上限。如果压缩后还是超限(理论上不应发生,但服务端窗口可能变化),不能无限压缩重试。框架会有保护机制,达到上限后承认失败。
请求格式问题(序列化错误)、不支持的参数等,通常不可重试,直接作为致命错误上浮,让循环结束本轮并通知用户。
工具执行也可能失败,但这与模型调用是不同的错误源。工具失败的处理在 tool_bridge 与具体工具内部:
典型工具错误
处理方式:结果回填,而非中断循环
关键区别:工具失败通常不中断 Agent 循环。失败的返回值作为「工具结果消息」塞回历史,模型看到后自行判断:
模型: 调 bash(cargo build) 工具: 执行失败,退出码 1,stderr 是编译错误 → 不中断循环,把"失败 + 错误输出"作为 tool_result 塞回历史 下一轮模型: 看到编译错误,决定怎么修复
这种「失败也是信息」的设计,让 Agent 能像真人一样从错误中学习——跑测试失败不是结束,而是给模型的反馈,模型据此调整策略。
例外:鉴权被拒
如果用户在权限询问时选择「拒绝」,工具不执行,返回一个「被用户拒绝」的结果。模型看到后通常会换个思路或询问用户。这也不中断循环(除非用户明确要取消整个任务)。
用户的主动动作也是「错误源」之一,但有专门的语义:
取消(Cancel)
用户在 Agent 跑的过程中按了取消(如 Ctrl+C 或 UI 的取消按钮):
关停(Shutdown)
用户彻底退出会话:
取消与关停是「协作式」的——框架发信号,各组件响应,而不是强杀。这保证了清理的完整性(如会话状态正确保存)。
最后是框架级的保护机制,防止 Agent 失控:
循环有最大轮数限制(可通过 --max-turns 配置)。达到上限后,无论模型是否还想继续,循环强制结束。这防止模型陷入「无限调工具」的状态。
**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 在各种情况下的行为。
至此,第三章全部完成。你已经从 Actor 模型、运行入口、核心循环、ChatStateActor、sampler 桥接到错误分层,完整理解了 shell 层的内部。下一章,我们将跨过桥,深入 sampler 层的内部——看清那个把 HTTP chunk 流变成统一事件的采样器,究竟是如何工作的。