大模型接入生产系统后,成本问题很快会从“一个 Token 多少钱”变成更难回答的工程问题:一次任务可能调用多个模型,输入里既有固定 Prompt 又有动态正文,供应商对缓存和推理 Token 的定义不同,失败请求甚至拿不到 usage。要让成本可控,不能只在月底查看账单,而要建立一条从原始响应到任务、批次、预算和账单的核算链路。
本文不记录任何具体单价。模型价格会调整,同名模型也可能出现不同版本、区域或计费层级;把某个时间点的价格写死在业务代码或文章中,反而容易制造错误。正确做法是保存带生效时间的价格版本,并以供应商当前官方价格页和最终账单为准。
一、先定义要回答的三个成本问题
一个可用的核算系统至少要回答:
- 单次调用预计花费多少?
- 一篇文章、一次日报或一批抓取任务总共花费多少?
- 内部估算与供应商真实账单为什么存在差异?
这三个问题对应三层数据:调用层记录 usage,任务层聚合业务成本,账单层负责最终校准。不要只保存每天的 Token 总量,否则无法定位是哪类内容、哪个模型或哪次重试推高了成本。
如果系统还没有稳定的任务边界,可以先参考AI 内容管线可观测性,为一次业务运行分配 runId,再为批次分配 batchId。
二、不要假设所有供应商的 usage 字段相同
OpenAI 兼容接口通常返回 prompt_tokens、completion_tokens 和 total_tokens,但“兼容”不等于计费语义完全一致。不同供应商或接口版本还可能提供缓存命中、缓存写入、推理 Token、音频 Token 等明细。
因此,建议同时保存两份数据:
rawUsage:原样保存供应商响应中的 usage,便于追溯;normalizedUsage:映射到站内统一字段,用于聚合和告警。
统一结构可以从下面这组字段开始:
function normalizeUsage({ provider, model, usage }) {
return {
provider,
model,
inputTokens: usage?.prompt_tokens ?? usage?.input_tokens ?? null,
outputTokens: usage?.completion_tokens ?? usage?.output_tokens ?? null,
cachedInputTokens:
usage?.prompt_tokens_details?.cached_tokens ??
usage?.cache_read_input_tokens ?? null,
cacheWriteTokens: usage?.cache_creation_input_tokens ?? null,
reasoningTokens:
usage?.completion_tokens_details?.reasoning_tokens ?? null,
totalTokens: usage?.total_tokens ?? null,
rawUsage: usage ?? null
};
}
字段缺失时保留 null,不要擅自填成 0。null 表示未知,0 表示供应商明确报告没有产生该类 Token,两者在成本审计时含义完全不同。
三、价格必须版本化,而不是散落在代码里
价格表至少需要模型、币种、单位、生效区间、输入价、输出价和特殊 Token 价格。每次计算都保存所使用的 priceVersion,这样即使下个月价格改变,也能复算历史任务。
{
"provider": "example",
"model": "model-version-id",
"currency": "USD",
"unitTokens": 1000000,
"effectiveFrom": "YYYY-MM-DDT00:00:00Z",
"effectiveTo": null,
"inputRate": null,
"outputRate": null,
"cachedInputRate": null,
"cacheWriteRate": null,
"reasoningBillingMode": "provider-defined",
"sourceUrl": "供应商官方价格页"
}
示例故意不填单价。部署时应由管理员根据官方页面录入并复核,而不是从第三方文章复制。模型别名也要解析到实际计费版本;如果响应能返回具体模型版本,优先记录响应值。
一次调用的基础计算可以写成纯函数:
function tokenCost(tokens, rate, unitTokens) {
if (tokens == null || rate == null) return null;
return tokens / unitTokens * rate;
}
function estimateCost(usage, price) {
const uncachedInput = usage.inputTokens == null
? null
: Math.max(0, usage.inputTokens - (usage.cachedInputTokens ?? 0));
const parts = {
input: tokenCost(uncachedInput, price.inputRate, price.unitTokens),
cachedInput: tokenCost(usage.cachedInputTokens, price.cachedInputRate, price.unitTokens),
cacheWrite: tokenCost(usage.cacheWriteTokens, price.cacheWriteRate, price.unitTokens),
output: tokenCost(usage.outputTokens, price.outputRate, price.unitTokens)
};
const known = Object.values(parts).filter(Number.isFinite);
return {
parts,
estimatedCost: known.length === Object.keys(parts).length
? known.reduce((sum, value) => sum + value, 0)
: null,
complete: known.length === Object.keys(parts).length
};
}
实际实现必须按供应商规则调整。尤其要确认缓存 Token 是包含在输入 Token 中还是独立计数,以及推理 Token 是否已经包含在输出 Token 中。没有看懂官方口径之前,不要简单相加,否则可能重复计费。
四、缓存 Token 与推理 Token 要单独建模
缓存命中通常不能直接等同于“免费”。有的供应商分别计价缓存读取和缓存写入,有的按折扣输入计费,还有的对缓存有效期或最小前缀有要求。核算时至少记录:
- 缓存写入 Token;
- 缓存读取或命中 Token;
- 未缓存输入 Token;
- 对应价格规则和价格版本。
推理 Token 也不能一概而论。部分推理模型会在 usage 明细中报告 reasoning tokens,但最终计费字段、可见输出字段与总输出字段之间的关系由供应商定义。正确的策略是保存原始明细,使用供应商文档规定的计费口径,并通过账单对账验证,而不是自行推断。
五、失败请求不能默认成本为零
请求失败有多种阶段:连接建立前失败、供应商接受后超时、流式输出中断、返回错误状态、客户端拿到内容但没有 usage。只有供应商明确说明未计费时,才能把成本认定为零。
建议给每次调用增加成本状态:
calculated:usage 完整且价格版本匹配;estimated:使用本地 Token 估算或不完整口径;unknown:请求可能已被处理,但没有可靠 usage;reconciled:已与供应商账单核对。
对于 unknown,预算系统可采用保守占用:按请求前估算的输入 Token 加最大输出上限预留预算,等账单到达后再释放或修正。这样不会因为大量超时和重试,让实时仪表盘错误地显示“零成本”。重试策略还应记录 attempt 和幂等业务标识,具体做法可参考大模型 API 429、超时和 5xx 排查。
六、从单次调用聚合到每批任务成本
每条成本记录应带上业务维度,而不只是模型维度:
{
"requestId": "供应商或本地请求标识",
"runId": "一次业务运行",
"batchId": "一批任务",
"articleId": "可选的内容标识",
"operation": "classify|summarize|draft|review",
"provider": "example",
"model": "model-version-id",
"attempt": 1,
"usageStatus": "calculated",
"priceVersion": "example-model-YYYYMMDD",
"estimatedCost": null,
"currency": "USD"
}
批次成本不是简单计算成功请求。它应该包含成功调用、重试调用和未知成本预留,并明确区分:
batchEstimated = calculated + estimated
batchReserved = unknown requests 的保守预留
batchExposure = batchEstimated + batchReserved
按 operation 聚合后,就能知道成本主要来自分类、总结还是写作;按 model 聚合后,则能判断模型路由是否符合预期。多模型的统一配置和回退关系可参考OpenAI 兼容 API 多模型统一配置。
七、预算控制要在调用前、调用中和调用后同时发生
只设置月度告警不够。更实用的是三层控制:
调用前:预算准入
根据本地分词器或供应商 Token 计数接口估算输入,再结合 max_tokens 和当前价格版本计算最坏暴露。如果项目、批次或租户的剩余预算不足,就拒绝、排队、缩短上下文或切换到已批准的低成本模型。
调用中:增量观察
流式接口如果只有结束时才返回 usage,就不要把未结束请求视为零成本。设置并发上限、最大输出上限和超时,并将正在运行请求的预留额计入预算。
调用后:阈值告警
告警至少覆盖日预算、月预算、单批任务、单篇内容和未知成本数量。可以设置多级阈值,例如提醒、限制新批次、人工确认;具体阈值应根据站点自己的账单和业务价值制定,不应照搬他人的百分比。
Prompt 变更也可能改变 Token 分布。把 Prompt 版本写入成本记录,结合生产环境 Prompt 版本管理,才能比较版本发布前后的输入长度、输出长度和成本变化,而不是只看总体账单。
八、最终以真实账单对账,不把内部估算当财务事实
内部 usage 核算适合实时控制,但供应商账单才是最终依据。建议每天或每个账期执行一次对账:
- 固化内部调用明细和当时使用的价格版本;
- 从供应商官方账单或用量接口取得同一时区、同一项目和同一币种的数据;
- 按日期、项目、模型或供应商允许的最细维度聚合;
- 计算内部估算与账单差额,但不要在粒度不同的情况下强行逐请求匹配;
- 将汇率、税费、赠送额度、批处理折扣、价格生效时间和账单延迟列为差异原因;
- 超出站内阈值时保留原始证据并人工复核;
- 对账完成后把记录标记为
reconciled,修正报表但不覆盖原始 usage。
多币种环境不要使用当天随手查询的汇率重算历史成本。应保存账单币种、内部展示币种、汇率来源和汇率日期;财务报表则遵循组织既定的会计规则。
九、上线前检查清单
- 是否原样保存供应商 usage 和响应中的模型版本?
- 字段缺失是否使用
null,而不是错误地填 0? - 价格是否带来源、生效时间、币种和单位?
- 缓存读取、缓存写入、推理 Token 是否按供应商口径处理?
- 超时、流式中断和无 usage 请求是否进入未知成本队列?
- 重试是否计入同一业务任务的总成本?
- 批次是否同时展示已算成本与未知成本预留?
- Prompt 版本、operation、runId 和 batchId 是否可追踪?
- 是否定期与真实账单对账并解释差异?
- 价格或 usage schema 变化时,是否有告警与复审机制?
结语
大模型成本治理的核心不是寻找一个永远正确的单价公式,而是保留可复算的数据链:供应商原始 usage、站内统一口径、带生效时间的价格版本、业务批次聚合、未知成本预留和真实账单对账。做到这些之后,系统才能在调用前阻止预算失控,在运行中识别异常,并在账期结束时解释每一类差异。