文章通过本地运行 Gemma 4 的对比实验,深入探讨了大模型 KV 缓存的工作原理及 Transformer 注意力机制的底层逻辑。同时,通过逆向分析 Claude Code 源码,揭示了其精密的缓存工程实现,并为开发者提供了大幅节省 Token 消耗和优化响应速度的实用技巧。
早上打开 Claude Code,敲第一句话,2%~10% 的套餐额度没了。午休回来继续干活,又一句话,10% 的额度蒸发。你有没有想过,这 token 到底花在哪了?我带着这个疑问,在本地用 Gemma4 跑小模型做实验——发现同一段对话,有些轮次要等 30 秒,有些只要 0.2 秒。为了搞清楚为什么,我会从 Transformer 的注意力机制开始讲,再到 Claude Code 的代码实现, Anthropic 在缓存上做了一整套精密工程。理解了这套机制,你就知道怎么让同样的套餐多撑 3-5 倍。
本文内容较长,建议按需阅读:
起因很简单:我想在本地体验一下大模型的 context caching(上下文缓存),看看到底能快多少。
我使用 Ollama 在 Mac (Apple Silicon, 16GB) 上运行 Gemma 4(8B 参数,约 9.6GB 模型),并编写了一个测试脚本进行多轮对话:首先输入一篇 670 token 的文章,随后连续追问 5 个问题。
每轮 API 返回两个关键指标:Prompt 处理时间(消化输入)和生成时间(吐出回答)。将 Prompt 处理时间单独提取出来,结果如下:
| 轮次 | Prompt 处理 | 生成 (Token 数) | 总耗时 |
|---|---|---|---|
| Turn 1 (喂文章) | 24,458ms | 5,095ms (68 tok) | 34s |
| Turn 2 (追问1) | 31,036ms | 22,653ms (365 tok) | 58s |
| Turn 3 (追问2) | 253ms | 2,511ms (46 tok) | 3.8s |
| Turn 4 (追问3) | 203ms | 2,029ms (36 tok) | 3.0s |
| Turn 5 (追问4) | 165ms | 1,870ms (37 tok) | 2.4s |
| Turn 6 (追问5) | 176ms | 1,235ms (26 tok) | 1.8s |
从 Turn 2 到 Turn 3,Prompt 处理时间从 31 秒直降到 0.25 秒,实现了 100 倍加速。而生成速度始终稳定在 13-20 tok/s,未受影响。
这说明加速仅发生在“消化输入”阶段,与“吐出回答”无关。
随后,我换用小模型 Qwen3.5(0.8B,约 1GB)进行同样的测试,观察模型大小的影响:
| 轮次 | Prompt 处理 |
|---|---|
| Turn 1 (喂文章) | 566ms |
| Turn 2 (追问1) | 173ms |
| Turn 3 (追问2) | 182ms |
| Turn 4 (追问3) | 212ms |
| Turn 5 (追问4) | 227ms |
| Turn 6 (追问5) | 240ms |
小模型全程维持在 200ms 左右,没有出现类似 Gemma 4 那种剧烈的速度变化。
这里有两个核心问题:
大模型生成文本时,基于 Transformer 的注意力机制。其核心公式为:
$$Attention(Q, K, V) = \text{softmax}\left(\frac{Q \cdot K^T}{\sqrt{d}}\right) \cdot V$$
其中 $Q$、$K$、$V$ 分别扮演以下角色:
KV 缓存的本质是将历史 token 的 $Key$ 和 $Value$ 存储起来。当处理新 token 时,只需计算其自身的 $Q$,然后直接检索已有的 $KV$。
这种机制之所以可行,是因为主流大模型(如 Claude、GPT、Llama 等)多采用 Decoder-only 架构。这种架构使用单向注意力(因果掩码),每个 token 只能看到它之前的 token。因此,前面 token 的 $KV$ 一旦算出便不再改变。
回到实验数据:
模型越大,$KV$ 计算越昂贵,缓存的收益就越显著:
| 指标 | Gemma 4 (4.5B active) | Qwen3.5 (0.8B) |
|---|---|---|
| 未命中耗时 | ~25,000ms | ~566ms |
| 命中耗时 | ~170ms | ~173ms |
| 加速比 | 148x | 3.3x |
| 命中时速度 | 3,000-5,000 tok/s | 3,200-3,900 tok/s |
缓存是无损的吗? 是的。Transformer 的计算是确定性的,从缓存加载 $KV$ 与现场计算的结果完全一致。
生成结果会进缓存吗? 生成结果(Output tokens)本身不进入 Prompt 缓存。因为每次生成的内容受 Temperature 等参数影响可能不同,直接缓存没有意义。
但有一个精妙的设计:在下一轮对话中,上一轮的生成结果会被拼接到 Prompt 中,作为“输入”的一部分。此时,这部分内容便会被缓存覆盖。
多轮对话的成本对比: 如果没有缓存,每轮对话都要重新计算所有历史,Token 消耗呈二次增长。有了缓存,情况则大不相同:
假设系统提示 20K tokens,每轮对话增加 1K tokens。
结论: 缓存节省了约 76% 的成本。这就是为什么“在同一个 session 持续对话”比“频繁开启新 session”更省钱的原因。
通过逆向 Claude Code 源码可以发现,Anthropic 在缓存上做了大量精细化处理。
Claude Code 发送的 API 调用并非一整块,而是精心拼接的结构:
CLAUDE.md),属于 Org 缓存。cache_control 标记。Claude Code 会监控 cache_read_input_tokens。如果该值比上次下降超过 5% 且绝对值大于 2000 tokens,系统会判定为缓存断裂,并分析原因(如系统提示词变更、工具增减、TTL 过期等)。
缓存遵循前缀匹配原则。只要从头开始的 token 序列发生变化,后续缓存将全部失效。
注意: 切换模型会导致缓存完全失效,因为不同模型的权重不同,$KV$ 张量无法互用。
答案是:几乎不能。 每个 sub-agent(如 Explore agent)都是独立的 API 调用,且存在以下差异:
因此,启动一个 sub-agent 基本等同于一次“冷启动”。
理解了机制,就能通过优化习惯来省钱。
btw 共享 session 和缓存。成本差异: 同样 10 轮对话,持续对话的成本仅为频繁开新 session 的约 1/5。
Pro/Max 用户的缓存 TTL 为 1 小时。如果因为午休或会议导致超过 1 小时未操作,缓存就会过期。
原理: 缓存 TTL 在每次读取时都会刷新。只要在过期前发送一次匹配前缀的请求,缓存就能无限续命。
方案设想: 可以利用脚本,每隔 55 分钟向 Claude Code 终端自动发送一条简单的 Prompt:
我断线了么?如果没断你只要简单说 ok。
通过这种方式,可以有效维持缓存状态,避免昂贵的冷启动开销。
- 本文转载自: x.com/minlibuilds/status... , 如有侵权请联系管理员删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!