在实际项目的模型选型讨论中,经常会出现一句话把成本问题一笔带过的现象:“某个模型更便宜,直接换过去就行。”比如当大家看到“GLM-5.3 成本仅为 FABLE 5 的八分之一”这类结论时,不要急着把它当既定事实写进方案。大模型 API 的计费并不是“一个单价 × 调用次数”这么简单,真正的账单由输入 token、输出 token、缓存命中、上下文长度和调用链路共同决定。如果只拿一份公开价格表对比,算出来的结论很可能和实际账单差出好几倍。
这篇文章从成本评估方法入手,先拆解大模型 API 的计费结构,再给出一个可复现的对比脚本,最后说明如何验证“八分之一成本”这类结论是否在自己的任务上成立,以及怎样避免被单点价格误导。无论是做技术选型、模型切换,还是日常的账单排查,都可以沿用这套流程。
1. 成本对比不是看单价表就够的,先拆解大模型 API 的计费结构
1.1 为什么按“千 token 单价”直接比大小往往会算错
很多平台在文档里都会列出每百万 token 的价格,看起来只要把两个模型的价格放在一起相除就能得出结论。但实际项目里,两个模型的计费口径经常不一致,常见的差异点包括:
- 同一段中文文本在不同模型的分词器下,token 数量可能相差 20% 到 50%。一个模型按 300 token 计费,另一个模型可能按 450 token 计费,即使单价相同,实际成本也不同。
- 输入和输出的单价往往不一样,输出 token 通常更贵。如果任务偏长输出,输出价格会成为总成本的主要部分,只对比输入价格没有意义。
- 部分模型支持上下文缓存,命中缓存的输入 token 会按折扣价计费,不命中的输入 token 按原价计费。缓存命中率不同,同样请求的账单就不一样。
- 不同模型对上下文的处理方式不同。有的模型会自动截断或压缩历史消息,有的模型必须把完整历史重新发送,导致每次请求的输入 token 数量完全不同。
所以,正确的做法不是比较“单价”,而是比较“完成同一个任务的实际支出”。单价只是成本模型里的一个参数,不是结论本身。
1.2 一次完整请求的成本由哪几部分叠加
一个标准的大模型 API 请求,账单通常可以拆成四个部分:
- 输入 token:用户发送的 prompt、历史会话、工具定义等所有进入模型的文本,按输入单价计费。
- 输出 token:模型生成的补全内容,按输出单价计费,一般比输入单价高。
- 缓存写入 token:第一次携带相同前缀请求时,系统把前缀写入缓存产生的费用。
- 缓存读取 token:后续请求命中缓存时,读取缓存前缀产生的费用,通常远低于普通输入价格。
计算单次请求成本的公式可以写成:
单次成本 = 输入token数 × 输入单价 + 输出token数 × 输出单价 + 缓存写入token数 × 缓存写入单价 + 缓存读取token数 × 缓存读取单价如果接口不暴露缓存的拆分字段,计算时就退化为前两项。生产环境中,多轮对话、批量任务和带系统提示词的任务,缓存字段对总账单的影响非常大,这一点放到第 4 节专门展开。
1.3 成本对比的必要前提:相同任务、相同输出语义
成本对比最容易犯的错误是“任务不一致”。同样叫“让模型总结一段会议纪要”,如果任务 A 要求 200 字以内的中文摘要,任务 B 要求 1000 字的英文要点,输出 token 差距可以达到 5 倍以上。没有统一任务样本,任何成本结论都没有可比性。
做对比前至少要统一这几个维度:
| 维度 | 对比时必须统一的内容 |
|---|---|
| 任务类型 | 文本摘要、代码生成、信息抽取、对话问答等要分开测试 |
| 输入长度 | 同一条输入,或按相同长度分布构造输入样本 |
| 输出长度 | 通过 prompt 约束或 max_tokens 参数保持一致量级 |
| 温度参数 | 同一 temperature,避免输出随机性放大成本波动 |
| 调用方式 | 同步、流式、批量接口分别统计,因为批量可能有折扣 |
| 缓存状态 | 冷启动和缓存命中后的成本要分开记录 |
只有在这些条件一致时,“GLM-5.3 成本仅为 FABLE 5 的八分之一”这句话才是一个可以验证的命题,而不是一个无法追溯的营销表述。
2. 搭建一个可复现的成本评估环境
2.1 环境准备和依赖安装
成本评估脚本只需要普通的 Python 环境,不需要额外服务。推荐 Python 3.10 以上版本,使用 requests 或各模型的官方 SDK 都可以。为了脚本通用,这里使用 OpenAI SDK 风格的接口,因为大多数平台都兼容这套请求格式。
python -m venv venv source venv/bin/activate pip install openai如果目标平台不提供 OpenAI 兼容接口,也可以直接用 requests 调 HTTP 接口,核心逻辑不变,区别只是请求头和响应报文。
评估环境的重点是记录每次请求的 usage 数据。绝大多数平台在响应里都会返回类似下面的结构:
{ "usage": { "prompt_tokens": 512, "completion_tokens": 128, "total_tokens": 640 } }部分平台还会返回prompt_tokens_details或prompt_cache_hit_tokens之类的字段,用来区分缓存命中部分。脚本里要优先收集这些字段,拿到最细粒度的数据。
2.2 用统一脚本记录请求级 token 消耗
下面这个脚本封装了一次 API 调用,并把请求文本、响应文本和 token 明细记录成一条 JSON 记录。实际评估时,把脚本跑在同一个环境、同一个网络条件下,避免网络重试和超时策略对结果产生干扰。
import json import time from openai import OpenAI def call_and_record(client, model, messages, output_record, max_tokens=1024): start = time.time() resp = client.chat.completions.create( model=model, messages=messages, max_tokens=max_tokens, temperature=0.2, ) usage = resp.usage record = { "model": model, "prompt_tokens": usage.prompt_tokens, "completion_tokens": usage.completion_tokens, "total_tokens": usage.total_tokens, "latency_ms": round((time.time() - start) * 1000, 2), "request": json.dumps(messages, ensure_ascii=False), "response": resp.choices[0].message.content, } if hasattr(usage, "prompt_tokens_details"): cached = getattr(usage.prompt_tokens_details, "cached_tokens", 0) record["prompt_cache_hit_tokens"] = cached or 0 output_record.append(record) return record记录字段里最关键的是prompt_cache_hit_tokens。如果不记录这个字段,后面计算成本时只能按全部输入原价计算,会高估那些命中次数多、上下文长但增量小的请求。
2.3 建立任务样本集,避免用单条提示词下结论
成本对比不能只跑一条提示词。单条结果受随机性和任务偏差影响很大,建议构造一个至少 20 到 50 条的样本集,覆盖真实业务中占比最高的几类任务。样本集保存成 JSONL,一行一个任务:
{"task": "summary", "input": "请用 150 字以内总结以下会议纪要:...", "max_tokens": 300} {"task": "extract", "input": "从以下合同中抽取甲方、乙方、金额和期限:...", "max_tokens": 200} {"task": "codegen", "input": "用 Python 写一个读取 CSV 并统计分组的函数:...", "max_tokens": 500}样本集需要满足三个条件:一是和线上任务同分布;二是输入长度有一定跨度,从几百 token 到几千 token 都有;三是输出长度通过 prompt 和 max_tokens 都做了约束,避免不同模型输出长度差异过大导致成本失真。
一个可供参考的样本构成是:摘要类 30%、信息抽取类 25%、代码生成类 20%、多轮对话类 15%、短文本改写类 10%。比例按自己的业务调整,重点是测试结果要能代表真实流量结构,而不是只代表最容易跑通的那一条示例。
3. 用实际脚本计算并对比单次任务成本
3.1 通过 API 返回的 usage 字段统计成本
有了样本集和调用记录,下一步就是按统一价格计算单次请求成本。计费价格不建议硬编码在脚本里,而是放到一个单独的配置文件中,便于按平台和有效期修改。下面是一个价格配置的例子:
pricing: glm_5_3: input_per_million: 0.5 output_per_million: 2.0 cache_write_per_million: 1.0 cache_read_per_million: 0.1 fable_5: input_per_million: 4.0 output_per_million: 16.0 cache_write_per_million: 8.0 cache_read_per_million: 0.8这里的数字只是演示结构,不是实际价格,落地前必须替换成目标平台当前文档中的真实报价。计算成本时,按照之前提到的公式逐条记录累加:
def compute_cost(record, price): prompt_tokens = record["prompt_tokens"] completion_tokens = record["completion_tokens"] cache_hit = record.get("prompt_cache_hit_tokens", 0) cache_miss = prompt_tokens - cache_hit cost = ( cache_miss * price["input_per_million"] / 1_000_000 + cache_hit * price["cache_read_per_million"] / 1_000_000 + completion_tokens * price["output_per_million"] / 1_000_000 ) return round(cost, 6)如果平台没有暴露缓存字段,就把cache_hit按 0 处理。此时计算结果会偏保守,适合做最坏情况估计;而平台实际对缓存命中的折扣不会体现在该结果里,所以还要结合平台文档单独测算缓存收益。
3.2 预估成本和实际成本之间的偏差来源
本地估算和平台账单对不上,通常不是脚本算错了,而是下面几个原因:
- 输出 token 被
max_tokens截断。响应里如果带了finish_reason="length",说明输出还没生成完就被截断,实际内容不完整但依然按完整 token 计费,同时会影响任务质量。 - 请求重试。每次超时重试都会重新产生 token 费用,账单上会出现一模一样的重复请求。
- 模型别名路由。部分平台用同一个模型名接入多家服务商,实际被路由到不同模型,价格自然不同。
- 上下文自动扩展。多轮对话中,每次请求都会携带完整历史,输入 token 随轮数增长,和单轮预估完全不一样。
所以在对比两个模型成本时,不能只看一两次调用,要统计整批样本的平均成本、中位数成本和 P95 成本。平均值会被个别超长请求拉高,中位数更接近普通请求体验,P95 则是容量规划时应该关注的上限。
3.3 验证“八分之一成本”类结论要检查哪些点
当有人说“A 模型成本只有 B 模型的八分之一”时,按下面的清单逐项核对:
| 检查项 | 核对内容 |
|---|---|
| 对比口径 | 比的是输入单价、输出单价,还是单次任务实际成本? |
| 样本任务 | 两个模型跑的是同一批任务吗?输出长度限制一致吗? |
| 计费时段 | 价格有没有包含限时折扣、新用户优惠或批量折扣? |
| 缓存状态 | 是否都处于冷启动状态?有没有一个模型命中缓存而另一个没有? |
| 输出质量 | 便宜的那一方的失败率、拒答率和格式错误率是否在可接受范围? |
| 并发与配额 | 便宜模型是否限制了并发数,导致需要更多实例从而摊低成本优势? |
只有这六项都核对通过,成本倍率才有工程意义。否则“八分之一”很可能只是某个特定 prompt、某个特定时段下的偶然结果。
4. 影响最终成本的隐藏因素:缓存、模型路由、超时与重试
4.1 上下文缓存如何改变实际账单
上下文缓存是目前影响大模型账单最明显的隐藏因素。它的原理是:平台把请求前缀按内容哈希缓存起来,下次请求如果前缀相同,就直接复用缓存,不再把整段前缀重新计算一遍。对系统提示词长、历史会话多的业务来说,缓存命中率可以占到总输入 token 的 80% 以上。
缓存对成本的影响可以从两个方向看:
| 场景 | 缓存不命中 | 缓存命中 | 结论 |
|---|---|---|---|
| 第一次请求 | 全量输入按原价计费 | 无缓存可用 | 冷启动成本高 |
| 后续相同前缀请求 | 重新计算,费用高 | 按折扣价计费,费用低 | 热请求成本明显下降 |
| 前缀经常变化 | 每次部分写入、部分命中 | 命中率不稳定 | 成本波动大 |
对比两个模型时,如果 A 模型缓存命中率 85%,B 模型只有 30%,即使两者单价相同,实际账单也会差出不少。因此评估脚本里必须单独记录缓存命中 token,并且用真实业务中的前缀分布来测试,而不是在脚本里人工拼一个固定前缀。
注意:缓存命中率必须在真实前缀分布下测试才有效。临时构造的固定前缀会得到虚高的命中率,会严重低估实际成本。
4.2 模型路由和降级策略对成本的影响
在线上系统里,成本对比往往不是“单模型 vs 单模型”,而是“路由策略 vs 路由策略”。很多团队采用主模型加备用模型的降级方案:主模型用低成本模型,遇到限流、超时或质量不达标时,降级到高成本模型。
降级策略会显著改变总成本。假设低成本模型单次任务 0.01 元,高成本模型单次任务 0.08 元,如果低成本模型的失败率是 15%,则平均成本变为:
平均成本 = 0.01 × 0.85 + 0.08 × 0.15 = 0.0205 元失败率从 15% 提高到 30% 时,平均成本变成 0.031 元,几乎翻倍。所以选型时不能只看模型单价,还要看这个模型在真实任务上的成功率。低成本模型如果经常触发降级,综合成本不一定比高成本模型低。
4.3 超时重试和流式输出带来的成本波动
请求超时重试是另一个容易忽略的成本项。一个请求如果第一次超时,第二次成功,那么第一次请求消耗的 token 同样会计费。把超时时间设得过短,会导致高并发下大量请求重复发送,账单成倍增长;把超时时间设得过长,又会拖慢用户体验。
建议的做法是:评估时按真实业务的重试策略统计“有效请求数”和“总请求数”,计算一个重试放大系数。成本公式修正为:
实际成本 = 单次请求成本 × (1 + 重试率)流式输出本身不改变 token 总数,但会影响超时和断连判断。客户端如果在中途断开流式连接,有些平台会按已经生成的 token 计费,有些平台会取消计费。这个差异也要在选型前确认,否则线上日志里出现大量半截响应,账单仍然照收。
5. 常见问题与排查路径
5.1 现象:账单金额和本地估算差别很大
可能原因:
- 出现了重试请求,账单里有重复记录。
- 缓存字段没有取到,漏算了缓存读取折扣或缓存写入费用。
- 平台对同一请求额外收取了系统级 token,比如自动注入的指令或安全提示。
- 一次业务操作触发了多个模型调用,本地却只算了一次。
检查方式:从平台导出按 request_id 维度的用量明细,和本地记录的request_id、prompt_tokens、completion_tokens逐条对比。先找多出来的请求,再找 token 数对不上的请求。
5.2 现象:同一请求在不同时间价格不同
可能原因:
- 平台的计费规则调整,价格表有版本差异。
- 请求前缀的缓存状态变化,第一次没有命中,之后命中,价格自然会降。
- 平台在不同时段的批量折扣不同,比如夜间批量任务有折扣。
- 通过不同的接入端点调用,价格本来就不同。
检查方式:保留每次请求的时间戳、模型名和端点信息,按小时或按天聚合再看趋势。如果时间、端点和模型名都一致,价格仍然不同,就要重点看缓存命中字段。
5.3 现象:长输入短输出的任务反而比想象中贵
可能原因:
- 多轮对话中每轮都带完整历史,输入 token 是轮数乘以单轮历史长度。
- 长输入没有触发缓存,每次都按全量输入原价计算。
- prompt 被平台自动拆分成多次内部调用,比如需要检索或分析的过程,产生了额外 token。
处理建议:这类任务优先开启上下文缓存,并且把系统提示词、工具定义等固定内容放在前缀最前面,保证前缀稳定。不要让用户消息随机插入系统提示词之前,否则缓存命中率会明显下降。
5.4 排查顺序建议
遇到成本异常时,按照下面的顺序排查:
- 确认对比口径:本地脚本和平台账单是否使用同一请求 ID 和同一统计周期。
- 确认请求数量:先数次数,再数 token,最后才算钱。次数对不上,后面都白算。
- 确认 token 明细:逐条比对
prompt_tokens、completion_tokens和缓存字段。 - 确认重试与降级:看日志里是否有失败后重试或降级转发的记录。
- 确认计费规则:阅读平台最新的计费文档,看是否有最低消费、按请求计费或附加费用。
注意:排查成本问题不要只看总额,一定要拿到请求级明细。总额只能告诉你“贵了”,请求级明细才能告诉你“贵在哪里”。
6. 成本评估的落地建议和可复用检查清单
6.1 把成本评估做成固定工作流,而不是一次性脚本
成本评估应该像性能测试一样进入迭代流程。每次更换模型、调整 prompt 或改变缓存策略,都重新跑一遍样本集。推荐把这个流程固化到 CI 或发布前的检查脚本中,至少包含四步:
- 准备样本集:从线上日志中抽样,保证任务分布真实。
- 跑批评估:对每个候选模型跑同一批样本,记录 token 和耗时。
- 计算成本:按统一价格配置计算单任务成本、成功率和缓存命中率。
- 生成对比报告:输出平均成本、P95 成本、失败率和缓存命中率,供选型决策。
把第 1 步到第 4 步固化成脚本后,任何人拿到新模型,都能在半小时内得到一份可对比的成本报告,而不是靠口头传说。脚本本身没有太高技术门槛,真正有价值的是样本集和统计口径的稳定性。
6.2 发布前成本检查清单
在做模型切换或大版本上线前,建议按表格逐项确认:
| 检查项 | 确认标准 |
|---|---|
| 计费价格 | 使用当前最新价格表,确认是否含税、是否含批量折扣 |
| 样本集 | 覆盖摘要、抽取、代码生成、多轮对话等主要任务 |
| 缓存命中率 | 真实前缀分布下的命中率已统计 |
| 重试率 | 按线上超时和限流配置实测 |
| 降级成本 | 主模型失败后的平均成本已计算 |
| 输出质量 | 同一样本上人工抽检通过率达标 |
| 并发配额 | 模型的 RPM/TPM 限制满足预估峰值 |
| 回滚方案 | 模型切换支持按流量比例灰度,可快速回退 |
这份清单在模型上线前过一遍,能避免大部分“上线后发现账单超预算”的情况。缺少任何一项,都应该在发布说明里明确标注风险。
6.3 成本对比结果的使用边界
成本是选型的重要维度,但不是唯一维度。单位成本只是“单次任务支出”,完整的成本还包括:接入开发成本、prompt 调试成本、推理延迟带来的用户体验损失、失败重试造成的人工运维成本,以及模型效果差异导致的产品指标变化。
因此,当看到“GLM-5.3 成本仅为 FABLE 5 的八分之一”这类信息时,正确的处理方式是把它当作一个待验证的假设,而不是最终结论。先用本文的样本集和脚本在真实任务上复测一遍,再结合成功率、延迟和效果指标做综合判断。真正有价值的不是“便宜八倍”这个数字,而是这个数字在什么条件下成立、你的业务是否满足这些条件。
对刚接触大模型成本评估的团队,建议从 20 条样本的小评估开始,把脚本跑通,把缓存字段接进来,再逐步扩大到完整线上分布。成本评估本身不复杂,复杂的是让评估条件贴近真实生产环境;只要口径统一、样本可复现、数据可追溯,任何成本结论都能经得起验证。