本节摘要:成本观测的第一课是口径:一次调用的 token 必须分三本账记——输入账(prompt 进去多少)、输出账(completion 出来多少)、缓存账(多少输入命中了 prompt 缓存、多少是新写入)。为什么要拆?因为单价差出一个数量级:输出单价通常为输入的 3~5 倍(示意),缓存命中价约为输入价的 1/10(官方,Anthropic 口径),缓存写入还要略加价。只记总数,你既不知道钱被谁吃掉,也不知道缓存帮你省了多少。本节给出记账与日结算代码(纯标准库),盘点三个典型口径错误,并给出成本突增的三个典型来源——它们是第 3.2 节分摊与第 5 章诊断的案发起点。
| 账本 | 记什么 | 单价量级(以某旗舰模型示意) | 说明 |
|---|---|---|---|
| 账一 · 输入 | 未命中缓存的输入 token | 3 美元/百万 token(示意) | prompt 中真正按原价计费的部分 |
| 账二 · 输出 | 模型生成的 token | 15 美元/百万 token(示意) | 通常是输入的 3~5 倍(示意,各家不同) |
| 账三 · 缓存 | 缓存命中(读)token | 约 0.3 美元/百万 token | 约为输入价 1/10(官方,Anthropic 口径) |
| 缓存写入 token | 约 3.75 美元/百万 token | 写入加价约 1.25 倍(官方,Anthropic 口径) |
三个推论值得背下来:
⚠️ 各供应商的缓存规则差异很大(有的自动前缀缓存、有的要显式标记缓存段落、有的不提供缓存计价),字段名与口径一律以其官方定价页为准,本节单价为示意值。
数据来源是第 2.2 节挂在 span 上的 usage(供应商响应字段,官方口径)。账本行 = 三本账 token 数 + 分摊键 + trace_id。
# token_ledger.py —— token 三本账:记账、算钱、出日结算(纯标准库) from collections import defaultdict from dataclasses import dataclass # 单价表:美元 / 百万 token(示意值,务必替换为供应商当前官方定价) PRICE = { "demo-large": { "input": 3.00, # 账一:输入 "output": 15.00, # 账二:输出 "cache_read": 0.30, # 账三:缓存命中(约为输入价 1/10,官方口径量级) "cache_write": 3.75, # 缓存写入(约 1.25 倍输入价,官方口径量级) }, } @dataclass class Usage: model: str input_tokens: int = 0 # 未命中缓存的输入 output_tokens: int = 0 cache_read_tokens: int = 0 # 命中缓存的部分(不再按账一计价) cache_write_tokens: int = 0 def cost(self) -> float: """返回本次调用费用,单位:美元。""" p = PRICE[self.model] m = 1_000_000 return (self.input_tokens * p["input"] + self.output_tokens * p["output"] + self.cache_read_tokens * p["cache_read"] + self.cache_write_tokens * p["cache_write"]) / m class TokenLedger: def __init__(self): self.rows: list[Usage] = [] def record(self, model: str, **tok) -> None: self.rows.append(Usage(model=model, **tok)) def daily_summary(self) -> None: acc = defaultdict(lambda: [0.0, 0, 0, 0, 0]) # 费用 + 三本账 + 缓存写 for u in self.rows: a = acc[u.model] a[0] += u.cost() a[1] += u.input_tokens; a[2] += u.output_tokens a[3] += u.cache_read_tokens; a[4] += u.cache_write_tokens total = sum(v[0] for v in acc.values()) for model, a in acc.items(): saved = a[3] / 1_000_000 * (PRICE[model]["input"] - PRICE[model]["cache_read"]) print(f"{model}: 费用 {a[0]:.4f} 美元 | 输入 {a[1]:,} | 输出 {a[2]:,}" f" | 缓存读 {a[3]:,} | 缓存写 {a[4]:,} | 缓存节省约 {saved:.4f} 美元") print(f"合计:{total:.4f} 美元") if __name__ == "__main__": ledger = TokenLedger() # 场景(示意):2 万 token 系统提示词首轮写入缓存,随后两轮命中 ledger.record("demo-large", input_tokens=800, output_tokens=600, cache_read_tokens=20_000, cache_write_tokens=0) ledger.record("demo-large", input_tokens=500, output_tokens=700, cache_read_tokens=21_000, cache_write_tokens=0) ledger.record("demo-large", input_tokens=600, output_tokens=400, cache_read_tokens=0, cache_write_tokens=20_000) ledger.daily_summary()
输出(示意):
demo-large: 费用 0.1185 美元 | 输入 1,900 | 输出 1,700 | 缓存读 41,000 | 缓存写 20,000 | 缓存节省约 0.1107 美元 合计:0.1185 美元
注意两个设计:命中缓存的输入不进账一(供应商计费时它按缓存价走,重复计价必错);缓存节省额单独算——这个数字是向管理层证明"缓存工程值得做"的最短路径。真实系统里 record 的调用点就在第 2.2 节的 llm.call span 闭合处,并附带 team/feature/user 键(第 3.2 节直接消费)。
total_tokens。这不是省事,是销毁证据——三本账一旦合并,成本归因(哪本账涨的)与缓存运营(省了多少)全部失效。历史数据救不回来,从今天开始拆。把三本账摆开,成本突增基本逃不出三类(第 5.1 节故障分类会引用此处):
账一涨(输入膨胀)──▶ 多轮会话历史重发变长 / 记忆注入加码 / few-shot 示例堆砌 账二涨(输出失控)──▶ max_tokens 形同虚设 / agent 生成冗长过程 / 结构化输出失败重试 账三变(缓存失效)──▶ prompt 前缀被改动导致缓存全 miss / 会话中断重写 / 命中率跌破基线
排查口诀:先看三本账哪本涨,再看分摊哪个维度涨,最后下钻到 prompt 版本——三步分别是本节、第 3.2 节与第 2.2 节版本标签的连用。
💡 建议给账本配一张"成本结构周报":三本账占比 + 缓存命中率 + 单次调用成本 P50/P95。三本账占比变了,往往比总额变了更早暴露问题(例如输出占比悄悄从 40% 爬到 60%)。
账记准了,但"全公司一个月 4 万美元"仍然无法行动。第 3.2 节把总账摊到团队、功能与用户头上,再和供应商的账单对一次——摊清了,责任与优化方向才会自己浮出来。