Agent 的沉默崩溃:为什么你的 Agent 会在生产环境里悄悄死掉

你的 Agent 跑着跑着突然不干活了——不是报错,是安静地停止工作。API 超时、幻觉循环、上下文溢出——每个问题都在悄悄杀死你的 Agent,而它连最后的遗言都不会留给你。本文从我做的多 Agent 系统的实战出发,聊 Agent 错误恢复的三个核心模式和一条铁律。

你有没有见过一个 Agent "病而不死"的状态?

输出还是 JSON,内容完全不对。Tool call 的格式完美,参数全是一坨。它看起来在工作,但已经在原地转圈转了十几轮,你在日志里看到的是同一个 API 以同样的参数被调了十几次,每一次都返回 429,然后它继续调。

这不是 bug。这就是 Agent 在无错误处理下的默认行为。

我最早在自己做的多 Agent 系统里发现这个问题,是因为一个 Agent 卡在"搜索文件→找到文件→调用读取工具→读取失败→又去搜索"这个循环里,整整跑了四十分钟。没有报错——每次调用工具本身都成功了。是工具返回的内容告诉 Agent "我没找到",然后 Agent 就觉得应该重新搜索。

后来我意识到,Agent 的错误处理跟前端后端完全不是一回事。

为什么 HTTP 的那一套搬不过来

如果后端 API 挂了,你写 try-catch,记错误日志,返回 500。如果数据库连不上,你抛异常,熔断,发告警。这些东西很成熟,三板斧就能搞定。

但 Agent 的错误不是一个异常能描述的。它可能遇到的是这些情况:

  • 工具调用返回了 HTTP 200,但内容是空列表——Agent 以为是自己搜索词不对,开始换着花样搜
  • 上下文太长,Agent 忘了自己最初的任务指令——于是它开始自由发挥
  • LLM 返回了一模一样的工具调用两次——不是重试,是幻觉,它以为第一次没成功
  • 外部 API 上了新版本,返回格式变了——Agent 解析不出来,prompt 里写的字段名全对不上

HTTP 的错误是二值的:成功或失败。Agent 的错误是连续的:从完全正确到勉强能用到完全不可用,中间有无数灰度。

三个恢复模式,按成本升序

我整理了三层恢复策略,每层都比上一层更贵更复杂,也覆盖更严重的错误。

Level 1: Retry with Backoff(最便宜,覆盖 80% 的问题)

这一层只解决一种问题:暂时的、可自愈的错误。网络抖动、API 限流、数据库连接池满——这些错误有个共性,等几秒钟自己就好了。

关键不在于要不要 retry,而是怎么判断该不该 retry

// 不要写这种东西
catch (err) {
  retry(fn) // 无脑重试
}

你要做一个 error classification。401 不能 retry,429 应该 retry,503 可以 retry 但次数要限制。

我自己做了一个简单的分类器:给每个工具返回值加一个 error_kind 字段。transient 类型的错误(限流、超时)自动走指数退避重试,最多三次,每次延迟指数增长加随机抖动。permanent 类型的错误(认证失败、参数错误)直接往上抛,让 Agent 自己去理解错误信息。

指数退避加 jitter 是必须的。 不加 jitter 的话,多个 Agent 实例同时重试会在同一个时间点打到同一个服务上,这叫惊群效应。你本来只是 API 反应慢,这一下把人家搞熔断了。

Level 2: Fallback(中间价,覆盖 95% 的问题)

当一个工具三番五次失败,或者你明确知道某个 API 时而可用时而不行,就该上 fallback 了。

Fallback 有两个维度:

Provider/工具级别 fallback: 搜索 API 挂了→换另一个搜索 API。Embedding 服务超时→切换到本地小模型。这在实际系统中很常用,因为 Agent 往往依赖多个外部知识源,任何单一源挂了都不能让整个 Agent 停摆。

语义级别 fallback: 当工具返回的内容不够好时,降级处理。比如文件读取失败,不抛错误,而是返回一条"文件不可用,以下是文件名作为替代"——Agent 拿到这个信息,虽然不能读内容,但至少知道文件存在,可以在回复里告诉用户"我找到了这个文件但暂时打不开"。

这个设计最反直觉的地方在于:有时候给出不完美的结果比不给结果好一万倍。 Agent 接收到不完美的结果后,可以调整策略。但如果接收到一个错误,它的选择只有"报错"或者"再试一次"。

Level 3: Graceful Degradation(最贵,但必须有的保险丝)

当 retry 和 fallback 都失败之后,还剩最后一层防线:优雅降级。

这个模式的本质是:告诉 Agent 出了什么事,让它自己做决定。

大多数 Agent 框架在兜底时的做法是抛一个 Python Exception。但这太粗暴了。Exception 不是 Agent 能理解的语言——它理解的是自然语言和结构化数据。

所以我会在兜底时返回一个结构化的"partial result":

{
  "status": "partial",
  "data": { "profile": { ... }, "orders": null },
  "errors": [
    { "component": "orders", "kind": "unavailable", "message": "订单服务暂时不可用" }
  ],
  "suggestion": "用户信息已获取,但订单信息缺失。可以回复用户基本信息,并说明订单数据暂时不可用。"
}

Agent 拿到这条信息后,它会自己判断怎么做——只展示部分信息、让用户等一会儿、或者换一个入口去获取数据。

什么时候不该恢复

有一个很反常识的结论:不是所有错误都值得恢复。

我见过最贵的账单不是来自 Token 消耗,而是一个 Agent 在认证失败的情况下不断 retry,因为被限流触发了 429,然后在 429 后提升权限重试,因为权限验证失败返回 403,继续重试……最后 API 提供商直接封了 IP。

有些错误是"不治之症":

  • 认证/鉴权错误:401/403——凭证是错的,重试一万次也没用
  • 配额耗尽:账户余额不够了——fallback 到另一个 provider,但别 retry 同一个
  • 数据不存在:用户删除了或者从没创建过——Agent 需要调整搜索策略,不是继续搜同一个词
  • 参数格式错误:Agent 自己传的参数就不对——这是 prompt 或 schema 的问题,重试只是烧钱

这些错误的第一要务不是恢复,是快速失败,并且把错误清晰地告诉上层。

Circuit Breaker:给 Agent 装上保险丝

在分布式系统里,Circuit Breaker 是个经典模式。把同一个东西搬到 Agent 系统里,效果非常好。

思路很简单:给每个外部工具/Provider 装一个计数器。连续失败 N 次后,电路断开——接下来的工具调用直接跳过,不真正发起请求。过一段时间(比如 60 秒)后,放一个探测请求进来试试——成功了就恢复,失败了继续保持断开状态。

这个模式在 Agent 场景下特别有用,因为 Agent 的决策闭环很快。一个工具要是挂了,Agent 可能在一轮推理里连续调用它三四次。没有断路器,这四次调用全部白费 Token。

断路器阈值可以按工具类型来调:

  • 搜索类 API(响应快、波动大):阈值 3 次,冷却 30 秒
  • 文件读取类(响应慢、稳定):阈值 2 次,冷却 120 秒

断路器的关键不是阻止调用,是让 Agent 知道"这个工具现在不能用",然后去走 fallback 路径。

Checkpoint:长链路的最后救星

当 Agent 跑了一个十几步的长链路,在最后一步上挂了——这时候你说"从头再来",谁听了都想骂人。

这个问题在多 Agent 协同场景中特别突出。A Agent 搜索资料,B Agent 分析资料,C Agent 写报告——如果 C 在写报告时超时了,你不能让 A 和 B 重新做一遍。

解决方案是 checkpoint。在每一个关键步骤结束后,保存当前状态。出错时恢复到最近一个 checkpoint,而不是从头开始。

实现起来其实不复杂,核心就是持久化 Agent 的状态快照:

  • 当前任务队列
  • 已完成的工具调用及其结果
  • 上下文缓存
  • 执行路径上的关键决策点

然后提供一个 restart from checkpoint 的接口。LangGraph 和 Temporal 都有现成的支持,但如果你是自建框架,手动存个 JSON 也比没有好。

最后一点心得

做 Agent 的错误处理和做 Web 后端有个本质区别:

后端的错误处理是防御——你提前想好每一种可能的错误,写好对应的 handler,剩下的让运维处理。

Agent 的错误处理是韧性——你的目标不是阻止所有错误发生,而是让系统在犯错后能自己爬起来。

所以我有一个很个人的判断标准:如果你的 Agent 在产生错误日志时,下一条日志不是你主动触发的恢复行为,而是另一个错误——那你的错误处理就是纸糊的。

好 Agent 系统的特征不是"不犯错",而是"犯了错也知道怎么继续活下去"。

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个