首页 > 教程攻略 > ai教程 >Agent 请求失败后,别再疯狂点击重新生成了

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

来源:互联网 时间:2026-08-02 20:36:06

假设你正在使用一个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_TOKENS
        state.has_escalated = True
        continue

这段逻辑出现在回复写入消息历史之前。

这样安排和一次普通重试还不太一样。此时程序保留的是同一份输入,只给模型更多输出空间。模型有机会完整回答当前任务,不会因为读到半截旧回复,又从头补一段背景说明。

如果 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 补上了三种恢复动作。

模型输出不够时,程序先扩大输出空间,再考虑续写。如果消息过长的话,程序压缩消息后重试一次,但如果是接口暂时不可用,程序会按递增等待时间重试。每条路径都有自己的次数上限,避免循环在失败状态里一直打转。

这套教学实现也留下了几个需要继续完善的地方,尤其是备用模型切换和紧急压缩后的消息完整性。它们说明恢复逻辑本身同样需要测试,不能只看状态有没有更新。

下一章,我们的话题会从一次任务的结束,转向任务本身的管理。待办列表能记录当前几步,但跨会话恢复、依赖关系和长期执行,还需要更完整的任务系统。