跳到主要内容

你为人类设置的速率限制,AI 智能体三秒钟就会让其饱和

· 阅读需 11 分钟
Tian Pan
Software Engineer

速率限制从来就不是一种公平性原语。它只是一个逐渐“演化”而来的销售工程指标——是三年前某个解决方案工程师在客户接入期间随手写进文档、被复制到套餐定义中,且由于从未有人触发过而从未被重新审视的一个数字。这个限制写着“每分钟 100 次请求”,其真实含义是“超出了任何理性的集成方案的需求”,因为当时平台上的每一个集成都是由人类在键盘前驱动的后端服务,而人类每分钟敲不了 100 次字。

然后,一个付费租户将一个智能体(agent)指向了该端点。智能体不会打字。它不会为了阅读响应而停顿。它没有需要在请求之间渲染的 UI。它执行一个规划循环,每一个推理步骤调用一次 API,而模型制定一个推理步骤只需要大约 30 毫秒的实际时间。智能体在 3 秒内就触及了每分钟的限额,在 3 分钟内触及了每小时的限额,而在轮值工程师的咖啡还没变凉之前,它就已经耗尽了每日配额。在限流仪表盘更新之前,技术支持的升级请求就已经送达了。

事故复盘会讨论智能体。但智能体并不是最有趣的部分。有趣的部分在于,速率限制从未真正起到人们认为的作用。它只是一个披着合约上限外衣的软性引导线,而那种暴露这一差距的集成形态迟早会出现——智能体只是最先出现的集成形态。

速率限制是针对已不存在的调用者校准的

回想一下这个数字是如何选定的。某个人——可能是该 API 的第一任产品经理——查看了来自仪表盘产品的分析数据,发现最活跃的客户每分钟大约进行 20 次调用,于是加上安全余量将限制设定为该值的 5 倍,然后就发布了。这个数字生效了。之所以生效,是因为当时平台上的每个调用者都是包裹在人类行为之外的胶水代码:表单提交、按钮点击、或是由上游另一个人类行为触发的 Webhook 同步。

人类驱动的流量有一定的形态。这种形态在排队论文献中有一个名称——有界到达间隔时间、低突发性,请求速率随席位数量而非计算量扩展。这种形态也是为什么平台上的“喧闹邻居”模型能够奏效的原因:最糟糕的邻居是席位最多的客户,而席位最多的客户付的钱也最多,因此上限大致与合约一致。公平性是人类节奏流量的副作用,而不是系统强制执行的属性。

智能体不具备这种形态。智能体的请求速率是规划循环迭代时间和每步工具调用扇出的函数,这两者都与席位数量无关。一个代表单个用户操作的智能体每秒发出的请求数可能比同一租户的所有人类用户加起来还要多,而且随着模型速度的提升,这种差距会越来越大。公平性的副作用消失了。那个沿用了十年的数字在智能体流量接入的第一个下午就失效了。

“速率限制”这一类别刚刚发生了分裂

曾经被称为“速率限制”的东西现在变成了两件事,平台必须决定提供哪一种。第一种是吞吐量合约(throughput contract)——即租户有权消耗多少上游资源,以实际映射到成本的单位来表示。第二种是滥用上限(abuse ceiling)——即平台无论合约如何都拒绝继续提供服务的地方,因为发生了某些病态情况,而最廉价的响应就是断开连接。

当调用者是人类时,这两者是同一个数字,因为人类能产生的吞吐量总是远低于滥用上限。但当调用者是智能体时,它们就不是同一个数字了。一个表现完美的智能体可能会在执行租户付费的任务时耗尽旧的组合限制,这意味着将这种饱和视为滥用是错误的。而一个配置错误的智能体可能会在毫无用处的情况下消耗掉该限制的十倍,这意味着为了迎合表现良好的情况而提高所有人的组合限制也是错误的。

这种分裂迫使人们对每个上限进行单独讨论。吞吐量合约变成了针对每个租户、每个工作负载类别的预算,销售团队可以据此报价,财务团队可以据此建模。滥用上限则变成了基于行为信号的平台级熔断器——例如请求形态熵、错误后的重试率、成功与失败的工具调用比例——只有当信号表明调用者无论合约如何都处于异常状态时,才会触发。

令牌桶应基于用户以外的维度挂载

技术上的修复是移动令牌桶。当一个用户生成一个请求流时,基于用户身份挂载令牌桶是行之有效的。而现在,基于用户身份的令牌桶可能空置,因为该用户会话下的 20 个并行智能体各自生成了令牌桶无法识别的流;或者更糟的是,令牌桶被不公平地耗尽,因为用户碰巧启动了一个智能体,导致人类会话的配额被后台进程吞噬。

令牌桶必须基于一个元组(tuple)挂载。这个元组必须包含租户,因为租户是计费单位。它必须包含工作负载类别——如交互式人类、定时任务、自主智能体——因为这些类别具有不同的可接受突发形态。它通常还需要包含模型或下游资源,因为耗尽前沿模型预算与耗尽缓存响应预算是不同性质的失败。元组才是速率限制的原语;而每用户限制只是它的一个退化特例。

一旦元组存在,其余的便水到渠成。免费层级获得比企业层级更小的每工作负载类别桶。当租户升级时,租户身份上的元数据会发生变化,桶的大小无需更改代码即可更新。失控的智能体只会限制其自身的工作负载类别桶;同一租户下的人类会话不受影响。多租户滥用事件会触发平台级熔断器,而不会迫使每个租户的令牌桶为了应对最坏的邻居而设置得过于保守。

突发信额曾是隐含的;现在它们必须变得真实

人类调用者可以免费获得突发流量(Burst),因为人类产生的突发流量规模小且不规律。用户点击“同步所有”按钮会产生五十个请求的突发,系统在毫无察觉的情况下就吸收了它,而每分钟平均值从未反映出秒级维度的真实形态。令牌桶机制之所以奏效,是因为突发流量足够小,可以通过平滑处理渡过难关。

Agent 的突发流量规模很大。一个被要求丰富一千条记录的 Agent 会以网络所能承载的最快速度发出动态的一千个请求,然后在当天的剩余时间里保持安静。全天的每分钟平均值看起来很正常;但令牌桶每次都会触发。诚实的回答是,Agent 的流量在设计上就是突发性的,平台要么通过将突发信额暴露为一等配额(Agent 在工作开始前可以申请的预分配容量)来适应这种情况,要么就让 Agent 通过 429 响应来摸索上限,而这正是刚刚发生的事情。

突发信额还迫使平台公开它一直拥有但从未发布过的一项信息:租户距离其上限还有多远。人类调用者永远不需要知道,因为人类调用者从未接近过上限。Agent 调用者必须知道,因为在任何非琐碎的任务开始后的第一秒内,Agent 调用者就在接近上限,而正确的退避行为取决于距离上限是 20% 还是 2%。暴露这些信息的响应头——剩余量 (remaining)、重置时间 (reset)、突发池深度 (burst-pool depth)——不再仅仅是一种礼貌,而是成为了集成契约的一部分。

契约界面必须指名 Agent 调用者

看看那些列出速率限制的公开文档页面。它们几乎无一例外地为每个等级设有一个上限,并附带一条关于联系销售以获取更高限额的脚注。这个页面读起来就像一张报价单,而它也确实一直如此。页面上没有写“此限制假设调用节奏符合人类习惯;Agent 节奏的调用有不同的限制”。页面上没有写“如果你将自主 Agent 指向此端点,相关的 SKU 在这里”。页面没有将 Agent 流量命名为一个类别,因为在编写该页面时,Agent 流量还不存在。

修复方法是让页面如实反映平台目前提供的内容。Agent 调用者的 SLA 应该紧挨着人类调用者的 SLA,并被明确命名,拥有自己的桶大小、突发行为和定价维度。集成文档应告诉开发者他们的集成属于哪个 SKU 以及原因。过去那种“你的席位数量是多少”的客户成功流程,现在增加了一个关于预期程序化调用速率的新问题,而答案决定了租户身份携带哪种等级的元数据。

当“速率限制”不再是单一事物时,契约界面就是这个样子的。这种拆分不是可选的;它在生产流量中已经真实存在。唯一的选择是:是文档页面承认这一点,还是让租户通过发送一段周五晚上的突发流量来发现它,从而让值班工程师必须在周一的复盘会上进行解释。复盘会会说原因是 Agent。但真正的核心原因是,契约只命名了一类调用者,而现在却在服务两类。

架构上的觉醒

在调用者群体发生变化之前,速率限制看起来像是一个实现细节。随后,它就变成了一个将关于谁在调用、为什么调用以及调用频率的所有假设一次性暴露出来的地方。公平模型曾是一个副作用;契约曾是一份报价;令牌桶曾是一个猜测;响应头曾是一种礼貌。当调用者不再是人类时,这些都不再成立。

那些继续支付相同事故成本的团队,是将下一次配额饱和事件视为单个 Agent 的 Bug 的团队。而那些不再支付此类成本的团队,是那些回顾被 Agent 刷爆的限额并意识到,回过头来看,该限额从来不是一个基础原语——它只是一个已经发生变化的调用者群体的冻结快照。现在的任务是定义现有的调用者类别,根据各自的工作负载形态为每一类分配限额,并在下一个 Agent 发现问题之前,发布让这种区分明确化的契约界面。

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