Agent失败恢复:像挑战极限一样设计自适应重试
一个Agent如果永远待在“舒适区”——只执行它确定能完成的任务——那它永远不会真正有用。真正的智能在于:遇到意外时,能像健美运动员一样,把失败当作训练的信号,调整姿势再试一次。
问题背景:为什么Agent需要“走出舒适区”?
你写过一个简单的Agent:它调用工具,解析用户指令,按顺序执行步骤。大多数时候它能顺利完成任务。但一旦出现意外——API返回500、参数格式错误、外部服务超时——它就直接崩溃,或者死循环重试同一个错误路径。
这和Model的“单次对话”模式不同:单次对话失败就返回错误,用户自己决定下一步。但Agent的任务通常是多步骤、依赖外部环境的。一个合格的Agent需要具备自我修正能力:当预期结果和实际结果不符时,能分析原因、调整计划、重新尝试,而不是简单报错或无限重试。
Elizabeth Smart在经历绑架后被健美挑战“打通了什么”(a click that went off in my mind),她明白:走出舒适区、面对评判、在硬截止线前证明自己——这正是Agent在面对失败时该有的态度。我们不需要Agent感到害怕,但我们需要它能够识别“我不确定这条路是否对”的时刻,并主动切换策略。
Agent架构中失败恢复的位置
一个完整的Agent架构包含四个核心模块:
- 规划器(Planner):将用户目标分解为子任务序列。
- 工具接入(Tooling):执行具体操作的API/函数。
- 记忆系统(Memory):存储中间状态、失败记录、策略历史。
- 执行器(Executor):调度任务,监控进度,处理异常。
失败恢复不是单独一个模块,而是横切在执行器中的自适应逻辑。以我常用的ReAct模型为例,规划器会生成一个计划,执行器按步骤调用工具,每步返回观测结果。当观测结果不符合预期(例如工具返回空数据或错误码),执行器需要:
- 暂停当前计划
- 记录失败上下文到短期记忆
- 将问题重新交给规划器,要求基于新信息重新规划
- 规划器生成替代方案,执行器继续
这个循环类似于人体抗阻力训练:第一次推不起来重量,你分析是发力角度问题,调整后第二次尝试就成功了。Agent的“训练”就是失败-分析-调整-重试的迭代。

核心流程:检测-分析-恢复-学习
以下是我在多个生产Agent中验证过的四步失败恢复流程:
第一步:失败检测(Detection)
不是所有错误都需要恢复。例如工具调用语法错误(参数缺字段)可以直接返回,让调用者修正。只有以下情况才进入恢复流程:
- 工具返回预期外的空结果(如搜索“如何做蛋糕”返回0条)
- 外部服务超时或限流(需要降级或等待)
- 多步骤后中间状态不一致(比如转账后余额未更新)
检测的关键是定义每个工具的预期输出模式。我通常用一个简单的Schema:
{
"tool": "search_web",
"expected": {"results_count": {"type": "range", "min": 1}},
"recovery": "fallback_to_alternative_search"
}
当实际结果不满足expected时,触发对应recovery。
第二步:原因分析(Analysis)
分析阶段要回答三个问题:
- 这是临时还是永久错误?(超时 vs 404)
- 是否需要调整工具参数,还是替换整个工具?(重试加&timeout=10 vs 改用Bing)
- 是否需要回溯到之前某一步重新执行?(数据库事务回滚+重新查询)
我习惯让Agent在失败后自动生成一个“诊断报告”,结构化为:
Error path: Step 3 (call_payment_api)
Reason: Connection timeout
Recovery candidates:
- Retry (max 3, exponential backoff)
- Switch to backup payment gateway (if available)
- Ask user for manual override
Selected: Retry then fallback
这个报告会写入短期记忆,供后续规划器参考。
第三步:自适应恢复(Recovery)
根据分析结果,执行恢复策略。常见的策略有:
- 立即重试:加入指数退避(退避因子=2,初始1s,最大30s)
- 降级执行:使用简化版工具(例如搜索失败时,用本地缓存数据)
- 分支重规划:放弃当前子任务,重新规划该步骤的替代路径(例如“调用翻译API”失败,改为“拆成字符级处理”)
- 回溯+重做:回滚到上一个确定性状态点,重新执行后续步骤(需要保存状态快照)
个人观点:很多Agent设计只用了第一种(重试),这是不够的。我在实际项目中遇到最多的情况是:重试了3次还是失败,然后Agent就放弃了。更可靠的做法是:给每种工具配置一个备用方案,当重试次数耗尽时,自动切换。例如:
recovery_plan = [
{"type": "retry", "max": 3, "backoff": 2},
{"type": "fallback_tool", "name": "search_bing"},
{"type": "user_input_required", "question": "搜索失败,请手动提供信息?"}
]
这样Agent就不会死在一个工具上。
第四步:经验学习(Learning)
失败恢复完成后,Agent需要将这次经验记录到长期记忆。至少记录:
- 原始任务和目标
- 失败步骤和原因
- 采用的恢复策略和结果(成功/失败)
- 如果成功,标记该策略为该场景的“推荐策略”
后续类似任务出现时,规划器会优先考虑曾经成功的策略,相当于Agent“长了肌肉”。

关键实现细节和踩坑记录
以下是你在写代码时最容易踩的坑:
1. 重试极限:不要无脑退避
指数退避听起来优雅,但如果所有Agent实例都在同一个时间点遇到错误(比如支付系统全挂),退避会导致请求峰谷同相位。建议加入随机抖动:
sleep_time = min(backoff * (2 ** retry_count) + random.uniform(0, 0.5), MAX_SLEEP)
2. 状态回滚粒度
如果Agent在步骤5失败,回滚到步骤0重新开始可能浪费大量token。更合理的是设置检查点(checkpoint),例如每3步保存一次完整上下文。
3. 冲突检测
当恢复策略需要切换工具时,必须检查新工具的输出格式是否匹配后续步骤的输入。例如原始步骤期望返回JSON,但备用工具返回Markdown,需要额外转换。我的做法是在工具定义中明确 output_format_schema,切换时Agent必须保证兼容。
4. 用户反馈时机
只有当所有自动恢复策略都尝试失败后,才应该询问用户。过早打断用户会很恼人。但注意:如果恢复策略涉及高风险操作(如转账),最好让用户确认后再执行。
简化版动手实现:一个带自适应重试的Agent骨架
下面是一个Python伪代码,展示失败恢复的核心循环。你可以直接改造成自己项目的基础。
import time
import random
class AdaptiveAgent:
def __init__(self, tools, memory):
self.tools = tools # dict: name -> callable
self.memory = memory # 长期记忆,存失败模式
self.max_retries = 3
def execute_plan(self, plan):
"""plan: list of steps, each step = {'tool': 'search', 'params': {...}}"""
for i, step in enumerate(plan):
tool_name = step['tool']
params = step['params']
attempts = 0
fallback_used = False
while attempts < self.max_retries:
try:
result = self.tools[tool_name](**params)
# 简单结果校验:是否非空
if self._validate_result(result, step):
break # 成功,退出重试循环
else:
raise ValueError("Result validation failed")
except Exception as e:
attempts += 1
if attempts >= self.max_retries:
# 重试耗尽,尝试备用工具
if not fallback_used and self._has_fallback(tool_name):
tool_name = self._fallback_name(tool_name)
fallback_used = True
attempts = 0
continue
else:
self._ask_user_or_rollback(step, e)
return False
time.sleep(self._backoff(attempts))
# 成功后记录本次经验
self.memory.record_success(original_tool=step['tool'],
used_tool=tool_name,
retries=attempts)
return True
def _backoff(self, attempt):
return min(2 ** attempt + random.uniform(0, 0.5), 30)
def _validate_result(self, result, step):
# 简单示例:确保不为None且可迭代(若期望有多个结果)
return result is not None and (not hasattr(result, '__len__') or len(result) > 0)
你可以根据需要扩展_has_fallback和_fallback_name逻辑,从工具配置中读取备用项。
总结:走出舒适区不是玄学,是算法
Elizabeth Smart选择健美,是因为有一个硬截止日期,和一个她害怕但值得挑战的目标。Agent的“舒适区”是那些它确定能成功的任务。但现实世界由不确定性主导。如果我们希望Agent真正有用,就必须赋予它挑战失败的能力——探测异常、分析原因、切换策略、记录经验。
下一次当你写Agent遇到retry逻辑时,问问自己:如果这个工具彻底不能用,我的Agent知道该换另一个工具吗?如果不知道,它还没有真正“走出舒适区”。
(如果你想深入了解记忆管理和状态回滚的实现,欢迎留言告诉我,我可以写续篇。)