上周三我在跑一个批量翻译脚本,大概 2000 条产品描述要过 Claude Sonnet 4.6,跑到第 300 条左右控制台开始疯狂刷红:
Error code: 429 - {'type': 'error', 'error': {'type': 'rate_limit_error', 'message': 'Number of request tokens has exceeded your per-minute rate limit (https://docs.anthropic.com/en/api/rate-limits); see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase.'}}
这个报错我已经见过不下十次了。429 就是速率限制,Anthropic 的 API 对每分钟的请求数和 Token 数都有硬上限。免费层(Free)每分钟才给 5 万 Token,付费的 Build 层也就 8 万。跑批量任务分分钟撞墙。
下面是我实测有效的 3 种方案,从手动到自动到彻底换思路,挑一个适合你的。
先说结论
| 方案 | 改动量 | 效果 | 适用场景 |
|---|---|---|---|
| 指数退避重试 | 加 20 行代码 | 能跑但慢,P95 延迟飙到 8-12s | 请求量不大,偶尔撞限制 |
| 并发控制 + 令牌桶 | 改架构,约 50 行 | 稳定但需要调参 | 批量任务,可接受降速 |
| 换聚合 API 网关 | 改 1 行 base_url | 限制直接拉高,基本不再 429 | 懒人方案 / 生产环境 |
搞清楚 429 到底在限什么
很多人以为 429 就是"请求太快",其实 Anthropic 的限制有三个维度,搞混了就白折腾:
graph TD
A[Claude API 请求] --> B{检查速率限制}
B -->|RPM 超限| C[429: 每分钟请求数]
B -->|TPM 超限| D[429: 每分钟 Token 数]
B -->|每日 Token 超限| E[429: 每日 Token 总量]
C --> F[看 retry-after 头]
D --> F
E --> G[只能等明天或升级]
F --> H[退避重试 / 降速 / 换网关]
响应头里藏了具体信息,很多人不看:
# 429 响应头里有这些字段,别浪费了
# anthropic-ratelimit-requests-limit: 50
# anthropic-ratelimit-requests-remaining: 0
# anthropic-ratelimit-requests-reset: 2026-04-23T10:15:30Z
# anthropic-ratelimit-tokens-limit: 80000
# anthropic-ratelimit-tokens-remaining: 0
# retry-after: 15
retry-after 告诉你该等多少秒。很多人直接 time.sleep(60) 一刀切,人家说等 15 秒你等 15 秒就行。
方案一:指数退避重试
最朴素的办法。撞了 429 就等一会儿再试,每次等的时间翻倍,加点随机抖动防止所有请求同时重试(thundering herd 问题)。
import anthropic
import time
import random
client = anthropic.Anthropic(api_key="your-key")
def call_with_retry(prompt, max_retries=5):
for attempt in range(max_retries):
try:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return response
except anthropic.RateLimitError as e:
if attempt == max_retries - 1:
raise
# 指数退避 + 随机抖动
wait = min(2 ** attempt + random.uniform(0, 1), 60)
print(f"429 了,第 {attempt+1} 次重试,等 {wait:.1f}s")
time.sleep(wait)
except anthropic.APIStatusError as e:
# 5xx 也重试,4xx(非429)直接抛
if e.status_code >= 500:
wait = 2 ** attempt + random.uniform(0, 1)
time.sleep(wait)
continue
raise
# 实测:2000条任务跑完耗时从预估40分钟拉到了2小时17分钟
# 中间重试了 187 次,不算优雅但至少跑完了
问题很明显——慢。我那个翻译任务本来 40 分钟能跑完,加了重试逻辑之后磨了两个多小时。而且如果你是 Free 层,RPM 才 5,重试到天荒地老也快不了。
方案二:并发控制 + 令牌桶限速
方案一是"撞了再说",方案二是"别撞"。用 asyncio + 信号量主动控制并发,再加个简易令牌桶控制每分钟 Token 消耗。
import asyncio
import time
from anthropic import AsyncAnthropic
client = AsyncAnthropic(api_key="your-key")
class TokenBucket:
"""简易令牌桶,控制每分钟 Token 消耗"""
def __init__(self, tokens_per_min=70000):
self.capacity = tokens_per_min
self.tokens = tokens_per_min
self.last_refill = time.monotonic()
async def consume(self, n):
while True:
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity,
self.tokens + elapsed * (self.capacity / 60))
self.last_refill = now
if self.tokens >= n:
self.tokens -= n
return
await asyncio.sleep(0.5)
# Build 层 TPM 是 80000,留点余量设 70000
bucket = TokenBucket(tokens_per_min=70000)
# Build 层 RPM 是 50,并发信号量设 8 比较安全
semaphore = asyncio.Semaphore(8)
async def translate(text):
estimated_tokens = len(text) // 3 + 200 # 粗估
await bucket.consume(estimated_tokens)
async with semaphore:
resp = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": f"翻译成英文:{text}"}]
)
return resp.content[0].text
async def main():
tasks_data = ["产品描述1", "产品描述2", ...] # 2000条
results = await asyncio.gather(
*[translate(t) for t in tasks_data],
return_exceptions=True
)
# 实测:429 出现次数从 187 次降到 3 次
# 总耗时约 55 分钟,比方案一快一倍多
效果好很多,429 基本消失了。但调参挺烦——信号量设多少、令牌桶容量留多少余量、Token 估算准不准,都得根据你的实际 Tier 和 prompt 长度来。我前两次参数没调好,要么还是偶尔 429,要么吞吐量上不去白白浪费配额。
还有个坑:estimated_tokens 那个粗估很不靠谱。中文和英文的 Token 比例差很多,我一开始按英文算的,实际中文 Token 消耗高了快一倍,又撞限制了。
方案三:换聚合 API 网关
折腾到第三天我烦了。
429 的根本原因是 Anthropic 按账户级别限速,Free 层 RPM 才 5、TPM 才 5 万。就算升到 Build 层(充 $5),RPM 也就 50。代码写得再优雅,天花板就在那。
聚合 API 网关的原理是后端有多个账户/通道做负载均衡,单个用户的限制会比直连高很多。OpenRouter、Together AI、ofox.ai 这类平台都是这个思路,其中 ofox.ai 拿了云厂商官方授权,走 Anthropic 和 AWS Bedrock 的官方通道,0% 加价对齐官方价格(OpenRouter 要收 5.5% 手续费)。
改动量真的就一行:
from openai import OpenAI
# 把 base_url 从 Anthropic 官方换成聚合网关
# 用 OpenAI SDK 兼容协议,连 SDK 都不用换
client = OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/v1"
)
response = client.chat.completions.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "翻译成英文:这是一段产品描述"}]
)
print(response.choices[0].message.content)
如果你想继续用 Anthropic 原生 SDK 也行:
from anthropic import Anthropic
client = Anthropic(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/anthropic"
)
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "翻译成英文:这是一段产品描述"}]
)
实测结果:同样 2000 条翻译任务,并发 15,全程 0 次 429,38 分钟跑完。P95 延迟大概 340ms,比直连 Anthropic API 还快一点(可能是香港的关系,不确定是不是每次都这样)。
踩坑记录
记几个我实际遇到的问题:
坑 1:Anthropic SDK 版本太旧没有 RateLimitError
我一台老服务器上 anthropic 包还是 0.18.x,anthropic.RateLimitError 直接 AttributeError。升到 0.49+ 才正常。
pip install anthropic>=0.49.0
坑 2:retry-after 头有时候不返回
文档说 429 响应会带 retry-after,但我有几次抓包发现这个头是空的。代码里不能只依赖这个值,得有个 fallback 的退避策略。
坑 3:asyncio.gather 里 return_exceptions=True 别忘了
不加这个参数,一个任务 429 整个 gather 就炸了。加了之后失败的会返回 Exception 对象,后面统一重试就行。
results = await asyncio.gather(*tasks, return_exceptions=True)
failed = [(i, r) for i, r in enumerate(results) if isinstance(r, Exception)]
print(f"失败 {len(failed)} 条,准备重试")
小结
三种方案各有各的适用场景。请求量小、偶尔撞一下 429,方案一的指数退避够用,别过度设计。批量任务要吞吐量,方案二的异步 + 令牌桶效果不错但调参需要耐心。跟我一样懒的话,或者生产环境不想在限速逻辑上花太多精力,换个聚合网关确实最省事——改一行 base_url 的事。
我现在的做法是方案二 + 方案三组合:用聚合网关拉高限制天花板,代码里保留基本的重试逻辑做兜底。毕竟任何 API 都可能偶尔抽风,防御性编程不亏。