Error Recovery - 错误恢复机制
L11 要回答一个问题:如何在 L06 的基础上让 Agent 遇到错误能恢复,而不是崩溃?
L06 给了我们骨架:三层压缩 + 连续性保护 + 控制权归属。但真实 Agent 执行时会遇到各种错误——输出被截断、上下文太长、连接超时。全部崩溃会中断工作流,全部忽略会丢失信息。怎么做?
本章讲三个机制:
| 机制 | 解决什么问题 | 核心设计 |
|---|---|---|
| max_tokens 恢复 | 输出被截断 | 注入续写消息 + 最多3次 |
| prompt_too_long 恢复 | 上下文太长 | 压缩历史 + 重试 |
| backoff 重试 | 连接失败 | 指数退避 + 随机抖动 |
这三个机制叠加在 L06 的 Context Compact 上,循环本身不变。理解 L11,就理解了 Agent 如何”错误恢复不崩溃”。
一、三种恢复路径:分类再处理
“错误不是例外,是正常分支。先分类再恢复,最后才暴露失败。”
这句话概括了 Error Recovery 的本质。不是所有错误都崩溃——先分类,再选择恢复路径。
三种错误类型
恢复路径对比
| 错误类型 | 触发条件 | 恢复策略 | 成本 |
|---|---|---|---|
| 输出截断 | stop_reason == "max_tokens" | 续写消息注入 | 低(不改历史) |
| 上下文过长 | prompt_too_long API 错误 | 压缩历史 | 高(丢失细节) |
| 连接失败 | timeout/rate limit | 指数退避 | 中(等待时间) |
恢复优先级:max_tokens → prompt_too_long → connection error → fail gracefully。
为什么这个顺序?
- max_tokens 最容易恢复(只需继续输出),成本最低
- prompt_too_long 需要压缩(改变历史),成本较高
- connection error 是外部问题,等待可能恢复
- 优雅失败是最后兜底
验证你的理解
Q1:为什么 max_tokens 恢复优先级最高?
:::details 答案 成本最低,最容易恢复。
max_tokens 只需注入续写消息,不改变历史,不丢失信息。
prompt_too_long 需要压缩历史,丢失细节,成本较高。
connection error 需要等待,时间成本中等。
恢复优先级原则:低成本优先恢复,高成本最后尝试。 :::
二、max_tokens 恢复:续写不重述
max_tokens 触发时,输出被截断——需要继续输出,但不能让模型”重述”。
续写消息设计
CONTINUATION_MESSAGE = ( "Output limit hit. Continue directly from where you stopped -- " "no recap, no repetition. Pick up mid-sentence if needed.")
def handle_max_tokens(messages: list) -> list: max_output_recovery_count += 1
if max_output_recovery_count <= 3: messages.append({"role": "user", "content": CONTINUATION_MESSAGE}) return messages # 继续循环 else: print("Max output recovery attempts exhausted") return None # 停止设计要点:
- 不重述:“no recap, no repetition”——不让模型浪费时间复述已输出内容
- 直接续写:“Continue directly from where you stopped”——从中断处接着写
- 中断点续写:“Pick up mid-sentence if needed”——允许句子中间继续
为什么不重述?
| 方案 | 问题 |
|---|---|
| 让模型重述 | 浪费 token,重复已输出内容,可能再次触发 max_tokens |
| 直接续写 | 高效,从中断点继续,不浪费 token |
类比:笔写到一半没墨了,换笔继续写——不是从头重写。
验证你的理解
Q2:CONTINUATION_MESSAGE 为什么强调”no recap”?
:::details 答案 防止模型浪费时间重述已输出内容。
如果让模型重述:
- 已输出内容再次输出 → token 浪费
- 可能再次触发 max_tokens → 进入恶性循环
- 用户看到重复内容 → 体验差
直接续写设计:从中断点继续,不浪费 token,高效恢复。 :::
三、prompt_too_long 恢复:压缩历史
上下文太长时,API 返回 prompt_too_long 错误——需要压缩历史。
auto_compact 实现
def auto_compact(messages: list) -> list: # 1. 提取对话历史(截断到80000字符) conversation_text = json.dumps(messages)[:80000]
# 2. 调用LLM生成摘要 prompt = """Summarize this conversation for continuity. Include: 1) Task overview and success criteria 2) Current state: completed work, files touched 3) Key decisions and failed approaches 4) Remaining next steps"""
summary = call_llm(prompt + conversation_text)
# 3. 返回续接消息 return [{"role": "user", "content": f"Compacted.\n{summary}"}]摘要四要素
| 要素 | 内容 | 为什么重要 |
|---|---|---|
| Task overview | 目标 + 成功标准 | 方向不偏离 |
| Current state | 已完成 + 文件修改 | 知道当前状态 |
| Key decisions | 决策 + 失败尝试 | 决策追溯 |
| Next steps | 下一步方向 | 能立即恢复 |
复用 L06 设计:auto_compact 的摘要四要素与 L06 连续性5要素一致——复用已有机制。
优雅降级
# 失败时优雅降级except Exception: return [{"role": "user", "content": "Previous context lost. Please restate your current task."}]设计原则:即使摘要失败,也不崩溃——告诉用户上下文丢失,让用户重新陈述。
验证你的理解
Q3:auto_compact 摘要为什么要包含”失败尝试”?
:::details 答案 防止模型重复错误方向。
如果摘要丢失”失败尝试”:
- 模型恢复后不知道哪些方向已失败
- 可能重复尝试错误方向
- 时间浪费,效率下降
摘要包含”失败尝试”:让模型知道哪些路不通,避免重复错误。 :::
四、backoff 重试:指数退避+抖动
连接失败时,需要等待后重试——但不能立刻重试,避免短时间大量请求。
指数退避设计
def backoff_delay(attempt: int) -> float: delay = min(1.0 * (2 ** attempt), 30.0) # 指数增长,上限30秒 jitter = random.uniform(0, 1) # 加入随机抖动 return delay + jitter退避序列
| attempt | base_delay | jitter | 总延迟 |
|---|---|---|---|
| 0 | 1.0s | 0-1s | 1-2s |
| 1 | 2.0s | 0-1s | 2-3s |
| 2 | 4.0s | 0-1s | 4-5s |
| 3+ | 停止 | - | - |
设计要点:
- 指数增长:1s → 2s → 4s,避免短时间大量重试
- 上限30秒:防止等待过长
- 随机抖动:防止多客户端同步重试
为什么需要 jitter?
问题:多客户端同时遇到错误 → 同时重试 → 同时冲击服务器 → 惊群效应。
解决:jitter 让每个客户端等待时间不同 → 错开重试时间 → 避免同步冲击。
类比:缓存击穿防护——多个请求同时发现缓存失效 → 同时查询数据库 → 数据库压力爆炸。jitter 错开请求时间。
验证你的理解
Q4:为什么 jitter 能防止惊群效应?完整链条是什么?
:::details 答案 jitter 错开多客户端重试时间,避免同步冲击服务器。
没有 jitter 的链条:
- 多客户端同时遇到错误
- 同时计算 backoff_delay(attempt相同 → delay相同)
- 同时等待相同时间
- 同时重试 → 服务器压力爆炸
有 jitter 的链条:
- 多客户端同时遇到错误
- 同时计算 backoff_delay
- 但 jitter 不同(随机0-1s)
- 等待时间错开(1-2s, 1.5-2.5s, 0.8-1.8s)
- 重试时间错开 → 服务器压力分散
类比:缓存击穿防护——随机过期时间错开请求。 :::
五、回顾:L11 在循环上叠加了什么
回到开头的问题:如何在 L06 的基础上让 Agent 遇到错误能恢复,而不是崩溃?
答案:
| 叠加机制 | 代码位置 | 循环的变化 |
|---|---|---|
| max_tokens 恢复 | stop_reason 检查后 | 不变——只是注入续写消息 |
| prompt_too_long 恢复 | APIError 处理 | 不变——只是压缩历史 |
| backoff 重试 | Exception 处理 | 不变——只是等待后重试 |
循环本身仍是 L01 的 while True + stop_reason + messages[]。L11 只是在”错误处理层”叠加了恢复机制。
与后续课程的关系
| 课程 | 关系 |
|---|---|
| L06 Context Compact | auto_compact 复用 L06 压缩机制 |
| L10 System Prompt | 恢复提示注入 |
| L12 Task System | 保护长任务流 |
核心洞察:Error Recovery 是”错误可恢复”的机制。理解 L11,就理解了 Agent 如何”分类错误再恢复”。后续课程都在扩展恢复机制——保护长任务、注入提示。
六、心智模型升级:从 L0 到 L2
读完这篇文章,你的理解经历了怎样的升级?
L11 核心概念的理解升级路径
| 概念 | L0(表面) | L1(关联) | L2(深层) |
|---|---|---|---|
| 恢复本质 | ”错误恢复" | "三种恢复路径" | "错误不是例外,是正常分支。有限重试+优雅失败。先分类再恢复,最后才暴露失败” |
| 续写机制 | ”继续输出" | "注入 CONTINUATION_MESSAGE" | "续写不重述——no recap, no repetition。类比笔没墨换笔继续写,不是从头重写” |
| 压缩恢复 | ”压缩历史" | "auto_compact 四要素" | "复用 L06 机制——摘要四要素与连续性5要素一致。失败尝试必须包含,防止重复错误方向” |
| 退避设计 | ”等待重试" | "指数退避 + jitter" | "jitter 防惊群——多客户端错开重试时间。类比缓存击穿防护,随机过期时间错开请求” |
| 优雅失败 | ”重试失败就停" | "告诉用户失败原因" | "有限重试上限3次 + 暴露失败原因。不是崩溃或无限循环,是设计决策” |
如何检验你达到了 L2?
用这个问题自测:
为什么恢复机制要有预算限制(最多3次),而不是无限重试?
:::details L2 层级回答 有限重试是设计决策——不是无限循环。
理由:
- 资源预算:无限重试会消耗 token 和时间,可能永远无法完成
- 用户反馈:重试3次后仍失败,说明问题可能需要用户介入
- 优雅失败:暴露失败原因,让用户决定下一步(修改配置、换模型、简化任务)
类比:TCP 重传上限——不是无限重传,超过上限就放弃连接。网络协议同样采用有限重试策略。 :::
本章目标:让每个核心概念都从 L0 升级到 L2。理解 L11,不只是记住三种恢复路径,而是掌握”如何分类错误再恢复”的设计原则。
七、复习可视化
Error Recovery 主流程
恢复状态机
choose_recovery 决策树
三种恢复策略对比
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!