那笔账单,不是来自一次天价请求
第一次觉得 AI 账单「涨得不讲道理」时,我本能去找一个罪魁祸首请求。
你懂那种:一不小心让模型写了篇小说;重试循环停不下来;有人把 staging 指到了生产密钥。这些故事都真实,而且复盘起来很干净。
但我找到的东西更安静,也更糟:几千个单独看都正常的请求。主模型很健康。短时故障时备份模型接了手。改了一版 prompt 之后缓存命中率悄悄下滑。同一个项目里还有第二个工作流,没人盯,因为「它只跑客服工单」。
系统没有坏。花费照样疼。
这才是生产环境 AI 里让人不舒服的模式。成本很少因为一次灾难性调用爆炸,而是靠重复流量、重试、故障转移、更长的 prompt、更大的输出,以及你在供应商仪表盘里填那个「每月数字」时根本没建模进去的用法,慢慢长出来。
如果你的应用只打一个供应商、一个模型,单一预算也许够用。一旦加上故障转移、多个模型,或多个功能共用同一个账户,那个数字就不再描述你真正在跑的系统。
为什么供应商仪表盘看起来精确,却仍会漏掉重点
供应商仪表盘通常擅长一件事:告诉你在那个供应商花了多少钱。
一旦 AI 功能离开原型阶段,团队真正会问的问题,它就弱了:
- 是哪个产品功能烧掉了钱?
- 花费来自主模型,还是事故期间的 fallback 流量?
- 重试有没有把成本算两遍,还是只有成功那次上了账单?
- 客服工作流还好,聊天工作流却快冲破月度计划了吗?
- 如果模型 A 超预算,模型 B 还应不应该继续回答,还是整次请求直接死掉?
这些是应用层问题。供应商计费是账户形状的。你的架构却是功能形状、工作流形状,而且常常是多供应商形状。
于是团队发明自己的控制手段:表格告警、抓发票的 cron、硬编码熔断、「下午四点后别用 GPT-4o」的 Slack 消息。有些管用。大多数是在第一张惊喜账单之后才出现。
更深的问题是概念性的。预算是一个控制面。 如果控制面和请求在系统里流动的方式对不上,预算要么粗到护不住你,要么钝到把产品一起关掉。
多模型工作流会让「一个数字」显得不诚实
想象一个工单分类器。主模型强、贵。备份 1 是另一家更便宜的替代。备份 2 是留给故障的最后手段。
安静的日子里,几乎每个请求都打主模型。嘈杂的日子——限流、区域抖动、供应商事故——流量溢到备份。你的「每月 AI 预算」还是一个数字。花费的形状却完全不同。
再加上常见的生产现实:超时后的重试、本应零成本的 ModelRiver 缓存命中、同一个生产工作流被 playground 打到、同项目里还有第二个工作流,或事件驱动流水线把一次用户动作扇出成多次模型调用。
单一上限表达不了你大概真正想要的策略:贵模型太烫时产品还能活着,但整个项目绝不能失控。

AI 成本控制的两层
和这个问题相处久了,最终让我记住的心智模型是「层」。

项目级上限。 覆盖项目内合格花费的项目级后盾。这是「别把公司卡刷穿」的数字。一旦已观察到的花费达到上限,新的线上请求会被阻断,且不尝试故障转移。项目预算用尽了,请求也就没运气了。
模型级护栏。 挂在工作流里主模型或某一个备份模型上的上限。这是「这个模型在这个周期内可以花这么多」的数字。槽位超预算时,跳过它,尝试下一个已配置的备份。工作流仍可能成功。贵模型只是在周期重置前不再接流量。
这不是:「整个工作流一共多少钱,不管哪个模型答的。」在故障转移设计里,这个总数是含糊的。你想停的是功能,还是某一个模型?那是不同的策略。当成同一种策略,很容易在你专门为事故准备备份的时候,把产品一起弄下线。
| 上限 | 触达时 | 行为 |
|---|---|---|
| 项目花费上限 | 该周期内项目花费已达或超过上限 | 阻断请求。不尝试故障转移。 |
| 模型成本护栏 | 该工作流的供应商/模型槽位已达或超过上限 | 跳过该槽位。尝试下一个备份。 |
| 所有尝试过的模型都超预算 | 因预算原因故障转移链耗尽 | 请求以预算错误失败。 |
在真实控制台里,这两层对应不同的控件。模型级上限在工作流编辑器里,显示为每个主/备模型下的 Cost guardrail(成本护栏);项目级上限在项目 Settings(设置) 里,显示为 Spending limit(花费上限)。

什么应该算花费(什么不该)
在相信任何上限之前,先定义电表。一条能干净映射到请求日志的实用规则:
只统计成功且有价格的请求。
也就是说:
- 成功的供应商调用,带真实估算价格,计入。
- 失败的供应商尝试不计入护栏计算,因为电表统计的是成功且价格为正的请求日志。实际供应商计费在某些失败场景下可能不同。
- 预算跳过,不计入。什么都没发给供应商。
- ModelRiver 缓存命中(prompt cache 或 response cache)价格为零时不计入,甚至不该需要预算检查。这不同于供应商侧的 prompt caching——后者不一定免费。
- 故障转移过程中产生的子跳过/错误行,不该只因为请求吵闹就抬高总额。
如果你跑事件驱动流水线,项目总额还有一层补充:属于另一个工作流的关联 AI 步骤,对项目来说仍然是真金白银。父请求扇出到子 AI 工作时,项目上限也应该看见这笔花费。那笔子工作的槽位护栏属于被关联的工作流,不属于父流水线。
目录定价也很重要。如果网关用 USD 模型价格目录估算成本,预算的可信度就取决于那个目录。未知定价不是小细节。对已设上限的模型槽位,诚实的行为是跳过该模型,而不是假装没有价格也能执行美元上限。对项目上限来说,可路由模型缺少定价是配置错误,应该阻断请求——不该被悄悄忽略。
周期是策略,不是装饰
按小时、按天、按周、按月编码不同的恐惧程度:
- 按小时 适合止血——调新功能或追失控循环时很有用。
- 按天 是仍在摸流量形状时的实用默认。
- 按周 贴合产品节奏和 on-call 轮转。
- 按月 对应财务对话和发票恐慌。
它们都需要同一套时钟。UTC 日历边界无聊且正确:整点、UTC 午夜、按周为周一 00:00 UTC、月初第一天。无论选哪个周期,都要在 UI 和错误载荷里露出 reset_at。没有可见重置时间的预算,只是一面没有钟的墙。

故障转移中途,模型超预算时会发生什么
这正是分层预算值得那点复杂度的时刻。
请求进来。项目仍在上限之下。主模型已经触达当日护栏。
正确行为不是「让用户失败」。正确行为是:
- 记录主模型因预算被跳过。
- 转到备份 1。
- 检查备份 1 自己的护栏。
- 仅当该槽位仍在预算内时才调用供应商。
如果备份 1 也耗尽、备份 2 还有空间,就继续。如果每个可用模型都因预算被跳过,再用清晰的预算错误失败——不是泛泛的 500,也不是伪装成供应商超时。
在请求日志里,这应该事后可重建。ModelRiver 在尝试下一个备份之前就会写入预算跳过记录,所以即使备份最终成功,那些跳过行也仍然可见。若请求最终失败,跳过记录会关联到父错误。运维应能回答「我们是花了钱,还是拒绝花钱?」而不必考古。

重叠上限:真正会发生的场景
两层都启用后,几个经常出现的情况:
- 主模型超日上限,备份仍开放。 工作流继续活着,流量转到仍有预算的下一个模型。
- 工作流看起来还好,项目已经结束。 每个模型槽位都还能显示剩余空间,但项目上限会阻断请求且不故障转移。后盾先赢。
- 一个工作流有上限,另一个没有。 合法设计。项目上限仍会汇总两者的成功计价花费。无上限不等于隐形,只是槽位层没有保护。
- 你已经付过钱的 prompt 命中 ModelRiver 缓存。 零价格,不需要预算检查。这是系统少数几次可以显得慷慨的时候。
- 主模型失败,备份成功。 失败尝试不计入护栏电表。成功的备份计入那个备份的槽位和项目总额。
- 链上每个模型都超预算。 请求以工作流预算错误失败,没有供应商调用。
还有一个很多人栽过的情况:测试模式。 如果工作流模拟或绕过线上供应商花费,预算执行就该让路。生产工作流在线上 playground 里是另一回事:能花真钱,就该尊重真上限。
软执行是诚实的取舍
下面这部分,营销文案喜欢跳过。
如果每次请求都无缓存地从数据库汇总日志来查预算,你就在热路径上加延迟。如果把花费缓存大约十秒,并发请求可能在缓存刷新前溜过去。突发流量下,实际花费可能略高于配置上限。
这就是软执行。它不等于「保证精确到分的硬停」。
对多数产品团队,软执行是正确的取舍。你要的是护栏,不是和每一次推理抢分布式锁。你该要求的是诚实:
- 说清楚执行是软的。
- 在成功的计价日志写入后失效花费缓存。
- 在 UI 里显示实际花费,哪怕短暂超一点。
- 基于已经观察到的花费阻断,而不是预测当前请求未来的 token 数。
最后一点很重要。估算「还没做」的请求成本是另一个更难的问题。只往后看的上限更简单,仍然能抓住真正伤人的模式:持续超支。
可观测性是功能的一半
看不见的花费上限,只是神秘中断生成器。
运维至少需要:项目已花 vs 上限与重置时间、每模型槽位进度、接近上限或已触顶的清晰信号、能区分预算跳过与供应商失败的请求日志,以及 project_budget_exceeded 或 workflow_budget_exceeded 这类错误类型,而不是「出了点问题」。缺少目录定价也需要单独的配置错误——不同诊断,不同修复。
没有这些,on-call 会把预算阻断当成供应商事故。有了这些,他们会把它当成它本来的样子:策略在生效。

你可能还会喜欢:不烧 token 测试 AI 工作流
如果你在设计 AI 成本控制:实用建议
无论是自建,还是用已有网关,这些设计教训都能迁移。
- 把「停某个模型」和「停整个项目」分开。 不同失败模式需要不同行为。
- 把对故障转移友好的上限放在模型槽位上。 否则你的可靠性功能会在事故期间变成成本放大器。
- 加一层项目后盾。 只有槽位上限,救不了五个无上限工作流在凌晨两点互相发现对方。
- 用电表定义成「成功且有价格的日志」。 数钱,不数感觉。
- 学习阶段用短周期,流量稳定后再拉长。
- 把未知定价当成配置错误。 没有价格的美元上限是演戏。
- 把跳过记成一等公民事件。 未来的你会需要解释主模型为什么从未运行。
- 别把花费上限和限流或内容护栏搞混。 不同工具,不同告警。
我们最后怎么实现的
我们把 ModelRiver 建成 AI 路由网关:项目、工作流、主备模型、请求日志、缓存——一旦「直接调供应商 SDK」不再可爱,你就需要这些不风光的机器。故障转移一旦存在,单一预算数字在我们自己的产品里就显得不对。于是我们加了分层花费控制。
在控制台里它们显示为项目 Spending limit(花费上限),以及主模型、备份 1、备份 2 上的每槽位 Cost guardrail(成本护栏)——就是前文截图里的同一套控件。线上请求按顺序检查:测试模式工作流跳过上限;零价格的 ModelRiver 缓存命中跳过上限;用「阻断请求且不故障转移」执行项目级上限;然后是带故障转移的模型级护栏。Overview 显示利用率。请求日志用 budget_guardrail 作为 response source 记录预算跳过。软执行是有意且有文档的。
如果你要操作手册,在 花费上限文档。产品页是 AI spending limits。
这不承诺什么
几条边界,说清楚:
- 不保证并发下零超支。
- 不逐行读取供应商最终发票。它使用 USD 目录估算价格。
- 不替代财务告警、采购审批,或对 API 密钥的成年人监管。
- 不替你决定产品策略。
这些不是脚注。它们是护栏和幻想的区别。
真正有帮助的转变
有用的变化不是「加一个更大的预算字段」。而是把预算当成策略:模型失败时、流量飙升时、某个功能开始吃掉其他功能的午餐时,花费被允许往哪里移动。
比「我们怎么再也不超支」更好的问题:
- 如果主模型太烫,用户还应不应该得到答案?
- 如果项目耗尽,任何工作流还应不应该继续花钱?
- 哪些成功调用计入,哪些吵闹的失败不该计入?
- 墙撞上时,明天的 on-call 能不能看懂为什么?
那些问题是设计工作。控制只是你如何把答案编码进去。
如果你想在运行中的控制台里看到这种形状,ModelRiver 有免费档,可以设项目花费上限、加每模型成本护栏,并在请求日志里观察故障转移行为。有意思的不是表单字段。而是看着主模型因预算被跳过,备份悄悄让功能继续活着。

