凌晨三点的重试风暴:agent 工作流里的超时与重试陷阱
无限重试把一次抖动放大成十倍账单
很多人在 agent 脚本里写的重试是循环套循环:上游返回 5xx 就重来,网络超时了也重来,模型输出到一半断流还是重来。听起来很稳妥,实际上一次持续十秒的抖动,可能被你的客户端放大成二十次请求,每一次都重新把几万 token 的上下文送上去。真正花掉预算的往往不是失败的那次调用,而是你为它准备的那几次补救。
更麻烦的是,重试会掩盖上游的真实状态。当补发请求和原请求几乎同时落到服务端,限流窗口里的配额被瞬间占满,于是 429 变多,429 又触发更多重试,最后你看到的不是网络问题,而是自己的重试风暴。我的习惯是把重试次数写死在配置里,最多两次,并且用指数退避加随机抖动,让并行跑的多个工作流不会在同一时刻齐步走。
- 给每个阶段设定独立的重试预算,而不是给整条工作流一个无上限的总开关。
- 重试前先检查剩余的上下文与预算,避免用更贵的长调用去填补一次便宜的失败。
- 把重试次数和最终成功率一起记进日志,否则你永远说不清钱花在哪一步。
超时阈值定在三十秒,却在首 token 之前断线
很多客户端默认三十秒超时,这个数字对普通接口够用,对需要长时间推理的 agent 调用却常常不够。尤其在工具调用密集的流程里,模型可能要先消化完大量上下文才吐出第一个字符,你在首 token 之前就把连接掐断,等于把一份完整推理的费用全部作废,然后还会因为重试再付一次。
正确的做法是把三种超时拆开:连接超时可以短,读超时要容忍慢首包,总超时按单轮任务的预算给。流式响应还要单独处理,已经收到若干片段时就别把它当成全盘失败,而是从断点判定是否可恢复。像 DeepSeek V4 Flash 这种偏低延迟的模型适合放在需要快速反馈的子任务里,而 DeepSeek V4 Pro 更适合较长的推理段落,两者首包时间差异明显,用同一套阈值去卡它们并不合理。
- 把连接超时、读超时和总超时分开配置,让每一类失败都能被准确归因。
- 为流式调用设置首包超时与片段间隔超时,避免长时间静默却无人察觉。
- 超时后先确认服务端是否仍在执行,再决定放弃重试还是继续等待结果。
重试时重排上下文,让缓存读写作废
agent 的上下文通常有个稳定前缀:系统提示、工具定义、历史对话。这部分正是缓存最值钱的地方。可一旦你在重试时重新拼装消息数组,比如把今天日期塞进系统提示、临时调整工具顺序、或者把一次失败的工具结果追加到不同位置,缓存前缀就变了,读取变成写入,写入再变成全量计算。
计费口径也值得认真看。有些服务只对新生成的 token 计费,另一些会把缓存读取和缓存写入都算进账单。在按真实 token 计费、包含缓存读写、没有月费的 Fluxo 上,这一点尤其直观,省下来的上下文会直接体现在账单里。使用 Fluxo 时,一把密钥加一个 base URL https://api.fluxorelay.com 就能串起 Claude、GPT、Gemini、DeepSeek、Kimi、GLM、Qwen、Grok 等模型,Claude Code、Codex、Gemini CLI 以及任何 OpenAI SDK 都能直接接上,换模型不必再改一堆配置。
- 保持消息前缀稳定,把时间戳和随机标识这类变化内容放到消息末尾。
- 重试时复用完全相同的消息数组,不要为了修正格式而重新排列工具定义。
- 把缓存读取、缓存写入和新生成 token 分开统计,才能算出重试的真实成本。
把 429、500、断线和取消混成同一种错误
错误处理写成一个大 catch 块,是 agent 代码里最隐蔽的浪费。参数错误重试一千次也不会成功,限流要等服务端建议的时长,服务端错误值得退避,用户主动取消则根本不该重试。把这些混在一起,结果就是该慢的没慢下来,该停的停不下来。
我通常把错误分成四类:不可重试的请求错误、需要退避的暂时错误、需要改写提示的上下文错误,以及属于用户意志的取消。分类之后,退避策略才有意义。响应里给出的等待建议要尊重,不要用固定两秒硬闯限流窗口。断线发生在流中段时,也要区分手动触发的工具是否已经产生副作用,否则重试会造成重复提交或重复扣费。
- 为四类错误分别定义处理策略,让重试只覆盖真正可能恢复的情况。
- 尊重服务端返回的等待建议,不要用固定短间隔强行穿透限流窗口。
- 把取消视为正常退出路径,避免用户关掉终端后后台继续消耗 token。
重试只补模型调用,工具副作用却执行了两遍
agent 不只是聊天,它会读文件、写文件、发请求。模型调用通常可以安全重试,工具副作用却往往不行。一个写入操作在超时后被重放,可能生成两份配置、两张工单、两次扣费。判断一次调用是否失败时,最好带上请求标识,让服务端或你自己的幂等表来裁决,而不是凭客户端超时下结论。
另一个常被忽略的细节是状态恢复。重试之后,工作流应该从检查点继续,而不是从头跑一遍前面所有步骤,否则前面几步的 token 会再被计算一次。像 Claude Sonnet 5.5 这样适合日常编码任务的模型,配合稳定的检查点,能把单次 agent 会话的总开销压得更可控;反之,盲目重跑整条链路,账单的增长速度会远超你的代码行数。
- 给每个工具调用附加幂等标识,让重复请求能被识别并安全合并。
- 在关键步骤保存检查点,重试只回滚到最近一次可信状态继续执行。
- 把预算上限写进调度器,单日消耗触顶后停止重试而不是继续兜底。
缺少请求标识与成本日志,重试问题永远说不清
如果你回答不出这轮任务到底发了多少次请求、多少 token 被重算、多少命中缓存,那重试策略就只能靠猜。我的做法是把请求标识、模型名、重试序号、首 token 时间、总耗时、输入输出与缓存 token 一起落到本地日志,跑几天再回头看,哪些超时其实是自己阈值定得太紧,一目了然。
成本日志还会顺手暴露模型选择的问题。同一段任务换到另一个模型,也许首包更快、缓存命中更高,总成本反而更低。按真实 token 计费的口径让你可以放心做这种对照实验,因为没有月费兜底,账单就是真实用量本身,重试多算的每一笔都藏不住。
如果你也常在深夜盯着重试日志估算成本,欢迎注册一个 Fluxo 账号,用一把密钥和一个 base URL 把这些实验跑起来。你邀请来的朋友付费后,你还能拿到一部分返佣;遇到具体问题,登录后在联系页留言即可。