首页
看点啥
插画图片
首页 看点啥 Agent 请求失败后,别再反复点击重新生成了

Agent 请求失败后,别再反复点击重新生成了

2026-07-29 0

设想一下,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增加了三类恢复动作。

模型输出空间不足时,程序会先扩大空间,之后才考虑续写。若消息过长,程序压缩消息并重试一次;若接口只是暂时不可用,则按照递增等待时间进行重试。每条路径都设有次数上限,以免循环始终困在失败状态。

这套教学实现仍有若干方面需要完善,其中尤其包括备用模型切换,以及紧急压缩后消息能否保持完整。这说明恢复逻辑本身也必须接受测试,不能只检查状态是否已经更新。

下一章将从单次任务的结束转向对任务本身的管理。待办列表可以记录眼前的几个步骤,但要实现跨会话恢复、依赖关系和长期执行,仍然需要更加完整的任务系统。

喜欢(0)

上一篇

什么是skill?HR最需要的8个skill有哪些?怎样开发一个Skill?(在 小艺Claw 语境下)

什么是skill?HR最需要的8个skill有哪些?怎样开发一个Skill?(在 小艺Claw 语境下)

下一篇

Vibe Coding 的“驾驭”指南:从“失控屎山”到“精准控制”

Vibe Coding 的“驾驭”指南:从“失控屎山”到“精准控制”
猜你喜欢