跳到主要内容

演变成产品决策的速率限制

· 阅读需 11 分钟
Tian Pan
Software Engineer

频率限制(Rate limit)过去曾是一个基础设施细节。当你遇到 429 错误,你会使用退避算法(backoff)重试,将溢出的请求排队,而 On-call 频道之外的人甚至根本不知道这回事。用户看到的响应只是比平时慢了几百毫秒。这就是故事的全部。

对于智能体(agentic)功能,这个故事不再适用。当一个智能体在执行多步计划的过程中,中途触及了供应商的每分钟 Token 数(TPM)上限时,失败并不会停留在基础设施层。它会表现为一个半成品的答案、一个在最后一次调用前卡住的工具循环,或者让用户盯着一个永远无法解决的加载动画。配额不再仅仅是后端容量数字,而变成了一个产品必须围绕其进行设计的约束条件 —— 就像产品围绕结账流程或空状态进行设计一样。

这种转变很容易被忽视,因为词汇表没有变。我们仍然说“频率限制”,仍然说“重试”,仍然说“排队”。但这些词所描述的事物已经变形。REST API 的频率限制管控的是单个请求。而 LLM 供应商的频率限制管控的是一个共享的、消耗极快的预算,每个并发用户、每个子智能体以及每次后台重试都在同时消耗它。将其视为基础设施细节,会让团队最终在事故复盘而非设计文档中才发现他们的容量模型。

配额是共享预算,而非个人津贴

首先要抛弃的心理模型是每个用户都有自己的频率限制。事实并非如此。供应商是按组织、按 API 密钥、按地区和按模型类别强制执行限制的 —— 你应用程序的整个用户群都从这同一个池子中提取资源。例如,Anthropic 的 API 对每个模型类别强制执行三个独立的维度:每分钟请求数、每分钟输入 Token 数和每分钟输出 Token 数。触及其中任何一个都会返回 429 错误。Azure 的配额是按地区、按模型、按部署类型分配的,每个共享该配额的部署都在竞争相同的 TPM 数值。

这一点非常重要,因为智能体工作负载异常擅长快速耗尽共享预算。单个智能体请求不仅仅是一次 API 调用。它是一个循环:一次规划调用、几次工具调用、一个将文档塞入上下文的检索步骤,可能还有一个运行自身循环的子智能体。在 TPM 方面,一个 200,000 Token 的上下文窗口与 50 个 4,000 Token 的 Prompt 成本相当。十个运行深度智能体会话的用户消耗的配额,可能比一万个发送简短聊天消息的用户还要多。

因此,容量问题不是“我们有多少用户”,而是“在共享预算耗尽之前,可以运行多少个并发智能体循环,以及那个倒霉的循环会发生什么”。后半句是产品问题,而这通常是直到发布当天才会被回答的问题。

“只需退避重试”是用户能感受到的延迟

带有抖动(jitter)的指数退避仍然是正确的基准重试策略。纯粹的指数退避会产生“惊群效应” —— 每个被限流的客户端都在同一时间表上重试并再次冲击限制 —— 因此通过抖动来分散它们是不可逾越的底线。这些都没有错。

错误在于将重试视为隐形的。在传统的 API 上,重试请求会增加几十毫秒。在智能体循环内的 LLM 调用中,一次重试可能会增加数秒,并且智能体可能需要在同一个计划中重试多次调用。供应商的 retry-after-ms 响应头会告诉你需要等待多久,当组织范围的预算真正饱和时,这种等待通常以整秒计算。将其乘以一个多步骤计划,这种“隐形”的重试就会变成长达 15 秒且屏幕上没有任何输出的停顿。

盯着加载动画的用户无法区分“模型正在思考”和“我们正在等待频率限制清除”之间的区别。两者看起来一模一样。但一个是功能在正常运作,另一个是功能在失效,如果你的 UX 以相同的方式渲染它们,那么你就交付了一个看起来与正常操作完全一样的故障模式。智能体运行的时间越长,用户在等待中投入的精力就越多,最终超时的感觉就越糟糕。

诚实的重试版本应该将等待视为用户可见的状态。告诉他们系统繁忙。显示已完成步骤的进度。给他们选择是继续等待还是接受部分结果。隐藏在加载动画背后的重试并不优雅 —— 它是一种押注,赌限制会在用户放弃之前清除,而这种押注是一个由沉默做出的产品决策。

降级模式是一项功能,而非错误路径

最重要的重构是:当你无法提供高质量的响应时,后备方案(fallback)不是一个错误。它是一种产品状态,值得像正常路径一样给予设计关注。

AI 系统中的优雅降级通常意味着分层后备。常见的顺序是:完整模型 → 较小模型 → 缓存或启发式结果 → 静态响应。每一级都会牺牲一些东西 —— 准确性、新鲜度、个性化 —— 以保持功能的响应性。关键不在于降级后的答案有多好,而在于系统表现得可预测且沟通清晰,而不是神秘地失败。

具体而言,有几个值得提前设计的降级阶梯:

  • 路由到更小、更便宜的模型。 同一系列中 TPM 压力较小的模型通常能以较低的质量完成任务。用户得到了答案;你备注这是快速模式的响应。
  • 削减步骤数。 如果一个完整的智能体计划需要 8 个工具调用,降级计划可能会只做 3 个就停止,并返回已发现的内容,同时明确标注“我提前停止了”。
  • 提供缓存或部分结果。 如果智能体在预算耗尽前完成了 6 个步骤中的 4 个,这 4 个步骤就是真实的成果。展示它们总比丢掉它们好。
  • 退回到静态或启发式答案。 对于某些查询,模板化响应或简单的规则确实比无限的加载动画要好。

将建立信任的降级模式与破坏信任的降级模式区分开来的是诚实。如果用户被告知“由于需求量大,我使用了一个更快的模型 —— 这是我发现的内容,你可以要求我重新进行完整尝试”,用户就能理解这种权衡并保持控制感。如果一个用户在不知情的情况下得到了一个较差的答案,且从未得知原因,他会得出产品不可靠的结论。降级状态需要文案,需要视觉处理,并且需要一个返回高质量模式的路径。这是设计工作,它必须发生在事故发生之前,而不是发生期间。

容量规划类似于连接池

一旦你接受提供商配额(quota)是一种有限的共享资源,正确的类比应该是数据库连接池,而不是 API 速率限制(rate limit)。连接池的大小是固定的。当它被耗尽时,新的工作要么在有限的队列中等待,要么被快速拒绝。你根据预期的并发量来确定池的大小,监控饱和度,并在达到上限之前——而不是之后——发出告警。

以同样的方式处理每分钟 Token 数(TPM)。估算典型 Agent 会话的 TPM 成本,乘以实际的峰值并发量,然后将其与提供商层级的实际限制进行对比。如果计算结果显示峰值需求超过了配额,你有三个坦诚的选择,这三个选择都需要有人负责:

  1. 购买更多容量。提升提供商层级,或预留吞吐量。Vertex AI 的 Provisioned Throughput 和 Azure 的 Provisioned Throughput Units 允许你预留容量以获得稳定的延迟,作为一种承诺进行购买。这是用资金换取可预测性。
  2. 主动降载。增加一个带有快速拒绝路径的有限内部队列,这样当预算耗尽时,用户会立即收到一个诚实的“请在一分钟后重试”,而不是漫长的超时等待。
  3. 主动降级。使用上述的回退方案,使峰值需求以较低的质量得到满足,而不是完全无法服务。

错误的第四个选项是不采取任何行动,让提供商的 429 错误成为你的容量规划。那不是计划。那是计划的缺失,它将一个可预测的负载问题转化为一个不可预测的面向用户的问题。

监控也应符合这一框架。将 TPM 和 RPM 的利用率作为实际限制的百分比进行跟踪,而不是原始计数。在 70% 或 80% 时发出告警,就像你在连接池中使用的阈值一样,这样团队就有行动的缓冲时间。关注节流事件(throttling events)和首个 Token 时间(time-to-first-token),因为延迟增加是预算收紧的早期信号。

在发布前协商限制和优先级层级

最后一点是技术含量最低但也最常被忽略的:在发布前与提供商沟通,而不是在事故发生期间。

提供商的速率限制并非固定不变。它们随使用层级和累计支出而扩展,并且通常可以根据要求提高——但处理请求需要时间,而“我们周二发布”是一个非常被动的请求立场。了解你的层级、当前的限制以及提升额度的提前量,这就是发布就绪状态(launch readiness),与负载测试同等重要。

如果你的应用程序有不同类别的流量——付费的企业版和免费版,交互式请求和后台批处理任务——请在预算发生竞争之前决定它们如何共享预算。当组织范围内的配额饱和时,必须有所让步。如果你没有决定谁该让步,答案就是那个恰好最后到达的请求,而那通常不是你会选择保护的请求。后台任务应该让位给交互式任务。免费流量应该在付费流量之前降级。多提供商网关可以通过故障转移(failover)到另一个主机上的相同模型,来绕过单个提供商的 429 错误。所有这些都是策略,提前决定的策略是产品决策;而在生产环境中由 429 错误决定的策略则是事故。

限制始终是产品的边界

更深层次的变化并不是速率限制变得更严格了。而是 Agent 循环(agentic loop)使它们变得可见。传统的请求要么成功,要么重试得足够快以至于没人察觉。Agent 的多步计划在工作过程中暴露了预算,此时用户正在观察,部分进展是真实的,而且除非你选择展示,否则“正在思考”和“被限流”之间的区别是不可见的。

这种可见性就是为什么配额现在成了一个产品层面。它改变了你构建的东西:一个拥有独立文案和设计的降级模式,一个像连接池一样的容量模型,一个决定谁在等待的降载策略,以及一个发生在发布前而不是停机后的与提供商的对话。

将速率限制视为基础设施细节的团队将不断在事故复盘中发现它。将将其视为设计约束的团队,在预算耗尽时,发布的功能将以他们选择的方式失败——而不是由加载动画(spinner)替他们决定。

决定当预算耗尽时你的功能该如何表现。如果你不决定,429 错误会替你决定,而它对你的用户可不会客气。

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