Agent开发中的429限流难题:重试机制与全局限流治理实战
做 Agent 项目的人几乎没有谁没被 429 这个状态码折磨过。我最近调一个多步骤 Agent任务是让智能体先查询资料、再调用某个数据分析工具、最后把结论整理成报告。结果日志走到第三步时突然冒出来一行exceeded retry limit, last status: 429 too many requests整个任务直接终止前面消耗的 token 和上下文全部白费。当时我的第一反应是“是不是代码写错了”查了半天参数没问题鉴权没问题逻辑也对。后来认真看了一眼时间线才发现问题不在代码而在于我这个 Agent 在短短四十秒内连续向同一个 API 终点发出了 20 多个请求。翻译成大白话就是——我太“热情”了服务器一时接待不过来。这篇文章我不会只贴一段“try sleep retry”的代码而是想把 Agent 场景下频繁触发的 429 问题讲透为什么 Agent 比普通后端服务更容易撞限流429 响应里藏着哪些关键信息重试策略怎么设计才叫“优雅”以及怎样把重试逻辑变成 Agent 编排层的一个能力而不是散落在各个函数里的补丁。1. 为什么 Agent 总是在“反复拉扯”中撞上 4291.1 一个典型的 Agent 任务执行现场如果你拆开一个普通 Agent 任务看它的调用序列会发现完全不像传统后端服务那样是一条直线而是一个不断自我循环的闭环while task not finished: response call_llm(thoughts, context) # 推理 actions parse_tool_calls(response) # 识别工具调用 results execute_tools(actions) # 调用外部工具 context update_context(context, results) # 把工具结果放回上下文每一步循环里至少要发生一次 LLM 推理调用而推理返回的 tool_calls 又会引发好几路并行工具请求。用户敲一句“帮我做一个市场调研报告”看起来是一个请求实际上在后台可能是“LLM 推理 → 搜索 → 读网页 → 再推理 → 再搜索 → 汇总 → 再推理 → 生成报告”这样一长串链路。这个过程本身就意味着一个用户请求对应的外部 API 调用数量是普通 Web 应用的十倍以上。普通 Web 应用一个用户操作通常只触发 1~2 次第三方 API 调用而且调用路径是提前设计好的不会偏离。Agent 则完全相反任务越复杂模型越倾向于反复试错、回退、重规划每次“想一下”都是一次完整的推理请求。在我的日志里一个中等复杂度的 Agent 任务跑完全程统计下来平均要消耗 15~30 次 LLM 调用。如果开了反思self-reflection机制这个数字还会再往上跳。顺便把概念理顺一下很多人问 Agent、LLM、AI 模型到底什么关系。像 DeepSeek、GPT-4o 这类的 LLM是 Agent 的“大脑”Agent 本身是那套让大脑去“思考-行动-观察”的编排框架。同一个 Agent把底层 LLM 从 A 家换成 B 家体验差异很大但 429 这类限流问题不仅不会消失反而会因为不同供应商限额规则不同而变得更复杂。1.2 Agent 的请求模式与普通应用的差异差异不只是“频率高”还在于“不可预测性”。传统 API 消费者可以靠压力测试来预估 QPS因为调用次数是代码静态决定的。Agent 不一样模型生成多少个 tool_calls、某一次工具执行失败后是直接重试还是重新规划这些都是动态的、随机的。你今天跑十次每一次请求总数和节奏都不一样。这种不可预测性导致你很难像传统服务那样“精确预留配额”。再加上不少流行的 Agent 框架有“自我修复”机制工具调用返回异常时Agent 不会直接把异常抛给用户而是把错误消息塞回上下文让模型“反思一下”再重新生成动作。听起来很聪明但代价是又一次甚至多次新的推理请求。好一点的框架会设置最大迭代次数但默认值经常是 20、30 甚至更高。遇到 API 抖动的时候这个循环就成了 429 的完美放大器。1.3 放大 429 概率的三类经典场景我个人总结在 Agent 开发中 429 高频爆发基本逃不脱这三类场景并行子 Agent 互相叠加请求量。用多 Agent 协作框架把任务拆给 N 个子 Agent 同时跑每个子 Agent 都有自己的推理和工具调用链共享同一个上游 API Key 的限额。哪怕每个子 Agent 只跑 5 次推理5 个子 Agent 加到一起就是 25 次连续请求很容易瞬间触顶。长时间运行的任务没有做速率控制。表格型的 Agent 在爬数据、抓列表、翻页的时候如果循环里没有限流逻辑输出几张结果页的功夫就发出上百个请求。许多 Agent 开发者只关注了重试却不关心“我到底应该多久发出一个请求”。重试逻辑本身引发二次请求风暴。这是最讽刺的一类也是最常见的一类。某个时刻上游服务短暂抖动一批请求同时失败然后又同时被固定退避逻辑唤醒同一时刻再次打满限额。于是 429 从“偶发”变成“持续”。在继续往下看算法之前先明确一个原则重试不是根因处理它只是缓冲。真正稳的做法是把请求速率控制在服务器能接受的节奏里同时在极端情况下做合理的退避与降级。2. 429 不等于“错误”它是服务器在告诉你“等多久再来”2.1 状态码本身意味着什么HTTP 429 Too Many Requests 在 RFC 6585 中的定义是在一定时间窗口内用户发送的请求数量已经超过了服务器的限额。它被归在 4xx 类别里但它跟 400、401、403 有本质区别。400 是“你的请求格式有问题改再多次也没用”401 是“你没有认证换张凭证再来”429 是“你的请求本身是合法的但你来得太密了慢一点再来”。换句话说429 是一个暂时性状态。它不意味着你的代码有逻辑 bug也不代表参数错误。它隐含的潜台词是请求值得重试但需要在合适的时机重试。这里我要特别提醒一点429 的“暂时性”也不是绝对的。我跟一些做 API 网关的朋友聊过部分服务商会把账户额度用尽也返回为 429而不是 402 Payment Required 或 403。比如你按月购买的套餐包含 10 万次调用用完了之后再来调用某些网关会直接返回 429。这种 429 不是“你太快了”而是“你今天已经没资格了”。这种情况下无论你怎么等待重试都不会成功反而会持续烧掉无效的网络开销和日志空间。判断的方法一般是看响应体里有没有“quota”“limit reached”“insufficient”之类的关键词。2.2 从响应头和响应体里挖掘限流信息服务器在返回 429 时通常会在响应头里附带一些“路标”响应头含义Retry-After建议等待的秒数或具体重试时间HTTP-date 格式X-RateLimit-Limit当前时间窗口内的总请求上限X-RateLimit-Remaining当前窗口内还剩多少请求额度X-RateLimit-Reset配额重置的时间戳其中Retry-After是重试逻辑最依赖的字段。它有两种合法格式一种是整数秒另一种是 HTTP 日期。大多数第三方 API 用的是前者返回Retry-After: 2这样的形式如果你遇到后者比如某些自建网关就需要把日期字符串解析成时间戳再计算等待时间。很多开源 SDK 并不处理日期格式这算是一个常见的实现缺口。响应体同样有信息量。以 LLM API 为例触达 RPM 限制和触达 TPM 限制响应体里通常会明确写出是“requests per min”还是“tokens per min”。我在实际调试里遇到过这样的返回{ error: { message: Rate limit reached for default-gpt-4o in organization org-xxx on requests per min (RPM): Limit 30, Used 30, Requested 1., type: requests, code: rate_limit_exceeded } }这里的 message 会直接告诉你当前时间窗口用了多少、上限多少。如果是 TPM 超限说明不是请求频率问题而是上下文太长单次请求消耗的 token 太多了。这时候修正的方法是缩短 prompt、做检索压缩或者换个上下文窗口更大的模型而不是一味地等一秒再重试——等再久只要 prompt 长度不变重试还是会撞墙。2.3 哪些状态码值得重试哪些必须停下给 Agent 的调用层做状态码分类是重试中间件的第一步。我的经验是用一个简单规则可以重试429、408、502、503、504。它们都表示“服务端暂时无法处理”大概率过一会儿就能恢复。不可重试400、401、403、404、405、413、415。这些是业务级错误或权限问题重试一百次结果一样不应该占用退避等待时间。需要单独判断409 Conflict、422 Unprocessable Entity。偶尔能通过“调整参数后重试”解决但绝不能盲目自动重试。当你把重试条件限定在“值得重试”的状态码上而不是“所有异常都重试”时Agent 系统的稳定性会大幅提升。这一点在模型调用之外同样适用工具 API、搜索 API、数据库 API 都要按这个原则做。3. 重试算法选型从“礼貌等待”到“聪明退避”3.1 固定延时为什么在 Agent 场景下不够用新手最容易写出来的重试逻辑长这样time.sleep(2) call_api_again()场景再复杂一点变成for i in range(3): try: resp call_api() break except HTTPError: time.sleep(2)这种写法的问题有两层第一固定等待时间很可能是错的。如果服务器告诉你Retry-After: 5你等到 2 秒就跑去问大概率又被拒回。如果服务器没有返回 Retry-After你又只能瞎猜。等待时间太短等于没等等待时间太长Agent 每次都干瞪眼看着时钟任务延迟成倍增加。第二多个并发请求会在同一时刻同时醒来。假设你有 5 个子 Agent 同时失败了大家都sleep(2)后重试那么第 2.001 秒你这边的请求量会瞬间恢复到峰值造成“冲击波”。服务器一查怎么 2 秒前刚限流现在又来一批同量的于是 429 继续返回然后你继续等 2 秒继续冲……这叫“重试风暴”retry storm是分布式系统里知名的事故模式之一。所以在 Agent 项目里固定延时重试不是“不够优雅”的问题而是真的会引起雪崩式故障。3.2 指数退避的公式与退避上限标准的替代方案是指数退避Exponential Backoff。公式很简单delay min(base_delay * 2^attempt, max_delay)base_delay是第一次重试前的等待基数attempt是从 0 开始的重试次数。举个例子重试次数base0.5 秒时的理论等待时间00.5 秒11 秒22 秒34 秒48 秒如果一直按这个指数增长第 7 次重试时就已经到了 64 秒显然不合理。所以必须设置一个max_delay上限让等待时间封顶比如 30 秒或者 60 秒。到了上限之后继续重试等待时间不再变长避免把整个 Agent 任务的延迟拉到不可接受的程度。这里有个容易被忽略的点重试次数和延迟上限必须跟 Agent 任务本身的超时预算放在一起考虑。我在跑多步骤 Agent 任务时给单次 LLM 调用的预算通常是 20~30 秒。如果重试 5 次每次的指数退避加起来已经超过 40 秒那这个任务大概率已经超时了。与其傻等不如在第二次或第三次重试失败后直接切换到备用方案。3.3 抖动防止重试风暴的关键指数退避解决了“等待时间合理”的问题但还没解决“多个客户端同时重试”的问题。解法是在退避时间上打一个随机扰动这就是所谓的 jitter。具体到实现常用三种抖动方案Full Jitter全抖动delay random(0, base * 2^attempt)在 0 到指数退避值之间随机取一个点。这种方式的平均等待时间是纯指数退避的一半左右适合求“尽快重试成功”的场景。Equal Jitter等量抖动delay random(base * 2^attempt / 2, base * 2^attempt)随机范围压缩到指数退避值的一半到全额之间。相比全抖动它更保守确保等待时间不会太低。Decorrelated Jitter去相关抖动以上一次的实际延迟为基础delay min(random(base, last_delay * 3), max_delay)思路是让相邻两次重试的时间间隔尽量不规则打破相关性。这个方案在 AWS 的经典博客里被大量推荐但它实现起来稍微复杂一点。我个人在 Agent 项目里默认用的是 Full Jitter因为它对整体延迟的拖累最小而且实现最直观。等到某个服务的 429 频率特别高、平均故障时间特别长时再切换到更保守的等量抖动或去相关抖动。再强调一次抖动的本质是为了“打散客户端之间的相位”而不是为了让等待时间更精确。没有抖动的退避在单请求场景下看不出问题在几十个请求并行失败的场景下就会立刻暴露。3.4 幂等性约束什么请求可以安全重试谈到重试绕不开一个问题这个请求被重试会造成重复的副作用吗Agent 的 tool call 范围很广搜索、读网页、写数据库、发邮件、下订单、调用支付接口。对于只读操作重试是绝对安全的对于写操作重试则必须谨慎。比如 Agent 调用了一个“创建订单”的接口第一次请求已经成功但响应在网络传输中丢失客户端不知道结果于是发起重试。如果服务端没有做幂等设计这个订单就会被创建两次。这种错误比 429 本身严重得多。给 Agent 开发者的建议对 POST/PUT/DELETE 这类会改变状态的请求在 header 里加一个Idempotency-Key或X-Request-ID服务端如果能识别幂等键重试就是安全的。如果上游服务不支持幂等键那么在 Agent 的业务闭环里做“结果确认”。比如先查询再创建或者把“是否已创建”记录在上下文中避免重复触发。对 LLM 推理调用本身重试一般是幂等的它不改变外部状态但要注意 token 成本。一次失败的 LLM 调用如果是在服务端已经完成生成、只是响应超时的情况下抛错重试会再烧一遍推理费用。这种重试虽然业务安全但钱包不一定会感谢你。4. 动手写一个贴合 Agent 项目的重试中间件4.1 设计目标透明、可观测、可降级写重试中间件之前先想几个目标透明调用方不需要在每个业务函数里塞 try/catch 和睡眠逻辑装饰器或包装器把重试行为集中起来。可观测每次重试发生了什么什么状态码等了多久最终是否成功都要落到日志里。Agent 任务的排查难度本来就高没有这些日志你再想定位“哪一步因为 429 重试了三回”几乎不可能。可降级中间件要能自由定义“重试到第几次就不等了直接走降级逻辑”比如切备用模型、丢给队列做异步任务、给用户返回一个临时提示。实现上我建议不要把重试逻辑写进 Agent 框架的每个模型调用函数里而是抽成一个独立的“API 调用层”让所有网络请求统一走它。4.2 Python 实现一个最小版重试装饰器先看一个不依赖第三方库的最小实现。这个版本只处理带requests的同步请求但思路可以平移到任何语言import random import time from functools import wraps class RetryableHTTPError(Exception): def __init__(self, response): self.response response super().__init__(fHTTP {response.status_code} - {response.text[:200]}) class RetryExhausted(Exception): pass def _extract_retry_after(response): raw response.headers.get(Retry-After, ) if raw.isdigit(): return float(raw) return None def retry_with_backoff( max_retries4, base_delay0.5, max_delay30.0, retry_status_codes(429, 408, 502, 503, 504), ): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries 1): try: return func(*args, **kwargs) except RetryableHTTPError as exc: if attempt max_retries: raise RetryExhausted() from exc server_delay _extract_retry_after(exc.response) if server_delay is not None: delay server_delay else: # 指数退避 全抖动 delay min(base_delay * (2 ** attempt), max_delay) delay random.uniform(0, delay) log_warning( fHTTP {exc.response.status_code}, fattempt{attempt}, delay{delay:.2f}s ) time.sleep(delay) raise RetryExhausted(max retries reached) return wrapper return decorator配合一个“把可重试状态码统一转成 RetryableHTTPError”的请求函数import requests def call_endpoint(method, url, **kwargs): resp requests.request(method, url, timeoutkwargs.pop(timeout, 30), **kwargs) if resp.status_code in (429, 408, 502, 503, 504): raise RetryableHTTPError(resp) resp.raise_for_status() return resp然后你的 Agent 里就可以直接写retry_with_backoff(max_retries5, base_delay0.5, max_delay30) def call_llm(prompt, modelgpt-4o): return call_endpoint( POST, https://api.example.com/v1/chat/completions, json{model: model, messages: prompt}, headers{Authorization: Bearer YOUR_KEY}, )这段代码的“骨架”价值大于“复制粘贴”价值。核心的几点优先尊重服务器的Retry-After。服务器都告诉你等多久了客户端就别自作聪明用本地公式算了。服务器没有给Retry-After时才用“指数退避 全抖动”兜底。重试次数到上限后抛出RetryExhausted让上层 Agent 编排逻辑决定是降级、切换模型还是直接终止任务。生产环境下_extract_retry_after还要处理 HTTP-date 格式。直接上email.utils.parsedate_to_datetime把日期字符串解析成时间戳再和本地时间相减得到秒数这不算复杂但一定要提前处理否则你会碰到“明明是 429 却完全忽略了 Retry-After”的隐蔽 bug。4.3 用 tenacity 优雅组合重试条件与退避策略实际生产项目里我一般直接上 tenacity。这个库把重试条件、停止条件、等待策略拆成了可插拔的组件代码读起来比手写循环清晰得多import logging import random import requests from tenacity import ( retry, stop_after_attempt, wait_random_exponential, before_sleep_log, retry_if_exception, ) logger logging.getLogger(__name__) def _is_retryable(exc): if isinstance(exc, requests.HTTPError): return exc.response is not None and exc.response.status_code in (429, 408, 502, 503, 504) return isinstance(exc, requests.ConnectionError) def _wait_retry(retry_state): exc retry_state.outcome.exception() server_delay None if isinstance(exc, requests.HTTPError) and exc.response is not None: raw exc.response.headers.get(Retry-After, ) if raw.isdigit(): server_delay float(raw) if server_delay is not None: return server_delay return random.uniform(0, min(0.5 * (2 ** retry_state.attempt_number), 30)) retry( reraiseTrue, stopstop_after_attempt(4), wait_wait_retry, retryretry_if_exception(_is_retryable), before_sleepbefore_sleep_log(logger, logging.WARNING), ) def call_llm(prompt, modelgpt-4o): resp requests.post( https://api.example.com/v1/chat/completions, json{model: model, messages: prompt}, timeout30, ) resp.raise_for_status() return resp.json()这里用_wait_retry自定义了等待时间有Retry-After就听服务器的没有就用指数退避加全抖动。before_sleep_log会在每次重试前打一条 warn 日志里面带有重试次数和原因排查 Agent 任务时特别有用。4.4 与 Agent 编排循环集成不只是包一层函数只给 LLM 调用包一层重试在 Agent 场景下并不够。因为 Agent 的每一次迭代不只是“调用一次 LLM”还包含“解析 tool_calls → 执行工具 → 更新上下文”。如果工具执行时遇到了 429而 LLM 已经完成了这一轮推理你重试工具调用之后还需要把新结果重新喂给 LLM让它继续。这个流程如果靠手工在循环里拼代码很快就会乱成一团。我的做法是在 Agent 的编排层封装一个execute_agent_step函数它代表“一轮完整的思考-行动-观察”而不是单个 API 调用。重试装饰器挂在这个层级上同时这个函数内部也要保证上下文的一致性retry_with_backoff(max_retries2, base_delay0.5) def execute_agent_step(state): # 1. 调用 LLM 生成动作 response call_llm(state[messages]) # 2. 解析 tool_calls actions parse_tool_calls(response) # 3. 执行工具调用工具 API 也可能触发 429单独处理 results [] for action in actions: result call_endpoint(action.method, action.url, jsonaction.body) results.append(result.json()) # 4. 把结果写回 state state[messages].append(format_tool_results(results)) return state这样设计的理由很简单Agent 的“一步”才是最小的重试单位。如果你只重试 LLM 调用而不重试整个步骤那么工具结果和上下文可能对不上。比如 LLM 生成了两个 tool_calls第一个成功、第二个超时你只重试第二个此时 LLM 在下一轮看到的上下文就是“残缺但有部分结果”的状态Agent 很可能会产生误导性的决策。与之配套我还会做一个AgentStepResult对象里面记录成功/失败、尝试次数、耗时、上下文长度。重试时如果需要回滚可以从上一个稳定的 checkpoint 重新开始。5. 从局部重试到全局限流治理给 Agent 装上“红绿灯”5.1 局部重试解决不了多 Agent 的并发问题说实话上面这些重试逻辑在单个 Agent 的单个任务里已经够用了。一旦你开始跑多 Agent 协作或者给多个用户提供并发的 Agent 服务局部重试的局限就出来了。场景是这样的你有一个 Agent 服务同时服务 5 个用户每个用户的任务又会拆成 3 个子 Agent。假设每个子 Agent 每秒产生 2 个请求那总请求速率就是每秒 30 次。如果上游 API 的限额是每分钟 60 次你早就爆表了。这时候每个请求的“个人退避”没有意义——因为即使你完美地退避了下一次请求还是会撞上同一个限额。这个问题的本质是你不光要在“单个请求失败后”做退避更要在“请求发出之前”做限速。后者叫做客户端流量整形client-side rate shaping。5.2 令牌桶请求队列的实现思路实现流量整形最常用也最直观的算法是令牌桶Token Bucket。思路很简单桶里有固定数量的令牌代表“当前还能发多少个请求”每发出一个请求就消耗一个令牌桶以固定的速率补充令牌比如每秒补 2 个桶满了之后多余的令牌丢弃当桶里的令牌不足时请求就得在队列里等待而不是立刻发出。在 Agent 项目里这个令牌桶可以放在一个全局的单例里所有请求在发出去之前先拿令牌import threading import time class TokenBucket: def __init__(self, capacity, refill_rate): self.capacity capacity self.tokens capacity self.refill_rate refill_rate self.last_refill time.monotonic() self.lock threading.Lock() def _refill(self): now time.monotonic() elapsed now - self.last_refill self.tokens min(self.capacity, self.tokens elapsed * self.refill_rate) self.last_refill now def acquire(self): while True: with self.lock: self._refill() if self.tokens 1: self.tokens - 1 return time.sleep(0.01)然后请求函数变成bucket TokenBucket(capacity10, refill_rate2) def call_endpoint_with_shape(method, url, **kwargs): bucket.acquire() # 先等令牌再发请求 # 后续与普通请求相同加上这层“红绿灯”之后再配合之前的退避重试Agent 整体对上游的冲击就被限制在了一个可控的节奏里。我自己项目里跑对比测试时同样的任务加了令牌桶之后 429 的出现频率直接降了一个数量级重试率从 35% 降到了 5% 以下。5.3 根据 429 反馈自适应调节请求速率有了令牌桶之后我们再进一步如果上游的限制是动态的呢很多 API 网关的限额不是死的。有的会根据你账号的历史调用情况、当前服务器负载实时调整有的 LLM 服务在高峰时段会临时降低单用户配额。这种情况下固定速率的令牌桶也会失准。我的自适应思路很简单把 429 的发生频率当作一个反馈信号动态调整令牌桶的补充速率。如果过去 1 分钟内的 429 比例高于 5%就把refill_rate下调 10%如果过去 5 分钟内 429 比例低于 1%就把refill_rate上调 5%每次调整都带一个小步长避免震荡。实现上只需要在令牌桶外面包一个“监控-调节”周期任务定期读取重试计数器的数据然后修改桶的 refill_rate。这个机制虽然朴素但在多服务商混合接入的场景下非常实用尤其是当你同时使用多个模型供应商每个供应商的限额规则都不一样时与其为每一家写死一套参数不如让客户端根据实际反馈自动适配。6. 实测模拟与踩坑记录6.1 搭建一个可控的 429 测试服务要验证重试逻辑第一步是有一个“你能控制它什么时候返回 429”的测试服务。我自己经常用 FastAPI 写一个几十行的 mockfrom fastapi import FastAPI, Request from fastapi.responses import JSONResponse app FastAPI() request_count {value: 0} app.get(/api/chat) async def chat(): request_count[value] 1 # 每 3 个请求里返回 1 次 429模拟限流 if request_count[value] % 3 0: return JSONResponse( status_code429, content{ error: { message: rate limited, type: requests, code: rate_limit_exceeded, } }, headers{Retry-After: 1}, ) return {message: ok, count: request_count[value]}更复杂一点可以加入随机延迟模拟服务器处理耗时和按比例触发的 429用来测试抖动是否真的能打散请求。测试的时候记得统计四个指标请求总量重试导致的总请求放大了多少倍成功率最终成功的任务占比平均延迟整个 Agent 任务因为重试增加了多少耗时重试占比有几次请求是第一遍就成功了有几次是重试之后才成功。这些指标决定了你的退避参数是否合理。如果成功率是上去了但每个任务平均多了 20 秒说明退避参数偏保守如果成功率上去了但重试占比超过 50%说明正常请求速率已经接近限额上限了你更应该去调令牌桶而不是优化退避算法。6.2 验证退避策略的关键指标用一个简单的日志格式来记录每次重试的上下文agent_step2, funccall_llm, attempt1, waited1.23s, status429, retry_after2 agent_step2, funccall_llm, attempt2, waited2.00s, status200, token_usage1200这种日志在排查“为什么 Agent 任务变慢”的时候简直是救命稻草。否则你只知道任务慢了却不知道是某个模型调用重试了 3 次还是某个工具调用在队列里等了 5 秒。我还建议在 Agent 的监控面板上把retry_count和429_rate单独画出来。这两个指标能直观反映你的限流治理是否有效。我有一个项目在没有全局令牌桶之前429 率是 25% 左右加上令牌桶和自适应调节之后降到 2% 以下。6.3 我在实际项目中踩过的五个坑最后把我在 Agent 开发过程里真正踩过的坑和对应解法一并写出来希望你能绕开。坑一把“配额耗尽”当成“速率限制”反复重试。有一次我的 Agent 半夜跑批处理日志里全是 429我以为只是请求太密集于是把退避时间拉长、重试次数加高。结果第二天一看是账户当天的配额已经用完了。这种 429 再等多久都没用正确做法是提前在请求层检查配额余量或者直接切换到备用账户、备用供应商。识别方式我前面也提到过看响应体里是否出现quota、limit reached、insufficient之类的词。坑二并行 Agent 同时重试把服务“打死”。我早期用多 Agent 协作框架每个子 Agent 各自带重试逻辑。某个上游 API 出现波动时多个子 Agent 一起重试请求量反而比平时还高导致限流时间被拉得更长。这是个典型的重试风暴。后来我把重试逻辑收敛到全局中间件并给所有请求加了令牌桶问题才真正解决。重试逻辑必须共享状态不能让每个子 Agent 在各自的真空里瞎猜。坑三忽略 token 成本重试导致费用翻倍。LLM API 和普通 HTTP API 不一样失败重试可能不是“零成本”。如果请求已经在服务端生成了大部分 token只是连接超时客户端通常无法知道。为了任务能继续你重试一次费用就再加一次。我之前跑一个长文档总结任务因为上游偶尔超时重试几次之后账单翻了一倍。现在我的策略是LLM 调用的重试次数严格控制在 2 次以内重试仍未成功就降级到更小的模型或直接向用户输出“当前模型繁忙”。坑四只重试 LLM 调用不重试整个 Agent 步骤。这个前面已经解释过如果 LLM 返回了 tool_calls执行工具失败后再重试 LLM模型看到的是残缺的上下文可能出现前后矛盾的动作。所以我的原则是工具调用失败时优先修正上下文后重试整个 step而不是在 step 内部做半吊子的局部重试。坑五日志里没有 attempt 信息出了事一脸茫然。最怕的不是 429而是 429 发生了但你不知道它在哪一步发生、重试了几次、最后怎么解决的。如果日志里只有一句“request failed with 429”排查效率极低。所以我在中间件里强制记录attempt、waited、status、function这些字段是 Agent 可观测性的最小集。文章写到这里技术上该讲的都讲得差不多了。最后说一点我自己的体会处理 Agent 的 429本质上是在“主动控制节奏”和“被动响应异常”之间找平衡。重试算法再优雅也只救得了“偶发”限流真正稳定的大规模 Agent 服务靠的是令牌桶控制整体速率、自适应调节应对动态配额、以及降级策略兜住极端情况。代码不在多核心是把“社交礼仪”做到位——该等的时候耐心等等的时候别都挤在同一个时间点冲上去该放弃的时候果断换条路走。如果你也在做 Agent 开发我建议你把重试逻辑和全局限流当成基础设施的一部分来设计而不是等出了事故再补。一个 Agent 模型的调用背后是你整个系统的稳定性把这个基本功打扎实后面叠加再多复杂能力也不容易在最容易大意的地方翻车。