LLM按请求和token计费,重度用户成本远超轻量用户,根源是功能设计本身低效而非模型选型问题。
我们工作中构建的大多数东西,成本结构几乎不变。一台服务器、一个数据库、几个 SaaS 席位,在安静的周二和上线当天价格差不多。然后你上线了第一个 AI 功能,第一次,一个点击有了价格标签。
这个价格在演示中很少出现。它出现在上线后第一张完整的账单上——一个在测试中几乎零成本的功能,被调用的次数远超任何人预期,调用的上下文也远超任何人意图。常见的反应是换个更便宜的模型。有时候确实有用。但更常见的情况是,账单高是因为功能的构建方式有问题,换个更便宜的模型只是降低了浪费性设计的单位成本。
所以本文讲的是设计本身。这些是我从第一个 sprint 就开始培养的习惯,无论是在客户的语音 Agent 项目还是我自己的产品上。
托管模型按请求计费,按输入和输出 token 量定价。由此产生三个后果,而这三条对习惯固定基础设施成本的团队来说都很容易被忽视。
使用量分布不均。 你的重度用户不只比轻度用户贵一点——可能贵好几倍,因为他们更频繁地触发这个功能,而且通常是更大的输入。用户平均成本会恰好掩盖那些真正造成问题的账户。
输入大小是不可见的。 没人看得见 prompt。一个功能每次请求都发送整个文档、完整的对话历史和一段长的指令块,在 UI 上看起来和一个只发送一段话的功能完全一样。差别只在账单上才显现出来。
循环会成倍放大。 一个 Agent 调用模型、读取结果、再调用的过程,可能产生多次调用来响应一次用户操作。重试 bug,或者一个永远无法判断自己是否完成的 Agent,可能产生数千次调用。
这些都不是避免 AI 功能的理由,而是从第一天就把模型调用当作按量计费资源来对待的理由,就像你已经对待 SMS 或支付手续费那样。
月度总数几乎说明不了什么。重要的数字是每个业务获利单位的成本:每处理一个电话、每处理一份文档、每个活跃用户。
这意味着每次模型调用都要记录足够的信息来回答"这是用来做什么的"。大致像这样:
// illustrative shape, one row per model call
type ModelCallLog = {
feature: "summary" | "classify" | "agent_step";
tenantId: string;
model: string;
inputTokens: number;
outputTokens: number;
used: boolean; // did the user keep the result, or regenerate it?
at: Date;
};
这是很小量的工程工作,没有它之后每个决策都是猜测。有了它你就能回答真正驱动支出的问题:哪个功能占了大头账单、前 10% 的用户成本与中位数相比如何、以及为那些被丢弃的结果花了多少钱。
选择与收入匹配的计量单位。在语音 Agent 上,比如我们为 CallGuard AI 和 CallSetter AI 构建的那些,自然的单位是通话分钟数,因为那是这些业务自身经济的计量方式。在订阅产品上它是每月活跃用户,因为订阅费为此支付。
这是大多数 AI 产品中最大的节省来源。
在变更时生成,而不是在查看时生成。 如果一条记录需要一个摘要,在记录变更时生成并存储它。不要在每次有人打开页面时重新生成。多次读取,一次写入,应该只花一次调用。标签、分类、提取的字段和 embeddings 同理。
缓存重复请求。 相同的请求比你想象的更常见,精确匹配的缓存简单且安全。大多数主要提供商也为重复的长前缀提供 prompt 缓存,所以把 prompt 中稳定的部分(指令、手册、策略)放在前面,可变的部分放在后面。同样的行为,更低的成本。
批量处理不紧急的任务。 过夜重处理、批量分类和回填通常可以通过提供商的批量接口完成,价格通常低于实时接口。这是调度决策,不是模型决策。
我在自己的产品上学会了缓存这个技巧。在 Upwork Scout 中,AI 匹配分数是按用户和职位存储的,所以同一个人的职位不会被评分两次。就这一个决策让模型支出与扫描器运行频率解耦了。我在 Cheap Filters First, LLM Last 中写了完整的匹配器逻辑,所以不在这里重复。
团队传入所有内容,因为判断任务需要什么更费功夫,而模型能应付。只是应付得很贵。
检索,而不是倾倒。 拉取少量相关段落而不是整个文档。这通常也能改善答案。
修剪历史。 长的对话不需要把之前每个回合都完整地重新发送。总结较早的回合,或只保留仍有意义的那些。
限制输出。 要求 UI 实际能显示的长度。"写一个摘要"没有限制的话可以写得比任何人读的都多。
更小的输入也意味着更快的响应,这在任何实时场景中都很重要,比如语音 Agent——来电者正坐在沉默中等待。
请求难度并不均等。很大一部分是常规任务:分类这条消息、提取这些字段、从提供的文本中回答。小模型处理这些通常也很好。只有一小部分真正需要前沿模型。
我使用的模式:对于每个任务,找到通过自己评估集的最便宜的模型,把任务发到那里,并在它表示无法完成时给出升级路径。这只有在切换模型不意味着重写功能时才能work,所以在产品代码和提供商 SDK 之间保持一层薄薄的抽象。即使像从 env var 读取模型名而不是硬编码这么小的改动,也能实现这一点。
有时候正确的模型是不用模型。如果规则、查表或正则表达式能可靠地完成任务,它更便宜、更快,而且更容易测试。
监控在问题发生后才告诉你。限制在问题变贵之前就阻止它。
按用户和按计划。 为每个计划设置配额,并在产品中强制执行,而不仅仅在定价页面上。在 Upwork Scout 中,每个用户每天获得 150 次 AI 评分。当达到上限时,匹配不会停止;会降级为确定性过滤器来完成当天剩余时间。用户仍然会收到提醒,我的账单仍然可预测。
每次 Agent 运行。 任何可能重复调用模型的 Agent 都需要对单次任务的步数、token 数和墙上时间设置上限,超出后停止并报告到达位置:
// illustrative: one budget object per agent run
const budget = { maxSteps: 12, maxTokens: 60_000, deadlineMs: Date.now() + 90_000 };
function canContinue(steps: number, tokensSoFar: number) {
return steps < budget.maxSteps
&& tokensSoFar < budget.maxTokens
&& Date.now() < budget.deadlineMs;
}
一个无法造成损害的 Agent 仍然可能累积账单。这是我在 Give an AI Agent Write Access One Verb at a Time 中写的隔离规则的成本版本。
按功能和按租户。 设置带警报的预算,既在提供商端也在自己的日志中,这样一个功能或一个账户的峰值在数小时内就被注意到,而不是在月底。为预算触发时会发生什么提前做好决定:回退到小模型、排队等待工作、或关闭功能并显示清晰消息。冷静时期决定的回退策略好过在压力下决定的宕机。
防止滥用。 公开的 AI 功能会吸引想要免费模型的人。速率限制、昂贵操作的登录要求以及输入大小检查,防止一个坏角色成为你最大的客户。
有时候一个功能构建得很好,但相对于它赚取的仍然太贵。那么解决方案是商业的,不是技术的:单独收费、放到更高计划中、添加带付费增补的配额、或缩小到它创造最大价值的场景。最好在用户期望免费之前就从基于真实使用的成本模型做出这个决定,而不是之后。
相反的情况也时有发生。团队因为账单单独看起来很大就削减了一个有价值的功能,而该功能的每单位成本相对于它赚取的来说是微乎其微的。每单位价值成本同样能平息这种争议。
如果我不得不把它压缩成一个 PR 审查的检查清单:
做到这些,一个不断增长的 AI 账单意味着一个不断增长的产品,而不是一个不断增长的问题。
我最初在 Null Studio 博客上写了一篇更长的、面向客户的版本。我很好奇其他人在这里做什么:你在代码中强制执行每用户限制了吗,还是仍然依赖提供商级别的警报?