先搞清楚:长上下文到底贵在哪
很多人第一次看到 Claude 的账单会愣一下——明明只发了一句”继续”,费用却比前一天发了三千字的对话还高。原因不在于你输出的那几个 token,而在于每一次请求都要把完整的 messages 数组重新算一遍输入。API 是无状态的,模型不记得你上一轮说过什么,所谓”对话记忆”是你自己在 messages 里一遍遍粘回去的。
换句话说,长上下文不是一份”存档”,而是一份每次都重新投递、每次都要重新过账的快递。一个 50 轮的技术支持对话,如果每轮都带完整历史,第 50 轮实际上是把前 49 轮的内容又买了一遍。
术语对齐:Claude 的 input_tokens 与 output_tokens 价格不同,输出通常贵数倍。缓存命中的 cache_read_input_tokens 价格远低于普通输入。所以”省 token”的第一优先级永远是压缩输入侧的历史,而不是抠输出字数。
下面这张表把账单里最常见的几个 token 字段摊开,它们决定了你的钱花在哪里:
| 字段名 | 含义 | 计价方式 | 常见踩坑 |
|---|---|---|---|
input_tokens |
本次请求未命中缓存的输入 | 全价输入费率 | 把整段知识库文档每次都塞进 system |
cache_creation_input_tokens |
首次写入缓存的输入 | 约为全价的 1.25 倍 | 缓存前缀频繁变动,永远写不进缓存 |
cache_read_input_tokens |
命中缓存的输入 | 约为全价的 0.1 倍 | 没设 cache_control,本可命中却全额付费 |
output_tokens |
模型生成的输出 | 输出费率,最贵 | 让它”顺便解释一下”,输出翻倍 |
| 工具往返累积 | 多轮 tool_use 叠加的历史 | 逐轮重复计入输入 | 工具返回的原始 JSON 全量留在上下文里 |
哪些 token 在每次请求里被重复计费
对话历史:最大的沉默成本
每一次请求,你传过去的 messages 里所有历史消息都会被算作输入。用户的问题、你自己上一轮的回复、工具返回的原始数据,全部在内。一段线性增长的对话,累计输入成本大致是二次增长的——第 10 轮便宜,第 80 轮开始烧钱,而恰恰是第 80 轮通常最需要上下文。
很多团队没意识到这一点,直到某天账单从几百跳到几千。一个不做压缩的 100 轮对话,累计输入 token 约为单轮的数十倍。
工具调用返回:隐形的体积来源
Agent 场景最典型的浪费:一次搜索返回 20 条结果、一个数据库查询返回 500 行 JSON、一个网页抓取返回整页 HTML。这些内容作为 tool_result 进入上下文后,后续每一轮都会跟着走。你以为只用了一次,实际上它在你剩下的 30 轮里被重复计费了 30 次。
工具的原始返回值是上下文膨胀的头号来源,而且通常是一次性用途——你只需要其中那三五个字段。把它原样留在历史里,等同于每天为一个用不上的附件付快递费。
system prompt:固定但不免费
system prompt 每轮都在,内容固定,是缓存的最佳候选。但如果你的 system 里塞了会变的东西——当前时间戳、用户名、随机排序的示例、动态注入的配置——缓存永远命中不了,你就一直在按全价买同一段文字。
三把刀:压缩、缓存、裁剪
第一刀:压缩历史
不要保留全部原文。策略上分三层:最近 N 轮保留完整原文;更早的轮次折叠成摘要;关键事实(用户身份、已确认的结论、未完成的待办)单独抽到一个结构化的 state 字段里长期携带。
摘要用什么模型做?不建议用最强的模型做摘要,那是用贵的刀切便宜的菜。用便宜的小模型批量摘要即可,成本可以忽略。
第二刀:缓存复用
给固定前缀打上 cache_control,让 system prompt 和稳定的大块资料进入缓存,后续请求走 cache_read_input_tokens 的低价通道。关键约束是:缓存以请求前缀为粒度,顺序在前的任何改动都会让后面全部失效。所以把最稳定的内容放最前面,把每轮都变的用户问题放最后。
{
"model": "claude-sonnet-4-5",
"system": [
{
"type": "text",
"text": "你是我们的工单助手。下面是完整的产品手册……",
"cache_control": {"type": "ephemeral"}
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "<产品手册全文,约 40k tokens>",
"cache_control": {"type": "ephemeral"}
},
{"type": "text", "text": "用户本次问题:退款流程是什么?"}
]
}
]
}
第三刀:按需裁剪
不是所有请求都需要全量上下文。把请求分类:简单问答(不需要历史,只带最近 2 轮)、多步任务(带摘要 + 最近 5 轮)、深度分析(带完整历史但走缓存)。按请求类型分配上下文预算,比统一策略能省掉大半无效输入。
做法 A:全量历史 + 无缓存
- 实现成本最低,一次写完
- 输入成本随轮次线性上涨,累计近乎二次
- 长对话后期延迟明显变高
- 适合:单轮、短对话、内部脚本
做法 B:前缀缓存 + 历史滚动摘要
- 需要设计
cache_control断点和摘要触发阈值 - 稳定前缀命中后输入成本可降到约十分之一
- 摘要本身有信息损失,需保留 state 字段兜底
- 适合:客服、Agent、长文档问答等主流量产场景
明确结论:只要你的对话或文档问答平均超过 5 轮,就应该上做法 B,没有悬念。做法 A 只在一次性脚本和纯单轮调用里合理,别拿它跑线上 Agent。
一套可以照抄的接入流程
- 先量基线把
response.usage里的input_tokens、cache_creation_input_tokens、cache_read_input_tokens、output_tokens全部打点到日志,按 request 维度聚合。没有这条基线,后面所有优化都是拍脑袋。 - 重排上下文结构顺序固定为:system(稳定)→ 长资料(稳定)→ 结构化 state(缓慢变化)→ 最近几轮原文 → 本轮用户输入。稳定段打
cache_control。 - 设定摘要阈值当历史超过设定的 token 预算(比如 60% 窗口占用)时触发摘要,把最早的一批轮次折叠。摘要调用放异步队列,用便宜模型跑。
- 裁剪工具返回值在 tool_result 进入上下文前做一层投影:只保留业务需要的字段,丢弃嵌套元数据和分页信息。这一条往往能砍掉 40% 以上体积。
- 分流请求按任务复杂度路由到不同模型和不同上下文策略,简单请求不要带着完整历史去敲最贵的模型。
- 回归验证效果压缩不是免费的,摘要会丢信息。准备一组固定测试集,跑优化前后的回答质量对比,质量掉点超过可接受范围就提高原文保留轮数。
实操小技巧:把 cache_control 打在长资料的最后一个块上,而不是每个块都打。缓存断点数量有限,用在该用的地方,多打反而增加写入成本。
具体能省多少,以及什么时候不该省
给一个粗略的量级感(价格为区间,具体以官方价目为准,随模型和档位差异较大):
| 场景 | 优化前输入成本 | 优化后输入成本 | 主要手段 |
|---|---|---|---|
| 20 轮客服对话 | 约 200k tokens | 约 30–45k tokens | 缓存命中 + 滚动摘要 |
| 长文档问答(40k 文档) | 每轮 40k+ | 每轮约 4–6k 有效计价 | 文档前缀缓存 |
| Agent 工具链(10 次工具调用) | 约 120k tokens | 约 40–60k tokens | tool_result 投影裁剪 |
| 单轮分类请求 | 约 2k tokens | 约 1.2k tokens | 去掉无关历史 |
注意第三行——工具链是压缩收益最高、风险也最低的一块,因为工具返回的原始 JSON 绝大多数内容模型根本用不上。如果你只做一件事,先做 tool_result 裁剪。
什么时候不建议压缩:合规审计、法律文书分析、代码 review 这类场景,摘要造成的细节丢失代价远高于 token 成本,宁可付费保留全文。另外,调试期不要为了省钱把日志精简掉,你会付出更多时间成本。
最后三条硬规矩
- 每次请求都问自己”这段内容这轮真的需要吗”——上下文里的每一个 token 都是要付钱的,包括上一轮那个已经解决了的报错堆栈。
- 让稳定内容命中缓存,让变化内容排在后面——缓存设计本质是排序问题,不是配置问题。前缀顺序错了,缓存等于没有。
- 优化必须带质量回归——省 token 的终点是省钱且效果不掉,只省钱的效果崩了,等于把成本转移到了重试和客诉上。