一次 404 让 fallback 链断了:错误分类是 Agent 的核心能力
OpenClaw 的 GitHub 上最近有一个 PR(#11446),标题是"handle HTTP 404 as failover reason to prevent broken model fallback chains"。
问题本身很小:Google Gemini 返回了一个 404(模型不可用或配额耗尽),OpenClaw 的 failover 链直接断了,错误原封不动甩给了用户。用户看到的是一条晦涩的报错:"Cloud Code Assist API error (404): Requested entity was not found."
按理说,404 应该触发 fallback——换一个模型继续。但它没有。
为什么 404 能把链路打断
OpenClaw 的模型 fallback 机制大致是这样的:配置一个模型链,比如 Gemini → Opus 4.6 → GPT-5-mini。主模型失败时,按顺序尝试下一个。
但"失败"是有条件的。不是所有 HTTP 错误都会触发 failover。OpenClaw 内部维护了一个"可 failover 的错误类型"列表:
- 402:账单问题
- 429:速率限制
- 401/403:鉴权失败
- 408:超时
注意到了吗?没有 404。
当 Gemini 返回 404 时,resolveFailoverReasonFromError() 不认识这个状态码。分类结果为 null。isFailoverError() 返回 false。failover 循环直接抛异常,不继续了。
这个 PR 的作者(#11446)发现,之前有四个人尝试修这个问题,每个人都漏了至少一个环节。错误处理的链路上有四个关卡,必须全部通过,failover 才能正常工作:
resolveFailoverReasonFromError():从 HTTP 状态码映射到 failover 原因resolveFailoverStatus():从 failover 原因映射到 failover 状态classifyFailoverReason():从错误消息文本匹配 failover 原因isFailoverError():综合判断这个错误是否可 failover
四个人分别只修了其中一到两个。结果 404 仍然打不通整条链路。
错误分类为什么这么难
表面上看,"给 404 加一个映射"很简单。一行代码的事。但实际操作中有两个麻烦。
第一个是语义歧义。404 在不同供应商那里含义不同。Google 用 404 表示"模型不存在或配额耗尽"。但在 OpenClaw 自身的工具链里,404 也可能表示"浏览器目标页面没找到"或"本地文件不存在"。如果无差别地把所有 404 都当成 failover 信号,会误触发 fallback。
这个 PR 的做法是加"假阳性排除"(false-positive exclusions)。文件系统的"not found"、Node 模块的"MODULE_NOT_FOUND"、浏览器的"target not found",这些统统排除在外。只有来自模型供应商的 404 才算。
第二个是错误信息的碎片化。供应商返回的错误消息格式不统一。Google 说"Requested entity was not found",另一家可能说"Model not available"或者只给一个裸的 404 状态码。你得用正则去匹配各种变体。而正则一多,维护成本就上去了。
问题更大一层:Agent 时代的错误分类体系
这个 bug 折射出的更深层问题是:Agent 系统需要一套比传统 HTTP 客户端复杂得多的错误分类体系。
传统 HTTP 客户端处理错误很简单:2xx 成功,4xx 客户端错误,5xx 服务端错误,按需重试。
Agent 系统的错误来源多了好几个维度:
| 错误来源 | 例子 | 正确的处理方式 |
|---|---|---|
| 供应商返回 429 | 速率限制 | 退避重试同一供应商,或 failover 到其他供应商 |
| 供应商返回 404 | 模型不可用 | failover 到其他模型 |
| 供应商返回 402 | 余额不足 | failover 到其他供应商,并告警 |
| 本地 exec 被 SIGPIPE | 管道截断 | 不重试,告知用户 |
| 网络瞬断 | undici fetch failed | 短暂退避后重试 |
| 上下文溢出 | context overflow | 裁剪历史后重试,或换更大上下文窗口的模型 |
每种错误需要不同的处理策略。如果分类错了,后果从"浪费一次重试"到"整条链路挂掉"不等。
自己做 fallback 链时怎么避免踩坑
如果你在自己的 Agent 系统里实现模型 fallback,几个建议。
穷举你能遇到的错误码。不要只处理"你以为会遇到的"。去每个供应商的文档里翻 error code 列表。Google、OpenAI、Anthropic 的错误码体系各不相同。至少要覆盖 400、401、402、403、404、408、429、500、502、503。
区分"可重试"和"可 failover"。429 通常可重试(同一模型,退避后再来),402 应该 failover(换供应商),400 通常不该重试(请求本身有问题)。这两个概念不要混在一起。
给错误分类加假阳性过滤。前面说了,同一个状态码在不同上下文里含义不同。加一层上下文判断:这个 404 是从模型供应商来的,还是从本地工具来的?
记录每次 failover 的原因。每次切换模型时,记下"从哪个模型切到哪个模型,原因是什么"。这条日志在排查问题时价值极高。否则你只知道"最终用了某个模型回复的",但不知道前面几个模型为什么失败了。
定期测试整条 fallback 链。模型供应商会改错误码、改错误消息格式。你的分类逻辑需要跟着更新。写集成测试,模拟各种错误码,验证 failover 行为符合预期。#11446 这个 PR 加了 13 个测试用例,覆盖了正常路径和各种边界情况。
一点感想
这个 bug 被四个人尝试修过四次,每次都不完整。不是因为这些人水平差,而是因为错误处理链路的"全覆盖"天然反直觉。你修了入口,以为修好了,但中间还有三道关卡没打通。
写错误处理代码的时候,从终点往回推比从起点往前推更靠谱。先问"failover 要成功触发,需要满足哪些条件",然后逐个检查每个条件是否对新错误类型成立。
参考链接
- OpenClaw PR #11446(404 failover 修复):
https://github.com/openclaw/openclaw/issues/11446 - 相关 issue #4992(原始报告):
https://github.com/openclaw/openclaw/issues/4992