AI 代理 API 限流:安全的重试策略
通过有上限的重试、带抖动的退避、幂等检查、共享配额、审计记录和人工升级,安全处理 AI 代理 API 限流。

代理遇到配额限制后,应放慢速度,记录每一次额外尝试,并在临时拒绝演变成更大范围的运营混乱之前停止。对一个手动点击按钮的人来说,“失败后重试”算得上足够的建议。对一个能以人眼无法跟上的速度发出请求的进程来说,这个建议却很危险。
真正困难的地方不是等待几秒。难点在于,当第一次请求可能在到达服务商之前失败、服务商完成操作后才失败,或多个代理运行耗尽同一份共享配额时,如何保留原始任务的含义。安全的设计应区分这些情况,并把人工介入定义为一种明确结果,而不是令人尴尬的例外。
429 是调度指令,不是可以反复猛撞的错误
HTTP 429 表示服务器拒绝请求,因为客户端在服务器控制的某个时间段内发送了过多请求。RFC 6585 定义了这个状态码,并说明响应内容应解释具体情况,也可以包含 Retry-After 标头。这段描述很重要:429 并不表示代理立即重复完全相同的请求就有机会成功。
代理首先应记录服务商、端点、凭据或账户身份、请求类别、响应状态,以及所有限流标头。然后把任务置于延迟状态。不要让语言模型在每次失败后用文字临时决定下一步。传输层需要确定性的行为,因为处于任务压力下的模型往往会尝试相邻端点、修改筛选条件,或创建第二条请求路径。这些临时发挥可能让调用数量成倍增加,却仍然得到同样的拒绝。
拒绝通常适用于比当前请求更大的配额桶。服务商常按账户、项目、令牌、IP 地址、端点族,或这些因素的组合设置限制。一个代理可能收到 429,而使用同一凭据的另一个代理暂时还能继续工作。这并不能证明第一个代理可以安全重试。它可能说明服务商使用了多个配额桶,也可能说明第二个进程正在消耗最后的可用容量。
为每个已知范围维护本地账本。如果服务说明限制按令牌计算,就按令牌分组请求。如果服务只提供账户级指导,在有证据证明情况不同前,应假设该账户的所有工作进程共享同一个配额桶。本地账本无法完全反映服务商的内部计数器,但可以避免自有工作进程盲目争抢。
IETF 的 RateLimit Fields 规范 RFC 9333 定义了 RateLimit-Limit、RateLimit-Remaining 和 RateLimit-Reset。这些字段描述配额状态,但不能消除已经收到的 429。使用它们来控制未来请求的节奏,避免队列增长速度超过服务商的接收能力。应按照服务商文档判断这些字段的适用范围,因为单凭标头可能无法知道它适用于一个端点,还是整个账户。
一个有用的状态记录如下:
{
"provider": "billing-api",
"scope": "account:ops-team",
"request_class": "write:invoice",
"next_allowed_at": "2025-04-08T14:12:31Z",
"remaining": 0,
"reset_after_seconds": 60,
"source": "HTTP 429 Retry-After"
}
代理运行器在派发下一次调用前应读取这条记录。对于小型命令行运行,在单个请求处理器中等待是可以接受的。等到存在多个工作进程、工具或恢复任务后,带有可见 next_allowed_at 的队列会更好。它能让调度器选择其他工作,而不是让进程一直占用,同时也能为操作人员清楚解释延迟原因。
重试预算可以阻止小故障变成洪峰
重试预算会给任务在调用失败后产生的额外工作设置硬上限。要同时计算尝试次数和经过的等待时间。只有固定次数的限制,在服务要求客户端等待几分钟时会失效;只有时间限制的策略,则可能让快速重试循环在几秒内耗尽账户配额。
为每个任务和每个共享服务商范围分别设置预算。任务预算回答的是:“这个目标可以承受多少不确定性?”范围预算回答的是:“当前所有工作最多可以给这个服务商造成多大影响?”如果十个任务各自重试三次,账户仍然可能收到三十个额外请求。这正是看似保守的策略变成请求洪峰的方式。
对于普通读取请求,我会设置较小的预算,并让截止时间短于答案本身的业务价值。对于写入操作,我会把更少的预算用于盲目重试。等待人工确认的成本,可能低于重复扣款、重复发送通知,或重复执行部署的成本。
把规则表示成代理不能随意覆盖的数据:
request_classes:
read:
max_attempts: 4
max_wait_seconds: 90
retry_statuses: [408, 429, 500, 502, 503, 504]
idempotent_write:
max_attempts: 3
max_wait_seconds: 120
retry_statuses: [408, 429, 502, 503, 504]
uncertain_write:
max_attempts: 1
max_wait_seconds: 0
retry_statuses: []
shared_scope:
max_delayed_requests: 25
max_concurrent_requests: 2
这段配置可以避免一种常见故障:代理提交写入后收到超时,以为操作没有发生,于是向已经承受压力的服务商再次发起请求。uncertain_write 类别会强制执行状态检查或升级处理。它不会因为代理坚持重试就奖励它,因为坚持并不会改变结果。
预算必须计算每一次实际尝试,包括库内部发起的重试。我见过应用程序声明了重试控制,但 HTTP 客户端、工作流运行器和代理又分别在底层重试。直到有人检查了这三个组件的默认设置,最终请求数量才不再神秘。应选择一个层负责重试。其他层要么配置为向上报告错误而不重试,要么明确配置其行为,并将其计入总预算。
预算耗尽后应返回带有上下文的终止结果,而不是通用失败。结果应说明原始请求是否已经发送、是否收到响应、发生了多少次尝试、任务等待了多久,以及下一步安全操作是什么。这样,代理可以继续处理无关工作,或给出准确的升级信息,而不是不断对自己说“再试一次”。
退避需要抖动和上限
指数退避会逐渐延长连续拒绝后的等待间隔,从而降低压力。抖动可以避免同时失败的多个工作进程在同一波同步返回。即使服务商公布了重置时间,这两者也都应放在客户端中,因为内部并发本身也可能造成惊群效应。
对于尝试次数 n,第一次重试时 n = 1,先计算上限,再在上限范围内随机选择延迟:
import random
BASE_SECONDS = 1.0
MAX_SECONDS = 60.0
def retry_delay(attempt_number: int) -> float:
cap = min(MAX_SECONDS, BASE_SECONDS * (2 ** attempt_number))
return random.uniform(0, cap)
这就是完整抖动。它可以避免所有工作进程严格等待 2、4、8 和 16 秒。固定且相同的延迟很受欢迎,因为日志更容易阅读。但它也让协调后的重试变得容易预测,这恰恰与繁忙服务所需要的方向相反。
当响应提供 Retry-After 时,应优先采用该值,而不是本地计算出的更短延迟。RFC 9110 允许 Retry-After 使用秒数延迟或 HTTP 日期两种形式。两种形式都要解析。如果由于机器时钟与服务商时钟不同,日期已经过期,应应用一个适度的最小延迟,而不是进入紧密重试循环。
不要把退避当作速率控制的替代品。退避在失败后才开始。令牌桶、漏桶或简单的调度器限制,则负责在失败发生前控制速率。如果服务商允许在已知时间段内发送已知数量的请求,就应将请求速率控制在公布额度以下,并为交互式工作保留空间。调度器也应限制并发量。二十个同时发出的请求,可能在任何工作进程读取 RateLimit-Remaining 响应之前就耗尽短时间窗口的容量。
一个实用的派发规则很简单:发送调用前,检查共享范围的下一个允许时间和可用并发槽位。收到 429 后,在安排任何重试前更新范围记录。这个顺序很重要。如果工作进程在发布拒绝信息前就安排重试,每个工作进程都可能认为下一个槽位属于自己。
限制延迟,也限制总等待时间。没有上限的指数曲线可能让任务推迟数小时,并让一个过时的运行在其前提失效很久后仍然存活。代理应在截止时间到达后终止任务,或将任务交给调度器,同时保留原始输入供之后复核。
幂等性决定重试是否安全
幂等请求在重复执行时具有相同的预期效果。这并不表示所有使用 HTTP PUT 的请求在每个应用中都无害,也不表示所有 POST 都危险。RFC 9110 将 PUT 和 DELETE 等方法描述为意图上的幂等方法,而 POST 通常不是幂等的。真正决定运营风险的是服务商的 API 合约。
读取请求通常可以重试,前提是代理接受返回的数据可能已经改变。删除请求也可能适合重试,例如删除已经不存在的资源会得到明确且可接受的结果。创建付款、邀请、支持工单、采购订单或部署,通常无法承受盲目重复。这些调用在超时或连接重置后最需要谨慎处理。
这个领域经常混淆两种不同状态:
- 明确没有到达服务商的请求,如果操作本身允许重复,就可以安全重新发送。
- 结果未知的请求,需要先核对状态,才能再次发送副作用操作。
客户端写入字节后发生连接故障,并不能证明服务商没有执行操作。服务器可能已经完成操作,只是响应连接断开了。代理不能根据没有响应这一点推断结果。
如果服务商支持幂等键,为每个逻辑操作生成一个稳定令牌,并在该操作的每次重试中重复使用。不要每次尝试都生成新令牌。新令牌会告诉服务商每次重试都是独立操作,从而失去保护作用。
POST /v1/transfers HTTP/1.1
Idempotency-Key: transfer-7c41b5b9-7f5b-4f51
Content-Type: application/json
{"source":"acct_17","destination":"acct_42","amount":12500,"currency":"USD"}
在发送第一次调用前,将令牌和任务及请求负载一起保存。如果运行器重启,它必须恢复同一个令牌。还要保存服务商返回的操作标识符。之后可以使用这个标识符发起核对调用,查询原始操作,而不必重新创建它。
如果 API 不提供幂等功能,可以寻找“创建后查询”的模式。代理可以在负载中附加客户端生成的外部引用,然后在不确定失败后按该引用搜索。如果 API 既不支持幂等,也没有可靠的查询方式,就不要自动化重复写入。应要求人工检查服务商记录。第一次重复副作用进入无法轻易撤销的系统前,这种方式会显得慢;之后你会发现它其实更快。
区分限流、服务故障和错误请求
把所有非成功状态一概视为相同的重试策略,会隐藏缺陷并浪费配额。选择延迟前先对响应分类。状态码、响应内容、服务商错误码和请求方法都很重要。
可以采用以下实用分类:
429表示降低速率、遵守服务商指导,并从共享限流预算中扣除。408、连接重置和部分5xx响应可能适合有限重试,但写入操作仍需要幂等或核对路径。400、401、403、404和422通常需要修正请求、调整授权,或由人工决定。重复发送没有意义。409需要按资源处理。它可能表示重复、版本冲突,或另一个工作流持有锁。- 服务商特定的配额耗尽错误可能要求等到计费周期或每日配额重置,这与短时突发限流不同。
不要把 503 Service Unavailable 当作可与 429 互换的状态。503 表示服务当时无法处理请求;429 表示客户端超过了限制。两者都可能携带 Retry-After,但 429 应让调度器降低受影响范围的本地吞吐量。503 则可能是区域、端点或整个服务商的问题。在指标和操作人员消息中保留这种区别。
代理行为也可能造成无效请求风暴。模型收到 422 后,可能不断使用不受支持的筛选条件调用端点,或者在凭据已撤销后重复尝试 401。应在重复的相同失败周围设置断路器。例如,如果同一次运行中,相同端点、方法和规范化错误码连续失败多次,就停止这条路径,并把错误详情作为约束返回给代理。不要允许代理通过改变空白、重新排列 JSON 字段,就假装自己在探索新选项。
规范化的请求指纹应排除密钥和易变标头。它应包含方法、端点模板、稳定的负载字段和 API 错误码。这样,运行器可以识别循环,同时不会记录凭据或完整的敏感请求内容。
共享凭据需要一个队列,而不是依赖代理自觉
共享 API 凭据带来了单个代理无法靠良好意愿解决的协调问题。除非调度器提供共同预算,否则每个进程只能看到自己的请求。按代理分别退避可以降低请求洪峰的可能性,却无法公平分配账户级的稀缺配额。
在共享凭据范围前放置出站队列,并有意识地分配优先级。经过人工批准的生产变更,可能需要优先于后台库存工作。一个发现一万条记录的长时间运行代理,应按服务商允许的速率逐页处理,而不是一次把所有分页请求塞进队列。
队列需要支持取消。如果代理的父任务结束,应在延迟请求唤醒前丢弃它们。否则,已停止的任务可能在之后继续消耗配额,让审计轨迹变得难以理解。取消时也必须释放预留的并发槽位。
对安全的读取操作使用请求合并规则。如果五次代理运行在短时间内请求同一个不可变记录,就执行一次请求,并将结果分发给所有等待任务。对于新鲜度会改变含义的读取操作,例如余额或审批状态,不要合并请求。目标是消除意外重复,而不是建立一个向决策代理返回过时事实的缓存。
限流常常暴露更深层的规划问题。代理每个项目调用一次详情端点,可能完全遵循了指令,却使用了错误的访问方式。在添加重试前,先寻找批量端点、分页控制、条件请求、webhook、导出任务或服务端搜索端点。这些改动可以在服务商拒绝请求前就减少调用数量。
Sallyport 可以让 API 凭据留在代理进程之外,同时记录每次出站调用,但调用方仍然需要队列和重试控制。凭据隔离可以减少密钥暴露,却不会改变服务商的配额。
人工升级应保留不确定性
当系统无法确定副作用是否发生、剩余等待时间超过任务期限、共享配额耗尽,或反复限流指向账户配置问题时,应交由人工处理。升级通知不应只是笼统的“API 失败”。它应是一份简洁的案例记录,让处理人员无需从零散日志中重建运行过程就能做决定。
应向操作人员提供以下事实:
- 任务要求的结果,以及停止执行的确切逻辑操作;
- 服务商、端点、方法、账户范围和经过清理的请求指纹;
- 尝试时间、状态、
Retry-After或限流标头,以及已经消耗的预算; - 操作是否有幂等令牌、服务商操作 ID 或核对查询;
- 下一步安全选项,例如等待、查询状态、增加配额、调整方案或取消。
默认不要在审批卡片中放入原始请求内容。它可能包含个人数据、内部文档内容,或一个看似无害、但不应被错误人员看到的值。显示简洁摘要,将详细访问作为有意执行的审计操作。
审批应请求一个决定,而不是仅仅征求“继续”的许可。对于不确定的写入操作,可以提供“查询现有操作”“使用同一个幂等令牌重试”“取消”,以及在确有必要时的“发送新操作”。最后一个选项应明确说明,它可能产生第二个副作用。当选项如实描述实际风险时,人们更容易做出正确判断。
持续访问权限的批准,与特定外部操作的批准,是两种不同的控制。会话可以继续获得授权,但代理仍可能需要在付款、生产写入,或限流事件后的重复请求前逐次接受审核。界面和日志都应将这两类决定分开。
查看升级案例时,应先做状态核对。通过幂等键、外部引用或操作标识符查询服务商。只有当查询显示操作没有完成,或服务商保证会抑制重复时,才进行重试。相比盲目重新发送,这个顺序只多花一次网络往返,却比修复已经进入下游账本的重复操作更快。
审计记录必须解释尝试的操作和等待过程
有用的审计记录回答的不只是“请求是否返回 200”。它应显示代理尝试了什么、什么授权允许了该操作、远程服务返回了什么,以及重试控制器如何做出反应。没有决策记录,一系列调用看起来就像粗心的重复,即使调度器确实遵守了文档中的 Retry-After 值。
在派发前记录一个事件,然后追加结果和调度事件。请求元数据中不要保存明文凭据。一段紧凑的序列可能如下:
14:11:02 action_requested task=sync-482 method=POST route=/records
14:11:02 action_sent attempt=1 idempotency=rec-91f2
14:11:03 action_result status=429 retry_after=30 scope=account:ops
14:11:03 retry_scheduled attempt=2 due=14:11:33 budget_wait=30
14:11:33 action_sent attempt=2 idempotency=rec-91f2
14:11:34 action_result status=201 provider_id=r_893
这段序列把逻辑操作与传输尝试区分开来。如果有人问为什么出现了两次 POST,记录会显示它们共享同一个幂等令牌,并遵守了服务商要求的延迟。如果第二次响应发生超时,下一个事件应是 reconciliation_required,而不是另一个自动的 action_sent。
防篡改日志在事故复盘中很有用:团队可以确认运行过程没有在事后删除不利的尝试。Sallyport 会从加密的哈希链审计日志中生成会话和调用日志,sp audit verify 可以在离线状态下检查链条,无需保管库密钥。当操作人员需要区分服务商限流、代理循环或较晚发生的人工重试时,这一点很有价值。
日志还需要保留期限和访问边界。即使令牌从未出现,请求路径、服务商账户标识和时间模式也可能暴露敏感的运营活动。记录足以重建决策的信息,然后限制可以搜索和导出记录的人员。
在代理于生产环境遇到故障前测试失败路径
限流设计只有在测试证明它会停止调用、保留幂等性并升级不确定状态后才算完整。只模拟一次 429 不够。还需要测试并发工作进程,以及远程服务可能已经执行操作之后才发生的故障。
在测试环境或可控的虚假 API 上运行以下流程:
- 启动三个使用同一模拟账户范围的代理任务,并让它们发出相同的安全读取请求。
- 对第一个请求返回带有
Retry-After: 10的429,然后确认调度器会延迟该范围内的所有工作,而不是让另外两个任务继续全速运行。 - 延迟后返回成功,并检查重试时间存在轻微差异,而不是同时形成一个洪峰。
- 使用稳定的幂等令牌发送写入请求,模拟虚假 API 保存记录后连接断开,并确认运行器会查询状态,而不是发起新的创建请求。
- 耗尽配置的时间预算,并确认操作人员收到经过清理的升级记录,同时已取消的工作不会在之后重新唤醒。
在虚假 API 处测量出站尝试次数,而不只是代理内部的函数调用次数。内部计数器会漏掉 HTTP 库或包装器增加的重试。还要测试重启:第一次不确定的写入后停止运行器,恢复持久化状态,并确认它会使用原始幂等令牌继续进行状态核对。
不要因为测试代理最终获得了成功响应,就给它奖励。应奖励它发出正确数量的调用、在得到指示时等待,并在掌握证据前保持不确定的写入未解决。一个通过重复外部操作来“完成任务”的代理,没有通过测试。
首先应实现的是带有明确终止结果的共享重试预算。它会把限流从“继续尝试”的邀请,变成一个有记录、有截止时间,并且在远程状态不确定时有人可以介入的运营决策。
常见问题
收到 HTTP 429 后,AI 代理应该做什么?
把 429 当作调度信号,而不是暂时的网络故障。按照服务给出的延迟停止发送请求;如果多个工作进程共享配额,就降低并发量,并将这次拒绝计入运行的重试预算。
多个 AI 代理可以共享同一个 API 限流额度吗?
不一定。限流可能按凭据、账户、端点、IP 地址或共享组织池计算。不同代理进程可能耗尽同一份配额,即使每个进程单独看起来都在合理运行。
什么是带抖动的指数退避?
指数退避会在连续失败后逐步延长等待时间,抖动则会随机改变等待时长。抖动可以避免同时失败的多个工作进程同时重试,从而再次形成请求洪峰。
哪些 API 请求可以安全重试?
只有在操作可以安全重复,或服务支持幂等性时才重试。读取操作通常可以安全重试;扣款、发送消息、部署和创建记录则需要幂等键,或在之后执行状态核对。
代理是否应始终遵守 Retry-After 标头?
服务发送 Retry-After 时应遵守它。如果没有该字段,就参考服务公布的限流标头和文档;两者都没有时,使用保守且有上限的退避策略,并在预算耗尽后停止。
AI 代理的重试预算是什么?
重试预算是对第一次尝试失败后,一次运行可以额外消耗的请求次数、等待时间,或两者的固定限制。它能避免一个被拒绝的请求悄悄变成数百个请求,进一步加重故障或耗尽共享配额。
代理什么时候应因限流请求人工帮助?
当代理无法判断副作用是否已经发生、等待时间会超过任务期限、账户级配额已经耗尽,或预算用完后故障仍在持续时,应升级给人工处理。升级信息应包含端点、时间戳、请求标识、状态、标头,以及操作是否可能已经完成。
HTTP 503 和 HTTP 429 是一回事吗?
把 503 视为服务不可用,把 429 视为明确的请求限流响应。两者都可能需要延迟重试,但 429 还应触发本地控制,降低请求速率和共享并发量。
操作网关能避免 API 限流失败吗?
网关可以让凭据远离代理,并保留外部操作的记录,但它不能让服务提供商授予更多配额。代理仍然需要限制重试次数、等待时间和同时发出的调用数量。
团队应记录哪些 AI 代理 API 限流信息?
按服务商、凭据、账户、端点组、状态码、重试次数和等待时间记录调用。还要把这些数据与触发调用的任务关联起来,因为高请求量可能来自合法的批量工作,也可能来自失去停止条件的代理循环。