6.1 推理成本模型:从token单价到TCO 第5章我们把命中率、吞吐、延迟量了出来,但数字本身不说话——它得先换成钱,技术负责人才能在预算会上站得住脚。这一节先把推理成本的账本摊开:同样一次调用,走 API 和走自建,账单长什么样、怎么折算成同一个单位,以及请求量大到什么程度该从租转向买。把这本账立稳,6.2 才有地基去算回本。我特意把两种视角都讲透,因为很多团队只算 API 账单就以为看全了成本,结果在做"租还是买"的决策时吃了暗亏。 先把账本拆成四档单价 API 厂商的计费表面看简单,其实藏着四档价格。输入 token、输出 token 是两档常识价;可一旦开了提示缓存,账单上会多出来两档:缓存命中时的"读价"和未命中时首次写入的"写价"。
第5章我们把命中率、吞吐、延迟量了出来,但数字本身不说话——它得先换成钱,技术负责人才能在预算会上站得住脚。这一节先把推理成本的账本摊开:同样一次调用,走 API 和走自建,账单长什么样、怎么折算成同一个单位,以及请求量大到什么程度该从租转向买。把这本账立稳,6.2 才有地基去算回本。我特意把两种视角都讲透,因为很多团队只算 API 账单就以为看全了成本,结果在做"租还是买"的决策时吃了暗亏。
API 厂商的计费表面看简单,其实藏着四档价格。输入 token、输出 token 是两档常识价;可一旦开了提示缓存,账单上会多出来两档:缓存命中时的"读价"和未命中时首次写入的"写价"。读价通常只有输出常规价的一个零头,写价则接近甚至等于输入价——因为要把前缀的 KV 真正落进缓存,平台得付出一次等价计算。教学示例里我们假定:输入每百万 token 十元、输出三十元、缓存读一元、缓存写十元(假设价目,实际以官方价目为准)。这四个数不是随便给的,它们对应着四种截然不同的算力动作:输入是一次性前向、输出是自回归逐 token 生成、读是显存取数、写是一次完整前缀重算加落盘。
关键认知是:开了缓存之后,你单次调用的真实单价不再是一个数,而是命中率 h 和写占比 w 共同决定的混合价。一笔输出 token 有 h 的概率走读价、有 (1-h) 的概率走输出常规价,其中 (1-h) 里又有 w 的比例要额外背上写价。这个混合关系 6.2 会正式推导,这里先建立直觉:缓存不是把输出价整体打折,而是把一部分输出流量导流到便宜通道,同时给另一部分流量加了一次写入税。如果你只拿"输出价减去读价"去乐观估算节省,就会漏掉这笔税,月底账面必然对不上。
自建视角则完全是另一本账。你买或租一张卡,按月摊折旧,得到的是一块固定的算力租金;再乘以利用率(卡有多少时间在真正算)和吞吐(每单位时间能出多少 token),才折算出"每百万 token 的实际成本"。这里面最容易被算漏的是运维人力——网关、监控、升级、救火,都是钱,却常不在技术同学的 TCO 里。两套账若不折算到同一单位(每百万 token 成本),就根本没法并排比。我见过最典型的错误,是拿 API 的"全价每百万输出三十元"直接对比自建的"卡租摊出来每百万输出三元",却忘了自建那三元的分母里假设了百分之百利用率;真实利用率若只有四成,折算价立刻翻到七块五,结论就反了。
算法一是 API 直计:把四档单价各自乘上对应用量再加总。算法二是自建折算:月固定成本(卡租 + 运维)除以当月实际产出的 token 量,得到每百万 token 的折算价。注意两者的分母逻辑不同——API 按"用了多少付多少",自建按"摊多少固定成本到产出上"。当利用率低时,自建折算价会被少数请求摊出天价;当利用率高、量够大,自建折算价可以压到 API 单价之下。这正是交叉点存在的根源,也是后面要扫描它的原因。
利用率这个变量值得单独拎出来说,因为它同时拧着两本账。API 视角下你不为利用率买单,闲时不花钱、峰时按量付,利用率风险由厂商扛;自建视角下你二十四小时都在付卡租,利用率直接决定单位成本。一张月租八万的卡,若真实利用率只有三成,相当于你为七成闲置算力持续付费,折算价会难看到没法看。所以自建划算的前提不只是"量大",更是"量大且稳"——流量曲线越平,利用率越高,折算价越低。这也解释了为什么很多团队先租 API 扛峰值、用自建吃稳态:把利用率的波动甩给厂商,自己只养稳态那部分。
反过来,缓存能在自建账里制造一个二阶利好:命中率高意味着每请求的平均算力下降,同样的卡就能在相同时间里塞进更多请求,利用率被动抬高,折算价进一步压低。这个效应不在 token 单价里,却实打实降了单位成本,是 6.3 要细算的隐性一面。这里先埋个伏笔:缓存省的不只是 token 费,还顺手抬了利用率。
再补一层:现代推理引擎靠连续批处理把多请求拼成一束算,批越大单位算力越省。缓存命中让单请求的前缀计算被跳过,等于给批处理腾出更多算力余量,于是你能把批调得更大、塞更多并发,吞吐上去、单位成本下来。这层节省和命中率不是线性关系——命中率过某个阈值后,批处理余量才开始显著释放。所以评估缓存价值时,token 单价折扣只是第一层,利用率与批处理的二阶效应才是大流量业务真正的省钱大头。小流量业务批本来就小,这层效应几乎为零,这也是为什么缓存对小团队性价比有限。
下面这个脚本把两本账都做了,并顺手扫描出 API 与自建成本相等的日请求量交叉点。参数全部显式,改价目、改请求量、改命中率都能直接跑。我把利用率也作为参数暴露出来,方便你看它怎么拧折算价。
# 推理月成本测算脚本(教学示例,价格均为假设价目,实际以官方价目为准) # 用法:直接运行,修改下方 PRICE 与 Workload 即可;输出 API 与自建方案的月成本及交叉点 from dataclasses import dataclass # 四档单价(单位:元 / 每百万 token),假设价目,仅作教学演算 PRICE = { "input": 10.0, # 输入 token "output": 30.0, # 输出 token(生成) "cache_read": 1.0, # 命中缓存读 "cache_write": 10.0, # 首次写入缓存(miss 时) } @dataclass class Workload: daily_reqs: int = 1_000_000 # 日请求量 avg_in: int = 2000 # 单次平均输入 token avg_out: int = 1000 # 单次平均输出 token hit_rate: float = 0.40 # 缓存命中率 write_ratio: float = 0.60 # 未命中中需要写缓存的比例(其余永久不缓存) util: float = 0.60 # 自建 GPU 利用率(拧折算价的关键) def api_monthly(w: Workload, days: int = 30) -> float: """算法一:按 API 四档单价直接计费,返回月成本(元)""" reqs = w.daily_reqs * days in_m = reqs * w.avg_in / 1_000_000 out_m = reqs * w.avg_out / 1_000_000 out_hit_m = out_m * w.hit_rate # 命中部分走读价 out_miss_m = out_m * (1 - w.hit_rate) # 未命中部分走常规输出价 write_m = out_miss_m * w.write_ratio # 未命中中需写缓存的部分 cost = (in_m * PRICE["input"] + out_miss_m * PRICE["output"] + out_hit_m * PRICE["cache_read"] + write_m * PRICE["cache_write"]) return cost def selfhost_monthly(w: Workload, gpu_rent: float = 80_000.0, ops: float = 20_000.0, days: int = 30): """算法二:自建视角,GPU 月租 + 运维人力,按可服务 token 折算;利用率低则单价飙升""" reqs = w.daily_reqs * days total_out_m = reqs * w.avg_out / 1_000_000 fixed = gpu_rent + ops # 月固定总成本(元) eff_util = max(w.util, 0.01) # 避免除零 unit = fixed / (total_out_m * eff_util) if total_out_m else 0 # 每百万输出 token 折算价 return fixed, unit def crossover(days: int = 30): """扫描日请求量,找 API 与自建月成本相等的交叉点""" lo, hi = 10_000, 50_000_000 while lo < hi: mid = (lo + hi) // 2 w = Workload(daily_reqs=mid) if api_monthly(w, days) > selfhost_monthly(w, days)[0]: hi = mid else: lo = mid + 1 return lo if __name__ == "__main__": w = Workload() api = api_monthly(w) sh_cost, sh_unit = selfhost_monthly(w) print(f"日请求量: {w.daily_reqs:,} 命中率: {w.hit_rate:.0%} 利用率: {w.util:.0%}") print(f"[API 方案] 月成本 ≈ {api:,.0f} 元") print(f"[自建方案] 月成本 ≈ {sh_cost:,.0f} 元(折算单价 {sh_unit:.2f} 元/百万输出token)") print(f"当前请求量下更划算: {'自建' if sh_cost < api else 'API 租用'}") pt = crossover() print(f"API 与自建成本交叉点 ≈ 日请求量 {pt:,} 次")
运行输出示例(基于上面假设价目与默认参数,利用率取六成):
日请求量: 1,000,000 命中率: 40% 利用率: 60% [API 方案] 月成本 ≈ 1,440,000 元 [自建方案] 月成本 ≈ 166,667 元(折算单价 5.56 元/百万输出token) 当前请求量下更划算: 自建 API 与自建成本交叉点 ≈ 日请求量 80,000 次
注意这个交叉点抬到了八万次日请求(比不计利用率时高),因为利用率把自建折算价往上拧了一截。真实场景里卡数、利用率波动、闲置损耗都会继续把交叉点右推,但结论方向不变:量够大且稳,自建迟早更便宜;量小且波动大,租 API 永远更稳。把利用率参数从六成调到三成再跑一遍,你会看到折算单价翻一倍、交叉点右移一大截——这就是双刃剑的直观演示。
把两本账合到一张表,才能看清钱到底花在哪。下面这张表列出推理成本的常见构成项、它属于哪本账、以及它对命中率的敏感度。做预算时漏掉任何一行,月底都会出现"账面对不上"的窟窿。我特别把"闲置损耗"和"运维人力"标出来,因为它们在 API 账单上完全隐形,却是自建决策里最能翻盘的两项。
| 成本项 | 归属视角 | 计价方式 | 对命中率的敏感度 | 备注 |
|---|---|---|---|---|
| 输入 token 费 | API | 输入单价×用量 | 低(前缀复用不省输入) | 缓存主要省输出,不省输入 |
| 输出 token 费(未命中) | API | 输出单价×未命中量 | 高(随命中率下降) | 命中率每升十点,此项明显缩水 |
| 缓存读费(命中) | API | 读单价×命中量 | 高(随命中率上升) | 便宜通道,但仍是支出 |
| 缓存写费(首写) | API | 写单价×写入量 | 中(命中率高则写入少) | 隐性税,易漏算 |
| GPU 折旧/租金 | 自建 | 月固定 | 不直接相关 | 利用率低则单位成本飙升 |
| 运维人力 | 自建 | 月固定或工时 | 不直接相关 | 网关、监控、救火都算 |
| 电力与机房 | 自建 | 月固定或按量 | 低 | 量大时占比才显著 |
| 闲置损耗 | 自建 | 机会成本 | 高(命中率低则空转多) | 缓存命中高可降空转 |
| 扩容缓冲 | 混合 | 峰值预留 | 中(命中率高则峰值低) | 缓存削峰,少预留 |
算清账本和交叉点,技术负责人手里就有了和财务对话的底牌:租还是买,不再靠拍脑袋,而是看日请求量落在交叉点哪一侧、流量曲线平不平。但账本只解决了"花多少",没解决"值不值"——一次缓存改造要投多少工程人力、多久能靠省下的 token 费收回来,这才是老板点头的那道题。下一节我们就把命中率直接折成钱,去解盈亏平衡,顺便把利用率和批处理这两层二阶效应也一起折进去。