你有一条 5 步流水线:research、summarize、code、review、deploy。其中「code」Agent 并发拆出 20 个子任务,每个都用同一个 key 调用 Anthropic 的 Messages API。这一波突发请求超过了你所在 tier 的每分钟请求数(RPM)上限,于是 code Agent 开始收到带 retry-after 头的 429 响应。等待 code 输出的 review Agent 设了 120 秒超时,在重试还没成功之前就放弃了。等待 review 的 deploy Agent 也跟着超时。整条流水线失败了——不是因为任务做不出来,而是因为一个 Agent 的负载超过了它在限速桶里应占的份额,失败又通过互不协调的超时一路向上传导。
最快的修复: 在高流量 Agent 前面加一个共享限速器,把它的峰值并发压到 tier 限额以下;同时把每个等待型 Agent 的超时设得比最坏情况的 retry-after 更长(把 429 当成临时错误,永远不要当成永久失败)。本文剩下的部分讲怎么定位到那个肇事 Agent,以及怎么把流水线加固到这种事不再发生。
rate limit 到底是怎么算的(2026 年 6 月)
下面的所有结论都取决于两个事实,而它们常被误解:
- Anthropic 的限额是按「organization + 模型类」算的,不是按 API key。同一个 org 下的所有 key 共用一个 Opus 4.x 桶和一个 Sonnet 4.x 桶。workspace 可以设更低的限额,但 org 的天花板永远叠加在上面。所以在 Anthropic 上,给每个 Agent 单独发一把 key 并不能把桶拆开。
- OpenAI 的限额是按「organization + 模型」算的,分 RPM、TPM、RPD、TPD 四个独立维度,任何一个超了都会返回
429。
两家用的都是持续补充的 token bucket,所以不存在「整分钟清零重置」这回事。60 RPM 实际上是按约 1 请求/秒来执行的;只要瞬时突发够猛,哪怕你这 60 秒的平均值没超也照样触发。Anthropic 还有一个 acceleration limit(加速度限制):用量陡增时,即使你没到稳态上限也可能返回 429——而这正是重试风暴会造成的局面。
收到 429 时,去读响应。Anthropic 返回的是:
retry-after: 12
anthropic-ratelimit-requests-remaining: 0
anthropic-ratelimit-requests-reset: 2026-06-17T18:04:30Z
anthropic-ratelimit-input-tokens-remaining: 41000
anthropic-ratelimit-tokens-reset: 2026-06-17T18:04:12Z
错误的 type 是 rate_limit_error,消息里会写明你撞的是哪个限额(RPM、ITPM 还是 OTPM)。OpenAI 的 429 带 Retry-After(以及 retry-after-ms)和 x-ratelimit-remaining-*。一定要严格遵守 retry-after——自己猜一个更短的等待时间,只会再换来一个 429。
你属于哪一类?
| 症状 | 最可能的原因 | 看哪一步 |
|---|---|---|
| 某一个 Agent 占了 key 上 80% 以上的请求 | 单一共享桶,一个贪婪 Agent | Step 1、Step 2 |
第一个 429 之后,总请求速率反而上升 | 重试风暴 / acceleration limit | Step 5 + 预防建议 |
| 只在高并发 fan-out 时挂,小规模测试没事 | 任务数 * 每任务调用数 超过限额 | Step 2 |
| 等待型 Agent 在被阻塞的 Agent 重试前就先挂了 | 超时短于最坏情况的 retry-after | Step 4 |
一个 429 触发了回滚或告警 | 错误处理把临时错误当成了永久错误 | Step 5 |
| 没有任何突发就秒撞限额 | 这是账单额度(insufficient_quota),不是 rate limit | FAQ |
常见原因
1. 所有 Agent 共用一个桶,其中一个很贪
所有 Agent——orchestrator、子 Agent、reviewer——都走同一个 org/模型桶。当某个高流量 Agent(比如并发拆 30 路的 code 或 research Agent)把桶占满,其他 Agent 全部被饿死,开始收到 429。
怎么判断: 给每个调用打 tag(见下文)再按 tag 拉用量。如果在出现 429 的那个时间窗里,某一类 Agent 占了 80% 以上的请求,它就是肇事者。
2. fan-out 把单 Agent 的用量乘了 N 倍
orchestrator 把 50 个任务派给 50 个并发实例,每个发 3 次 LLM 调用——也就是近乎同时打出 150 次请求,远超 RPM 只有几十的 tier。流水线当初是按 5 个任务测的(15 次调用,没问题),50 个的情形从没被建模过。
怎么判断: 用 并发任务数 * 每任务调用数 算出峰值并发,和你的 tier RPM 比。如果理论峰值超过限额,不加并发控制的 fan-out 就会级联。
3. 等待方的超时短于被阻塞方的重试窗口
Agent C 等 Agent B 90 秒。B 撞了限速,正在遵守一个 120 秒的 retry-after。C 在 90 秒就超时挂了,B 连重试的机会都还没轮到。这纯粹是超时协调的 bug。
怎么判断: 把链路里每个超时都画出来。对每个 Agent,算出它的最坏重试延迟(允许重试次数内 retry-after 的累加)。任何一个上游超时只要短于下游的最坏延迟,就会级联。
4. 各阶段之间没有队列——同步阻塞
各阶段是同步串起来的:result_b = call_b(result_a)。如果 B 因为限速变慢,调用它的线程就阻塞;每个并发任务一个线程,它们堆积起来把线程池耗尽,让突发雪上加霜。
怎么判断: 看各阶段之间是同步调用还是异步队列连起来的。如果是中间没有缓冲的直接同步 HTTP 调用,限速延迟就没地方可吸收。
5. 错误处理把 429 当成致命失败
收到 429 时,Agent 抛出一个笼统的 PipelineError。orchestrator 捕获后把任务标记为永久失败,并触发补偿动作(回滚、告警)。任务只是被限速了,并没有真的失败——处理逻辑却把临时错误升级成了致命错误。
怎么判断: 看你的处理逻辑有没有把 RateLimitError(临时,退避后重试)和 AuthenticationError 或内容策略错误(永久,不要重试)区分开。如果所有错误都汇到同一个 handler,rate limit 就会被误判。
6. 重试在放大突发,而不是把它排空
每个 Agent 各自重试,固定等 1 秒。5 个 Agent 同时重试,限速窗口内的全局请求速率直接翻倍,于是触发 acceleration limit,把一次 2 分钟的故障拖成 20 分钟。
怎么判断: 测一下第一个 429 之后的请求速率。如果是上升而不是下降,那就是重试放大在主导——你需要的是带 jitter 的退避加熔断,而不只是重试。
最短修复路径
Step 1:给每个调用打 tag,定位瓶颈 Agent
给每个请求打上 Agent 名字的 tag,这样用量就能按消费者拆开:
client.messages.create(
model="claude-sonnet-4-6",
messages=messages,
metadata={"user_id": f"agent:{agent_name}:run:{run_id}"},
)
然后在 Claude Console 的 Usage 页面(platform.claude.com > Usage)看按 Agent 的用量拆分,或者用 Rate Limits API 和 Usage API 程序化拉取。占了突发的那个 Agent 就是你的目标。
Step 2:在高流量 Agent 前面加一个共享限速器
关键是这个限速器要被同一个 Agent 的所有并发实例共享,并且设得比 tier 低,给其他 Agent 留出余量。如果你用 LangChain,直接用它内置的 token-bucket 限速器,不要自己造轮子:
from langchain_core.rate_limiters import InMemoryRateLimiter
from langchain_anthropic import ChatAnthropic
# 40 RPM ≈ 0.66 req/s;在 org 桶下给其他 Agent 留余量
limiter = InMemoryRateLimiter(
requests_per_second=0.66,
check_every_n_seconds=0.1,
max_bucket_size=5, # 限制突发大小
)
code_llm = ChatAnthropic(model="claude-sonnet-4-6", rate_limiter=limiter)
在 LangGraph 里,再加一道分支 fan-out 上限,让 map-reduce 节点不会一次全开:
graph.invoke(state, config={"max_concurrency": 5})
如果不依赖框架,一个 asyncio.Semaphore 也能做到同样的事——同一个 Agent 的每个 worker 共用一个 semaphore 实例:
import asyncio
# 由 code Agent 的所有并发实例共享
_code_agent_sem = asyncio.Semaphore(5)
async def code_agent_call(task):
async with _code_agent_sem:
return await generate_code(task)
注意 InMemoryRateLimiter 是按进程的。如果你的 Agent 跑在多个 worker 或多台机器上,要用 Redis 给限速器做后端(或者在前面放一个像 LiteLLM 这样的 proxy),桶才是真正共享的。
Step 3:别再想用多发几把 key 来「拆桶」
在 Anthropic 上,同一个 org 下的多把 key 共用同一个桶,这么做什么也得不到。真正能做隔离的办法,按工作量从小到大:
- 给不同 Agent 用不同的模型类。 Opus 4.x 和 Sonnet 4.x 的桶是分开的。把低流量的 orchestrator 路由到 Opus、高流量的 worker 路由到 Sonnet(或 Haiku),它们就不再争抢同一个桶。
- workspace 限额,用来封顶一个吵闹的 Agent,让它吃不掉整个 org 桶(Console > Settings > Limits)。workspace 限额只能设得比 org 限额低。
- 分开的 organization,给确实独立的产品用。用多个账号来绕开单个 org 的限额是违反服务条款的;但分开的、合规的 org 不是。
OpenAI 的桶是按 org+模型,所以逻辑一样:靠模型分桶,不是靠 key。
Step 4:用队列解耦各阶段,并给足超时
在各阶段之间放一个异步队列,让某一阶段的限速延迟不会直接把下一阶段超时拖死。把消费端的超时设得比最坏情况的 retry-after 链更长:
import asyncio
code_output_queue: asyncio.Queue = asyncio.Queue(maxsize=100)
async def code_worker(task):
result = await code_agent_call(task) # 遵守 Step 2 的限速器
await code_output_queue.put(result)
async def review_worker():
while True:
result = await asyncio.wait_for(
code_output_queue.get(),
timeout=300, # 比最大重试窗口更长
)
await review_code(result)
Step 5:把 429 归类为临时错误,并遵守 retry-after
读响应头,绝不自己猜,再加一点 jitter,让重试的几个 Agent 不至于齐刷刷地一起冲:
import random, asyncio, anthropic
class RateLimitError(Exception):
"""临时错误——退避后重试,不要标记为永久失败。"""
def classify(exc: anthropic.APIStatusError):
if exc.status_code == 429:
retry_after = int(exc.response.headers.get("retry-after", 60))
return RateLimitError(), retry_after + random.uniform(0, 5) # 加 jitter
if exc.status_code in (500, 503, 529): # 529 = Anthropic overloaded
return RateLimitError(), 5 + random.uniform(0, 5)
raise exc # 400/401/403 是永久错误——不要重试
把重试上限设住(5 次是常规),并用熔断器把 Agent 包起来,这样持续限速时整个阶段会暂停,而不是一直猛锤。这正是终结原因 6 里那种重试放大的关键。
怎么确认已经修好
- 复现那波突发。 按峰值 fan-out 跑流水线(你预期的最大
任务数)。装上限速器后,盯响应头里的anthropic-ratelimit-requests-remaining——它应该会逼近 0,但不应再冒出429。 - 注入一个强制
429。 临时把某个 workspace 限额设得极小(或在 code Agent 上 mock 一个429),确认 review 和 deploy Agent 是在等待而不是失败。不应触发任何回滚或告警。 - 检查限速后速率没有反弹。 把测试期间的请求数/分钟画出来;任何
429之后,速率应该下降(说明退避在起作用),而不是上升。
预防建议
- 把高流量和低流量的 Agent 路由到不同的模型类,让它们待在不同的桶里;别指望在同一个 Anthropic org 下靠多发 key 来隔离。
- 上线前先算出峰值 fan-out 时的并发调用数;如果超过 tier RPM 的约 70%,就加共享限速器或 semaphore。
- 把等待型 Agent 的超时设得比下游 Agent 能撞到的最坏
retry-after链更长(通常每次尝试 120 秒以上)。 - 各阶段之间用异步队列,让限速延迟不会传导成上游超时。
- 把
429归类为带退避和 jitter 的临时错误;绝不要仅凭一次限速就触发回滚或补偿。 - 把按 Agent 的用量加进看板,任一 Agent 超过预算 80% 时告警;
429比率超过约 5% 时单独告警。 - 给吵闹的 Agent 设 workspace 限额,让单个消费者吃不掉整个 org 桶。
- 至少给重试突发留 20% 的速率余量;稳态下绝不要按 100% 的限额来规划。
- 显式测试级联:在最高流量的 Agent 上强制一个
429,确认其余 Agent 照常运行。
常见问答 (FAQ)
Q: 在 Anthropic 上,给每个 Agent 单独一把 API key 能把 rate limit 拆开吗? A: 不能。截至 2026 年 6 月,Anthropic 的 rate limit 是按 organization 和 模型类 算的,所以同一个 org 下的每把 key 都从同一个 Opus 4.x / Sonnet 4.x 桶里取。要隔离 Agent,应该路由到不同的模型类、设 per-workspace 限额,或用分开的 organization——而不是多发 key。详见 Anthropic 的 rate limits 文档。
Q: 我没有任何流量就秒撞 429,为什么?
A: 这通常是账单额度,不是 rate limit。OpenAI 在 org 撞到 spend limit 时会返回带 type: insufficient_quota 的 429,Anthropic 则在你到达 tier 的当月 spend 上限后封掉用量。去控制台的账单/用量页面看一眼;限速方面的修复对额度问题没用。
Q: 实际中 retry-after 该等多久?
A: 读响应头,严格照办。Anthropic 的 retry-after 和 anthropic-ratelimit-requests-reset(RPM)或 anthropic-ratelimit-tokens-reset(TPM)对齐。OpenAI 发的是 Retry-After 和 retry-after-ms。如果头缺失,默认 60 秒。再加一点随机 jitter,让并发的几个 Agent 不会同一瞬间一起重试。
Q: 服务商能调高我的限额吗?多快? A: 两家都给付费账号调限额。Anthropic 会随累计消费跨过阈值自动升 tier(最高到 Tier 4),你也可以在 Console > Settings > Limits 申请更多;OpenAI 同样按消费自动升级 usage tier。急用时,找 sales/support 比重构流水线更快,但无论如何都要按留余量来设计。
Q: 我的突发确实很大,怎么在不把一切串行化的前提下削峰?
A: 用 token-bucket 限速器:允许有上限的突发,然后回落到稳态速率(InMemoryRateLimiter 的 max_bucket_size,或 Python 的 limits / JS 的 bottleneck)。把突发设成每分钟限额的大约 20%。批处理作业还可以考虑 Anthropic 的 Message Batches API,它有自己独立的限额池。