Agent 请求失败后,别再反复点击重新生成了
假设 Agent 此刻正在替你处理一次测试失败。
Agent 先阅读报错日志,再沿着日志定位相关配置文件,随后模型开始生成修改方案,回答却在半个代码块处中止。疑惑之下,你点击重新生成,可能会得到完整结果,但更多时候依旧卡住。还有一种情况更直接,接口会立即返回 529 overloaded,这一轮甚至没有留下半句话。
若 Agent 遇到任何失败都采用无脑重试这种相同处理方式,后续很容易出现问题。
回答遭到截断后,程序必须判断是否保存已有的半段内容。例如上下文过长时,无论原请求重发多少次都会超过限制;服务过载时,连续点击重试也仍然无法发出请求。
s11_error_recovery 这里要做的,是由 Agent Loop 按照失败发生的位置选择恢复动作。它无法保证模型永不中断,却能使部分可恢复错误不再直接终止整轮任务。
程序可采取的动作取决于失败位置
本章将处理以下三种情况:
| 现象 | 程序获得的信息 | 恢复动作 |
|---|---|---|
| 模型输出触及长度上限 | 响应里的停止原因 | 扩大输出上限,必要时继续写作 |
| 请求的上下文过长 | 调用接口期间抛出异常 | 紧急缩短消息后再次请求 |
| 接口受到限流或服务发生过载 | 调用接口期间抛出 429、529 类错误 | 按照递增间隔等待后再次请求 |
接口异常与输出截断有很大区别。
模型输出被截断时,程序已经取得响应,只是内容尚未结束。上下文超限以及 429 和 529 则发生在请求模型期间,程序无法获得正常响应,因此只能转入异常处理分支。
把三种恢复路线集中起来观察,就更容易辨别它们分别调整的是输出空间、消息历史,还是请求节奏。
回答中途停止时,首次处理应先扩大输出空间
本章首先假定模型可输出 8000 token。
当模型因输出长度不足而停止时,程序第一次不会将这段回答写进消息历史,而是先把上限提升至 64000,再携带原任务和原消息记录重新请求模型。
if response.stop_reason == "max_tokens":if not state.has_escalated:max_tokens = ESCALATED_MAX_TOKENSstate.has_escalated = Truecontinue
这部分逻辑位于回复写入消息历史之前。
这样的安排并非普通重试:程序保留同一份输入,仅为模型扩大输出空间。模型因此有机会完整回答当前任务,也不会由于看到半截旧回复而重新补充一遍背景说明。
只有 64000 token 依旧不足时,程序才保存当前输出,并添加一条续写要求:
Output token limit hit. Resume directly — no apology, no recap. Pick up mid-thought.
这条提示只要求继续写,不允许模型重新解释已经说过的内容。续写最多执行三次,超过次数后循环结束。
设置这一限制很有必要。模型若连续多次无法完成输出,通常意味着任务粒度过大或回答方式不合适。继续追加续写请求,最终可能只会得到长度惊人、阅读体验却没有改善的回答。
上下文过长时,保留最近五条消息仅是应急措施
上下文超过限制后,模型接口会直接拒绝本次请求。
教学代码执行紧急压缩时并未生成摘要,而是只留下消息历史中的最后五条,并在这里增加一条说明:
def reactive_compact(messages: list) -> list:tail = messages[-5:]return [{"role": "user","content": "[Reactive compact] Earlier conversation trimmed. " "Continue from where you left off.",}, *tail]
压缩后的消息会替换原来的消息历史,循环随后重新调用模型。
这条路线与 s08 的常规上下文压缩并不处于同一层级。s08 尽可能保留任务目标、工具结果和既有结论,s11 的紧急压缩则只负责尽快降低请求长度。
假设 Agent 此前读取了十几个文件,而最近五条消息正好只包含工具结果和模型的一句中间判断,那么更早的用户要求、工具调用来源及项目约束可能都已离开消息历史。模型虽然能继续生成内容,却不一定还记得为何会走到这里。
正因如此,本章仅允许执行一次紧急压缩。压缩后仍然超限,程序便直接返回错误。继续删除消息固然可以缩短上下文,但也可能把任务删到只剩标题。
另一个实现细节是,这里直接截取最后五条消息,并未像 s08 一样检查工具调用与工具结果能否保持配对。该方式适合用于解释恢复方向,却不宜原样移入需要严格维护消息格式的系统。
处理 429 和 529 的关键在于控制重试节奏
429 代表限流,529 代表服务暂时过载。由于任务内容没有改变,消息历史无须裁剪,程序只需等待一段时间后再次尝试。
本章采用指数退避,让等待时间逐步延长:
| 失败次数 | 基础等待时长 |
|---|---|
| 第 1 次 | 0.5 秒 |
| 第 2 次 | 1 秒 |
| 第 3 次 | 2 秒 |
| 第 4 次 | 4 秒 |
| 后续 | 最多 32 秒 |
每轮等待还要加入少量随机时长。
若多个 Agent 同时收到 529,并严格等待 0.5 秒后一同重试,服务刚刚恢复便会再次承受一批请求。随机抖动可以稍微错开重试时间,避免压力重新集中在同一时间点。
代码将 429 和 529 放到 with_retry() 中处理,其他异常则重新抛至外层循环。这种分工使恢复逻辑更为清晰:临时服务问题由重试函数负责,长度问题由消息压缩逻辑应对,无法判断的错误则在记录后结束任务。
教学代码中的备用模型其实尚未接通
若连续三次出现 529,且已经配置备用模型,代码便会更新当前模型名称:
state.current_model = FALLBACK_MODEL
仅看这一行代码,似乎已经成功切换备用模型。
但问题发生在接口调用环节:请求函数把当前模型保存成了 lambda 的默认参数:
lambda mt=max_tokens, mdl=state.current_model:client.messages.create(model=mdl, ...)
后面的重试仍会反复调用同一个 lambda。即便恢复状态中的模型名称已经发生更新,mdl 仍会保留 lambda 创建之时的旧值。
所以,教学代码目前虽然会输出切换备用模型的日志,之后的重试却依旧调用原模型。这是 s11 中值得单独记录的一处实现缺口。
若要让切换立刻生效,请求函数必须在每次调用时读取恢复状态:
def request():return client.messages.create(model=state.current_model,system=system,messages=messages,tools=TOOLS,max_tokens=max_tokens,)
恢复逻辑里有一个常见误区:状态字段已经修改,不代表下一次操作一定会读取这个字段。中间如果缓存了参数、闭包保存了旧值,状态变化就只停留在日志里。
这份教学代码还忽略了哪些内容
| 位置 | 当前处理方式 | 实际系统仍需补齐的部分 |
|---|---|---|
| 紧急压缩 | 留下最近五条消息 | 保护工具调用和工具结果之间的关联 |
| 服务端建议的等待时间 | 延迟函数支持这一参数 | 目前的调用位置没有读取接口响应所含的等待信息 |
| 重试次数 | 循环共发起请求 10 次 | 清楚区分总尝试次数与额外重试次数 |
| 备用模型 | 更新当前模型的状态 | 确保下次请求读取最新模型 |
| 输出续写 | 固定至多三次 | 依据新生成的内容判断还有没有继续的价值 |
这些简化不会妨碍对本章主线的理解。s11 所要说明的是,错误恢复会调整不同对象:有时需要改变输出空间,有时应当改变消息历史,有时则只需改变请求节奏。
本章总结
本章为 Agent Loop 增加了三种恢复动作。
模型输出空间不足时,程序先扩大空间,然后才考虑续写;消息过长时,程序压缩消息并重试一次;若接口只是暂时不可用,程序则按照递增的等待时间再次尝试。每条路线都设置了次数上限,防止循环持续困在失败状态。
这套教学实现仍有几个方面需要继续完善,备用模型切换和紧急压缩后的消息完整性尤其突出。这些缺口表明,恢复逻辑本身也必须接受测试,不能只检查状态是否完成更新。
下一章将从单次任务如何结束,转向任务本身怎样管理。待办列表只能记录当前几步,而跨会话恢复、依赖关系和长期执行还需要一套更完整的任务系统。
-
07.29
cf朝歌遗迹如何通关
-
07.29
地牢猎手6法师最强随从是谁
-
07.29
镭明闪击贝菈如何玩
-
07.29
炉石传说标准星灵贼卡组如何搭配-炉石美服登顶星灵贼卡组推荐11月
-
07.29
余烬频段值得玩吗 余烬频段玩法简介
-
07.29
实况足球八周年庆典攻略:活动全览、卡池排雷与8号球星加点指南
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏