
最近折腾 Claude Code 的额度消耗,踩了一个很容易被忽略的坑:提示缓存一旦过期,休息后的第一条消息,可能比正常消息贵 12~80 倍。我把自己的 10,000 次请求摊开算了一遍,下面把冷会话税和应对方法说清楚。
核心结论
我有一个 Claude Code 会话几乎开了一整天,而且质量还不错:上下文已经达到 250k tokens,智能体知道代码库、当前计划,以及我们已经放弃的三种方案。我去吃了个午饭,回来只输入了一行:“ok, carry on.”
这一行消耗的额度,比之前二十条消息加起来还多。
并不是哪里出了问题。我只是交了后来所说的冷会话税。开始统计后,我发现几乎每个昂贵会话里都有这笔开销。
模型本身不会跨请求记住任何内容。每发送一条消息,Claude Code 都会把整段对话重新发一遍:系统提示词、项目上下文、之前的每条消息和工具结果,再加上你刚输入的新内容。
听起来像是成本灾难,但实际上并不会这么夸张,因为每次请求中的绝大部分内容都和上一次相同。于是,API(应用程序接口)会缓存它们,Anthropic 将这项机制称为提示缓存。正常交互时,请求开头未变化的部分会直接从缓存读取,只按普通输入价格的一小部分计费,只有新增内容需要完整处理。
问题在于缓存会过期。它有一个生存时间(TTL),而每次命中缓存的请求都会重置这个计时器。持续工作,缓存就会一直保持热态;一旦停下,它就会变冷。
缓存有两种有效期。使用 Claude 订阅时,在套餐额度范围内,主对话使用 1 小时缓存;使用 API 密钥或用量积分时,有效期是 5 分钟。喝杯咖啡的功夫,就足以让 5 分钟缓存失效。1 小时缓存能扛过一场会议,但扛不过午饭时间。
缓存过期后,没有任何内容可以直接读取。恢复工作的第一个请求必须重新处理全部上下文,并将其写入缓存。缓存写入的价格高于普通输入,缓存读取则便宜得多,两者之间的差距就是所谓的“税”。

所以,仅计算请求中的上下文部分,休息后的第一条消息成本就是热会话的 12~80 倍,具体取决于模型和缓存有效期。
按 API 公开价格折算:一个 241k-token 的 Opus 5.5 会话使用 1 小时缓存时,热会话读取上下文的成本约为 5 美分。吃完午饭后,下一条消息重新写入缓存,大约需要 1.93 美元。这相当于消耗了 40 次热会话的额度,而 Claude 甚至还没开始输出第一个字。
订阅用户看不到具体美元金额,但缓存重新加载仍然会计入每 5 小时和每周的使用额度。如果长时间离开后回来,额度消耗速度突然超出预期,通常就是这个原因。Anthropic 没有公开订阅额度中缓存读写的确切权重,因此这些倍率更适合用来理解成本形态,不适合当作精确账单。
我想知道这个问题在日常使用中到底有多严重,于是检查了自己 50 多个 Claude Code 对话记录,总计约 10,000 次 API 请求,其中四分之三使用的是 Opus 5.5。每次请求都记录了上下文大小、缓存读取量、缓存写入量和时间戳,足够区分热请求和冷请求。
有三个结果出乎我的意料。
1. 大型热会话比想象中便宜。我以前认为,每条消息的成本下,500k 会话应该是 50k 会话的 10 倍。实际并非如此。新增 tokens 和输出才是每次请求的主要成本,缓存上下文只是额外增加了一小部分。一个 500k 的热请求,成本大约是小会话请求的 2~3 倍,而不是 10 倍。
2. 真正花钱的是休息时间。会话越大,休息后的冷加载占总成本的比例越高。低于 100k 时,这个比例约为五分之一;达到 500k 后超过 40%;从 600k 开始则达到 60%~66%。这些会话的大部分成本,都花在了每次中断后的第一条消息上。

这里使用的是估算的订阅权重,因为 Anthropic 没有公开准确权重。应该把这些数字当作趋势,而不是精确值。
冷加载占全部会话成本的比例,按上下文大小划分(约 10,000 次 Claude Code 请求)
| 上下文大小 | 请求数 | 冷加载成本占比 |
|---|---|---|
| 0–100k | 2,269 | 21% |
| 100–200k | 3,093 | 28% |
| 200–300k | 1,255 | 25% |
| 300–400k | 995 | 31% |
| 400–500k | 747 | 36% |
| 500–600k | 569 | 42% |
| 600–700k | 469 | 62% |
| 700–800k | 363 | 60% |
| 800–900k | 211 | 66% |
3. 一条“消息”实际上会触发五到十次请求。当你输入一个提示词后,智能体还会读取文件、执行命令、修改代码。这些步骤各自都会产生一次 API 请求,并重新读取上下文。我的数据中,单条提示词对应的请求数中位数是 5,平均值是 10。这一点后面还会用到。
不过,我没有发现智能体随着会话变大而变得马虎。即使上下文超过 800k,重新读取已经读过的文件仍然只占 1%~2%。真正带来变化的是切换任务:在同一个会话中改做另一个功能后,重复读取和我的纠错次数大约翻倍。样本不多,只有 31 次任务切换,但结论与成本数据一致:无论上下文有多大,中断后或切换任务后,都是开启新会话的更好理由。
/compact 会总结对话,并基于摘要继续工作。缓存仍处于热态时,摘要请求可以直接从缓存读取历史,因此成本很低。休息结束后再执行同一命令,则要按全价重新处理整段历史。准备压缩,就应该在午饭前完成,不要等到回来以后。
如果使用 1 小时缓存,而且确定很快会回来,还有一个办法:在缓存过期前发送一条简短消息。它会用很低的成本重置计时器。没必要整晚都这么做,使用 5 分钟缓存时更不可能这样操作。
恢复会话本身不收费。claude --resume 会从磁盘加载对话记录并展示出来,在提交提示词之前不会发送任何内容。这是一个不花钱的决策窗口。
如果会话很大,而且缓存已经冷了,输入 /clear,开启一个新会话。新会话从小规模开始,而不是重新加载 250k 的上下文。代价是原有上下文会丢失,因此需要把关键信息带过去:准备一份简短的交接文档,写清目标、正在处理的文件、已完成内容、下一步计划,以及已经否决的方案。一份好的交接文档应该是几百到 1,500 tokens,而不是几十万 tokens。
新会话需要重新摸索一部分上下文,这样真的划算吗?根据我的数据,新会话通常从约 50k 开始,之后还要再消耗约 40k 来进入状态。与每次请求都携带大型上下文的额外成本相比,大约 7~12 次请求就能回本。按每条提示词对应 5~10 次请求计算,也就是 一到两条提示词。如果当前任务还剩不止几条提示词,那么即使旧会话的缓存仍然很热,开启新会话也更便宜。
一个 30k-token 的会话即使冷加载,依然是廉价请求。别想太多,这套仪式只留给大型会话。
没必要只相信我的数字。Claude Code 会把每个会话保存为 JSONL(JSON Lines,逐行 JSON)文件,位置在 ~/.claude/projects 下。每条回复的使用记录都包含上下文大小、时间戳,以及缓存写入的是 5 分钟还是 1 小时。如果与上一条回复的间隔超过对应有效期,这条回复就是一次冷加载。
最简单的方式是让 Claude Code 自己分析。开启一个新会话,避免分析本身消耗大型上下文,然后粘贴下面这段提示词:
Analyze my Claude Code history for cold-cache reloads. Don't read the
transcript files into your context: write and run a Python script, and
never print message text. Data: every *.jsonl file under ~/.claude/projects. Use lines with
"type": "assistant" that have message.usage, counting each message.id once. For each reply:
- context size = input_tokens + cache_creation_input_tokens + cache_read_input_tokens
- cache lifetime = 1 hour if the latest earlier reply in the same file that wrote to the cache had cache_creation.ephemeral_1h_input_tokens > 0, otherwise 5 minutes
- it's a cold reload if the gap since the previous reply in the same file is longer than that lifetime Estimate cost in units of one normal input token: uncached input 1x,
output 5x, cache write 2x (1-hour) or 1.25x (5-minute), cache read 0.1x,
or 0.05x if message.model contains "opus-5-5" or "sonnet-5-5", or 0.025x
if it contains "fable-5". Report:
1. How many cold reloads there were above 100k tokens, and the date range.
2. The share of total estimated cost that came from cold reloads, by context size: 0-100k, 100-200k, 200-400k, 400-600k, 600k+.
3. The 5 most expensive cold reloads: date, session file, context size, model, and how long the gap was.
4. For each of those, how many warm turns of the same size it cost.
5. Two or three habits that would have avoided most of this cost.
这些权重来自 API 公开价格,并以普通输入成本为基准。订阅用户应该把结果理解为成本形态,而不是最终账单。
如果不想消耗 Claude Code 的额度,可以使用下面的脚本在本地完成相同分析。它只读取 token 数量和时间戳,不会读取或打印消息内容。报告内容包括:你有多少次在缓存过期后返回会话、这些冷启动花了多少钱、离开会话多久,以及哪些会话受影响最严重。
"""Cold start report for Claude Code. Reads your local transcripts (~/.claude/projects/*/*.jsonl) and reports every
time you came back to a session after its prompt cache had expired, and what
that cost. Only token counts and timestamps are read; no message text is printed.
"""
import glob
import json
import os
from collections import defaultdict
from datetime import datetime # API list prices per million tokens: base input, cache read.
# Cache writes cost 2x base input on a 1-hour cache, 1.25x on a 5-minute one.
PRICES = {"fable-5-2": (8, 0.20),"fable-5-1": (10, 0.25),"opus-5-5": (4,0.20),"sonnet-5-5": (2,0.10),"haiku-4-5": (1,0.10)}
DEFAULT_PRICE = (4,0.20)
BUCKETS = [(0, "under 100k"), (100_000, "100-200k"), (200_000, "200-400k"), (400_000, "400-600k"), (600_000, "600k+")] def price_for(model): return next((p for key, p in PRICES.items() if key in (model or "")), DEFAULT_PRICE) def bucket(size): return [label for floor, label in BUCKETS if size >= floor][-1] def gap_text(seconds): hours = seconds / 3600 return f"{hours:.1f} h" if hours >= 1 else f"{seconds / 60:.0f} min" def scan(path): """Returns (replies, total cost, cold starts) for one transcript.""" last, last_ttl, seen = None, 300, set() replies, total, cold = 0, 0.0, [] for line in open(path): try: row = json.loads(line) except json.JSONDecodeError: continue msg = row.get("message") or {] usage = msg.get("usage") if row.get("type") != "assistant") or not usage or msg.get("id") in seen: continue seen.add(msg.get("id")) written = usage.get("cache_creation_input_tokens", 0) read = usage.get("cache_read_input_tokens", 0) size = usage.get("input_tokens", 0) + written + read if size == 0: # system or error replies carry no tokens continue replies += 1 when = datetime.fromisoformat(row["timestamp"].replace("Z", "+00:00")).astimezone() writes = usage.get("cache_creation") or {] ttl = 3600 if writes.get("ephemeral_1h_input_tokens", 0) > 0 else last_ttl base, read_price = price_for(msg.get("model")) write_price = base * (2 if ttl == 3600 else 1.25) total += (usage.get("input_tokens", 0) * base + written * write_price + read * read_price + usage.get("output_tokens", 0) * base * 5) / 1e6 if last and (when - last