对话感知的速率限制:为什么逐请求限流会破坏多轮 AI
你的 AI 功能在测试中运行完美。单轮问答,毫无问题。但在生产环境中,当真实用户进行一场 10 轮调试对话时,它却失败了——不是因为模型出了问题,而是因为你的速率限制器是为一个完全不同的世界设计的。
标准 API 速率限制是专为无状态 REST 调用设计的粗糙工具。每个请求被视为一个独立的、大致等量的消耗单元。对于 CRUD 端点而言,这种模型运行良好,因为每次调用确实具有可比性。但对于多轮对话,这种模型就行不通了——每一个后续轮次的成本都在递增,一次用户交互可能触发数十次内部模型调用,而会话中途被切断造成的损害,远比一次失败的单次查询严重得多。
对话内部隐藏的成本爆炸
多轮对话中 Token 成本的复利增长是造成这种错配的根本原因,其严重程度超出大多数团队的预期。
对话第 1 轮的成本是某个基准 Token 数——比如用户问题 300 个输入 Token 加上模型回复 200 个输出 Token。第 2 轮现在包含完整的历史记录:用户的第一条消息、模型的第一次回复以及新的用户消息。到第 10 轮时,模型的输入包含了 9 轮之前的交流。在一次典型的调试会话中,这可能使输入 Token 消耗推高至第 1 轮的 7 倍。用户主观感受到的是"进行了一次对话",但 API 看到的 Token 消耗却像是多个繁重工作负载同时到达。
在代理上下文中,这一问题进一步加剧。当用户要求 Copilot"重构这个文件"时,代理会在内部读取文件、规划编辑、执行修改、重新读取以验证、处理工具结果,然后再次循环。一次用户交互最终转化为 10-20 次独立的模型调用。每次调用都从同一个速率限制桶中提取资源。一个使用习惯正常的用户在任务中途收到 429 错误,却完全不明白为什么——他们并没有发送 47 个请求,只发送了一个。
GitHub Copilot Agent 模式的情况就是一个典型例子。Agent 模式的代理循环消耗会话预算的速率是普通聊天的 10-20 倍,因为每次循环迭代都是一次独立的 API 调用。那些在对话模式下运行良好的用户,首次切换到 Agent 模式时立即遭遇限流。他们的使用量在主观意义上并没有激增,但基础设施却不这么认为。
"会话预算"的真正含义
解决方案不是提高原始限制——而是改变测量对象。与其将单个请求或 Token 作为无差别的流来计数,不如将资源消耗附加到会话上, 以会话作为限流的单位。
会话具有在整个对话期间持续存在的唯一 ID。源自该会话的所有 API 调用从同一个池中提取资源。当池耗尽时,该会话受到限制——而不是用户的下一个独立对话。新会话获得新预算。效果是:一个用户在一分钟内运行三次简短的独立对话,不会因为上个小时进行的一次 30 轮调试线程而受到影响。
双窗口结构在这里很重要。每分钟 Token 预算控制交互延迟——这是保护模型服务层免受即时峰值冲击的方式。每小时或每天预算控制总体成本。会话跟踪适用于两者。当 Anthropic 将缓存输入 Token 排除在每分钟 Token 计数之外时,这并非偶然;这是一种承认:复用的上下文代表着根本不同的计算负载,不应与全新推理的消耗同等对待。
会话 ID 还创造了有益的行为激励。试图通过关闭对话并打开新对话来重置速率限制的用户会发现,新会话获得新预算,但这与其账户绑定,而不是与窗口实例绑定。你无法通过打开五个聊天标签来囤积预算。GitHub Copilot 基于账户级别而非聊天窗口级别的会话跟踪,明确体现了这种设计选择。
实现很直接:客户端在对话开始时生成一个 UUID,在每个请求中将其作为请求头(X-Session-ID)包含在内,服务器维护一个会话 ID → 已消耗预算的映射。会话条目在不活跃后过期。复杂性在于双窗口滑动计数器,而不在于会话跟踪本身。
重试风暴与语义去重
当对话受到限流时,客户端会重试。这在预期之中。问题在于重试协调不当时。
同步重试是一 种典型的故障放大器。如果 10,000 个客户端同时触发速率限制,并且都采用 2 秒固定退避,那么 10,000 个请求将在第 2 秒同时到达,很可能再次导致同样的失败。
重试间隔需要引入抖动——一个随机化的组件,将请求分散在时间轴上。采用 10% 抖动窗口时,一个客户端在 1.83 秒后重试,另一个在 2.17 秒后,还有一个在 1.94 秒后。服务器看到的是平滑的恢复曲线,而不是同步的冲击波。
但对于对话式 AI 而言,真正的问题不是协调好的机器重试,而是协调好的人工重试。在多步推理任务中,第 10 轮失败的用户点击"重新生成"。客户端可能同时在后台自动重试。两个相同的请求现在都在进行中。如果两者都成功且模型输出是非确定性的,用户会看到不一致的行为。如果两者都再次遭遇速率限制,用户的预算被白白扣取两次。
幂等键在基础设施层面解决了重复问题。客户端包含一个由会话 ID、轮次编号和输入哈希派生的键。服务器检查它是否已经在处理或已经处理过具有该键的请求。如果是,则返回缓存的响应。这防止了重复扣费并消除了人工重试与机器重试之间的竞争条件。
语义去重将这一机制扩展到近似重复请求。"你能帮我调试这个吗?"和"请修复这个问题"是意义上相同的请求,朴素的基于哈希的去重会错过这种情况。对请求进行向量化,并与同一会话最近处理请求的短 TTL 缓存进行比较,可以将这些请求折叠为一个。对于重试风暴,这个窗口很短——30 到 60 秒——但足以吸收人工重试模式。
优雅降级是一种设计选择
速率受限的多轮对话的标准失败模式,是在 15 轮推理任务的第 10 轮突然收到一个 429 错误。用户丢失了在这 10 轮中积累的所有上下文。他们必须重新开始,在重启上消耗预算,并因上下文丢失而得到更差的结果。基础设施通过显著恶化用户体验来"保护自身",比根本没有 AI 功能还要糟糕。
优雅降级是替代方案。它要求将速率限制不视为一堵二元的墙,而是视为降低质量而非可用性的信号。
最简单的实现是主动发出信号。当会话剩余 Token 预算低于某个阈值时——比如每小时预算的 20%——服务器在响应头中包含一个指示会话正在接近限制的信号。客户端可以从大型模型切换到较小的模型,通过总结早期轮次来减少上下文窗口,或警告用户当前对话正在接近其自然限制。这些方式中的任何一种都比在响应中途出现不透明的 429 要好。
在速率限制压力下进行模型降级的做法被严重低估。从前沿模型切换到较小模型可以降低每轮 Token 成本,从而在不触碰预算上限的情况下获得更多轮次。用户会注意到后期轮次的轻微质量下降,这通常是可以接受的——尤其是对于长会话末尾通常出现的总结或收尾类任务。
上下文压缩是另一个杠杆。一个 20 轮对话,其中第 1-15 轮仍在完整上下文窗口中,其 60-70% 的 Token 预算花在历史记录上,而非新的推理上。将第 1-10 轮总结成一个紧凑摘要并删除原始消息,可以将上下文成本减少一半或更多。服务器端压缩对用户是透明的。客户端压缩(UI 显示"早期轮次已被总结")诚实且通常可以接受。
断路器在不同层面发挥作用。当一个提供商返回的错误率升高时——即使不是速率限制原因——断路器触发 ,将流量路由到备用提供商。这不仅仅关乎速率限制;它关乎对话功能所需的更广泛可靠性保证。被中断的会话与被速率限制截断的会话对用户的影响是相似的。正确的失败响应是相同的:保持连续性,降低质量,不要突然终止。
API 对客户端应有的接口
今天大多数 LLM API 暴露的信息足以让精心设计的客户端处理这些情况,但它们并不让这变得容易。速率限制响应头是存在的,但它们是每分钟计数器,而不是会话预算。调用者必须从原始聚合数字推断会话状态。
接口应该暴露的内容:
- 剩余会话预算(不仅仅是账户级别的 TPM)——这样客户端可以做出每次对话的决策,而无需实现自己的核算
- 在触及硬性限制之前发出"即将到达限制"的信号——这样客户端可以主动降级而不是被动应对失败
- 缓存与非缓存 Token 消耗的区分——因为这些代表不同的计算负载,应该分别计算预算
- 对话范围的幂等性——这样会话内的重试可以在基础设施层面被折叠,无需客户端实现
这些功能中的一些开始作为平台功能出现。会话预算跟踪和每次对话可观测性通过 LLM 网关产品越来越多地提供。原生 API 支持仍然稀少。
为什么这是一个架构问题,而非调优问题
当对话开始触及速率限制时,诱人的做法是提高数字——获取更高层级,协商更高的 TPM 上限。这解决了当前负载的即时症状,但没有解决底层设计错配问题。
应用于对话工作负载的逐请求速率限制将永远是不准确的,因为度量单位是错误的。被消耗的资源是会话,而被测量的指标是请求。如果你围绕多轮对话构建功能,却像对待独立事务一样限流每一轮次,你将在每个新的规模上持续遇到相同的失败模式。
底层原语——会话预算、重试去重、优雅降级、主动信号——并不复杂。它们是应用于一个仍然利用不足的领域的标准分布式系统模式。实现它们的基础设施工作是有据可查的。组织层面的工作更简单:停止围绕假设无状态请求的模型来设计你的速率限制策略,因为你正在发布的功能并非无状态。
多轮 AI 功能是一个完整的产品。它们的限流策略需要反映这一点。
- https://aionx.co/ai-comparisons/ai-chatbot-rate-limits-compared/
- https://community.openai.com/t/how-to-handle-rate-limits-when-building-a-chatbot-with-openai-api/1357992
- https://platform.claude.com/docs/en/api/rate-limits
- https://developers.openai.com/api/docs/guides/rate-limits
- https://github.com/orgs/community/discussions/193263
- https://www.theregister.com/2026/04/15/github_copilot_rate_limiting_bug/
- https://portkey.ai/blog/rate-limiting-for-llm-applications/
- https://www.getmaxim.ai/articles/retries-fallbacks-and-circuit-breakers-in-llm-apps-a-production-guide/
- https://api7.ai/blog/rate-limiting-guide-algorithms-best-practices
- https://portkey.ai/blog/retries-fallbacks-and-circuit-breakers-in-llm-apps/
- https://markaicode.com/implement-graceful-degradation-llm-frameworks/
- https://www.vellum.ai/blog/how-to-manage-openai-rate-limits-as-you-scale-your-app
- https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- https://docs.github.com/en/copilot/concepts/usage-limits
