我们给一个功能接入了大模型 API:用户提交内容,后端实时调用 LLM 做分析、返回结果。灰度时一切美好,响应又快又准。可一旦放量、真实流量涌进来,两件事同时炸了:一是接口开始大面积失败,日志里铺天盖地的 429 Too Many Requests——被大模型服务商限流了;二是月中财务找上门,说这个功能的 API 调用费用,几天就烧掉了一大笔预算,照这个势头月底要爆表。一边是大量请求失败,一边是花钱如流水,我被这"既贵又不稳"的双重暴击,逼着重新审视自己调用大模型的姿势。
复盘下来,问题的根子在于我用了一种最朴素、也最危险的姿势去调用大模型:来一个请求,就同步地、原原本本地调一次 LLM API,不加任何节制。这在低流量下没问题,可大模型 API 和普通的内部接口完全不同——它有严格的速率限制(RPM/TPM,每分钟请求数/token 数),而且每一次调用都按 token 实打实地收费,还很贵。我把它当成一个"随便调、调多少都行、不要钱"的普通接口,流量一高,自然既撞上了限流的墙(429),又烧穿了成本的底。
这就是几乎每个把大模型 API 投入生产的团队都会撞上的一课:大模型 API 是一种"昂贵、有限、且不稳定"的外部资源,调用它必须像对待一种稀缺资源那样,做好限流、重试、缓存、降级和成本控制,而不能像调用一个免费的内部接口那样肆无忌惮。这篇文章,就从这次"又贵又不稳"的事故出发,把生产级调用大模型 API 的工程实践,一次讲透。
先摆几个关于调用大模型的想当然
动手复盘前,先把我自己曾经深信、后来被账单和 429 教育的几个念头摆出来。
| 想当然的念头 | 残酷的真相 |
|---|---|
| "来一个请求就调一次 LLM, 天经地义" | 会瞬间撞上速率限制(429), 还烧穿成本 |
| "调用 API 嘛, 和调内部接口一样" | 它有 RPM/TPM 限流, 按 token 收费, 又贵又有限 |
| "被限流了重试一下就好" | 不带退避的盲目重试会火上浇油, 让限流更严重 |
| "每次都实时调, 保证结果最新" | 大量重复/相似请求白白浪费钱, 缓存能省一大笔 |
| "灰度没问题, 放量就稳了" | 低流量掩盖了限流和成本问题, 放量才暴露 |
这些念头的共同病根,是把大模型 API 当成了一个"和普通后端接口没区别"的东西,随调随用。可它本质上是一种受严格配额限制、且每次调用都有真金白银成本的稀缺外部资源。要看清这次事故,得先理解大模型 API 这两个和普通接口截然不同的特性。
第一件事:大模型 API 的两个"硬约束"——限流与计费
大模型 API 有两个普通内部接口没有的硬约束,理解它们是一切的前提。第一个是速率限制(Rate Limit):服务商会限制你每分钟能发多少请求(RPM)、每分钟能处理多少 token(TPM)。一旦超过,它就直接拒绝你,返回 429 Too Many Requests。这不是它"扛不住",而是它故意给你设的配额上限,保护它自己的服务、也对应你购买的套餐等级。第二个是按量计费:每次调用,按输入 token + 输出 token 的数量收费,而且不便宜。这意味着,调用次数和 token 用量,直接等于真金白银。
我那次事故,正是同时撞上了这两堵墙。来一个请求就同步调一次、不加任何控制,流量一高:请求频率瞬间超过 RPM 上限 → 被限流,大量 429;同时调用次数飙升、每次又是完整的 token 消耗 → 成本失控。下面这张图,把这个"双重暴击"画出来:
看懂这张图,事故的根就清楚了:大模型 API 的"限流"和"计费",决定了你不能、也不该把每一个用户请求都无脑地、实时地转化成一次 LLM 调用。你和大模型 API 之间,必须有一层"调度和保护",来控制调用的频率(应对限流)和数量(应对成本)。把大模型当成一种需要精打细算地使用的稀缺资源,而非随取随用的廉价接口——这是用好它的思维起点。接下来,我们就一层层把这层保护建起来。
第二件事:应对 429——指数退避重试 + 客户端限流
先解决"被限流大面积失败"。被动的一面,是遇到 429 时,要带"指数退避"地重试,而不是立刻重试。大模型服务商的 429 响应里,通常还会带一个 Retry-After 头,告诉你"过多久再来"。盲目立即重试是火上浇油——大家都在被限流的瞬间一起重试,只会让限流雪上加霜。正确做法是退避:第一次等 1 秒、第二次等 2 秒、第三次等 4 秒……再加点随机抖动,错开重试的时机。
import time, random
# 应对 429:指数退避 + 抖动重试, 别盲目立即重试
def call_llm_with_retry(prompt, max_retry=5):
for attempt in range(max_retry):
try:
return llm_client.complete(prompt)
except RateLimitError as e:
if attempt == max_retry - 1:
raise
# 优先听服务商的 Retry-After, 否则用指数退避
wait = e.retry_after or (2 ** attempt)
wait += random.uniform(0, 1) # 加抖动, 错开并发重试
time.sleep(wait)
raise RuntimeError("LLM 重试多次仍失败")
但退避重试只是被动补救。更主动的一面,是在客户端自己就做限流,根本不让请求频率超过服务商的配额。用一个令牌桶(rate limiter),把发往 LLM 的请求速率,主动控制在 RPM/TPM 上限之下——这样请求不是"撞了墙再退回来",而是"在墙内有序排队",从源头避免 429。
from threading import Semaphore
# 客户端主动限流:把并发/速率控制在配额之内, 从源头避免 429
# 简单版:用信号量限制同时在飞的请求数
llm_semaphore = Semaphore(10) # 最多 10 个并发调用
def call_llm_limited(prompt):
with llm_semaphore: # 拿不到名额就排队等, 不会瞬间打爆
return call_llm_with_retry(prompt)
# 更精细:用令牌桶按 RPM 控制速率(配合 token 数控 TPM)
# 让请求"匀速"地发出去, 而不是瞬间涌出一大批撞限流
"客户端限流(主动防)+ 退避重试(被动救)"是应对速率限制的标准组合:前者让你的请求速率始终待在配额内、平稳有序;后者在偶尔还是超了时,优雅地退避恢复,而非雪崩。关键认知:面对一个有配额的外部服务,与其等它拒绝你再补救,不如自己先把节奏控制好——主动适配它的限流,远比被动承受它的限流要从容。
第三件事:用缓存,把"重复的花费"省下来
再解决"成本失控"。最立竿见影的一招,是缓存。很多场景下,大量请求的输入是重复或高度相似的——同样的问题被不同用户反复问、同一段内容被反复分析。如果每次都实打实地调一次 LLM,等于为同一个答案反复付费。把"输入 → LLM 输出"的结果缓存起来,相同输入直接返回缓存,既省了钱、又省了时延、还顺便绕开了那部分请求的限流压力。
import hashlib
# 缓存 LLM 结果:相同(或语义相同)的输入, 直接返回缓存, 不重复付费
def call_llm_cached(prompt):
# 用输入的哈希作为缓存 key
key = "llm:" + hashlib.sha256(prompt.encode()).hexdigest()
cached = redis.get(key)
if cached is not None:
return cached # 命中缓存, 零成本、零延迟、不占配额
result = call_llm_limited(prompt)
redis.setex(key, 3600, result) # 缓存 1 小时(按业务时效性定)
return result
# 进阶:语义缓存(semantic cache)
# 对"意思相同但措辞不同"的问题, 用向量相似度匹配已有答案
# 比如"今天天气如何"和"今儿天气怎么样"可命中同一缓存
缓存的效果往往惊人——在很多真实场景里,大量请求其实集中在少数高频输入上,一个缓存就能把实际的 LLM 调用量砍掉一大半,成本和限流压力随之骤降。基础的是精确缓存(输入完全相同才命中);进阶的是语义缓存(用向量相似度,让"意思相同、措辞不同"的输入也能命中),能覆盖更多重复。对于昂贵的大模型调用,'能不调就不调'永远是省钱第一原则,而缓存就是实现'不调'最直接的手段。当然,缓存的时效性要按业务定——时效性强的内容缓存短一点,稳定的内容可以缓存久一点。
第四件事:用异步队列削峰,别让流量直冲 LLM
对于那些"不需要用户实时等结果"的场景(比如内容审核、批量分析、生成摘要),还有一招能从根本上抹平限流压力:异步队列削峰。不要让用户请求直接、同步地触发 LLM 调用,而是把请求先丢进一个消息队列,后端再用一组"消费者"以受控的、平稳的速率从队列里取出来、调用 LLM。这样,无论前端流量怎么忽高忽低,发往 LLM 的速率始终是平稳、可控的——突发的洪峰被队列这个"蓄水池"给削平了。
# 异步削峰:请求先入队, 消费者以受控速率匀速调 LLM
# 生产者:用户请求来了, 不直接调 LLM, 而是入队, 立刻返回"处理中"
def submit_task(user_input):
task_id = gen_id()
queue.push({"id": task_id, "input": user_input}) # 入队, 秒回
return task_id # 前端凭 task_id 稍后来取结果
# 消费者:以匹配 LLM 配额的速率, 平稳地从队列取任务处理
def consumer_loop():
while True:
task = queue.pop()
result = call_llm_cached(task["input"]) # 受控速率 + 缓存
save_result(task["id"], result)
# 消费者数量 × 单个处理速率, 控制在 LLM 的 RPM/TPM 之内
# 洪峰来时, 任务在队列里排队, 而非瞬间打爆 LLM —— 用"延迟"换"稳定"
异步削峰的精髓,是用"可接受的延迟"换取"调用速率的平稳可控"。它特别适合那些对实时性要求不高的 LLM 任务——用户提交后,过一会儿再看结果,完全可以接受。通过控制消费者的数量和速率,你能把对 LLM 的调用牢牢压在配额之内,无论前端来多大的流量洪峰。"实时同步调用"和"异步队列调用"是两种范式;凡是业务允许的地方,异步削峰都是应对 LLM 限流最优雅、最稳健的架构。
第五件事:从源头降本——选对模型、控住 token
限流和缓存之外,还要直接向"成本"开刀,核心是两件事。其一,给任务选对模型,别盲目用最贵最强的。大模型有不同的档位,能力越强越贵。很多任务(分类、抽取、简单问答)用更小、更便宜、更快的模型就完全够用,没必要动辄上旗舰模型。其二,精打细算地控制 token 用量——输入和输出的 token 都是钱。
# 降本之一:按任务难度选模型, 简单任务用小模型
def choose_model(task_type):
if task_type in ("classify", "extract", "simple_qa"):
return "small-cheap-model" # 简单任务, 小模型够用且便宜十几倍
return "large-powerful-model" # 只有复杂任务才上旗舰模型
# 降本之二:精简 prompt, 控制输出长度, 都是省 token
resp = llm_client.complete(
prompt=trim_prompt(prompt), # 精简输入: 去掉冗余上下文/示例
max_tokens=200, # 限制输出长度, 别让它长篇大论
# 还可以:压缩历史(摘要)、用更短的指令、批量合并请求
)
# token 用量 = 成本, 每一处精简, 都在直接省钱
这两件事的累积效果非常可观:把一批本可以用小模型的任务从旗舰模型上迁下来,成本可能直接降一个数量级;把过长的 prompt 精简、把输出长度限制住,又能再省一截。用大模型,要有一种"成本意识":时刻想着这次调用用的是不是恰到好处的模型、花的是不是必要的 token。这和之前聊 Agent、聊 RAG 时反复出现的"能用确定性手段就别用模型、能用小的就别用大的"是一脉相承的——把昂贵的算力,精准地用在真正需要它的地方。
第六件事:降级兜底 + 成本监控告警
最后两道防线。其一,降级兜底:当 LLM 实在不可用(持续限流、服务故障、或你主动为了控成本而熔断)时,要有一个不依赖 LLM 的"退路",而不是让功能直接崩。比如返回一个规则引擎的结果、一个缓存的旧答案、或者一句友好的"AI 服务繁忙,请稍后再试"。其二,成本监控:把 token 消耗、调用次数、花费实时监控起来,设预算告警,别等月底账单才发现超支。
# 降级:LLM 不可用时, 走不依赖它的退路, 别让功能整个崩
def analyze_with_fallback(text):
try:
return call_llm_cached(text)
except (RateLimitError, ServiceError):
return rule_based_analyze(text) # 降级到规则引擎/缓存/默认结果
# 成本监控:实时统计 token 和花费, 接近预算就告警/熔断
def track_cost(input_tokens, output_tokens, model):
cost = input_tokens * PRICE[model]["in"] + output_tokens * PRICE[model]["out"]
daily_cost = cost_counter.incr(today(), cost)
if daily_cost > DAILY_BUDGET * 0.8:
send_alert(f"今日 LLM 花费已达预算 80%: {daily_cost}")
if daily_cost > DAILY_BUDGET:
enable_degrade_mode() # 超预算自动降级, 把成本焊死在上限
# 让"成本"像"磁盘""连接数"一样, 成为一个被监控、有上限的可控资源
这两道防线的意义是:降级保证了"AI 出问题时,功能不至于彻底崩"(可用性兜底);成本监控保证了"花费永远在掌控之中,不会失控暴涨"(成本兜底)。尤其成本监控配上"超预算自动降级",等于给账单装了个"硬上限"的保险丝——再怎么也烧不穿。到这儿,生产级调用大模型的方方面面就齐了。我把它收成一张决策图:
把这套体系建起来,大模型 API 就从"又贵又不稳的隐患"变成"成本可控、稳定可靠的能力"。最后,拧成几条可直接照做的铁律:
- 别把每个用户请求都无脑实时转成一次 LLM 调用,它有配额、要收费, 是稀缺资源。
- 客户端主动限流 + 429 指数退避重试,把速率压在配额内, 偶尔超了优雅恢复。
- 用缓存(精确/语义)把重复调用省掉,"能不调就不调"是降本第一原则。
- 不要实时的场景用异步队列削峰,用可接受的延迟换调用速率的平稳可控。
- 按任务难度选模型、精简 prompt 控输出,从源头省 token、省钱。
- 一定要有不依赖 LLM 的降级兜底,AI 挂了功能也别整个崩。
- 把 token 和花费纳入监控,设预算告警 + 超预算自动降级, 给成本焊死上限。
一张大模型 API 生产化速查表
把生产调用大模型的各项实践汇成一张表,接入 LLM 时对照着配。
| 问题 | 手段 | 作用 |
|---|---|---|
| 大面积 429 限流 | 客户端限流 + 退避重试 | 速率压在配额内, 优雅恢复 |
| 重复调用浪费钱 | 精确/语义缓存 | 相同输入零成本复用 |
| 流量洪峰冲垮配额 | 异步队列削峰 | 用延迟换平稳可控的速率 |
| 成本太高 | 选小模型 + 控 token | 从源头省钱, 可降一个数量级 |
| LLM 不可用功能崩 | 降级到规则/缓存兜底 | 保住基本可用性 |
| 账单失控 | 成本监控 + 超预算熔断 | 给花费焊死硬上限 |
| 不知道钱花哪了 | 按功能/用户统计 token | 定位成本大头, 精准优化 |
退一步:把大模型当成一个"昂贵的外部依赖"来治理
修好这些问题后,我对"如何用大模型"的认知也升级了。我意识到,之前我一直把 LLM API 当成一个"神奇的智能黑盒",满脑子都是它能力多强、效果多好;可真正把它投入生产、扛住真实流量后,我才看清它另一重身份——它本质上就是一个昂贵的、有配额的、会失败的外部依赖,和我们调用的支付网关、短信服务、第三方 API 并无二致。而对待任何一个外部依赖,我们早就有成熟的工程范式:限流、重试、熔断、降级、缓存、监控。这些范式,原封不动地适用于大模型 API。
这个认知转变很重要:它让我从"被 AI 的光环晃了眼、只盯着效果"的兴奋里冷静下来,回到了扎实的工程地面上。把大模型接入生产,真正的挑战往往不在'让它效果好'(那是模型和提示词的事),而在'让它在真实流量、成本约束、故障场景下,依然稳定可控'——而后者,靠的全是这些朴实无华的工程治理手段。这也再次印证了这个系列关于 AI 应用反复强调的那个信念:模型是光鲜的引擎,但决定一个 AI 产品能否真正落地、能否长久运行的,是围绕它构建的那一整套'不性感'却至关重要的工程。
写在最后
这次"又贵又不稳"的事故,给我最深的体会,是它让我对"成本"这个维度有了全新的敬畏。在过去做传统后端时,我几乎从不需要在写代码时考虑"这次调用要花多少钱"——内部接口的调用,边际成本近乎为零。可大模型时代,情况彻底变了:每一次 API 调用,都是一笔实实在在的开销;'调用次数'和'token 用量',第一次成了需要像 CPU、内存一样被精打细算地管理的一等资源。"能不能不调""能不能用更小的模型调""能不能缓存起来不重复调"——这些过去想都不会想的问题,如今成了 AI 应用工程里的核心考量。这是一种思维方式的根本转变:从"性能优先"到"性能与成本并重"。
而这件事更深的启示在于:一项技术从"令人惊艳的演示",到"可持续运行的产品",中间隔着的,正是这些关于成本、稳定性、可控性的工程治理。Demo 里,你只需调一次 API、看一个漂亮的结果;可生产中,你要面对成千上万次调用带来的限流、成本、故障,要让它在预算之内、在配额之内、在故障发生时,依然稳稳地服务于每一个用户。这中间的鸿沟,不是靠更强的模型去填的,而是靠限流、缓存、削峰、降级、监控这些一砖一瓦的工程去填的。这次事故于我,是一堂关于"AI 工程化"的扎实启蒙——它教会我,拥抱大模型澎湃能力的同时,更要带着工程师的冷静与克制,为它的每一次调用算好账、为它的每一种故障留好后路。唯有如此,我们才能让 AI 这股强大却昂贵的力量,真正可持续地、稳健地,落到产品和用户的实处。愿你我都能在这场 AI 浪潮里,既做仰望星空、惊叹于模型之强的探索者,也做脚踏实地、精算每一分成本与每一次稳定的工程师。
如果你也在把大模型往生产上接,不妨在上线前先过一遍这份清单:有没有客户端限流和 429 退避?有没有给高频输入加缓存?不要求实时的部分能不能走异步队列?简单任务是不是用了更便宜的小模型、prompt 和输出有没有精简?LLM 挂掉时有没有降级退路?token 花费有没有接入监控和预算告警?这六个问题,每一个对应着一类我曾经踩过的坑;把它们一一答好,你的 AI 功能才能既扛得住真实流量,又花得起这笔账。大模型的能力让人心潮澎湃,但让这份能力真正可持续地服务用户的,永远是这些冷静、扎实的工程功课。愿你在惊叹模型之强的同时,也别忘了为它系好成本与稳定的安全带。
—— 别看了 · 2026