博客

并非所有 429 都可重试:LLM API 的限流、退避与重试预算

429 可能意味着“稍微放慢一会儿”,也可能意味着“在有人介入之前你已经没有配额”。在日志里,两者看起来完全一样,直到你检查某一个响应头;这个区别决定了重试是有帮助,还是只是在烧时间。

并非所有 429 都可重试:LLM API 的限流、退避与重试预算

限流错误不是一种情况,而是两种,它们在日志里看起来一模一样,直到你去检查某一个响应头。一种是临时限流:按指示等待,请求就会通过。另一种是配额或支出上限:重试(包括 SDK 的自动重试)会一直失败,直到访问恢复,所以花在退避上的功夫纯属浪费。这正是 LLM API 限流问题中仅凭状态码无法告诉你的部分,两家主要厂商都在各自的限流页面上记录了这种区别,并给出了明确的应对指引。本指南会讲清楚如何对429进行分类、如何正确使用Retry-After,构建一份不会失控的重试预算,并从架构层面理解限流为什么会这样表现。

LLM API 限流的判断:可重试还是不可重试

首先要读的是厂商自己的文档:OpenAI 的限流指南和Anthropic 的限流页面都明确列出了哪些情况值得重试。

对于临时限流,OpenAI 文档描述了429响应,其错误类型为rate_limit_error、错误码为slow_down,意思是你的请求速率上升得太快;指引是:当Retry-After存在时遵循它,降低请求速率,然后再逐步提升。Anthropic 记录的限流按每周期请求数和 token 数计量;超出后会返回一个429,说明超出的是哪一项限流,并附一个retry-after响应头,指示需要等待多久。这两种都属于“等待后重试”的情况。

配额与支出上限则不同。Anthropic 指出,当用量达到组织的支出上限时,响应是一个429,其错误类型为rate_limit_error,与限流是同一类型,但没有retry-after响应头;并且重试(包括 SDK 的自动重试)会一直失败,直到访问恢复;在 Messages API 上,你可以通过检查 error details 里的 code 是否为强制的支出上限来区分这两种情况。其文档还指出,提升 tier 可以恢复访问,而这是人的决定,不是退避策略。OpenAI 的文档从另一个方向说得同样明确:Retry-After可能会出现在429响应上,这类响应由临时限流引起,并且它“并不意味着配额、计费或其他需要用户操作的错误可以通过重试解决”。其硬性支出上限也会让受影响的请求返回429,解决办法是调整上限,而不是等更久。

由此得到本文围绕的分类规则:一个429带有Retry-After的响应是限流信号:照它执行。一个429不带Retry-After的响应,应当被视为“先检查配额、计费和 tier”,而不是可以更用力重试的邀请。厂商也可能把同一个状态码用于其他错误,所以在选择恢复动作前要先看错误正文。

正确解读 429 的 Retry-After

当Retry-After存在时,要精确理解它的语义:它是一个最小等待时长,而不是目标值。OpenAI 的指引是至少等待这么久,并在其上再加一点随机延迟,避免多个客户端在同一时刻一起重试。后半句很容易被忽略,而忽略的代价很高:一群 worker 都严格按响应头等到秒,窗口一开就会集体冲上去。

当响应头缺失或无效,或你在处理它覆盖范围之外的情况时,退回到带抖动的指数退避:先短暂等待,然后在每次失败后增加延迟,并用随机化把重试分散开。同样的逻辑也适用于临时过载响应;OpenAI 文档描述了503响应,带有service_unavailable_error、错误码server_is_overloaded,指引是:当Retry-After存在时遵循它,然后重试;如果错误持续,就加大每次尝试之间的延迟。

同一份文档里还有一处细节:一个slow_down即使你的流量处在每分钟请求数和每分钟 token 数的限制之内也可能出现,因为它反映的是流量增长得多快,而不是你是否用尽了配额。文档给出的补救办法是逐步抬升的斜坡,而不是阶跃式变化。

带抖动的指数退避不是可选项

把抖动当作“有则更好”的团队,往往会重新发现它为什么是标准做法。确定性的退避会让客户端同步:所有在同一时刻失败的 worker 等待同样的时长,并在同一时刻再次发出请求,把一个短暂的限流转成自己制造的流量尖峰,让限流一直处于触发状态。抖动让重试彼此去相关。配合Retry-After,模式很简单:等待指示的最小值再加一个随机比例,只有在没有任何指示时才使用指数增长。这两部分都不稀奇,也不特定于某家厂商;它是任何同步 API 集成的最小可行重试策略。

重试预算:限制尝试次数与总时长

能避免大多数重试风暴的指引既枯燥又具体:同时限制尝试次数和重试的总时长。只有次数没有截止时间,一连串等待会堆在一个用户请求后面;只有截止时间没有次数,一串快速失败会反复冲击端点。两者都要,而且都要由所有调用方共用的同一段代码来执行。

团队违反自己预算最常见的方式是嵌套。如果你的应用管理重试,而底层的 SDK 也自动重试,实际尝试次数是相乘而不是相加,总时长也会不可预测地拉长。OpenAI 的文档直接谈到了这一点:如果你在应用里管理重试,就禁用 SDK 重试,或在你的限制里把它们计算进去,以免嵌套的重试循环把请求数量翻倍。它还警告说,每个官方 SDK 都会按各自的设置重试符合条件的429和503响应,并且对Retry-After,尤其是长延迟场景,会因 SDK 版本和配置而异,所以要验证你安装版本的行为,而不要假设服务端给出的延迟会被完全尊重。

最后,要决定预算耗尽时会发生什么,并让它足够显眼:快速让请求失败,记录分类结果(Retry-After是否存在),并在出现不可重试的429时告警,因为这类错误意味着有人需要动手。悄悄重试一个配额错误直到超时,就是这样把五分钟的故障变成五十分钟。

为什么令牌桶限流会拒绝平均速率合规的流量

一个常见的意外:你测到的平均速率看起来明显低于上限,请求却仍被拒绝。机制本身能解释这件事。Anthropic 文档指出,其 API 使用令牌桶算法,容量持续补充到最大值,而不是按固定间隔重置,所以即使当天平均量不大,一次突发也能耗光桶。因此,削平流量比压低每日总量更有价值。一个在客户端给请求定速的队列,加上每个调用方的API 并发上限,对稳定性的帮助超过任何重试策略。

突发并不是问题的唯一形态。Anthropic 还记录了加速限制:用量的急剧上升本身就能产生429错误,指引是逐步抬升流量并保持稳定的用量模式。实际理解是,容量规划应包含你增长的速度,而不只是你发送的量:把斜坡预热,避免在部署时出现阶跃变化,并让流量模式足够稳定,使看板上的尖峰不会变成拒绝率的尖峰。

分层限流:组织、workspace、project

两套生态里的限流都在不同层级执行,知道在哪一层会改变你要修的地方。Anthropic 在组织层执行服务配置的限流,并额外支持按 workspace 用户自定义限流,这可以让某个 workspace 的上限低于组织,以免单个负载把其他负载饿死。OpenAI 通过响应头暴露 project 范围的 token 限制,当 project 范围的 token 限制生效时这些响应头可能出现。Anthropic 的 Message Batches API 又加了一层:一份跨模型共享的独立限流,加上对处理队列中批处理请求的上限,请求在被成功处理之前一直计入。

架构上的含义是:在你自己的系统里镜像厂商的结构——按负载、按团队或按产品面拆分 workspace 或 project,这样失控的批处理任务只会拖累它自己的那一份,而不是共享池。这也是归因重要的地方:当限流触发时,第一个问题是“谁消耗了容量”,回答它所需的指标就是用量归因指南。

按租户设计这些层级,并把配额与成本归因接到同一套结构上,是多租户指南的主题。

把限流响应头当作控制信号

响应头不只是诊断信息,它们是实时的控制信号。OpenAI 暴露了一组响应头,分别针对请求和 token 覆盖上限、剩余量和重置时间,再加上前面提到的 project 范围变体;重置值是时长风格的字符串。一个能读取剩余数量和重置窗口的客户端,可以在自己拿到429之前就先给自己限速:当剩余容量很低或重置即将到来时,预先放慢队列,而不是一头撞上去。文档里有两点提醒:响应头是否存在及其含义应比对厂商当前页面确认,数值也不应硬编码进客户端。把它们当作自适应限流器的输入,而不是写死的常量。

本文的位置,以及清单

本文是错误码指南所介绍的那片水域的深水区:那篇梳理错误码,这篇处理429本身。要减少遇到它的频率,缓存指南讲了如何从源头削减重复工作,而聚合层讨论讲了什么时候多厂商层是容量问题的正确架构答案。你所用服务的端点和协议细节都在它的文档里。

清单汇总在一处:

  1. 只重试可重试的错误:临时限流和过载,而不是配额、计费或任何需要人处理的错误。
  2. 如果Retry-After存在,至少等待这么久,再加一点随机延迟。
  3. 如果它缺失或无效,就使用带抖动的指数退避。
  4. 在共享代码里同时对尝试次数和总时长设预算。
  5. 禁用或把 SDK 的重试计算进去,避免循环嵌套。
  6. 在客户端给流量定速:排队、削峰,并对每个调用方的 API 并发上限设限。
  7. 按 workspace 或 project 镜像限流,并做好用量归因,让限流决策有明确的负责人。
  8. 自适应地读取限流响应头,并对不可重试的429进行告警,而不是重试它们。

这些都不光鲜。它决定了集成在压力下是优雅降级,还是把每一次容量波动都变成一次事故复盘。