一个客服问答功能,上线前按"每次调用 1,800 token × 每天 2,000 次 × 单价"算出每月约 3,000 元。第一个月账单 9,200 元。查用量日志:日均调用次数对得上,但平均每次消耗的 token 是 4,900,不是 1,800。
三个偏差叠在一起:多轮对话让输入按轮次累积、每次回答比预估长、以及一批"超时重试"的调用没产出一分钱价值。所以"报价"与"账单"的差距从来不在单价,而在你没算进去的那些 token。
1. 一次调用其实有四项计价
| 项 | 计费方式 | 典型单价关系 |
|---|---|---|
| 输入(prompt) | 按 token | 1× |
| 输出(completion) | 按 token | 3–4× |
| 缓存写入 | 按 token(一次性) | 1.25× 左右 |
| 缓存命中 | 按 token(折扣) | 0.1× 左右 |
第二行是报价阶段最容易低估的:输出单价是输入的几倍。所以"回答变长一点"在成本上的影响,比"prompt 多几句话"大得多。
第四行则是省钱的唯一杠杆,但它有前提——前缀必须逐字节稳定(详见第 4 节)。
// 同一个功能的两种输出风格,成本差一倍
简洁模式: 输入 1,200 + 输出 200 → 1,200 × 1 + 200 × 3 = 1,800 单位
详细模式: 输入 1,200 + 输出 800 → 1,200 × 1 + 800 × 3 = 3,600 单位
// 输入没变,只因为"回答更长",成本翻倍
2. 多轮对话的成本是 O(N²),不是 O(N)
第 N 轮的输入包含前 N−1 轮的全部内容。所以 10 轮对话的总输入量 ≈ 1+2+…+10 = 55 个"单轮输入"。
| 轮次 | 本轮输入 | 累计输入(相对单轮) |
|---|---|---|
| 1 | 1× | 1× |
| 3 | 3× | 6× |
| 5 | 5× | 15× |
| 10 | 10× | 55× |
| 20 | 20× | 210× |
第四行就是"客服功能"的账单来源:一次 10 轮的会话,成本是单次问答的 55 倍,而用户感知只是"聊了 10 句"。
// 两种会话策略的成本对比(10 轮,每轮新增 300 token)
全量携带: 输入总量 55 × 300 = 16,500 token
滑窗(3 轮): 输入总量 ≈ 10 × 900 = 9,000 token ← 省 45%
摘要 + 滑窗:输入总量 ≈ 3,600 token + 摘要调用成本 ← 省 70%+
// 代价:滑窗会丢早期约束,摘要会丢细节 —— 这是产品决定,不是技术决定
3. 重试与取消:付了钱但没拿到东西
| 情况 | 成本 | 有无产出 |
|---|---|---|
| 正常完成 | 1× | 有 |
| 超时后重试成功 | 2× | 有(第一次白付) |
| 超时后重试仍失败 | 2×+ | 无 |
| 用户取消(服务端未停) | 1× | 无(生成仍在跑) |
| 流式中断但后端跑完 | 1× | 部分 |
第四行与 S21 的中断主题相关:断连不等于取消,服务端若没收到取消信号会把剩余 token 全部生成完——账单照付,用户早走了。
// 一次"重试放大"的真实形状
基础调用 4,000 token/次,失败率 12%,重试 2 次
有效成本 = 4,000 × (1 + 0.12 + 0.12×0.012) ≈ 4,485 token
// 看起来只多 12%,但如果失败原因是"超时"(每次跑满上下文),
// 实际放大约 20%–25%,而不是账单上看到的 12%
4. 缓存命中:省钱的唯一杠杆,也是最脆的
前缀缓存(prompt caching)只对完全相同的开头生效。任何一处不同——包括空格、时间戳、用户名的位置——都会让它从"命中"变成"未命中"。
| 前缀内容 | 是否稳定 | 命中 |
|---|---|---|
| 固定的 system prompt + 工具描述 | 稳定 | ✅ |
| 每请求注入的当前时间 | 不稳定 | ❌(整段失效) |
| 用户信息放在 system 里 | 每用户不同 | 部分命中 |
| 动态内容放在末尾(user 消息) | 稳定前缀 | ✅ |
第二行是最常见的自毁动作:为了"让模型知道现在几点",在 system prompt 里拼了 当前时间:…,于是缓存永远不命中。正确做法是把可变内容放到末尾。
// 前缀设计的唯一原则:稳定的在前,易变的在后
[system: 角色 + 规则 + 工具 schema] ← 永不变
[retrieved: 检索片段] ← 每次变,但在末尾之后
[user: 问题 + 当前时间] ← 最后
// 顺序一乱,前面的缓存全部作废
5. 归因:三个维度缺一不可
没有归因就没有优化方向。最小可用口径是三元组:
| 维度 | 回答什么 | 怎么打 |
|---|---|---|
| 功能 | 哪个功能在烧钱 | 调用入口打标 |
| 租户/用户 | 谁在用得多 | 请求头带 id |
| 任务类型 | 哪类任务贵(问答 vs 摘要 vs 生成) | 按 prompt 模板分类 |
归因的价值不只是报表——它同时是异常检测:某租户的平均轮次从 3 涨到 12,通常意味着产品侧改了交互,或者上游在死循环重试。而这类异常在总成本曲线上只是一个「缓慢上升的斜坡」,很难被单独发现。三者的组合还能直接指向动作:"某租户 + 摘要类 + 10 轮以上会话" 这个组合如果占了 40 % 成本,优化方向就明确了(限制轮次或改摘要策略),而不是笼统地"优化 prompt"。
6. 排错:五种"账单比预期高"
| 现象 | 八成原因 | 处理方向 |
|---|---|---|
| 平均 token 是预估的 2–3 倍 | 多轮累积 | 滑窗 / 摘要 |
| 缓存命中率接近 0 | 前缀里有动态内容 | 把可变内容挪到末尾 |
| 用量稳定但成本涨 | 输出变长(模板改了) | 查输出 token 分布 |
| 某天成本翻倍 | 重试风暴 | 查超时与失败率 |
| 单租户占比异常 | 无配额 | 按租户限流 |
第二行值得单独强调:命中率 0 时,账单通常是"应该有 60 % 折扣却没拿到"的差额,而不是"忽然变贵"。所以它是"一直贵",只是没人对比过。
-- 一个可用的成本查询(按功能 × 任务类型聚合)
SELECT feature, task_type,
COUNT(*) AS calls,
SUM(input_tokens) AS in_tok,
SUM(output_tokens) AS out_tok,
SUM(cache_hit_tokens) AS cached,
ROUND(SUM(input_tokens + output_tokens * 3) / COUNT(*)) AS avg_unit
FROM llm_usage_log
WHERE dt = CURRENT_DATE - 1
GROUP BY feature, task_type
ORDER BY avg_unit DESC;
-- 判据:avg_unit 明显高于"单轮基准"的行,就是多轮累积的重灾区
7. 边界:什么时候不该优化 token
| 场景 | 为什么不必 | 说明 |
|---|---|---|
| 调用量很小 | 省下的钱 < 优化成本 | 先看总量 |
| 质量是硬约束(医疗/法务) | 省钱会降质 | 不优化输入 |
| 已在预算内 | 无收益 | 只监控 |
| 延迟比成本重要 | 缓存可能增延迟 | 权衡 |
| 一次性任务 | 无长期成本 | 不管 |
判断口径一句话:成本优化的顺序是"先归因、再改策略、最后才抠 prompt"——因为大头永远在"多轮累积"与"输出变长",而不是"某句话写长了三个字"。而缓存是唯一"改一处、永久生效"的杠杆,前提是前缀稳定。
8. 自测
- 一次调用有哪四项计价?哪一项的单价通常是输入的几倍?为什么这会放大"回答变长"的影响?
- 多轮对话为什么是 O(N²)?10 轮相对单轮是多少倍?
- "超时重试"的实际放大比例为什么常高于失败率本身?
- 前缀缓存失效的最常见原因是什么?正确的前缀顺序应该怎么排?
- 成本归因的三个维度分别回答什么?为什么缺一个就无法定向优化?
- 成本优化的正确顺序是什么?什么情况下根本不该优化?
9. 一句话总结
Token 账单的偏差几乎从不在单价,而在"没算进去的那些":多轮累积是 O(N²)(10 轮 = 55 倍单轮)、输出单价是输入的 3–4 倍(回答变长比 prompt 变长更贵)、重试与未取消的生成是"付钱无产出"。唯一"改一处永久生效"的杠杆是前缀缓存,而它只对"稳定前缀"生效——把动态内容挪到末尾即可。判据:先归因(功能 × 租户 × 任务类型)、再改策略、最后才抠 prompt。