第一次在线上环境撞见Selected model is at capacity. Please try a different model.这个报错时,我的第一反应是有点懵。整条错误文案读起来像客服话术,但它确确实实出现在生产日志里,而且频率高到一度把我们的接口成功率打到个位数。那时我们正给一个内部知识库工具接大模型接口,白天业务高峰一上来,告警群几乎被这条报错刷屏。我第一反应是"是不是我们并发写太高,被限流了",结果查了一圈状态码、配额、余额,发现根本不是限流,是模型服务端真的"接待不过来"。
这个报错在调用各类大模型API时相当常见,尤其当某个新模型刚发布、某个免费额度渠道涌进大量请求,或者你用的第三方聚合平台把上游配额共享给太多人时,它就会频繁出现。本文就从我这个踩坑者的角度,把这条报错的成因、定位方法和能落地的处置方案一次性说清楚,包括代码层面的重试策略、模型降级、网关路由,以及架构层面的缓存和队列设计。
1. 拆解 "at capacity":这行报错到底在说什么
很多人第一次看到这个报错,下意识会去检查自己的代码,以为是请求参数写得不对,或者API Key配错了。其实这行报错的指向非常明确:模型服务端已经达到它的容量上限,暂时无法再接受更多的推理请求。它不是说你本地出了问题,而是你请求的那台"模型算力池"满了。
1.1 字面意思与隐藏信息
逐个词拆解:"Selected model is at capacity",选中的模型处于容量满载状态;"Please try a different model",建议你换个模型试试。这里有两个关键信息:
- 报错提到了
model,但问题不一定和具体某个模型本身有关,而是和这个模型背后的服务资源池有关。同一家服务商的同一个模型,通常部署在一组专门的GPU实例上,这些实例的计算资源是有限的。当并发请求量超过了这批实例能承载的最大吞吐量,新进来的请求就会被拒绝,并返回类似文案。 Please try a different model是服务商给用户的第一条"自救建议"。因为不同模型的资源池是独立的,某个旗舰模型可能挤满了人,但旁边的轻量级模型还很空闲。换一个模型,相当于绕开拥堵路段。
我还观察到,这类报错往往不是持续性的,而是间歇性、跟随负载波动的状态。上午10点调用同一批模型完全正常,下午2点业务高峰一来,同样的请求就开始被拒;过了高峰期又自动恢复。这就是典型的容量资源动态变化。
1.2 大模型服务为什么会"满载"
要从根源上理解,就得知道大模型 API 背后的基础设施逻辑。服务商维护的是一个大型 GPU 集群,这个集群被切成多个资源池,分别服务不同的模型版本。每次你调用 API,请求会先经过接入网关,再被路由到对应模型的推理实例上。
每个推理实例有几个硬性瓶颈:
- 显存与算力上限:一块GPU的显存决定了模型能占多大空间,算力决定了每秒能跑多少token。在硬件固定的情况下,吞吐量有天花板。
- 并发连接数限制:一个推理实例可能同时只能处理几十个或几百个请求,超出后只能排队或直接拒绝。
- 动态扩容延迟:理论上服务商可以靠自动扩容扛住流量,但大模型的部署不是普通Web服务能秒级扩容的。加载一个几十GB甚至上百GB的模型权重,再预热好推理状态,可能需要几分钟到几小时。流量尖峰来了,扩容的速度跟不上。
所以"at capacity"本质上是算力调度场景下的正常现象,并不代表你的账号有问题,也不代表服务商"挂了"。它更像是热门餐厅高峰期没空位,你需要在门口等位,或者换一家有座位的餐厅。
1.3 最容易踩中这个报错的实际场景
结合我和身边朋友的经历,下面几类场景几乎必踩这个报错:
- 新模型刚发布时:每次头部厂商发新旗舰模型,都会出现一波"尝鲜潮"。新模型的资源池往往初期没铺开,大量用户涌入后瞬间满载。这时候报错最密集。
- 免费或低价额度渠道:一些聚合平台、免费API渠道,本质是共享上游某个账号的配额。用的人一多,上游容量很快被打爆。这类渠道在"at capacity"之外,还常伴随机限流和超时。
- 业务流量有明显的波峰波谷:比如你的应用是to B办公工具,早高峰和历史高峰的访问量是平时的好几倍,峰值时段很难避开容量问题。
- 长时间运行的任务重试扎堆:你自己或者合作方写了不少定时任务,某个固定时间点同时触发大量调用,把原本还算宽裕的容量瞬间打满。
在这些场景里,遇到报错的第一反应不应该是"我这边的参数做错了",而应该按下一节的思路去做完整排查。
2. 完整排查链路:问题到底出在哪一层
接到报错后,我建议你先别急着改代码,按照从"本地"到"远端"的顺序做一轮系统检查。我排查这类问题的顺序基本固定:先确认是不是自己调用姿势问题,再检查账号配额,最后才认定是服务端容量问题。这套顺序能省下大量瞎猜的时间。
2.1 直接用 curl 复现请求,看原始响应
第一步,绕过所有代码框架和SDK封装,用最原始的 HTTP 请求做一次复现。比如:
curl -i https://api.example.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $API_KEY" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 50 }'用-i参数能把响应头和响应体都打印出来。重点看三件事:
- HTTP状态码:
429 Too Many Requests和503 Service Unavailable是最常见的容量类状态码。429在语义上更偏向"限流",503更偏向"临时过载",但两者在容量满载时都可能出现。如果返回200但业务字段里报错,那就不是传输层问题。 - 响应体里的错误类型:很多服务商会在JSON里带
error.code或error.type,排查时要把这个值记下来,它比一句话文案信息量大得多。 - 响应头是否有
Retry-After:这个头字段会明确告诉你建议等多少秒再重试。例如Retry-After: 30,意味着30秒后再试比较稳妥。
如果 curl 也复现了同样的报错,那问题基本不在你的应用代码层,继续往下查。
2.2 区分容量、限流、配额这三类错误
大模型的错误类型看着多,归根结底要分清三件事:容量不够、请求太频繁、额度用完。我习惯用一张表把这三类错误的核心差异列出来,排查时对号入座:
| 常见错误特征 | 典型状态码 | 本质原因 | 能不能靠"等一下"解决 |
|---|---|---|---|
at capacity/overloaded | 503, 429 | 模型资源池过载 | 能,但时间不确定,高峰过后恢复 |
rate limit exceeded | 429 | 你的请求速度超过账号限制 | 能,按限速窗口等待即可 |
insufficient quota/billing | 400/402 | 余额不足或配额用完 | 不能,需要充值或等待配额重置 |
常见误区是:一看到429就把锅甩给限流,其实是容量问题。虽然状态码相同,处理策略却不一样。限流通常按固定窗口(比如每分钟200次)恢复,等几十秒基本能好;容量过载则取决于高峰期持续多久,可能是几分钟,也可能是几小时。
如果你用的是第三方聚合平台,还要额外留意:平台可能在上游容量本来就紧张的情况下再叠加一层自己的限流策略,这时候报错信息里可能同时出现at capacity和rate limit的字样,别只盯着其中一个。
2.3 检查本机的并发与连接管理
排查完远端,回头再看自己这层。很多莫名其妙报错其实是客户端自己"制造"出来的:
- 你是不是开了上百个并发协程/线程同时请求同一个模型,且没有任何排队机制?
- 你是不是在循环里密集调用,比如批量处理几千条数据,没用并发限制和暂停?
- 你的 HTTP 客户端有没有正确设置超时时间?如果超时设置太短,请求被服务端正常接收但因排队而处理稍慢,客户端提前断开,也会造成"错误率飙升"的假象。
如果你在日志里发现报错集中在某个批量任务的启动瞬间,基本都是并发峰值过大导致的。这个可以从客户端做并发控制解决。我在第四节会专门讲请求队列的做法。
3. 立刻能落地的对策:从重试到切换模型
确认是真实的容量问题之后,下一件事就是设计应对策略。这里我给出一套从"最省事"到"最工程化"的方案,你可以按自己的场景组合使用。
3.1 指数退避重试:写代码时最该先做对的一件事
几乎所有主流SDK都会建议对临时性错误做重试,但很多人写重试只是简单地"睡1秒再来一次"。这在大模型接口场景下是有问题的:如果服务端确实处于容量满载,并且大量客户端一起重试,不仅自己恢复不了,还会把服务端压得更垮,拉长整体故障时间。
正确的做法是指数退避 + 随机抖动。比如第一次失败等1秒,第二次等2秒,第三次等4秒,叠加固定比例的随机值,打散重试请求在时间轴上的分布:
import time import random def retry_with_backoff(func, max_retries=5): base_delay = 1.0 # 初始等待秒数 max_delay = 60.0 # 上限60秒,避免无限等待 for attempt in range(max_retries): try: return func() except CapacityError as e: if attempt == max_retries - 1: raise e # 指数退避:2^attempt * base_delay,再加随机抖动 delay = min(max_delay, base_delay * (2 ** attempt)) delay = delay + random.uniform(0, delay * 0.3) time.sleep(delay) return None这段代码里的关键不在循环次数,而在random.uniform(0, delay * 0.3)这个抖动。如果所有客户端都严格按1秒、2秒、4秒的重试节奏走,它们大概率还会撞在同一个时间点重试,等于把一次流量尖峰复制了好几份。加抖动之后,重试请求会相对分散,成功率反而更高。
还可以根据服务端返回的Retry-After做一次适配。如果服务端明确告诉你等30秒,那就没必要傻傻按自己的指数去试,直接按它的建议来。
3.2 模型降级链:从旗舰模型切到轻量模型
换模型是报错文案里直接给出的建议,实际操作中也是性价比最高的一招。关键是我们要把"换模型"从手动操作变成自动策略。
我习惯给每个业务场景配一条模型降级链。以聊天场景为例,可以这样设计:
| 优先级 | 模型 | 适用情况 |
|---|---|---|
| 1 | 当前主力模型(如最强旗舰模型) | 正常时期,追求上限效果 |
| 2 | 同级备选模型 | 主力模型容量满时切换,效果接近 |
| 3 | 轻量级模型 | 前两个都不可用时兜底,速度优先 |
| 4 | 本地规则/模板答案 | 全部不可用时保证业务不中断 |
具体实现上,建议把这块逻辑封装成一个简单的路由函数,而不是在业务代码里到处散落着if/else:
MODEL_CHAIN = ["claude-sonnet-4-5", "claude-haiku", "gpt-4o-mini"] def get_chat_response(user_message): for model in MODEL_CHAIN: try: return call_model(model, user_message) except CapacityError: logger.warning(f"model {model} at capacity, switch to next") continue return fallback_rule_response(user_message)这里有个容易忽略的点:降级链的顺序不只是按"能力上限",也要按成本排序。旗舰模型效果最好也最贵,轻量模型效果稍弱但便宜。容量紧张时自动切到便宜模型,其实还能顺手控制成本,不至于在高峰期被费用账单吓一跳。
如果你们的业务对效果极其敏感,不想牺牲模型能力,那就在资源层面多准备几个服务商的同级别模型作为冗余。市面上有做模型网关的开源项目(比如One API风格的代理层),支持配置多个上游渠道,按优先级或权重自动切换,挂了一个自动换另一个。这种方案的好处是业务代码基本不用改,把降级逻辑收敛在网关层。
3.3 网关层的重路由与负载均衡
如果你的系统里不止一个服务在调用大模型,那么在每个服务里各自实现重试和降级是不现实的,很容易出现资源竞争和配置不一致。更干净的做法是在网关层做统一处理。
网关层通常做三件事:
- 多Key轮询:把多个API Key配置成池子,请求时自动轮询或随机选择。某个Key触发了账号级限流,可以自动换下一个Key继续,规避单个账号的QPS限制。
- 多服务商路由:把不同厂商(或同一厂商不同区域的端点)配置成权重路由。正常情况下流量按比例分发;当某个服务商返回
at capacity时,网关把请求路由到另一个服务商。 - 统一超时熔断:当某个上游连续N次报容量错误,网关自动把它熔断一段时间,不再往里发新请求。熔断时间过后再放少量探测流量。
网关层的实现需要结合你自己公司的技术栈。用开源网关改配置,还是自己写一层代理,取决于团队维护能力。但哪怕只是简单做一个多Key轮询的代理,也能明显降低容量问题的影响面。
4. 把容量问题纳入架构设计:请求编排与语义缓存
重试、降级属于"遇到问题解决问题"。如果这个报错在你的场景里出现得实在太频繁,那就要往源头想想:是不是你的调用模式本身不够友好?这一节讨论的是更偏架构层面的预防性措施。
4.1 语义缓存:挡住高频重复流量
大模型应用的调用里,有相当一部分是"重复问题"。特别是企业内部知识库、客服FAQ、工单分类这类场景,用户翻来覆去问的问题就那么几个。如果每次用户提问都真实调用一次大模型,把钱和时间花在完全相同的请求上,既浪费又容易撞上容量限制。
合理的做法是给大模型调用加一层语义缓存。大致流程是:
- 用户输入问题。
- 先把问题做文本向量化(Embedding)。
- 去缓存库(如向量数据库或支持向量检索的Redis)里找相似度超过阈值的历史问题。
- 如果找到了,直接返回当时的答案,不再调用大模型。
- 如果没有找到,才调用大模型,并把问题和答案写入缓存。
这套缓存尤其适合"答案相对固定"的场景。我做过一个内部业务,上线语义缓存后,高峰期大模型调用量直接降了四成左右,容量报错自然少了。
但要注意,缓存不是万能的。如果业务需要实时性很高的回答(比如分析当天的数据报表),或者每个用户的问题都高度个性化,缓存命中率会很低,反而增加一层管理复杂度。是否需要设计缓存,评估标准很简单:同一个问题在一个周期内(比如一天/一周),会不会被大量不同用户反复问?如果会,就可以上。
4.2 并发控制与请求队列:主动削峰填谷
另一个从源头减少容量压力的策略是做客户端侧的"流量整形"——在流量到达上游之前,把突刺削平。
最简单的做法是用信号量/并发池限制调用数:
import asyncio from asyncio import Semaphore semaphore = Semaphore(10) # 最多允许10个并发调用 async def call_with_limit(prompt): async with semaphore: return await call_model(prompt)这个做法适合单机部署、并发有限的场景。如果你有大批量离线任务要跑,比如给历史工单做批量分类,那就别一股脑并发打出去,而是放进任务队列里,按固定速率消费。比如每秒最多发5个请求,让上游负载始终稳定在安全水位。
除了限速,还可以考虑服务商提供的批量接口。有一部分服务商提供batch模式,允许你提交一个JSONL文件,服务端在资源空闲时统一处理。这种模式天然避开了容量高峰,而且单价通常更便宜。缺点是响应慢,适合对实时性不敏感的后处理任务。
判断自己是该做并发控制还是该上批量任务,核心看业务对延迟的要求。用户在线等待的交互场景只能用并发控制;离线批处理老老实实走排队最划算。
4.3 业务降级:在不可用时保住核心体验
就算做了降级链和容量规划,极端情况下所有模型都不可用也是可能的。这时候如果业务逻辑强依赖大模型返回,整个系统就会跟着瘫痪。不如在设计之初就想好:大模型挂了,我们还有什么可用的兜底方案?
举一个实际例子。我们做过的客服工单助手,功能包括"语义检索工单记录"和"AI生成摘要"两部分。容量高峰时,AI生成摘要可以暂时跳过,只返回检索到的原始工单列表,并在前端提示"摘要服务暂不可用"。用户看到的信息变少一点,但核心的"找到历史工单"这件事没断。
这类降级设计的关键在拆分功能优先级。每个需要大模型支持的功能都问一句:它是不是核心闭环里不可跳过的一环?如果不是,就准备好一个"关闭它"的开关。开关可以是配置中心里的一个布尔值,也可以是前面代码里的 fallback 分支。
这里额外提醒一点:业务降级的开关要能被运维在紧急时刻快速触发。不要把它埋在代码里要靠发版才能生效。用配置中心、环境变量、甚至数据库里的一个标志位都行,关键是出问题时运维能一秒按下去。
5. 我在生产实战里踩过的坑和一些经验看法
这一节写点实际的、常规文档里不会告诉你的东西。都是真金白银换来的教训。
5.1 确定性重试会雪崩,分布式环境尤甚
我第一次处理这个报错时,直接在服务里调用了SDK自带的重试参数,把重试次数设为3次。结果业务高峰时,原本只打给服务商一份请求,因为每个客户端都重试3次,实际进入上游的流量变成了好几倍。服务商被流量冲得更满,报错更多,然后更多重试……最后故障时间翻倍。
这个教训让我明白:任何重试策略都必须考虑"所有客户端同时重试"的放大效应。分布式系统里遇到负载类错误,正确做法是后退得更激进一点——少做重试、加抖动、留出错峰空间。宁可让部分请求快速失败,也不能让所有请求一起缠着上游不放。
5.2 光换模型不够,还要同步处理超时与熔断
有一段时间我们只做了模型切换,没改HTTP客户端的超时时间。结果上游容量不足时,请求不是立即返回at capacity,而是先进队列排队,迟迟不给响应。我们设的等待时间比较久,这些请求就一个个在连接池里占着位置,直到堆积到本地也撑不住,接口整体卡死。
后来我们把超时时间大幅缩短了:只给上游留出比模型正常推理稍多的余量。一旦请求超过阈值还没返回,就直接判定失败走降级逻辑。宁可快速失败,也不要慢速悬挂,这在大模型调用里是特别重要的一条准则。
熔断也是一样。当一个模型持续在报容量错误,得有一个"跳过它"的机制。我见过有人在降级链里把失败的模型排在最后,结果每次都先撞一次失败的才轮到下一个能用的,白白增加延迟。正确思路是让网关记住"谁最近在报错",自动把这个模型冷却一段时间。
5.3 在真实的生产环境里,"at capacity"其实是一种常态
运营一段时间后我的体会是:容量报错不会消失,只会悄悄转移。一个新模型发布,它来了;业务大促,它来了;某个热点事件让用户量突然暴涨,它也来了。与其幻想"彻底解决",不如把它当成一个会周期出现的常态信号来设计系统。
我现在设计任何一个大模型相关服务,都会在架构评审时默认加一笔"容量不可用时的表现"。这一步想清楚了,后面上线才不会被告警追着跑。每次容量报错出现,我都会顺手把它当一次演练:重试有没有生效?降级链是否按预期切换?监控告警是否真的齐全?这些在风平浪静时想不起来的东西,恰好是这类报错能带给我们的最大价值。