错误恢复与重试
工具失败、参数错误、模型误判与“做到一半”的处理。
一句话理解:Agent 的错误分四层:模型 API、工具执行、模型决策、做到一半;每层由不同的角色处理,核心原则是“能自动恢复的交给程序,能自我纠正的交给模型,都不行就安全地停下来”。
传统软件出错,通常是一个请求失败、返回一个错误码。Agent 出错要复杂得多:它可能在第 12 步因为支付接口超时而失败,而前 11 步已经创建了工单、发出了通知。此时简单地“整个重来”,会给用户发第二条通知、建第二张工单。
很多 Agent 在 Demo 阶段看起来很稳,是因为 Demo 环境里接口从不超时、数据从不缺失。上线后第一周,错误恢复往往是投入最大的一块。这一课给你一套分层思路,失败类型的完整分类见 失败分类,这里聚焦 Harness 层面“怎么恢复”。
四层错误,四种处理者
| 层级 | 典型错误 | 谁来处理 | 策略 |
|---|---|---|---|
| 模型 API 层 | 限流、服务过载、网络中断、请求超时 | Harness(SDK) | 指数退避重试;持续失败则暂停任务 |
| 工具执行层 | 接口超时、5xx、参数不合法、数据不存在 | Harness 捕获,交给模型判断 | 瞬时错误程序重试;业务错误以“错误结果”返回模型 |
| 模型决策层 | 选错工具、编造参数、原地打转 | 模型自我纠正 + Harness 兜底 | 可行动的错误信息;重复检测;步数上限 |
| 做到一半 | 多步写操作中途失败或进程崩溃 | Harness 的状态与幂等设计 | 检查点续跑;幂等键;补偿操作 |
模型 API 层:交给 SDK,但要知道边界
Claude API 的官方 SDK 会对连接错误、限流(429)和 5xx 服务端错误自动做指数退避重试,默认重试两次,并遵守 retry-after 响应头;次数可以配置。你需要额外想清楚的是:
- 重试耗尽之后:长任务不应该因为一次 529(服务过载)就整体失败,应该把任务状态保存下来,稍后续跑。
- 不该重试的错误:400 参数错误、401 认证失败重试一百次也不会好,应立即停止并报警。
- 花费上限类的 429:Claude API 文档特别指出,触达用量档位的月度花费上限时返回的 429 没有
retry-after,在额度恢复前会一直失败,重试无意义。
工具执行层:把错误“翻译”给模型
工具出错时,最常见的坏做法有两个:让异常直接把程序打崩,或者吞掉错误返回空结果(模型会以为“查无记录”并据此下结论)。
正确做法是把错误作为工具结果返回,并明确标记为错误。Claude API 的 tool_result 提供了 is_error 字段,官方文档建议写“可行动”的错误信息:说明出了什么问题、下一步可以尝试什么。文档还提到,当工具调用缺少必填参数时,把错误返回给模型,模型通常会补全信息后重试两三次,然后才向用户致歉;要从根源上杜绝参数不合法,可以在工具定义上启用严格模式(strict: true)。
同一个错误,两种写法:
| 写法 | 模型看到的内容 | 模型可能的反应 |
|---|---|---|
| 差 | error |
原样重试,或放弃 |
| 差 | 空数组 | 误判为“该订单没有支付记录”,得出错误结论 |
| 好 | 支付服务超时(已重试 2 次)。订单基础信息可正常查询,可先基于订单状态给出初步判断,并提示用户支付数据暂缺 | 降级回答,并诚实说明数据缺口 |
| 好 | 订单号 ORD10O1 格式不正确:第 6 位是字母 O,应为数字 0 | 修正参数后重试 |
一个实用分工:瞬时错误(超时、5xx)由工具层自己先重试一两次;重试后仍失败、或属于业务错误(数据不存在、权限不足),再交给模型决定。 这样既不浪费模型的步数,又保留了模型换路径的能力。
模型决策层:检测“原地打转”
模型有时会陷入循环:用同样的参数反复调用同一个工具,或在两个工具之间来回切换。Harness 应该检测这些信号:
- 同一工具、同一参数在最近 N 步内重复出现;
- 连续多步没有新的工具调用结果进入上下文;
- 步数或花费接近上限,但任务清单没有推进。
检测到后,可以先给模型一条提示(“你已用相同参数查询 3 次,结果不会变化,请换一种方式或说明无法完成”),仍无改善就停止并交给人。更多停止策略见 停止条件,何时升级到人工见 什么时候停下来找人。
最难的情况:做到一半
来看可执行的售后处理 Agent 的一次运行:
此时有三种恢复选择:
| 选择 | 做法 | 风险 |
|---|---|---|
| 整体重跑 | 从步骤 1 重新开始 | 创建第二张工单 |
| 从失败处续跑 | 跳过已完成的步骤,重试步骤 4 | 需要知道“哪些已经做过” |
| 补偿回滚 | 关闭工单 T-8812,标记任务失败 | 需要每个写操作都有对应的撤销动作 |
绝大多数业务场景应该选“从失败处续跑”,它依赖三个设计:
1. 先记意图,再执行,再记结果。 每个写操作执行前,先把“准备做什么”写进任务状态;执行后写入外部系统返回的 ID。进程在任何时刻崩溃,恢复时都能判断这一步是“没做”“做了一半”还是“做完了”。
2. 写操作必须幂等。 给每个写操作生成一个幂等键(例如“任务 ID + 步骤名”),外部系统收到重复的键时返回第一次的结果,而不是再创建一次。
3. 恢复前重新确认现实。 距离上次运行可能已经过去几个小时,订单可能已被人工处理。续跑前先重新查询关键业务状态,而不是相信记忆里的旧数据。
下面是一段伪代码,用来说明这三点如何组合(不对应任何特定 SDK):
# 伪代码:带意图记录与幂等键的写操作
def run_write_step(task, step_name, action, params):
record = task.steps.get(step_name)
if record and record.status == "done":
return record.result # 已完成:直接复用结果
idempotency_key = f"{task.id}:{step_name}"
task.save_step(step_name, status="started", params=params) # 先记意图
result = action(params, idempotency_key=idempotency_key) # 外部系统按键去重
task.save_step(step_name, status="done", result=result) # 再记结果
return result
任务状态的结构设计与检查点粒度,会在 任务状态与 Checkpoint 中详细展开。
框架里的“做到一半”陷阱
框架的恢复机制也有自己的语义,必须读懂:
- LangGraph:官方文档明确说明,用
interrupt()暂停后恢复时,整个节点会从头重新执行,而不是从暂停的那一行继续。因此在interrupt()之前的代码必须是幂等的,否则可能重复写库。 - Claude Agent SDK:会话恢复的是对话历史,不是文件系统;因轮数或预算上限停止时,会返回
error_max_turns或error_max_budget_usd这类结果类型,并带上会话 ID,可以用更高的上限恢复该会话继续。
一张恢复决策图
最后一个分支最容易被忽略:写操作超时不等于没执行。 支付、发送、创建类操作超时后盲目重试,是重复扣款和重复通知的主要来源。
常见误区
- “失败了就整个重跑。” 只读任务可以,包含写操作的任务几乎一定会产生重复副作用。
- “错误信息越短越好,省 Token。” 错误信息是模型做下一步决策的唯一依据,几十个字的说明远比一次无效重试便宜。
- “重试次数越多越稳。” 无限重试会放大故障、烧掉预算。每一层都要有上限,并且上限耗尽后有明确的去处。
产品经理检查清单
- 每个工具的错误,模型看到的是什么文本?能据此决定下一步吗?
- 哪些写操作支持幂等?不支持的,超时后怎么确认状态?
- 任务中途失败,用户看到的是“失败”还是“已完成 3/5 步,第 4 步失败,可重试”?(呈现方式见 失败的呈现)
- 恢复运行前,会重新检查哪些业务状态?
- 模型 API 持续不可用时,长任务是直接失败,还是保存进度稍后自动续跑?
练习
为「可执行的售后处理 Agent」的 5 个工具(查询订单、查询支付记录、创建工单、发送通知、更新状态)填写下表,用时约 45 分钟:
| 工具 | 读 / 写 | 可能的错误 | 工具层是否自动重试 | 返回给模型的错误信息 | 是否幂等 | 做到一半时的恢复方式 |
|---|---|---|---|---|---|---|
| 创建工单 | 写 | 超时 | 否 | 创建请求超时,结果未知,请先按幂等键查询是否已创建 | 是(按幂等键) | 查询后决定是否重试 |
延伸阅读
- Handle tool calls:
tool_result格式、is_error与错误信息写法。 - Claude API errors:HTTP 错误类型与 SDK 自动重试行为。
- LangGraph Interrupts:恢复时节点重新执行的规则。
下一步
当任务要跑几十分钟甚至几个小时,错误恢复就和“后台执行、断点续跑、进度汇报”绑在一起了,这是下一课「长时间运行的任务」的主题。