[控场AI]
· 9 分钟阅读· 4,612 字

防止单个客户烧爆你的 OpenAI 账单:按客户管控成本

防止单个客户烧爆你的 OpenAI 账单:按客户管控成本

在请求路径内按客户实时折算并限制 LLM 成本,先降级再阻断,避免单客烧穿利润。

文章针对 LLM API SaaS 产品中供应商账单无法按客户归因的痛点,提出一套完整模式。核心步骤包括:解析每次调用的 token 用量并立即折算成美元,正确区分缓存输入与带日期的模型快照;把预算检查放在请求路径中、供应商调用之前,而非事后定时任务;流式响应需开启 `include_usage` 并在 `finally` 中记账;超过预算 80% 时先降级到更便宜的模型,达到 100% 再用明确错误阻断;多实例环境用 Redis 原子递增按客户、按月累计并设置过期。文章还诚实指出并发超支与存储故障两个边界,并建议将按客户归因的成本与收入对比,从而识别需要重新定价或设限的套餐。作者已将模式打包为开源库 LLMBrake。

运营一个基于 LLM API 的 SaaS 产品时,你的供应商账单往往只是一个总数。OpenAI 和 Anthropic 允许你按组织或项目设置限额,但这些限额保护的是你的账户安全,而非利润空间——它们无法告诉你哪个客户贡献了这笔支出,也无法在不影响其他客户的前提下单独限制某一个人。

这种失控有两种典型表现:

  1. 失控的脚本用户:有人对你的功能进行脚本化调用,或者客户端 bug 陷入重试循环,某一个账户在一夜之间烧掉真金白银。
  2. 悄无声息的亏损账户:一个购买 20 美元套餐的客户每天发送超长上下文,其 token 成本远超他支付的费用。你之所以看不见,是因为成本没有按客户归因。

rss source: How to stop one customer from blowing up your OpenAI bill

下面是一套同时解决这两个问题的模式,以及需要警惕的边界情况。

一、按客户把每次调用折算成美元

每一次 chat completion 都会返回 token 用量。要立刻将其转换为美元,因为不同模型之间的 token 数量并不可比:

cost = (uncachedInput * inputPrice + cachedInput * cachedInputPrice + output * outputPrice) / 1_000_000

几个容易踩坑的细节:

  • 缓存输入 token:OpenAI 会在 usage.prompt_tokens_details.cached_tokens 中报告它们,且计费费率更低。忽略这一点会导致成本被高估。
  • 带日期的模型快照:像 gpt-5.4-mini-2026-03-17 这样的模型名,应当解析到基础模型的价格。
  • 价格表:把它集中放在一个地方,附上出处链接和你读取该价格的日期。价格会变动。作者在 2026-10-09 查看 OpenAI 定价页时,gpt-5.5 标价为每 100 万 token 输入 5 美元、输出 30 美元,gpt-5.4-mini 为 0.75 / 4.50 美元。依赖之前请先核实。

缓存输入 token(prompt caching) 是 OpenAI 等供应商为大段重复前缀提供的折扣机制:当多次请求携带相同的前缀内容(如系统提示、长文档、few-shot 示例)时,供应商会缓存这段前缀的计算结果,后续命中缓存的输入 token 按更低费率计费,通常只有标准输入价的十分之一左右。对 SaaS 产品而言,这既是省钱机会也是核算陷阱——如果按标准输入价把所有 prompt token 都计入成本,会显著高估实际支出,导致降级或阻断过早触发。反过来,若把缓存单价套用到未命中的 token 上,就会低估成本、让亏损账户悄悄溜过。因此解析用量时必须区分 cached_tokens 与普通 prompt_tokens,并分别乘以对应单价。缓存并非永久有效,供应商通常有几分钟到一小时的 TTL,命中率会随用户行为波动,所以成本核算要基于供应商实际返回的用量字段,而不是自行假设的命中率。

二、调用前检查,调用后记录

调用前:  spent = store.get(customer, month)
         if spent >= budget        → 阻止
         if spent >= 0.8 * budget  → 切换到更便宜的模型
调用供应商
调用后:   store.add(customer, month, cost)

这个检查必须放在请求路径中、调用之前。一个每晚给你发邮件告知损失的定时任务属于事后取证,而不是保护。

请求路径内检查指的是把预算判断嵌入处理用户请求的同步代码流中,与实际的供应商调用处于同一执行链,从而保证每一次调用前的事实依据都是最新累计值。与之相对的定时巡检(cron job)只在固定时间点读取账单,两次运行之间的所有花费都处于监控盲区,尤其无法应对突发性的脚本刷量或重试风暴。实现时要理解读-判断-调用这三步本身不是原子的:在高并发下,多个请求可能几乎同时读取到相同的、尚未超预算的余额,然后一起放行。单进程 Node.js 环境因事件循环串行执行可以缓解,但多实例或无服务器架构下必须依赖外部原子操作。因此这一模式是“软保护”,用于防止大多数失控场景,而非金融级的硬性封顶;若业务要求严格上限,需要在调用前预留估算额度。

三、正确处理流式响应

当使用 stream: true 时,只有设置了 stream_options: { include_usage: true },OpenAI 才会报告用量,且它出现在最后一个 chunk 中。包装异步迭代器,将 chunk 原样透传,并在 finally 块中记录成本,这样即使客户端提前停止读取也仍会执行。

流式响应(stream: true) 下供应商以 Server-Sent Events 形式逐块返回文本,token 用量不会像非流式那样在一次完整响应中给出。OpenAI 要求显式传入 stream_options: { include_usage: true },之后最后一个 chunk 会携带完整的 usage 对象,其 choices 数组为空。如果忘记这个参数,用量字段根本不会出现,成本记录会全部丢失,按客户核算直接失效。另一个坑是客户端可能在中途断开连接或主动停止读取,此时若把记录逻辑写在消费循环之后,代码可能被中断而跳过记账。正确做法是在 for await 迭代器外层用 try/finally 包裹,把 store.add 放在 finally 里执行,保证无论正常结束还是提前退出都能落账。还需注意透传 chunk 时不要修改内容或顺序,否则会破坏上层对 SSE 的解析。

四、先降级,再阻断

对付费客户直接硬性切断是很糟糕的体验。更温和的第一步效果更好:超过预算 80% 后,把同一个任务路由到更便宜的模型。很多用户不会察觉,而你的利润得到了保护。达到 100% 时,用一个具体的错误来阻止,这样你的 UI 可以显示“您已达到本月的 AI 使用上限。是否升级?”,而不是抛出一个模糊的失败。

五、使用能处理并发的存储

内存计数器对单进程来说足够。但面对多个实例或无服务器函数时,要使用原子递增,例如 Redis 的 INCRBYFLOAT llmbrake:{month}:{customer} {cost}。按月份作为键,预算无需定时任务即可重置;加上 EXPIRE,旧月份会自行清理。

两个诚实的注意事项:

  • 超出:并发的在途请求可能各自通过检查,然后一起超支,通常超出那几个调用的成本。如果你需要严格上限,可以在调用前预留一个估算成本,调用后再结算。
  • 存储故障:决定是失败开放(继续服务,承担一些未计量的支出风险)还是失败关闭(保护支出但中断功能)。对大多数产品而言,失败开放加告警是正确选择。

失败开放(fail open)与失败关闭(fail close) 是限流与配额系统面对自身依赖故障时的两种取舍策略。当 Redis 等外部计数器不可达、超时或报错时,失败开放意味着跳过预算判断、继续放行请求,代价是这段时间的支出未被计量,存在被刷风险,但用户功能不中断;失败关闭则相反,直接拒绝请求以保护成本,代价是正常付费用户也会遭遇服务不可用。选择取决于业务性质:面向 C 端、以留存和体验为先的产品通常选失败开放,并配合告警和降级日志,在恢复后补记或人工排查;面向企业内部、成本敏感或存在滥用风险的场景则可能选失败关闭。无论选哪种,都应把故障事件上报到监控系统,避免静默失效。实践中可以设置短暂的本地降级缓存作为折中,或对故障期间放行的请求打上标记,便于事后归因。

六、将成本与收入对比

一旦支出按客户归因,就可以把它与客户付给你的费用(例如 Stripe 订阅)关联起来。一张包含客户、AI 成本、收入和利润率的表格,通常足以发现哪些套餐需要重新定价或设置更低的上限。

库版本

作者把这套模式打包成了 LLMBrake,一个 MIT 许可的小型 TypeScript 库(约 500 行,零运行时依赖):

npm install llmbrake
import { createGuard, isBudgetExceededError } from "llmbrake";

const guard = createGuard({
  budgetUsd: (ctx) => (ctx.plan === "pro" ? 20 : 2),
  downgradeAt: 0.8,
  downgradeMap: { "gpt-5.5": "gpt-5.4-mini" },
});

const chat = openai.chat.completions;
const create = guard.wrap(chat.create.bind(chat));

const res = await create(
  { model: "gpt-5.5", messages: [{ role: "user", content: "Hello" }] },
  { customerId: user.id, plan: user.plan, feature: "chat" },
);
// 达到上限时 → 抛出 BudgetExceededError

它涵盖了缓存 token 定价、流式用量、降级步骤、带类型的阻断错误、仅元数据的用量事件,以及一个可插拔的存储,默认失败开放。你可以直接使用,也可以复制这套模式。还有一个浏览器内演示(真实库,模拟供应商):https://llmbrake.com/demo?ref=devto

披露:作者构建了 LLMBrake。一个托管仪表盘(共享计数器、告警、按客户的 AI 成本与收入对比)正在 app.llmbrake.com 上进行早期访问。

核心要点

  • 在请求路径中、调用之前检查预算,而不是依赖事后的定时任务。
  • 把 token 用量立即折算成美元,并正确区分缓存输入与带日期的模型快照。
  • 在阻断之前先降级到更便宜的模型,保护付费客户体验与利润。
  • 多实例环境下使用 Redis 之类的原子计数器按客户、按月累计成本。
  • 把按客户归因的 AI 成本与收入关联,识别需要重新定价或设置上限的套餐。
#OpenAI 账单#LLM 成本控制#按客户计费#token 成本#AI SaaS 成本#预算守卫#Redis 原子计数#LLMBrake
分享:

相关推荐