← 返回资讯

API 超支怎么防:从预警到熔断的三层防线怎么设

2026-08-07

我见过的 API 超支,几乎没有一次是「慢慢涨上去的」。真实的形状是阶跃:某天下午上线了一个版本,账单曲线从那一刻起抬高一个台阶,然后稳稳地保持在新高度,直到有人发现。原因通常特别朴素——有人把提示词里的一段例子从两条扩到十条;有个重试逻辑在特定错误码下没有退出条件,转起来了;某个对外接口没做鉴权,被人拿去当免费代理刷。

这类问题的共同点是:**它们在发生的那一刻就已经定型了,剩下的只是时间在计费。**所以防超支这件事,真正决定损失大小的不是你的预算估得多准,而是你多久能发现。一个估得很粗但两小时内就报警的体系,比一个估得很精却要等月底对账的体系强一个数量级。

下面讲的是一套分级防线:硬上限兜底、速率告警提前、结构性告警再提前一层。三层不是替代关系,缺哪层都会在某个场景漏掉。

第一层:硬上限,最后那根保险丝

硬上限的作用不是「控制成本」,是限制单次事故的最大损失。它一定会以业务中断的方式生效,所以设计它的时候脑子里要装的问题是「万一炸了,我最多能接受赔多少」,而不是「我这个月大概要花多少」。

以 LiteLLM Proxy 为例,虚拟 key 上有三个直接相关的字段(字段名逐字来自官方文档,具体行为与版本差异以官方文档为准):

字段类型拦的是什么
max_budget浮点这把 key 的累计花费上限,单位 USD。触到就是花完了
rpm_limit整数每分钟请求数上限,拦的是「调用次数暴涨」
tpm_limit整数每分钟 token 数上限,拦的是「次数没涨但每次变得巨大」

这三个要一起设,因为它们拦的失败模式不同。死循环会先撞 rpm_limit;提示词膨胀或者有人往上下文里塞了整本文档,请求数可能一点没变,但 tpm_limit 会先响。只设其中一个,另一类事故就是裸奔。

max_budget 则是终点线。它的问题在于:等它触发,钱已经花完了——它保护的是「不再继续花」,不是「没花」。所以它的数值该按「正常业务绝不可能达到」的水平来设,比如预期用量的两到三倍这种量级(具体倍数看你的业务波动有多大,促销季和平峰期显然不是一个数)。设得太贴近预期用量,结果就是业务高峰期被自己的保险丝掐断,运维半夜起来调额度,几次之后大家就开始把额度设得很松,防线名存实亡。

配额还有个容易忽略的维度是有效期。生成 key 的时候可以带 duration(官方文档里出现的示例值形如 "30min""30d"),给临时用途的 key 一个到期时间。很多超支的源头是「三个月前给外包同学开的一把测试 key,项目结束了没人收回」。有到期时间的话,这类僵尸 key 会自己死掉。

创建 key 时把这些字段一次配齐的调用形式,可以参考 LiteLLM 虚拟 key 那篇里的完整参数说明。这里要强调的只有一句:不要用 master key 直接跑业务。master key 是用来签发别的 key 的,它自己没有额度概念,一旦泄漏或者被某段代码直接引用,上面这些防线全部作废。

第二层:速率告警,盯的是斜率不是总数

绝大多数团队的预算告警长这样:花到预算的 80% 报警。这个设计的问题是,80% 这个信号来得太晚,而且它到达的时间取决于事故发生在月初还是月末。

同样一个把消耗翻三倍的 bug:月初第三天上线,你会在第十天左右收到 80% 告警,还有机会止损;月末第二十五天上线,等 80% 响的时候你可能已经超了。同一个 bug,同一个阈值,损失差好几倍——这说明阈值挂在累计值上,本身就不是个稳定的信号。

更早的信号是日均消耗速率。它的好处是当天就能看出来:昨天花了 X,今天花了 3X,这个比值第一天就成立,不需要等累计值爬到任何位置。

具体做法不复杂:

  1. 每天固定时间(比如凌晨)拉一次各 key 的累计花费,存下来
  2. 相邻两天做差,得到当日消耗
  3. 用最近七天的当日消耗算一个基线(中位数比平均值稳,能扛住单日毛刺)
  4. 当日消耗 ÷ 基线,超过设定的倍数就报警

外推也很直接:当前速率 × 本周期剩余天数 + 已花费 = 预计周期结束时的花费。这个数超过预算就该报,哪怕现在累计值才用了三成。它把「还有多久会出事」这个真正有用的信息直接摆在了告警里,而不是让值班的人自己去算。

拉花费数据可以用 /key/info,它返回指定 key 的花费情况。官方的花费追踪是通过数据库在 key / user / team 三个层级自动进行的,所以你既可以按单个 key 采,也可以直接按团队维度看聚合,取决于你的告警要落到哪一层。

两个实践上的坑:

  • **别只盯总量。**总花费平稳不代表没事,很可能是一个业务降了、另一个涨了,两边抵消。速率告警一定要按 key(或 team)分别算,聚合层只做二次确认。
  • **要区分「涨了」和「换了」。**换模型、改批处理窗口这类计划内变更也会让速率跳变。所以告警渠道里最好能顺手看到最近的发布记录,否则每次上线都要人工去 mute 一遍,几轮之后没人再看这个告警了。

更完整的采集口径和面板设计,调用量监控这篇讲得更细,这里不重复。

第三层:结构性告警,比花费更早的那批信号

前两层盯的都是钱。但钱是结果,结果永远比原因晚。真正最早暴露问题的是几个结构指标,它们在花费还没明显变化的时候就已经动了:

**平均输出 token 数。**这是最灵敏的一个。提示词改动、模型换代、格式约束失效,都会先反映在这里。一个「简要回答」改成「详细说明」的编辑,在花费曲线上要好几天才看得出来,在平均输出 token 上第一批请求就体现了。

**重试率与重试后成功率。**重试是隐性成本的重灾区:失败的那次调用产生的 token 一样要算钱,而且用户完全感知不到。重试率从 1% 涨到 8%,账单会跟着涨,但请求成功率可能依然漂亮——因为重试最终成功了。这块的成本结构在重试的隐性成本那篇里拆得比较细。更麻烦的情况是重试率高但重试后成功率也低,那说明你在为一批注定失败的请求反复付费,这时候该做的是熔断而不是加大重试次数。

**缓存命中率。**缓存失效是最安静的成本上涨。命中率从 60% 掉到 10%,请求数、错误率、延迟都可能只有轻微变化,但花费直接翻倍。掉命中率的原因往往是无意的——有人在提示词前缀里加了个时间戳,或者把用户 ID 拼进了系统提示的开头。

这三个指标加上按维度切分的成本归属,构成了一套比错误率更早的先行指标,网关可观测性那篇里有更系统的说明。这一层的价值在于:当它响的时候,钱通常还没花多少,你有时间从容处理,而不是一边止血一边排查。

告警要能定位到人,否则等于没有

这一条我想单独讲,因为它比阈值怎么设重要得多。

一条查不到归属的告警,等价于没有这条告警。「本月 API 花费超预算 30%」发到群里,会发生的事情是:所有人看一眼,觉得可能是别的组,然后继续干自己的活。没有人认领,也没有人有足够的上下文去查。这样的告警发几次之后,大家会自动把它当背景噪音。

归属能力必须在发 key 的那一刻就建好,事后是补不回来的。生成 key 时把 user_idteam_id 填上,把项目名、环境、负责人这类信息放进 metadata;用 models 把每把 key 能调的模型限死,这样一把 key 的花费上限在结构上就是可推理的。做到这一步之后,告警内容才可能长成这样:

key-xxxx(团队:推荐算法组 / 环境:prod / 负责人:某某)近 24 小时消耗为七日中位数的 4.2 倍,按当前速率外推,本周期预计超支。

这条告警有人认领,因为它点名了。而且收到它的人可以立刻用 /key/info 查这把 key 的花费明细,用三层追踪往上看是团队层面的问题还是单个应用的问题。

一个很常见的反模式是「一个应用一把 key,全公司共用一套配置模板」。这样做省事,但事故来的时候你只能知道「某个应用超了」,不知道是哪个环境、哪条链路。key 的粒度决定了你排查的最小单位——粒度粗,排查就得靠翻日志;粒度细,看告警就够了。粒度也不是越细越好,细到每个请求一把 key 就变成运维负担了,一般按「团队 × 环境 × 关键业务线」切是比较能落地的档位。

超支之后:分级预案要提前写好

告警响了,接下来做什么?这个问题必须在平静的时候想清楚,因为真出事的时候没有人能冷静地做取舍——现场的默认反应是「先别停,再观察观察」,而观察的每一分钟都在计费。

按业务代价从小到大,四级预案:

**一级:降档。**把非关键路径切到更小的模型。分类、摘要、意图识别这类任务,小模型往往够用。代价是质量小幅下降,用户大概率感知不到。这一级应该做成配置开关,能在几分钟内切换,而不是需要发版。

**二级:降频。**收紧非关键路径的 rpm_limittpm_limit。比如把批量处理任务、后台预计算、数据清洗这类不需要实时性的东西压下来,把额度让给在线请求。代价是这些任务变慢或积压。这一级的前提是你的 key 拆得够细,否则一压就把在线业务一起压了——这又绕回上面那句,粒度是提前决定的。

**三级:停非核心功能。**关掉那些「有更好、没有也能用」的 AI 功能:智能推荐、自动标签、内容润色。代价是功能降级,用户可见,最好配一句明确的降级提示,别让页面变成静默失败。

**四级:停 key。**用 /key/block 停用具体的 key,确认没问题之后用 /key/unblock 恢复。这是最后手段,代价是这把 key 对应的业务直接中断。

关于第四级有一点要强调:**block 优于删。**删掉一把 key,它的配置、归属、花费记录关联可能一起没了,事后复盘时你会发现自己毁掉了最关键的证据——到底是哪个 team_id、配了哪些 models、什么时候开始异常的。block 只是让它不能用,所有信息都还在,确认误报之后还能 unblock 回来。真正确定不再需要了,等复盘写完再清理。

还有一点:预案要写成一页纸,标明每一级由谁决定。「降档」这种低代价动作应该授权给值班的人直接执行,不需要等审批;「停 key」这种会中断业务的,要写清楚谁有权拍板、通知哪些人。没有授权边界的预案,实际执行的时候会卡在「我不敢做这个决定」上,那几十分钟的犹豫比事故本身贵。

开发和 CI 的额度必须独立

这块单拎出来,因为它是超支来源里占比很高、又最容易被漏掉的一类。

开发环境和 CI 的消耗有几个特点:波动极大(今天写测试可能一整天不调,明天调一次就跑几千条)、无人值守(跑在半夜的流水线,出问题没人看见)、失败模式特别脏(一个写错的循环边界能跑一整晚)。我听过不止一次的故事是:某个同学在测试脚本里循环调用,忘了 break,晚上下班回家,第二天早上账单已经出来了。

隔离做法很直接:

  • **CI 单独一把 key,max_budget 设成一个很小的数。**它是一次性的、可以随时重发的,额度花完就花完了,重新生成一把就是——但生产环境的额度一分没动。
  • **rpm_limit 卡死。**CI 不需要高并发。把速率卡在一个明显偏低的水平,能把「一夜跑失控」这种事故的损失锁在一个可接受的范围内。
  • **duration 设短。**给开发环境的 key 一个短有效期,到期自动失效。想继续用就重新申请一次,顺便清掉那些「项目早结束了但 key 还活着」的历史遗留。
  • **models 白名单收窄。**开发和测试不该有权限调用最贵的那档模型,除非在专门验证它。用 models 直接限死,比在代码里写规范可靠得多。
  • **metadata 里标上环境。**这样成本报表里可以一眼分出研发消耗和生产消耗。这两个数混在一起的时候,你既算不准生产的单位成本,也看不出研发消耗是不是失控了。

一个附带好处是:把研发消耗单独统计出来之后,你会得到一个相当有说服力的数字,用来判断某个 AI 功能的开发过程本身值不值这个投入。这个数在混算的账单里是永远看不到的。

定期对账:网关的账和厂商的账要能对上

最后一层保险是对账:把网关侧统计的花费,和厂商账单上的实际金额,定期比一次。频率按业务规模定,量大的话每周一次不算多。

对得上,说明你的观测口径是可信的,前面所有告警才有意义。对不上,差异本身就是最有价值的信息,常见的几种原因:

  • **有绕过网关的直连调用。**某个老服务里还硬编码着厂商的原始 API key,从来没接网关。这是最常见的一种,也是最该优先处理的——这部分消耗完全在你的所有防线之外,max_budgetrpm_limit 一个都拦不到。
  • **有厂商侧的计费项没进你的统计。**比如某些附加能力、某些不走标准调用路径的功能。
  • **统计口径本身有差。**失败请求算不算、重试算几次、缓存命中的请求怎么记,网关和厂商的口径未必一致。这类差异是稳定的,摸清楚一次就行,之后按固定系数校正。

前两种要修,第三种要记录下来当作已知偏差。真正危险的是差异比例在变化——上个月差 3%,这个月差 12%,那多半是有新的直连调用冒出来了。所以对账要记的不只是这次差多少,而是差异比例的历史曲线。

另外,网关侧的花费换算依赖你配置的价格表,厂商调价、你换了模型档位、或者用了不同的计费模式,都会让换算失真。这块的口径问题在月度预算怎么估那篇里有更细的讨论。总之一句:网关的花费数字是拿来做趋势判断和告警的,最终以厂商账单为准。

自查清单

  1. 每把业务 key 是否同时设了 max_budgetrpm_limittpm_limit?只设一个的话,另外两类事故没有防线。
  2. max_budget 的数值是按「绝不该达到」设的,还是按预期用量设的?后者会在业务高峰期误伤,几次之后防线就废了。
  3. 告警是挂在累计值上还是消耗速率上?只有 80% 阈值的话,月末发生的事故基本来不及止损。
  4. 告警内容里有没有 team_id、环境、负责人?没有归属的告警不会有人认领。
  5. 平均输出 token、重试率、缓存命中率这三个先行指标,有没有单独的异常告警?它们比花费早。
  6. 分级预案(降档 → 降频 → 停非核心 → /key/block)写下来了吗?每一级由谁拍板,写清楚了吗?
  7. 开发和 CI 是否用独立的 key,配了小额度、低速率、短 duration、窄 models 白名单?
  8. 网关统计与厂商账单多久对一次?差异比例是在收敛还是在扩大?

以上涉及的接口和字段名以官方文档为准,不同版本的行为可能有差异,落地前请以你实际部署的版本的官方文档核对一遍。

相关阅读