背景 ~ Agent 调试
问题群 -> 连接器 -> provider 网关 -> ClaudeCode Agent CLI -> LLM
一个问题群内不同轮次的请求(使用者问不同问题), KV Cache 到底是复用还是新算
答案是:每次都是新算,不跨轮次复用
群里发一条消息 → 触发一次 Agent CLI 会话 → agent 完成推理并回复,这一个完整闭环就是"一轮"
这一轮内部,prefill 阶段算出来的 KV Cache 在后续 decode token 时可以复用,但回复发出去、进程退出后,Cache 就没了,
下一轮(群里再发一条新消息)会重新启动进程,重新 prefill 整个 prompt,KV Cache 从头算,跟上一轮没有任何复用关系
原因在于当前 连接器 + Agent CLI 的会话模型:
每轮消息触发独立进程:群里每条新消息都会启动一个新的 CLI 会话(即一个新的推理进程),进程结束后 KV Cache 随之释放,
上下文通过 prompt 注入:历史轮次的内容以文本形式压缩/摘要后注入到新一轮的 system prompt 中(就是 prompt 头部的 session 上下文)
而不是通过 KV Cache 延续
KV Cache 仅在同一轮次内有效:同一轮推理中,prefill 阶段计算 prompt 的 KV Cache,后续 decode token 可以复用;但本轮结束后进程退出,Cache 不落盘也不跨进程传递。所以本质上是 stateless 推理,每轮都是"冷启动"------重新 prefill 整个 prompt(含历史摘要),没有跨轮次的 KV Cache 复用
这也是为什么 prompt 头部有 auto-compact 机制:当上下文过长时自动压缩历史,避免每轮都 prefill 越来越长的 prompt
KV、前缀、提示以及语义缓存机制

精确匹配 Exact Match (前 3 种)、模糊匹配 Fuzzy Match(语义缓存)
1️⃣ KV Cache(KV 缓存,One Request)
- 作用:单条请求内部生成 token 时,保存每一步 Transformer 注意力计算出来的 K、V 向量,不用重复计算历史 token
- 范围:只属于当前这一个用户请求,请求结束就销毁
- 场景:同一个对话的一轮生成(一次回复),每新出一个 token,只算新 token 的 Q,直接复用已经算好的历史 K/V,大幅提速
就是 Transformer 推理最基础的 KV 缓存,不能跨请求共享
2️⃣ Prefix Cache(前缀缓存,Shared KV)
- 作用:跨请求共享相同前缀序列的 KV
- 例子:很多用户都用同一个系统提示词,这一段公共 prefix 的 KV 算一次,多个请求直接复用这份 KV,不用每个请求重新跑一遍
- 示例:一个 session 中的不同回答(不同的情况,共同复用前缀相同prompt)
- 关键点:必须 token 序列完全一模一样的开头才能命中,属于精确匹配。图中箭头代表多个请求复用同一套 KV 数据
3️⃣ Prompt Cache(提示词缓存,Billed KV)
billed 收费的,开账单的
什么是 prompt cache
API 提供商(如 Anthropic、OpenAI)的一种优化------如果多次请求的 prompt 前缀相同,服务端会缓存这段前缀的 KV Cache,后续请求只按增量部分计费和推理,降低延迟和成本
和 Prefix Cache 很像,也是精确匹配;但这里 KV 是付费 / 计费粒度的缓存
多用于云 LLM 服务商:完整 prompt 缓存,命中后,这部分 token 不计入计费或者少计费;图上的账单符号代表计费相关
Prefix 侧重引擎内部显存复用;Prompt Cache 侧重业务 / 计费层面的完整 prompt 复用
这个表述其实看着和 Prefix Cache 没啥区别,直接看下面示例 2 更好
4️⃣ Semantic Cache(语义缓存,Saved Answer,带警告⚠️)
- 不再要求输入文字完全一样,做语义模糊匹配
- 逻辑:来了一个用户问题,算向量,向量库检索历史相似问题;如果语义接近,直接返回之前保存好的回答,不走大模型推理
- ⚠️图里红色三角警告:风险点:
- 语义匹配不准,问题看着像,实际含义不一样,返回错误答案
- 时效性问题,旧答案已经过时
四种不同的缓存存储
KV cache(模型内部,针对单次推理)
Prefix cache (模型上,针对任何推理)
Prompt cache (面向云服务商,特定产品,比如 Anthrophic ,跟计费服务有关,比如收费价格)

1. KV cache
- 存储对象:attention tensors(注意力 K/V 张量)
- 作用域:
scope: one request,仅单个请求内部有效,请求结束销毁 - 匹配:精确匹配
- 本质:推理时,把历史 token 算出来的 K、V 张量保存在显存,decoding 阶段不用重复计算。不能跨用户、跨请求共享
The KV cache stores attention tensors for one request.
KV 缓存为单个请求存储了相关数据
2. Prefix cache(前缀缓存)
- 存储对象:
same tensors, kept on the server:同样是 K/V 张量,但常驻服务端显存,可以跨多个请求复用 - 命中 key:
hash chain over token ids:对 token id 序列做哈希链,识别完全一致的前缀 token 序列 - 场景:大量请求共用一套 system prompt,前缀只算一次,多个请求直接复用这份 KV 张量
和普通 KV Cache 区别:普通 KV Cache 绑定单次请求;Prefix Cache 把 KV 张量放在服务端全局,多请求共享
Prefix caching stores those same tensors on the server, keyed by a hash chain over token IDs.
前缀缓存机制将相同的张量存储在服务器上,这些张量通过 token ID 的哈希链进行键控
3. Prompt cache
- 存储对象:同样复用 KV 张量,但绑定计费
same reuse, with a price sheet - 命中 key:
exact token prefix,严格完整 token 前缀匹配 - 面向云服务商:命中缓存时,这部分 prompt token 不计费 / 少计费;没命中,就要付费重新计算
Prefix Cache 偏向引擎内部显存优化;Prompt Cache 偏向业务计费层面的缓存
Prompt caching is the provider's billed version of that same lookup, at 0.1x the base input rate on a read against a 1.25x premium on the write.
Prompt caching是提供商提供的计费版本,其查询速度是基础速率的 0.1 倍;而写入操作的速率则在此基础上乘以 1.25 的溢价。
4. Semantic cache(语义缓存)
- 存储对象:
finished response strings:已经生成完成的回答字符串,不再存模型张量。 - 命中 key:
cosine similarity余弦相似度,向量检索,模糊匹配。 - ⚠️风险:
fuzzy match, can return a wrong answer
输入不需要完全一样,语义近似就返回旧回答,会出现 "问题看着像,但实际语义不同,返回错误答案"。
A semantic cache stores finished response strings, keyed by cosine similarity over an embedding.
语义缓存存储已完成的响应字符串,这些字符串的键是根据嵌入向量之间的余弦相似度来确定的。
Prefix Cache 的组织方式 - 哈希链 hash‑chain over token IDs
A block only matches if every block before it matched 一个 block 想要命中缓存,它前面所有 block 必须全部命中
图上上下两趟请求
- Request A(历史已经处理完成):
[G1][G2][G3][G4][B5],每个绿色是 16token 完整 KV block - Request B(新到来请求):
[G1][G2][G3][O4][O5]
- G1、G2、G3:block hash 全部匹配 ✅,直接复用 KV 缓存
- 第 4 个 block 发生变化(first miss,打叉)
- 一旦第 4 块不匹配:从此位置往后所有 block(O4、O5)全部放弃缓存,全部重新完整 prefill(橙色)
底部公式:
hash = parent block's hash + this block's token ids
当前 block 的哈希 = 上一个父 block 的 hash + 当前 block 的 token ids
👉 链式强依赖,后一块 hash 直接依赖前一块的 hash 结果。
底部总结文字:
Reuse is exact and positional, so one changed token early costs you every block after it.
复用是严格按位置精确匹配;只要前面任意位置改动 1 个 token,该位置之后所有 block 全部作废,无法复用
还有小字:a partial block at the tail is never indexed 末尾残缺 block 永远不会建立缓存索引

hash‑chain over token IDs:每个 KV 块的哈希,依赖【父块哈希 + 当前块 token id 列表】,串成一条链式指纹
作用:只要 token 前缀序列完全一样,就会生成完全一样的哈希链(跨多个用户、多个请求),服务器全局缓存对应的 KV 张量,不同请求直接复用,不用重跑 prefill

Only whole blocks get a key → 只有完整 block 才会生成缓存 key
图里这个 slide 就是解释:为什么有些 prompt 看上去前缀完全一样,但就是命中不了全部缓存 ------ 结尾落到 block 边界之内,尾部残段永远要重复 prefill
绿色块:完整 block,每块 16 tokens
- 3 个完整 block:
16+16+16 = 48 token - 完整 block 会做 hash,生成 block key,存入缓存,可以被后续请求复用,跳过 prefill 重算
Prefix Caching:全局服务器缓存,跨多个用户、多个请求复用相同提示词前缀的 KV,key 就是这套 token id 哈希链
假设块大小 = 4 个 token,token id:
Block0: [101, 202, 303, 404] # system prompt:"你是一个乐于助人的助手"
Block1: [505, 606, 707, 808] # 用户queryA:"帮我写一首关于春天的诗"
Block2: [909, 111, 222, 333] # 用户queryB:"帮我总结下面这段文字"
哈希链计算规则(链式)
H0 = hash( [101,202,303,404] ) # Block0,没有父块
H1 = hash( parent_hash=H0, current_tokens=[505,606,707,808] ) # Block1依赖H0
H2 = hash( parent_hash=H0, current_tokens=[909,111,222,333] ) # Block2依赖H0
哈希链:
- 请求 1 完整 token 序列
Block0+Block1→哈希链:H0 → H1 - 请求 2 完整 token 序列
Block0+Block2→哈希链:H0 → H2
请求流程
- 第一个请求到来:
[101,202,303,404,505,606,707,808]- 计算哈希链:
H0 → H1 - 缓存没命中,完整 prefill,算出 Block0、Block1 的 KV 张量
- 服务器全局缓存表写入:
H0→KV0张量,H1→KV1张量
- 计算哈希链:
- 第二个请求到来:
[101,202,303,404,909,111,222,333]- 计算哈希链:
H0 → H2 - 查全局缓存:
H0命中!直接拿已经算好的KV0; H2未命中,只需要 prefill Block2,不用重新跑 system prompt 那 4 个 token- 保存 H2 对应的 KV2
- 计算哈希链:
Block0 的 token id 完全没变,所以父哈希 H0 不变
哪怕后面接完全不同的用户 query,只要前缀 token id 完全一致,哈希链前面的部分就完全匹配,直接复用 KV,省去 prefill 算力
如果前缀 token 变了,哈希链整条后续全部失效
比如 system prompt 改了一个 token:Block0' = [101,202,303,999],得到新哈希H0';哪怕后面 Block1 的 token 完全不变,H1' = hash(parent=H0', Block1_tokens),和旧 H1 完全不一样,缓存无法命中 。
这就是hash‑chain"链式" 的含义:父块哈希改变,所有子块哈希全部跟着变,保证不会错误复用不匹配的前缀 KV
Prompt Cache详解
Anthropic Prompt‑Caching 就是 Anthropic 对自家实现的Prefill‑Caching 的产品名称 ,不是另一种全新技术;增加了显式cache_control断点标记、TTL、最多 4 个断点、lookback‑20 block 约束、专门 usage 计费字段(cache_creation_input_tokens / cache_read_input_tokens)
它增加了多断点能力 :可以设置最多 4 处 cache 断点,把 prompt 切多段独立缓存,这是普通朴素 prefill‑caching 没有的能力;朴素 prefill 缓存只能从序列最开头匹配,而 Anthropic 可以在序列中间设置断点,只缓存某一段
Anthropic prompt‑caching:可以在任意 block 打断点,只缓存该 block 及前面,不需要从整个 prompt 最开头一致

Anthropic Claude cache_control 计费模型
- hidden provider system content:服务商内部隐藏系统内容,用户看不见、自己写不了
- tool definitions:工具定义
- system prompt:系统提示词
- conversation so far:历史对话
- current message:本次最新用户消息
what the provider actually matches on:服务商就是拿这一整条完整 token 序列做缓存匹配
中间:三段不同计费规则

- 第 1 次跑:新建缓存,绿色段付 1.25 倍
- 第 2 次、第 3 次复用同一份缓存:同一段落走蓝色 0.1x,不再收 1.25x 写入溢价
👉 只要同一份缓存块被读取≥2 次,整体就是省钱
如果每个 prompt 前缀都独一无二,永远不复用,那每次都要交 1.25x 创建费,反而变贵

The breakpoint belongs on the last block that never changes 缓存断点(cache_control 标记),应打在【永远不变的那一块内容末尾】
分为左右对比:❌错误打标记方式、✅正确打标记方式
左边:Breakpoint on the wrong block(错误示例 ×)
Prompt 从上到下顺序:
- tool definitions(工具定义,固定不变)
- system prompt(系统提示词,固定不变)
- conversation so far(历史对话,每轮请求都会变化)
- user message + timestamp(当前用户消息 + 时间戳,每轮变)
👉 错误做法:把 cache_control 标记打在最底部的 user 消息块上
the marked block changes every call, so the hash never matches and you pay a fresh write every time
被标记的这块内容每一次调用都在变,哈希永远匹配不上,每一轮都触发全新的缓存写入(cache_creation 1.25x 溢价)
哪怕上面tool definitions / system prompt文本完全没变,但是标记点后面的整套序列包含变化的内容
结合因果 KV 约束:标记点代表缓存「从序列开头一直到标记点」全部 token
因为末尾一直在变,整个大前缀哈希对不上,缓存永远不命中,白白多花 1.25 倍的钱
右边:Breakpoint on the stable boundary(正确示例 ✔)
绿色虚线框:identical across requests → 跨请求完全不变的稳定块
包含:tool definitions + system prompt + conversation so far
marker(绿色星星)打在这一整块稳定内容的最后边界 ,稳定块结束的地方,后面才轮到变化的user message + timestamp
右侧绿色对勾说明:
the prefix matches, so this reads back at the discounted rate
前缀完整匹配成功,后续请求可以走折扣价cache_read_input_tokens(0.1x)读取缓存
关键点:标记不是打在 "固定内容内部",而是打在整段不变内容的末尾边界。标记之后,才开始放每轮会变动的内容
图下半部分:缓存回溯查找机制
premium ˈpriːmiəm
n. 保险费;(正常价格或费用以外的)加付款,加价;很高的价值,额外价值;额外补贴,津贴;奖品,奖金
adj. 高价的,高品质的

the read walks backward through a fixed window 读取缓存时,会向前回溯一个固定大小窗口(这里最多 20 个 content 块)
an earlier cache write, now out of the window 曾经写过的有效缓存块,现在超出了这个回看窗口
never found, so the hits stop silently 找不到缓存,hit悄悄停止,不会报错,直接全量重跑 prefill
- Claude 查找缓存,只会在固定大小的回溯窗口内向后找历史的缓存写入记录
- 如果之前的缓存写入,时间太久,滑出这个窗口,就找不到旧缓存
- 不会报错,静默缓存失效,感知不到,直接重新执行一次昂贵的 cache_creation(1.25x)
The lookback finds earlier writes, not stable content, so put a second marker where the write actually landed.
回溯功能会查找早期的写入内容,而非稳定的内容,因此请在实际写入的位置添加第二个标记
不要只依赖一个标记;长 prompt,在实际稳定分段的结尾多打标记,防止窗口滑动丢失缓存命中
举例子(Agent 多轮循环场景)
块定义:每一条 message = 1 个 content 块。Lookback 窗口 = 20 块
- Block0:超大静态 System Prompt(希望永久复用,打上
cache_control标记,第一次请求成功 write 缓存) - Block1‑Block25:Agent 多轮工具调用、用户交互,一轮一轮往后追加新块
第 1 轮:断点打在 Block0 末尾,断点在 Block0,向前回溯,命中 Block0 缓存 ✅ hit,只算 Block1
[Block0(带cache_control)] Block1
第 21 轮:现在序列变成
[Block0] Block1 Block2 ... Block20 Block21
现在把新的断点打在Block21 末尾 ,系统从 Block21 向前回溯,最多看 20 个块:扫描范围:Block1~Block21
Block0 已经在回看窗口外面了!虽然 Block0 文本一字未改,但找不到之前的 cache write 记录
现象:silent miss 静默失效,没有报错,整个前缀全部重新 prefill,缓存完全不生效,还以为缓存还在工作
prompt caching 的经济学,The economics of prompt caching
Anthropic charges 1.25x the base input rate to write an entry and 0.1x to read it, with a higher write multiplier if you want it for a longer time. OpenAI applies the same two multipliers on its current models.
在写入条目时,Anthropic 代理的收费是基础输入速率的 1.25 倍;而读取条目时的收费则是 0.1 倍。如果希望条目保持活跃状态更长时间,那么写入的收费还会更高。OpenAI 在其当前模型中也采用相同的两种收费机制
The premium cost is recovered in subsequent requests since anything reused inside the TTL will avoid any recomputation.
在后续的请求中,会回收这些高级成本。因为只要在 TTL 时间内再次使用这些资源,就无需进行重新计算
A read can only find an entry that some earlier request wrote, and writes happen only at a breakpoint you placed.
读取操作只能找到某个早期请求所创建的条目,而写入操作则只会在你指定的断点处发生
Each call checks your breakpoint, and on a miss it walks backward through a limited number of blocks looking for an older write.
每次调用都会检查你的断点位置;如果断点未被命中,程序就会向后执行有限的几个块,以寻找更早的写入操作
Anthropic caps that at 20 blocks, so adding more than 20 blocks of conversation between two calls pushes the last write out of range and the hits stop.
Anthropic 限制在20个方块,因此两次通话之间若对话超过20个方块,会导致最后一次写入超出范围,命中将停止
示例1
ephemeral ɪˈfemərəl
adj. 短暂的;(主指植物)短生的,短命的
n. 只生存一天的事物;短生植物
这里 Avi Chawla 的例子只用了单个断点放在 system 块 ,表现行为和朴素 prefill 缓存几乎一模一样
所以会感觉 "看着像 prefill caching",其实底层就是这套机制,只是 API 层做了增强
代码
py
import anthropic
client = anthropic.Anthropic()
# 很长的静态系统提示词,重复400遍
LONG_INSTRUCTIONS = "You are a precise technical editor. " * 400
def ask(question: str):
return client.messages.create(
model="claude‑sonnet‑4‑6",
max_tokens=512,
system=[
{
"type": "text",
"text": LONG_INSTRUCTIONS,
"cache_control": {"type": "ephemeral"}, # 把这块标记为可缓存
}
],
messages=[{"role": "user", "content": question}],
)
# 连续发2次不同用户问题,系统prompt完全不变
for question in ["Summarize section 3.", "Now rewrite it for a beginner."]:
resp = ask(question)
u = resp.usage
print(f"write={u.cache_creation_input_tokens} read={u.cache_read_input_tokens} uncached={u.input_tokens}")
输出:
write=2823 read=0 uncached=14
write=0 read=2823 uncached=17
三个 usage 字段含义
cache_creation_input_tokens:write
本次请求新写入缓存的 token 数量。代表这部分 token 这次做了 prefill,计算出 KV,存入临时 ephemeral(短暂) 缓存
cache_read_input_tokens:read
本次请求直接命中读取已有缓存的 token 数量,不需要重新做 prefill,节省算力
uncached = input_tokens
input_tokens:最后一个 cache_control 断点之后的所有输入 token;这部分永远不走缓存,每次都完整重新计算、按标准 input 费率计费
代码含义
第一轮请求:"Summarize section 3." 输出:write=2823 read=0 uncached=14
read=0:没有旧缓存可以读,这是第一次跑write=2823:把带cache_control的长系统提示词(2823 tokens)完整 prefill,生成 KV 并且写入 ephemeral 缓存uncached=14:最后一个cache_control断点之后的所有输入 token
第一轮行为:冷启动,构建缓存,无命中
第二轮请求:"Now rewrite it for a beginner."
关键点:system 块完全不变,只换 user 问题;cache_control 标记还在同一个 system 文本块上
输出:write=0 read=2823 uncached=17
write=0:没有新 token 写入缓存,不需要再算一遍长系统提示的 KVread=2823:直接从 ephemeral 缓存读取那 2823 个 token 对应的整套 KV,跳过这一大段的 prefill 计算uncached=17:最后一个cache_control断点之后的所有输入 token
第二轮行为:缓存命中,直接复用之前算好的 KV,只需要 prefill 新的用户问题
ephemeral:临时缓存,在 Anthropic 服务器内存存活数分钟,不是持久存储 ;进程回收、超时就消失,不是永久存磁盘
两次调用ask()是完全独立的 HTTP 请求 ,不是同一个会话;靠服务器侧全局 prefix cache(哈希链匹配)实现跨请求复用,不是客户端内存保存任何东西
约束条件
system 块文本必须一字不差 ;只要改一个 token,链式 KV 全部失效,回到 write>0、read=0
不能超出 lookback 20 块窗口;这个例子 system 是单块,不会触发窗口问题
省钱本质
- 第一轮:要做完整 2823 token 的 prefill(昂贵)
- 第二轮:2823 直接读缓存,只对少量新 user query 做 prefill
示例2 - 体现 Anthropic 多断点 Prompt‑Caching
Anthropic 多断点 Prompt‑Caching:最多 4 个cache_control断点,可以独立缓存多段;中间部分可复用,不需要整个 prompt 从头完全一致
模拟样本结构
构造 3 段可缓存块,打 2 个断点:
- Block‑A:全局固定系统大背景(不变),
cache_control={"type":"ephemeral"}→ 断点 1 - Block‑B:业务规则文档(不变),
cache_control={"type":"ephemeral"}→ 断点 2 - Block‑C:每轮变化的用户问题(每次不同,断点之后,永远 uncached)
- 每个带
cache_control的 block,必须落在末尾向前 20 个 content block 窗口内,否则标记静默失效 - 每个断点缓存:本 block 以及该断点之前到上一个断点之间全部内容
- 最多同时 4 个断点
py
import anthropic
client = anthropic.Anthropic()
BLOCK_A = "【全局背景】" + ("You are expert." * 200) # ~1400 tokens
BLOCK_B = "【业务规则】" + ("Follow these rules." *200) # ~1400 tokens
def ask_multi_breakpoint(user_question: str):
return client.messages.create(
model="claude‑sonnet‑4‑6",
max_tokens=512,
system=[
{"type":"text", "text": BLOCK_A, "cache_control":{"type":"ephemeral"}}, # breakpoint‑1
{"type":"text", "text": BLOCK_B, "cache_control":{"type":"ephemeral"}}, # breakpoint‑2
],
messages=[{"role":"user","content": user_question}]
)
# Request1:第一次请求,全部miss,写入两段缓存
resp1 = ask_multi_breakpoint("Q1: do task one")
u1 = resp1.usage
print(f"Req1 miss write={u1.cache_creation_input_tokens}, read={u1.cache_read_input_tokens}, uncached(input_tokens)={u1.input_tokens}")
# Req1 输出预期: write ≈ 2800,read=0,uncached≈12
# 含义:BlockA+BlockB 合计 2800token 全部做 prefill,写入缓存;Q1 为 uncached
# Request2:完全相同A+B,换问题
resp2 = ask_multi_breakpoint("Q2: do task two")
u2 = resp2.usage
print(f"Req2 hit write={u2.cache_creation_input_tokens}, read={u2.cache_read_input_tokens}, uncached={u2.input_tokens}")
# Req2 输出预期: write=0,read≈2800,uncached≈13
# 两段全部命中,A+B 的 KV 全部复用,只 prefill 新Q2
✨关键差异场景:修改中间某一段(朴素 prefill 直接全崩,Anthropic 多断点可以部分命中)
朴素 prefill caching:只要前缀任意位置改 1 个 token,整个前缀缓存彻底失效,全部重算 prefill
Anthropic 多断点:只失效被修改的断点以及后面的断点;前面不受影响的断点依旧可以命中
把 BLOCK_B 修改(业务规则微调,Block_A 保持原样不变):
py
def ask_modified_B(user_question:str):
MODIFIED_BLOCK_B = "【业务规则(修改版)】" + ("Follow these rules."*200)
return client.messages.create(
model="claude‑sonnet‑4‑6",
max_tokens=512,
system=[
{"type":"text","text":BLOCK_A, "cache_control":{"type":"ephemeral"}}, # breakpoint‑1 不变
{"type":"text","text":MODIFIED_BLOCK_B, "cache_control":{"type":"ephemeral"}}, # breakpoint‑2 修改
],
messages=[{"role":"user","content":user_question}]
)
resp3 = ask_modified_B("Q3: do task three")
u3 = resp3.usage
print(f"Req3 partial‑hit write={u3.cache_creation_input_tokens}, read={u3.cache_read_input_tokens}, uncached={u3.input_tokens}")
Req3 预期输出行为
read ≈1400:Block_A 依旧命中缓存,直接复用 KVwrite≈1400:Block_B 被改动,断点 2 失效,重新 prefill Block_B,写入新缓存uncached≈14:Q3 普通输入 token
总逻辑:
cache_read_input_tokens = Block_A(1400)
cache_creation_input_tokens = 修改后的 Block_B (1400)
朴素全局 prefix 缓存做不到这一点:只要中间 B 改了,A 也必须全部重算 prefill。Anthropic 多断点可以保留 A 的缓存收益。
对比

实际中的 System Prompt
System Prompt 缓存:一些 API(如 Anthropic)把 system prompt 作为独立字段,单独缓存
但大部分 API provider 网关是把所有内容拼成一个大 prompt 发给模型的
比如当前每轮 prompt 结构大致是:
text
[base_instructions] ← Provider 框架指令,每轮完全相同
[system-reminder: memory] ← 可能变化
[system-reminder: skills] ← 可能变化
[system-reminder: compact] ← 每轮不同!历史摘要
<botmux_routing> ← 静态
<identity> ← 静态
<session_id> ← 静态
<role> ← 静态
<user_message> ← 每轮不同
关键问题:<system-reminder: compact>(auto-compact 的历史摘要)会随着轮次增长而变化,它被插在静态前缀和用户消息之间。如果 compact 内容变了,prefix cache 的命中点就断在了 compact 之前------只有 base_instructions 那部分能命中缓存,后面的 等静态内容因为中间隔了变化的 compact,反而没法命中。这个时候就需要 prompt cache,可以将固定的中间一段进行 cache
本质上:
prefix cache 要求缓存的是"连续前缀",而群会话的 prompt 结构是"静态前缀 + 变化的 compact + 静态 role + 动态消息",compact 像一堵墙,把 prefix cache 的有效命中范围限制在了 compact 之前的那一小段
这里的 system prompt 构成
项目中,角色部分来自业务方自定义配置,框架部分由 ClaudeCode/CodexCode 运行时注入,历史部分由 auto-compact 自动压缩生成
最终一轮的完整 prompt 是从 到 <user_message> 之间的所有 XML 块:
角色定义( 块)
来源:bot 在开发机仓库中的配置,通过 ~/custom_workdir 和 skill 体系定义
内容:角色定位、群聊上下文规则、环境约定、Skill 路由、证据分级、输出要求等
比如 prompt 中 块就是它的完整内容
路由/身份/会话元信息
<xx_routing>:xx 框架注入的通信规则(怎么发消息、@规则等)
:当前 bot 的身份(名称、open_id)
<session_id>:当前会话 ID
/ :本轮消息的发送者和 @ 对象
系统级提醒( 块)
ClaudeCode 框架自动注入,如 auto-compact 提示、memory 提示、skills 列表等。这些不在任何仓库文件中,是框架运行时生成的
历史上下文
前几轮对话的摘要/压缩版本,由 auto-compact 机制生成。以 形式注入到新一轮 prompt 头部
网上关于 Prefix cache 和 Prompt cache 区别解读
Prefix Cache(vLLM/SGLang 引擎侧)
只能缓存请求最开头的连续 token 序列,必须从第 0 号 token 开始,一旦开头序列发生变化,直接失效
市面上两种 Prompt Cache,要严格区分
-
① 云厂商狭义 Prompt Cache(Anthropic Claude
cache_control):不是任意分散片段,但是可以手动设置多个独立的缓存断点,多个连续块,可以不在最开头 。它不是 "乱序、分散不连续碎片",是多段连续子序列 ;每一个 cache 块内部必须是连续 token;块与块之间可以夹可变内容。这就是问题场景 compact 后能救回来的原因:即使会话历史在前面不断变化,可以把后置的 system/role 片段标记为独立cache_control块,单独缓存这一段 KV -
② 很多博客里把引擎侧 prefix cache 也叫 prompt cache,这是术语混淆,它依旧只能缓存最开头的前缀
Transformer 因果注意力硬约束:不能缓存一个序列中间孤立的、前后被篡改的单块 KV。中间 token 一变,后面所有 KV 全部失效。Claude 的 cache_control 是要求你把可变部分切出来,缓存块必须是完整连续单元,块之间允许插入可变内容,不是随便抓 prompt 里零散的几个 token 片段直接复用
示例 2 中的 prompt cache 和 prefix cache 的 hash‑chain over token‑id区别
hash‑chain over token‑IDs(开源推理引擎:vLLM Prefix‑Caching)
- KV 被切为固定大小 token 块(例如 16token)。
- 每一块的 hash =
hash(本块token_ids, prev_block_hash),链式依赖前一块哈希;形成全局链式指纹 - 特点:
- 完全自动,不需要开发者打任何标记;引擎自动扫描全部 token 块
- 只要序列中任意位置 1 个 token 改动,该位置之后所有 block 的 hash 全部失效
- 只能复用从 token 0 开始的连续完整前缀;不能在序列中间任意切分独立缓存段
- 缓存 key 绑定完整 token 流;不能指定 "我只想缓存 A 段,B 段每次重算"
- 例子:
[A,B,C,D],修改 B → B、C、D 全部缓存失效,A 也不能单独复用,因为 hash 链断裂
本质:隐式、全自动、从序列头部驱动的前缀复用
Anthropic Prompt‑Caching(显式多断点)
Anthropic 底层内部同样会做前缀 hash,但不是一条全局 hash 链 ;而是最多 4 个独立的、开发者手动指定的断点 hash
- 开发者在
content_block上手工打上cache_control:ephemeral; - 每一个断点:计算从请求开头到该 block 结尾的完整字节哈希,作为一条独立缓存 key,存入 KV 缓存表Claude
- ⚠️关键点:
- 多个断点 = 多条独立缓存条目,不是一条链
- 修改第 2 个断点所在 block :仅第二个断点以及后面的断点失效;前面的断点仍然可以命中
- 约束:标记 block 必须落在末尾向前
lookback=20 content‑block窗口;最多 4 个断点;每个断点最少 1024token
- 例子:
Block_A(断点1), Block_B(断点2), Q(uncached)- 修改 Block_B:断点 2 失效、重新写缓存;断点 1 (A) 仍然命中复用
本质:显式、开发者驱动、多段独立前缀缓存
附录-待整理内容
链路分层
-
API 层(真实业务调用的 Anthropic Python SDK / HTTP 接口)
- 输入:
system块带上cache_control={"type":"ephemeral"}标记;messages 放动态用户 / 助手对话 - API 层职责:透传这个标记到后端,做参数校验、计费、命中统计、返回 cache_hit 的 metadata 给客户端
- ❗API 层不做 token 哈希、不存 KV 张量、不做缓存检索,它只是接口外壳
- 输入:
-
API 层下一层:推理网关 / 请求调度层(Gateway‑Scheduler)
这就是 API 直接往下的那一层,做几件核心工作:
这个案例:断点放在 system 末尾,所有请求的 system 前缀完全不变,只有后面 messages 变化;等价就是传统前缀 prefill 缓存,所以观感完全一致
- 把 messages+system 做 tokenizer,转成 token ID 序列;识别
cache_control断点标记,标记出缓存截止 token 位置(本例就是 system 块的末尾 token)。 - 对断点之前全部 token 序列计算哈希指纹,去全局缓存索引查询:有没有已经持久化好的 KV‑cache 块。
- 命中:把已经持久化的 KV 状态加载进 GPU 显存;只对断点之后的动态 token 做增量 prefill。
- 未命中:完整跑一遍断点前的 prefill,把这一段 KV 张量存入全局缓存,打上这个 token 序列的 hash、TTL。
-
推理引擎层(模型 worker,如 Claude 内部推理 runtime)
- 接收调度层下发的:可复用 KV 块 + 需要新 prefill 的后缀 token。
- Transformer 前向:直接复用加载好的 KV,只对后面动态部分做注意力计算;进入 decode 循环生成输出。
- 普通单次请求结束,本请求的局部 KV 直接释放;被 cache_control 标记的那一段 KV 会被留在全局缓存池,受 LRU/TTL 管理,供其他请求复用CSDN博...。
-
存储层:全局 KV 缓存池
- 热:GPU 显存;次热:CPU 内存;冷:远端存储。
- 存的不是文本,是每一层 transformer 的 Key/Value 张量 + 对应 token 序列的 hash 索引。
完整端到端数据流
SDK代码
↓
API层(HTTP/sdk):透传 cache_control 标记,校验参数 → 返回带上 cache_hit 的 response
↓
【调度网关层|API的下一层】
1. tokenize完整messages,识别断点位置=system末尾token
2. 计算[0...断点位置]这一段token序列的hash
3. 查询全局KV缓存索引
├─命中:加载这一大段KV张量到GPU;只对后面动态messages做增量prefill
└─未命中:完整跑system部分prefill,把system的KV写入全局缓存池
↓
推理worker:复用已有的KV,跑后缀prefill + decode生成token
↓
token detokenize,流式返回;缓存池保留system段KV等待下一次请求复用