做 Agent,先把 Prompt Cache 当成系统架构来设计!

15 阅读11分钟

原文链接:AI 小老六 mp.weixin.qq.com/s/u0TAQvMr_…

很多团队做 Agent 时,第一反应是调模型、调工具、调提示词措辞。模型要不要换成更强的,工具是不是还缺一个,system prompt 里是不是再加几条规则。

这些当然重要,但在长程 Agent 里,真正决定系统能不能跑得久、跑得稳、跑得便宜的,往往不是某一句提示词写得多漂亮,而是整个上下文怎么被组织。

尤其是 Claude Code 这类编码 Agent。一次任务不是一问一答,而是几十轮模型调用、上百次工具调用、持续膨胀的上下文、不断追加的代码片段和执行结果。每一轮都可能带着几十万 token。如果每次请求都让模型重新处理完整上下文,成本和首字延迟都会很快失控。

所以我现在更愿意把 Prompt Caching 看成 Agent 工程里的基础设施问题。它不只是一个 API 优化参数,更像数据库索引、CDN 命中率、缓存分层设计。你不把它纳入架构,后面再怎么优化 prompt,收益都有限。

Prompt Cache 到底缓存的是什么

Prompt Caching 的核心很简单:如果两次请求从开头开始有一大段完全相同的内容,模型可以复用前一次已经算好的前缀状态,只处理后面新增的部分。这件事可以从两个层面理解。

KV Cache 通常发生在单次推理内部。模型生成第 2 个、第 3 个 token 时,不需要反复计算前面 token 的注意力状态。

Prompt Cache 或 Prefix Cache 更偏跨请求复用。上一轮请求已经处理过的系统提示词、工具定义、项目上下文、历史消息,只要下一轮的前缀完全一致,就可以直接命中缓存。

图片

图 1:Prompt Cache 前缀复用示意图

这个机制最怕的不是内容长,而是前面那段内容频繁变化。只要前缀被改动,缓存就失效。长上下文产品要想成立,第一件事不是压缩一切,而是保护那些本来可以复用的稳定前缀。

Claude Code 真正在保护的是稳定前缀

Claude Code 能承载长时间编码任务,不只是因为模型能力强,也因为它对上下文布局非常克制。

一个编码任务里,真正稳定的内容其实很多:基础系统规则、工具定义、项目级说明、CLAUDE.md、工作区约束、前面已经发生过的对话。真正变化快的,通常只是最新一轮用户请求、工具结果、临时状态和报错。

合理的编排方式应该像这样:

image.png

图 2:Claude Code 上下文分层与缓存布局

这个顺序不是为了让人读起来顺,而是为了让缓存命中率更高。越稳定、越昂贵、越值得复用的内容,越应该靠前;越短期、越个性化、越容易变化的内容,越应该沉到后面。

很多系统的 cache miss,不是模型或平台的问题,是 prompt layout 自己把缓存打碎了。

四个最容易被忽略的 cache killer

第一个是把时间戳、请求 ID、当前分支、临时状态写进 system prompt 顶部。语义上看,这些信息确实像系统状态;工程上看,它们每轮都变,放在最前面就是主动放弃缓存。

第二个是工具定义顺序不稳定。工具集合没变,但序列化时顺序随机,下一轮前缀就对不上。很多人会忽略这个细节,因为功能没有错,账单和延迟却会悄悄变差。

第三个是会话中途切模型。看起来很省:简单问题用小模型,复杂问题换大模型。但缓存通常和模型绑定。主会话已经在一个模型上积累了很长前缀,临时切过去可能要重新构建整段缓存。省下的小模型单价,未必抵得过重建前缀的成本。

第四个是动态增删工具。某一轮只需要读文件,就把写工具拿掉;下一轮要改代码,再把写工具加回来。这种做法看起来干净,实际上工具定义本身就是缓存前缀的一部分。工具集一变,缓存链就断。

这些问题的共同点是:它们都不是传统意义上的 bug。系统能跑,回答也可能正确,只是每一轮都在付不该付的钱。

工具不要频繁移除,要延迟加载

MCP 工具越来越多之后,一个直接的压力是工具 schema 太重。如果每轮都把完整参数定义塞进去,token 成本会很高。

直觉上可以动态裁剪工具,但这会破坏前缀。更稳的办法是保留稳定的工具骨架,只把重 schema 延迟到真正需要时再加载。

image.png

图 3:工具延迟加载与稳定骨架

这类设计背后的思路很清楚:不要为了减少一点眼前 token,去破坏可以长期复用的缓存前缀。稳定骨架比短期瘦身更重要。

模式切换不一定要改 system prompt

Plan Mode 是一个很好的例子。

很多人会想,进入计划模式时,应该把执行类工具移除,只留下只读工具;退出计划模式时再恢复。问题是,工具集合一变,缓存就断。

更好的方式是把模式切换做成工具调用或消息状态。比如保留工具集合不变,通过 EnterPlanMode 和 ExitPlanMode 表达状态变化。约束可以通过后续消息告诉模型:当前处于计划模式,只能分析,不能执行写操作。

这样系统行为变了,但上层前缀没变。

这件事对 Agent 产品很有启发。状态切换不一定靠改 prompt 模板完成,也不一定靠增删工具完成。很多状态都可以做成 append-only 的消息、工具调用或事件记录。只要前缀稳定,缓存就能继续工作。

Compaction 也要缓存友好

长上下文迟早会遇到压缩问题。最粗暴的做法是另起一个请求,用一个全新的 system prompt 让模型总结旧对话。

这很浪费。新请求的 system prompt、工具集合、消息结构都变了,之前那条很长的上下文前缀完全复用不上。越到上下文快满的时候,越容易在最贵的位置做最不缓存友好的调用。

更好的做法是 cache-safe fork:沿用父会话完全相同的 system prompt、工具定义和历史消息,把“请压缩上下文”的指令作为最后一条新消息追加进去。

image.png

图 4:Cache-safe Compaction 分叉流程

本质上,压缩也不应该是“换一套 prompt 干活”,而应该是同一条缓存链上的分叉调用。

system-reminder 的价值不只是提醒模型

很多 Agent 系统都会遇到动态状态:当前时间变了,文件被外部修改了,用户切换了模式,工作目录变了,某个工具结果需要被临时强调。

这些信息确实要告诉模型,但不应该因此改 system prompt。

更合适的做法是把动态状态放到最新消息或工具结果末尾,用类似 <system-reminder> 的标签隔离出来。模型知道这是系统级提醒,但缓存前缀不会被污染。

<system-reminder>
The current time is 2026-05-08T09:41+08:00.
The user is currently in Plan Mode. You can only read files.
File foo.py was modified externally since last read.
</system-reminder>

这类标签的工程价值不在于“看起来更像系统提示”,而在于把变化快的信息从配置层降到消息层。它让动态状态可以每轮更新,同时不破坏上方稳定结构。

我认为这是做 Agent 时非常值得借鉴的模式:凡是变化快、又必须告诉模型的东西,优先考虑放在尾部消息里,而不是塞进 system prompt。

RAG 系统最不该把检索结果塞进 system

做 RAG 的人很容易犯一个错误:把 retrieved chunks 拼到 system prompt 里。

这在语义上好像没问题,因为检索结果确实是“上下文”。但从缓存角度看,这是灾难。每个 query 的检索结果不同,system prompt 也跟着变,缓存几乎必然失效。

更好的分层方式是:

image.png

图 5:RAG 分层与检索结果放置位置

检索结果应该待在最后一层。它是每轮变量,不是系统规则。

如果要进一步提升命中率,RAG 检索链路也要尽量确定性:chunk 边界固定,元数据字段顺序固定,相同分数下有稳定的 tie-breaker,拼接模板不要随机变化。否则同一批材料只是顺序变了一下,前缀也可能对不上。

缓存命中率应该进入正式监控

既然 Prompt Cache 已经影响成本和延迟,就不应该只在调试时看一眼。

至少要监控这些指标:

指标为什么要看
prompt_cache_hit_tokens 占比判断长前缀是否真正被复用
prompt_cache_miss_tokens定位哪些请求在反复重算
TTFT 冷热分布缓存命中通常会直接影响首字延迟
前缀长度变化观察是否有动态内容进入了上层前缀
模型切换前后命中率判断省小模型的钱是否抵消了缓存损失
工具定义变更次数工具 schema 抖动往往会带来隐性成本

缓存命中率低,不应该只是“最近有点贵”。在长程 Agent 产品里,它更像一个架构退化信号。

DeepSeek 的方向:把长上下文缓存做成默认能力

Anthropic 的 Prompt Cache 更强调开发者如何显式设计缓存边界。DeepSeek 的 Context Caching on Disk 则走了另一条路线:默认开启,用户无感,只要后续请求和已有请求存在重叠前缀,就尽量从磁盘缓存中复用。

这件事很关键。因为大多数开发者不会每天研究缓存断点、TTL 和前缀匹配细节。平台如果能把缓存做成默认能力,长上下文应用的门槛会明显降低。

DeepSeek V4 Preview 还把重点放在更长上下文和更低计算成本上。公开信息里提到 V4-Pro 与 V4-Flash 支持 1M 上下文,并通过更激进的注意力与压缩机制降低长上下文的计算和显存压力。

可以把它理解成两层优化:

image.png

图 6:DeepSeek 长上下文压缩路径

这和 Prompt Cache 是同一个大方向:长上下文不只是“能不能塞进去”,而是“能不能经济地反复使用”。

Anthropic 和 DeepSeek 的缓存策略差异

两家的设计重点不完全一样。

维度AnthropicDeepSeek
TTL显式 5 分钟 / 1 小时两档,写入有溢价无固定 TTL,通常在数小时到数天内自动清理,写入无溢价
命中机制cache_control 断点 + 前缀逐 byte 匹配,可做部分前缀复用完整 cache prefix unit 完全匹配
用户感知需要理解规则,主动设计缓存边界默认开启,应用侧几乎不用改代码
工程重点给开发者更强的显式控制让长上下文复用更自动化
观测方式通过缓存相关 usage 字段和平台能力观察暴露 prompt_cache_hit_tokens 与 prompt_cache_miss_tokens

这不是谁绝对更好,而是产品取舍不同。Anthropic 更像给工程师一把精细工具,DeepSeek 更像把缓存变成默认基础设施。

对应用开发者来说,最现实的启发是:无论平台自动化到什么程度,稳定前缀仍然重要。默认缓存可以帮你省很多事,但如果你的 system prompt 每轮都变、工具顺序每次都随机、RAG chunks 被塞在最前面,再好的平台也很难救。

Prompt 设计已经变成系统设计

过去谈 Prompt Engineering,大家关心的是角色设定、示例、输出格式和措辞。现在做 Agent,问题已经变了。

真正要问的是:

  • • 哪些内容跨任务稳定,应该放到最前面?
  • • 哪些内容只是当前轮变量,应该沉到最后?
  • • 哪些状态变化可以用工具调用或消息表示,而不是改 system prompt?
  • • 哪些工具 schema 可以延迟加载?
  • • 哪些 RAG 内容必须保持确定性排序?
  • • 哪些缓存指标应该进监控和告警?

这已经不是写 prompt 的问题,而是上下文工程的问题。

我的判断是,未来 Agent 系统的好坏,至少会被两件事拉开差距:一是模型本身的推理能力,二是上下文是否被当成可复用资产来管理。

前者很显眼,大家都盯着 benchmark。后者更隐蔽,但会直接体现在账单、延迟、稳定性和产品可用性上。

如果只记一个原则,我会这样写:把 Agent 的上下文当成 append-only log 设计,不要把 prompt 当成每轮都能随手重写的模板。

这句话听起来朴素,但很多长程 Agent 的成本问题,都是从违反它开始的。