跳到主要内容

那些与实际限流不符的提供商频率限制响应头

· 阅读需 11 分钟
Tian Pan
Software Engineer

响应头显示你还有每分钟 480,000 个 token 的剩余额度。但在你仅消耗了 240,000 个之后,429 错误就降临了。你的调度器一直在根据一个运行环境根本不会遵守的数字进行自动扩缩,墙上的燃尽图显示的是文档里的理论值,而限流器执行的却是另一套完全不同的规则。

这种故障往往需要很长时间才能被察觉,因为路径上的每个组件都在执行其宣称的功能。供应商返回了一个响应头。你的客户端解析了它。你的调度器读取了它。你的仪表盘绘制了它。这些层级都没有损坏。真正出问题的是那个假设:即响应头是一份契约。

它不是契约。它只是一个提示,源自一个从未被设计为下游控制循环关键输入的内部计费系统,而它所声称的内容与限流器强制执行的内容之间的差距,大到足以引发一场停机事故。

为什么响应头与限流器不一致

你解析的响应头 —— x-ratelimit-remaining-tokensanthropic-ratelimit-tokens-remaining 或供应商提供的等效字段 —— 所回答的问题与你的调度器所询问的问题并不相同。你的调度器想知道:“在接下来的一秒钟内,我可以安全地发送多少个 token?”而响应头回答的是:“在我们序列化该响应的那一刻,为了某些我们尚未完全说明的用途而维护的计数器余量如下。”

这两个不是同一个问题,且其实现至少在四个方向上存在偏差。

第一个分歧在于时间窗口 (time window)。文档中说是“每分钟 token 数 (TPM)”,大多数工程师假设这意味着 60 秒的滑动窗口。在实践中,供应商按分钟以下的间隔执行限流 —— 有时是 1 秒桶,有时是 10 秒桶,有时则是与宣传的每分钟数字毫无关系的具有突发容量的令牌桶算法。Azure 的文档直接承认了这一点:即使你的每分钟平均值在限制范围内,分钟内窗口的突发流量也会触发 429 错误。响应头告诉你的是分钟级别的情况,而限流器则在按秒计数。

第二个分歧在于预估与核销 (estimation versus reconciliation)。在模型生成结果之前,输出 token 的成本是不可知的。供应商的处理方式是提前预留估算的输出容量 —— 通常使用最坏情况下的 max_tokens 值 —— 并在响应完成时根据实际使用情况进行核销。在预留与核销之间,在途预算是按估算值扣除的,而非真实值。如果你的应用程序设置了 max_tokens: 4096 但模型通常只返回 300 个,那么在每次调用的持续时间内,计入配额的使用量将是现实情况的 13 倍以上。响应头忠实地反映了估算值,而你的仪表盘却将其视为预测值。

第三个分歧在于统计口径 (what counts)。大多数供应商在限流计算中不包括缓存的输入 token,但同样的供应商可能会在响应报告的 input_tokens 中包含它们,并可能按折扣价收费。位于你和供应商之间的网关 —— Portkey、LiteLLM、Kong 或内部网关 —— 可能无法正确复制这种排除逻辑。LiteLLM 在 2025 年发布的几个版本中存在一个已知 bug,它将缓存的 token 计入 TPM 限制,即使上游供应商并不会这样做,这意味着网关会对供应商本可以愉快处理的请求返回 429。你的响应头报告的是网关的视角;你的重试逻辑是基于供应商的视角;两者从未达成一致。

第四个分歧在于多部署计数器的作用域 (multi-deployment counter scope)。如果你的流量分布在多个网关节点、多个区域或共享配额的多个模型部署中,则单次响应的响应头仅反映本地计数器所看到的情况。Azure API Management 的令牌限制策略明确指出:计数器是每个网关本地的,不会跨实例聚合。你的总消耗量可能会超过限制,而每个单独节点的响应头却显示有充足的余量,因为没有节点知道其他节点的账目。

撞上“限流墙”的自动扩缩器

这类事故最常见的版本是自动扩缩器将 x-ratelimit-remaining 视为可靠的容量信号。逻辑看起来很合理:如果供应商说我们还有余量,就发送更多在途请求;如果剩余量降至阈值以下,就退避。工程师们出于好意实现了这一点,将响应头视为处理任何其他背压机制的方式。

但自动扩缩器是根据一个计数器在未来几秒钟内做出决定的,而供应商可能会在几毫秒内重置该计数器,或者将其作用域限定在你并不独占的部署中,或者在尚未计费的请求之后异步更新它。其结果是一个控制循环,其中输入变量和约束变量在不同的时钟上、针对不同的单位、在不同的作用域内运行。

当系统处于稳定负载下时,这是不可见的。每次调用的偏差很小,但它是结构性的且单向的:响应头始终显示比限流器实际允许的更多的余量,因为响应头是快照,而限流器是窗口。在突发情况下 —— 比如产品发布、流量迁移或任何你的调度器本应平稳处理的事件 —— 这种偏差会叠加。调度器进行扩容,撞上了实际的限流墙,看到了 429 错误,将其视为瞬态错误并重试,此时重试流量便与原始流量开始竞争配额 —— 而由于响应头尚未更新,系统仍然认为配额还剩三分之二。

这就是这种失效模式:仪表盘显示为绿色,而面向用户的延迟却掉下了悬崖。值班工程师读着调度器三分钟前读过的同样的绿色仪表盘,并得出结论:一定是网络出了问题,因为限流的数学逻辑显然是没问题的。

相互矛盾的指标

一旦你开始同时记录 Header 和实际的 429 事件,第二种不匹配就会显现出来:供应商控制台中的指标与你日志中的指标不一致。

供应商监控视图中的 Token 使用量通常是成功处理并计费的请求记录。然而,速率限制(Rate-limit)的执行是在接收到每个请求时应用的——包括那些被拒绝、从未计费,或因估算过高而后来被下调核算的请求。因此,你可能会在生产环境中看到 429 错误,而供应商的 TPM 图表却显示你远低于公开的限额。该指标报告的对象与流量限制器(throttler)作用的对象并非同一群体。

这不仅仅是容量规划中的不便。这意味着你与供应商支持团队之间的沟通是从互不兼容的数据开始的。你的追踪显示在 240k TPM 时出现了 429 错误;供应商的仪表盘显示你的峰值为 180k TPM。按照各自的定义,两者都是“正确”的。但哪一个都无法帮你理解需要改变什么。你最初信任的 Header 现在是三个谁也无法调和的数字之一,而复盘会议将花更多时间争论哪个图表才是权威的,而不是讨论真正失效的控制循环。

稳健模式的样貌

你无法修复 Header —— 它们归供应商所有。你能做的是停止将它们视为唯一的事实来源,并基于在运维层面真实反映自身局限性的信号来构建控制循环。

将 Header 视为粗略的提示,而非预算。 读取它是为了检测趋势——例如“剩余量下降速度超过预期”——而不是将其作为下一个请求的准行证。你的调度器应该规划的实际预算是根据持续观察到的吞吐量得出的保守值,而不是 Header 宣传的乐观值。

将供应商返回的 Token 计数作为事实来源,并将其传回你的核算系统。 当响应返回实际的 input_tokensoutput_tokens 时,将这些数值写入你的流量限制器参考的同一个计数器中。一旦有了核算后的实际值,就永远不要根据本地的调用前估算值来运行流量限制器。估算用于准入控制;实际值用于下一个准入决策。

按应用程序实际返回的最坏情况预留输出容量,而不是按 max_tokens 如果你的 Prompt 几乎总是返回 800 Token 以下,就不要按 4096 来预留。这种代价是双重的:一是给你自己的流量限制器带来了人为压力,二是由于本地核算系统认为空间已耗尽而拒绝了原本可以处理的机会。

定期进行差异审计。 每天一次,对你的日志进行抽样,检查 429 事件及其之前的 Header。如果 429 发生前显示的平均余量大于限额的几个百分点,你就证明了该 Header 不具备预测性,你的控制逻辑应该停止像对待真理一样对待它。

将“我是否被限流”的信号与“我是否应该减速”的信号分开。 429 是关于限流的基准真值。不断下降的 remaining 值是一个提示。带有健康 remaining 值的 200 OK 只是缺乏证据,而不是不存在问题的证据。将你的退避机制(backoff)挂钩到 429 响应上,而不是 Header 上,这样你的调度器就会开始追踪现实,而不是追踪文档。

如果你运行网关,请审计它对缓存 Token 和预留容量的处理。 网关是最容易导致 Header 与限流决策不一致加剧的地方,因为网关试图执行一套 TPM 策略,而流量的实际 TPM 成本是由供应商掌握的。网关对 “TPM” 的看法是一种猜测;供应商的才是事实;如果两者在沉默中发生偏离,你的流量整形(traffic shaping)就是虚构的。

更深层的问题

Header 与限流之间的差距是 AI 基础设施中一个更大模式的缩影:供应商侧的信号被设计为信息参考,而你的应用程序却将其视为承重结构。同样的模式也出现在 Token 数量估算、缓存命中建议、模型版本别名以及仪表盘中的“软”限制上。每一个数字在其代表的含义上都是诚实的,但在其能承受的重量上却是不诚实的。

解决办法不是要求供应商使他们的数据更具契约性——他们不会这样做,而且不这样做也是对的,因为另一种选择是暴露确实会发生漂移的内部核算细节。解决办法是设计控制循环,将供应商侧的数据视为其实际上的嘈杂、滞后且范围受限的信号,并将你的可靠性预算放在你端到端拥有的信号上:你自己观察到的吞吐量、你自己衡量的成功率,以及你自己核算后的 Token 计数。

那张在运行时执行其他标准时却还在读取文档的燃尽图并不是图表本身的 Bug。它是对图表所记录内容的范畴错误(category error)。一旦你不再要求 Header 成为一份契约,当流量限制器依然执行契约时,你也就不会再感到意外了。

References:Let's stay in touch and Follow me for more thoughts and updates