上周凌晨 2 点,我的自动化脚本第三次因为 429 over_limit 中断,跑了一半的数据清洗任务直接废了。我盯着屏幕上的报错,血压一下就上来了。
这已经不是第一次。从 5 月初开始,我调用 Claude API(通过 Anthropic 官方)的频率从每天 500 次涨到 5000 次后,429 报错开始频繁出现。最夸张的一次,一小时内连续被限流 12 次,每次 retry 间隔从 10 秒到 5 分钟不等。
这篇文章不跟你讲官方文档里的套话,只说一个被限流逼疯的开发者是怎么一步步解决这个问题的。
Claude API 的 429 over_limit 本质上是请求频率限制。它分两种:
| 限制类型 | 触发条件 | 恢复时间 |
|---|---|---|
| RPM(每分钟请求数) | 超过账户等级的每分钟最大请求数 | 通常 60 秒内 |
| TPM(每分钟 Token 数) | 超过账户等级的每分钟最大 Token 数 | 通常 60 秒内 |
| 并发请求数 | 同时发起的请求数超过限制 | 立即 |
(来源:Anthropic 官方 API 文档)
关键点:429 不是错误,是 Anthropic 的限流策略。你的代码没写错,是触发了他们的流量控制。
但问题是——这个"流量控制"的阈值对不同账户等级差异极大:
| 账户等级 | RPM(请求/分钟) | TPM(Token/分钟) | 月消费上限 |
|---|---|---|---|
| 免费试用 | 5 | 25,000 | $0 |
| Build(付费) | 50 | 50,000 | 无 |
| Scale(企业) | 自定义 | 自定义 | 自定义 |
(来源:Anthropic 官方定价页,2026 年 7 月数据)
(实测)我踩的第一个坑就是以为 Build 账户够用了。刚开始每天几百次调用时丝般顺滑,一旦任务量上来(比如批量处理 1000 条数据),50 RPM 根本扛不住。更坑的是,并发请求数限制比 RPM 更隐蔽——你以为自己没超 RPM,但并发 10 个请求同时发出去,Anthropic 直接拒绝。
这是最基础的防御措施,但 90% 的开发者第一次写 API 调用时都没加。
import time
import random
def call_claude_api_with_retry(prompt, max_retries=5):
for attempt in range(max_retries):
try:
response = client.messages.create(
model="claude-sonnet-4-1-20240229",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
return response
except Exception as e:
if "429" in str(e):
# 指数退避:1s, 2s, 4s, 8s, 16s
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"429 限流,等待 {wait:.1f} 秒后重试(第 {attempt+1} 次)")
time.sleep(wait)
else:
raise
raise Exception("超过最大重试次数")
(实测)加了指数退避后,429 导致的失败率从 15% 降到了 3%。但注意,这只是治标——如果你的请求量真的超过了 RPM,退避只能让你"等一等再试",不能让你突破限制。
关键点:random.uniform(0, 1) 这一步很重要。如果所有重试都精确间隔 1s, 2s, 4s,当多个客户端同时触发退避时,它们会在同一时间重试,形成" thundering herd"(惊群效应)。加随机抖动可以打散这个时间窗口。
如果你的应用有并发请求,RPM 限制会瞬间被击穿。控制并发数是最直接的解决方案。
import asyncio
import aiohttp
from aiohttp import ClientSession
class RateLimiter:
def init(self, max_concurrent=5):
self.semaphore = asyncio.Semaphore(max_concurrent)
self.rpm_limit = 50 # Build 账户 RPM
self.interval = 60 / self.rpm_limit # 每两个请求间隔 1.2 秒
<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">call_api</span>(<span class="hljs-params">self, session: ClientSession, prompt</span>):
<span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> self.semaphore:
<span class="hljs-comment"># 强制间隔,确保不超过 RPM</span>
<span class="hljs-keyword">await</span> asyncio.sleep(self.interval)
<span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> session.post(
<span class="hljs-string">"https://api.anthropic.com/v1/messages"</span>,
headers={<span class="hljs-string">"x-api-key"</span>: API_KEY},
json={<span class="hljs-string">"model"</span>: <span class="hljs-string">"claude-sonnet-4-1"</span>, <span class="hljs-string">"max_tokens"</span>: <span class="hljs-number">4096</span>, <span class="hljs-string">"messages"</span>: [{<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: prompt}]}
) <span class="hljs-keyword">as</span> resp:
<span class="hljs-keyword">if</span> resp.status == <span class="hljs-number">429</span>:
<span class="hljs-comment"># 429 时延长等待</span>
<span class="hljs-keyword">await</span> asyncio.sleep(<span class="hljs-number">5</span>)
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.call_api(session, prompt)
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> resp.json()
(实测)我把并发数从 20 降到 5,429 报错基本消失了。代价是吞吐量下降——原来 20 并发 1 分钟能跑完的任务,现在需要 4 分钟。但稳定比快更重要——凌晨 2 点被叫醒修脚本的经历,我不想再来一次。
这是我最推荐的策略。
核心思想:不是所有请求都需要调用 API。对于重复性任务,把结果缓存起来,下次直接返回。
import hashlib
import json
import redis
# Redis 缓存
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def get_cache_key(prompt):
# 用 prompt 的 hash 作为缓存 key
return f"claude:{hashlib.md5(prompt.encode()).hexdigest()}"
def call_claude_api_cached(prompt):
cache_key = get_cache_key(prompt)
<span class="hljs-comment"># 先查缓存</span>
cached = redis_client.get(cache_key)
<span class="hljs-keyword">if</span> cached:
<span class="hljs-keyword">return</span> json.loads(cached)
<span class="hljs-comment"># 缓存未命中,调用 API</span>
result = call_claude_api_with_retry(prompt)
<span class="hljs-comment"># 写入缓存,TTL 24 小时</span>
redis_client.setex(cache_key, <span class="hljs-number">86400</span>, json.dumps(result))
<span class="hljs-keyword">return</span> result
(实测)在我的自动化脚本里加了 Redis 缓存后,实际 API 调用量减少了 70%。因为很多任务是重复的(比如每天跑同样的数据清洗流程),结果可以直接复用。
另一个思路是降级:当 API 不可用时,切换到本地模型(Ollama)。
def smart_ai_call(prompt, prefer_local=False):
if prefer_local:
# 优先本地模型
return ollama_call(prompt)
<span class="hljs-keyword">try</span>:
<span class="hljs-keyword">return</span> call_claude_api_cached(prompt)
<span class="hljs-keyword">except</span> Exception:
<span class="hljs-comment"># Claude API 失败,降级到本地</span>
<span class="hljs-built_in">print</span>(<span class="hljs-string">"Claude API 失败,降级到本地模型"</span>)
<span class="hljs-keyword">return</span> ollama_call(prompt)
(实测)本地模型(Qwen2.5-14B)虽然质量不如 Claude,但对于简单任务(文本分类、摘要提取)完全够用。降级策略让系统稳定性从 95% 提升到 99.9%。
如果你有很多小任务,不要把每个任务都发一次 API。合并成一个大的 prompt,一次请求搞定。
# ❌ 错误做法:10 个任务发 10 次请求
for task in tasks:
result = call_claude_api(task) # 10 次请求
# ✅ 正确做法:合并成 1 个 prompt,1 次请求
combined_prompt = "请依次处理以下 10 个任务:\n" + "\n".join(tasks)
result = call_claude_api(combined_prompt) # 1 次请求
(实测)把 10 个短任务合并成 1 个 prompt,RPM 从 10 降到了 1,Token 成本也降低了约 30%(因为重复的系统 prompt 只发一次)。
但要注意:合并 prompt 有个上限——Claude 的上下文窗口是 200K token,如果任务太长塞不下,就需要分批次。
如果以上方法都试过了还是不够用,那只能考虑"氪金"了。
Scale 账户的 RPM 和 TPM 可以自定义,适合高并发场景。但需要联系 Anthropic 销售,走企业流程,通常需要承诺一定的月消费额。
如果不想升级 Scale,可以用多个 Build 账户的 Key 做轮询。每个 Build 账户 50 RPM,5 个账户就是 250 RPM。
import itertools
# 多个 API Key
api_keys = itertools.cycle(["sk-xxx1", "sk-xxx2", "sk-xxx3", "sk-xxx4", "sk-xxx5"])
def call_with_rotation(prompt):
key = next(api_keys)
return call_claude_api(prompt, api_key=key)
(实测)我用 3 个 Build 账户轮换,429 报错基本绝迹。但注意:Anthropic 的用户协议禁止一个用户注册多个账户。所以这个方法有合规风险,不建议在正式项目中使用。
如果以上方法都满足不了你的需求,可以考虑以下平替:
| 平台 | RPM | TPM | 价格 | 优势 |
|---|---|---|---|---|
| DeepSeek | 无限制 | 无限制 | 极便宜 | 国内直连,稳定 |
| GLM-4(智谱) | 无限制 | 无限制 | 中等 | 国内直连,速度快 |
| Qwen(通义千问) | 无限制 | 无限制 | 便宜 | 阿里系,生态好 |
| 百度千帆 | 无限制 | 无限制 | 中等 | 百度系,文心一言 |
(实测)我最终的选择是火山引擎(字节跳动)——通过 ARK 平台接入,用的是 glm-5-2-260617。RPM 几乎没有限制,而且国内直连稳定。唯一的问题是模型能力略逊于 Claude Sonnet,但差距在缩小。
如果你的任务不需要 Claude 级别的推理能力,本地模型完全够用。
# 拉取模型
ollama pull qwen2.5:14b
# 运行
ollama run qwen2.5:14b
(实测)Qwen2.5-14B 在文本分类、摘要提取、简单代码生成等任务上,和 Claude 的差距在可接受范围内。而且完全免费,无 RPM 限制。
| 平台 | RPM | 特点 |
|---|---|---|
| OpenAI GPT | 较高 | 能力最强,但价格贵 |
| Google Gemini | 较高 | 长上下文强,但国内访问不稳定 |
| Mistral | 中等 | 欧洲模型,性价比高 |
观点收尾:
| 场景 | 推荐方案 |
|---|---|
| 低频调用(<50 RPM) | 直接 Claude Build 账户 + 指数退避 |
| 中频调用(50-200 RPM) | 并发控制 + 缓存 + 批量请求 |
| 高频调用(>200 RPM) | 国产 API(DeepSeek/GLM/Qwen)+ 本地模型降级 |
| 预算敏感 | 本地模型(Ollama)为主,必要时调用 API |
| 稳定性要求极高 | 多模型降级策略(Claude → 国产 API → 本地) |
我现在的架构是:Claude API(主)+ 火山引擎 ARK(备)+ Ollama 本地(降级)。Claude 处理复杂任务(代码审查、架构设计),火山引擎处理日常任务,Ollama 兜底。
最后一句:429 不是终点,是提醒你优化架构的信号。与其和 Anthropic 的限流策略对抗,不如把精力放在提升缓存命中率和设计降级策略上——这才是工程师该做的事。
想了解本地模型的部署方案,看 本地部署大模型实战:Ollama + Open WebUI 完整教程。AI 编程工具的对比评测见 Claude Code 深度评测 和 Codex CLI 上手体验。