一次 404 让 fallback 链断了:错误分类是 Agent 的核心能力

一次 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 才能正常工作:

  1. resolveFailoverReasonFromError():从 HTTP 状态码映射到 failover 原因
  2. resolveFailoverStatus():从 failover 原因映射到 failover 状态
  3. classifyFailoverReason():从错误消息文本匹配 failover 原因
  4. 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
← 返回博客列表