前言
前段时间有个师妹去面淘天,一面技术面聊到大模型推理优化。面试官问了一个看起来很基础的问题:
👔 说下 Prefix Caching 的原理,然后再说下 Agent 框架怎么保证不破坏缓存?

她当时挺自信,因为之前用过 Anthropic 的 Prompt Caching 接口,觉得不就是开个开关的事。
她和面试官说:"只要开启 prompt caching 这个 API,就自动会命中了。"面试官摇了摇头:
👔 那你说说,为什么很多人开了这个功能,缓存命中率还是接近零?
她愣了一下,说可能是网络延迟。面试官又问:
👔 那 Agent 框架里,system prompt 每次请求一模一样,就一定能命中吗?
她想想说应该能吧------面试官没再追问,只是说"你回去再想想"。
读完这篇文章,你能搞明白:
- Prefix Caching 到底缓存了什么------KV Cache 的可复用性
- 前缀必须逐字节相同------改一个字符就全部失效
- Agent 框架为什么容易破坏缓存------动态注入/字段顺序/时间戳
- 常见的缓存杀手------那些你意识不到的细节
- 怎么保证 Agent 不破坏缓存------缓存友好的请求构造
- 面试话术三层模板------60 分答法和 90 分答法的差距在哪
不管你是做 Agent 开发的工程师,还是需要在面试里讲清推理优化的开发者,这道题都值得提前想清楚。开拆!
一、Prefix Caching 到底缓存了什么
Transformer 每生成一个 token 时,都需要让这个 token 去看前面所有 token 并计算注意力。如果每生成一个新 token 就把前面所有 token 重新算一遍,开销太大。所以推理引擎会把每个 token 算出来的 Key 和 Value 向量存起来------这就是 KV Cache。

一次大模型请求的推理分两个阶段:
- Prefill(预填充):把输入 prompt 一次性喂给模型,并行计算所有输入 token 的 KV 向量存入 KV Cache。计算量跟输入长度成正比。
- Decode(解码):模型一个个吐出新 token,每吐一个只需查询已缓存好的 KV,不需要重新计算历史部分。
Prefix Caching 的核心思路:如果两次请求的前面一段 token 完全相同(逐字节相同),这段的 KV Cache 就可以直接复用,不需要重新计算,只对新增部分做 Prefill。
这不是什么魔法开关,它的本质是利用 KV Cache 的可复用性,前提是前缀必须逐字节一致。光说"开了就行",等于没说。
二、引擎怎么识别相同前缀
主流实现思路:把 KV Cache 按固定大小的块组织,用**前缀树(Radix Tree)**管理这些块。

具体来说:把 token 序列按块切分,每个块算一个哈希值------这个哈希不只基于本块内容,还要把前面所有块的哈希串联上去,保证路径唯一。请求进来时从根节点开始沿 token 序列在前缀树里查找,命中的块直接复用 KV Cache,从第一个不匹配的块开始往后都得重新计算。
代表性实现包括:vLLM 的 Automatic Prefix Caching、SGLang 的 RadixAttention、Anthropic API 的显式 Prompt Caching(通过 cache_control 断点告诉服务端"这里往前的内容请缓存")。
像 Anthropic 的缓存命中率部分通常只收正常价格的 10% 到 25%------前缀越长、复用率越高,首 token 延迟和计算成本都大幅下降。
三、前缀必须逐字节相同
这是整篇文章里最重要的一句话:前缀必须逐字节相同。

哪怕只改动了一个字符、一个空格、一个标点符号,或 JSON 序列化时字段顺序变了,这段前缀在引擎眼里就是全新的内容。缓存直接失效,从这个位置往后的所有内容都得重新计算。
这也是为什么很多人自己写的 Agent,看起来复用了 90% 的历史对话,但实际命中率却很低------破坏缓存往往发生在你意识不到的那些细节里。
四、Agent 框架为什么天生容易破坏缓存
一个典型的 Agent 循环:用户提问 → 模型输出(可能带 tool_use) → 执行工具拿到结果 → 模型再思考 → 再调工具 → 循环好几轮 → 最后给回复。

一次对话下来可能调用大模型 5-10 次。system prompt 800 字的角色设定+工具说明+few-shot 示例,每次请求都要带上。如果每次调用都让模型把这 800 字从头读一遍重新算一遍,账单和延迟都会非常难看。
Agent 框架天生容易破坏缓存,原因在于:请求结构里有很多"看起来一样但其实每次都在变"的部分。 system prompt 本身可能确实没变,但工具结果、对话历史、动态注入的元数据,每一轮都在变。只要这些变动的部分插在了前缀中间,后面的所有内容就全部缓存失效了。
五、常见的缓存杀手
常见的缓存杀手,每一个都隐蔽:
杀手一:动态时间戳/日期注入。 有些 Agent 框架会在 system prompt 里注入"当前时间是 2026-07-10 15:30:22"------每次请求时间都不同,system prompt 就变了,缓存直接失效。解法:把时间戳放在请求的最后面(后缀),不要放在前缀里。
杀手二:JSON 字段顺序不稳定。 工具定义用 JSON 描述,如果序列化时字段顺序不固定(比如 Python dict 在不同版本里顺序不同),每次序列化结果不同,前缀就变了。解法:序列化时按字段名排序,保证确定性输出。
杀手三:随机 ID / Session ID。 有些框架给每次请求生成随机 request_id 并注入 system prompt,导致前缀每次都不同。解法:request_id 放在请求末尾或 HTTP header 里,不要放进 prompt 前缀。
杀手四:工具结果插在中间。 如果请求结构是 systemtool_resultuser_message,tool_result 每次不同,它后面的 user_message 就永远命中不了缓存。解法:把不变的内容(system prompt + tool definitions)放前面,把变化的内容(对话历史+工具结果)放后面。
杀手五:对话历史的格式化方式变化。 第一轮对话用 {"role":"user","content":"你好"} 格式,第二轮突然变成了 User: 你好 格式------前缀就断了。解法:全流程用统一的序列化格式,不要中途换。
六、怎么保证 Agent 不破坏缓存
保证 Agent 不破坏缓存的核心原则:把不变的内容放前面,把变化的内容放后面。
一个缓存友好的请求结构:
css
[system prompt (固定)] → [tool definitions (固定)] → [few-shot (固定)] → [对话历史 (增长但不变旧)] → [本轮新消息]
前三个部分是"冷区"------内容固定,缓存命中率接近 100%。对话历史是"温区"------旧消息不变(前缀复用),只新增最新消息。本轮新消息是"热区"------每次都不同,但这部分本来就不长。
关键工程实践:
- system prompt 里不要注入动态内容(时间戳/ID/随机值)
- JSON 序列化保证字段顺序确定(按 key 排序)
- 工具定义固定不变,放在对话历史之前
- 对话历史只追加不修改(改了旧的就会断缓存)
- 用 Anthropic 的
cache_control断点显式标记缓存边界
这些实践看起来都是"小细节",但每个都直接影响缓存命中率。缓存命中率从 0% 到 90% 的差距,往往就在这几个细节里。
七、从架构师视角看缓存设计的几个工程取舍
从架构师视角看缓存设计的几个工程取舍。
取舍一:缓存断点的位置------放几个。 Anthropic 允许最多 4 个 cache_control 断点。放太多会增加管理复杂度,放太少可能漏掉可缓存的部分。工程上建议:system prompt 一个断点,tool definitions 一个断点,对话历史每 N 轮一个断点。
取舍二:对话历史的缓存策略------全量 vs 摘要。 对话越长,前缀越长但缓存命中的绝对量也越大。但如果对话太长(超 100 轮),应该做摘要压缩------但摘要改变了前缀内容会导致缓存失效。工程上建议:每 20 轮做一次摘要,摘要后的内容作为新的"固定前缀",老对话归档。
取舍三:多用户场景的缓存共享。 不同用户的对话历史不同,前缀不共享。但 system prompt + tool definitions 是跨用户共享的。工程上建议:把共享部分放最前面(跨用户命中),用户特定部分放后面(仅单用户命中)。
取舍四:缓存的有效期管理。 Anthropic 的缓存默认 5 分钟过期。如果 Agent 两次请求间隔超过 5 分钟,缓存就失效了。工程上建议:长间隔对话用"心跳请求"保活(每 4 分钟发一个最小请求),或接受缓存失效的代价。
取舍五:缓存命中率的监控。 不监控就不知道缓存有没有生效。工程上建议:每次请求记录 cache_read_input_tokens 和 cache_creation_input_tokens,计算命中率。命中率低于 50% 时排查缓存杀手。
取舍六:缓存友好 vs 逻辑灵活的权衡。 有时候为了缓存友好,需要牺牲一些逻辑灵活性(比如不能在 system prompt 里注入动态上下文)。工程上判断:如果缓存能省 50%+ 的成本,值得牺牲灵活性;如果省不到 10%,不值得为了缓存改架构。
八、面试话术:考官想听的是什么
回到面试场景。这道题考的不是"你知不知道 Prefix Caching 这个功能",而是"你有没有理解它的工作原理和 Agent 框架里的缓存陷阱"。
常见错误回答一:"开了 prompt caching 就自动命中"。 这是零分------面试官追问"为什么很多人开了还是命中率接近零"时直接卡住。
常见错误回答二:"system prompt 一样就能命中"。 方向对但不够------面试官会追问"如果 system prompt 里注入了时间戳呢"。
高分答题模板:三层结构。
第一层(抛原理):"Prefix Caching 的本质是 KV Cache 的可复用性------如果两次请求的前缀逐字节相同,这段的 KV Cache 直接复用,只对新增部分做 Prefill。它不是魔法开关,前提是前缀必须逐字节一致。"
第二层(讲缓存杀手):"Agent 框架天生容易破坏缓存:动态时间戳注入、JSON 字段顺序不稳定、随机 ID、工具结果插在中间、对话历史格式变化------每个都会让前缀断裂。解法是把不变的内容放前面(system+tools),变化的内容放后面(history+new message),system prompt 里不注入动态内容。"
第三层(升华):"缓存命中率从 0% 到 90% 的差距就在这些细节里。用 cache_control 断点显式标记缓存边界,监控 cache_read_input_tokens 计算命中率,低于 50% 时排查缓存杀手。"
60 分 vs 90 分对比:
| 追问点 | 60 分回答 | 90 分回答 |
|---|---|---|
| "为什么开了还是不命中?" | "网络延迟" | "前缀不逐字节相同:时间戳注入/JSON字段顺序/随机ID/工具结果插中间,任一个都会让缓存断裂" |
| "system prompt 一样能命中吗?" | "能" | "不一定:如果system里注入了动态内容(时间戳/session_id)就不行;必须保证前缀完全逐字节一致" |
| "Agent 怎么保证不破坏缓存?" | "不知道" | "不变内容放前面(system+tools),变化内容放后面(history+message);system不注入动态值;JSON按key排序;用cache_control标记断点" |
| "KV Cache 复用的原理?" | "缓存了结果" | "Prefill阶段算出所有输入token的KV向量存入KV Cache;如果前缀相同,KV Cache直接复用只算新增部分;用Radix Tree管理缓存块" |
加分项提示: 如果你能主动提到"Anthropic 缓存命中部分只收 10%-25% 的价格,缓存命中率直接影响成本------从 0% 到 90% 的差距就在那几个细节里",面试官会认为你有成本意识。
总结
回到开头那道面试题。"Prefix Caching 原理是什么?Agent 框架怎么保证不破坏缓存"------这道题考察的是你对推理优化和 Agent 架构设计的双重理解。
- Prefix Caching 本质:KV Cache 可复用性,前提是前缀逐字节相同。
- 引擎实现:Radix Tree 管理缓存块,按哈希路径匹配。
- 前缀必须逐字节相同:改一个字符/空格/标点,或 JSON 字段顺序变了,缓存全部失效。
- Agent 天生容易破坏缓存:动态时间戳/JSON字段顺序/随机ID/工具结果插中间/格式变化。
- 保证不破坏缓存:不变内容放前面,变化内容放后面,system 不注入动态值,用 cache_control 标记断点。
- 缓存命中率 0%→90% 的差距就在细节里:监控 cache_read_input_tokens,低于 50% 时排查缓存杀手。
Prefix Caching 不是魔法开关,它的本质是 KV Cache 的可复用性。 真正决定缓存命中率的不是"开没开这个功能",而是你的请求结构有没有给缓存留出"逐字节相同的前缀"。
你的 Agent 缓存命中率是多少?排查过哪些缓存杀手?欢迎评论区交流。