提示词、上下文与那笔为它买单的缓存
如果说 Prompt Engineering 是在优化”一句话怎么说”,那么 Context Engineering 就是在优化”每一轮对话中,模型看到的所有信息应该是什么”。这本质上是一个以平衡完备性与简洁性为核心的约束优化问题。
这一篇聚焦其中一个最容易被忽视、却直接花真金白银的角落:提示词的组装方式与缓存。
先看一个真实的账单故事
Agent,工作正常。两周后你的账单是预期的四倍。你翻模型用量日志,发现cache_read_input_tokens 接近零,cache_creation_input_tokens 却是满的——提示词每一轮都在从头重建。你检查系统提示,发现顶部有一句 Date.now(),是你当初为了”让助手知道当前时间”顺手加的。每一轮时间戳都不同,每一轮都是缓存未命中,每一轮都付全价。
修复只需一行。但教训更大:缓存节省在它坏掉之前是不可见的,而提示词有五六种方式静默地打破它。
一、提示词是一个组装出来的结构
一个有用的心智模型
,从顶部(最不可能变)到底部(最可能变)排列。flowchart TD
A["身份 / 系统指令<br/>(周级稳定)"] --> B["工具 schema + 描述<br/>(发布级稳定)"]
B --> C["项目 / workspace 上下文<br/>(会话级稳定)"]
C --> D["Skill 索引 + 冻结的记忆快照<br/>(会话级稳定)"]
D --> E["较早的对话轮次<br/>(易变)"]
E --> F["最近的工具结果<br/>(易变)"]
F --> G["最新的用户消息<br/>(易变)"]
style A fill:#d8f0d8
style B fill:#d8f0d8
style C fill:#d8f0d8
style D fill:#d8f0d8
style E fill:#ffe5cc
style F fill:#ffe5cc
style G fill:#ffcccc
稳定与易变的分界线,大致就是可缓存与不可缓存的分界线。设计提示词,大部分工作就是把东西放到这条线正确的一侧,并让它们待在那儿。
OpenCode、Hermes Agent、OpenClaw 以及领先的商业编程 Agent,都大致按这个顺序构建系统提示,并用一个确定性的合并,保证在没有实质变化时两次调用的字节序列完全一致。
二、不可变规则
最令人意外、也是大多数团队靠违反它才学会的规则:系统提示一旦构建就冻结。
如果一个工具在循环中途运行并写入了 MEMORY.md,正在运行的系统提示不会改变。这个更新会在下一个会话可见,而不是这一个。Hermes Agent 明确强制了这一点——文件支持的记忆更新故意不反映到运行中的提示里。领先的编程 Agent 也这么做。原因是机械性的:任何对前缀字节序列的改动,都会让其后每一轮的缓存失效。
这条规则有两个值得吸收的推论:
- 你能让提示词缓存在长会话里保持温热,当且仅当没有东西在飞行途中重写前缀。后台记忆写入去磁盘,在下一次会话启动时读取。
- “活的”提示词比冻结的贵,常常贵一个数量级。如果一个功能感觉需要活的提示词更新(“每轮给模型看当前时间”),把它放进易变的尾巴,而不是稳定的前缀。
三、缓存,用供应商中立的话说
供应商真正缓存的是你消息流的一个前缀。如果下一个请求的前缀和上一个请求的前缀逐字节匹配,供应商就跳过重新处理那些 token,只按正常价格的一个零头计费。机制因供应商而异:
- OpenAI 形状的 API 自动缓存前缀,无需标记——如果你的 token 匹配了更早的请求,就拿折扣。
- Anthropic 形状的 API 需要显式的
cache_control块。你可以标记最多四个断点,供应商独立缓存到每个断点。 - 其他供应商(Bedrock、Gemini、Vertex)介于两者之间,通常通过 SDK 的归一化层暴露。
无论哪种,给你的提示词构建器的规则都一样:保持前缀字节一致,把变化放到末尾。
关于 TTL
2026 年中,Anthropic 的临时缓存默认约每个断点五分钟,可选延长到约一小时(每 token 价格有溢价);OpenAI 形状的自动缓存用类似的供应商管理窗口。这些数字会动,调优前查你供应商的当前定价。但架构上的权衡是稳定的(连续轮次间隔几秒到几分钟)用短 TTL 就够;突发式会话(用户问一句、走开半小时、回来)值得为长 TTL 的溢价买单。正确的设置来自看你真实流量里的轮次间隔——让你的 Agent 拉出直方图再选阈值,不要靠眼估。四、什么会打破缓存
几乎所有诱人的东西都危险。常见的罪魁,具体地说:
- 前缀里的
Date.now()或任何时间戳。每轮一个新值,每轮一次未命中。 - 工具注册表变更。加或减一个工具会改变 schema 字节,而它坐在前缀早期。按 (agent, model) 组合缓存 schema 数组,但要明白注册表变动是昂贵的。
- 非确定性排序。如果你从
Object.entries()或文件系统遍历组装提示词而不排序,顺序会随运行时版本、OS、心情而变。OpenClaw 用一个静态的CONTEXT_FILE_ORDER映射,Hermes Agent 用一个固定的段落列表。选一个顺序,把它钉死。 - 后台记忆写入更新运行中的提示词——就是上面那条不可变规则,重申一遍因为它最容易不小心引入。
- 注入共享前缀的用户特定数据。如果多个用户打同一个 Agent,逐用户数据属于尾巴;前缀应该对用户无关。
- 空白和格式漂移。多一个换行就算一次未命中。如果你用模板生成提示词,锁死空白。
- 区域相关的格式化(对数字用
toLocaleString()、对日期用format()),在不同机器上产生不同字节。 - 一个保存时重写提示词模板的自动格式化器/linter。一个 reformat-on-save 工具插入一个尾随换行或规范化引号,会在下次服务部署时静默让每个缓存前缀失效。
最短的调试路径
指纹——渲染后前缀的 SHA——跨轮次观察这个值。如果没有实质变化时指纹却变了,你就有一个泄漏。分层指纹
一个覆盖整个前缀的指纹能抓到漂移,但它不告诉你漂移从哪来。便宜的升级是给前缀的每一层各记一个指纹:
debug: {
prefixFingerprint: sha(prefix.bytes).slice(0, 12),
identityFingerprint: sha(prefix.identity).slice(0, 12),
toolsFingerprint: sha(prefix.toolSchemas).slice(0, 12),
contextFingerprint: sha(prefix.projectContext).slice(0, 12),
memoryFingerprint: sha(prefix.frozenMemory).slice(0, 12)
}
当整体哈希漂移时,逐层哈希定位原因。工具哈希跨部署变化,通常是启用工具的增减或一次描述编辑;上下文哈希在会话中变化,通常是 workspace 遍历重排或一个上下文文件被磁盘上重写;记忆哈希在会话中变化,就是不可变规则被违反了。逐层视图把*“缓存在某处坏了”变成”有人编辑了一个工具描述”*——就一行日志。
五、和上一专题的呼应
如果你读过上一专题(Claude Code),这一篇的很多结论会似曾相识:
- Claude Code 的四级渐进压缩和断路器,解决的正是”前缀被填满后怎么办”,而它的 Fork 模式要求
CacheSafeParams五个字段字节级一致才能命中父级缓存——这就是本篇”保持前缀字节一致”的极致实践。 - Explore / Plan Agent 主动剥离 CLAUDE.md 以省 token,注释里写”每周省 5-15 Gtoken”——这是”用量大时,前缀里每一个可省的 token 都是真金白银”的直接体现。
换句话说
原理(为什么前缀要稳定、什么打破缓存),上一专题讲的是一个顶级 Agent 怎么把这个原理用到极致。两者互为印证。小结
- 系统提示不是字符串,是稳定前缀 + 易变尾巴的组装结构。
- 前缀一旦构建就冻结;后台更新去磁盘,下个会话生效。
- 缓存的是前缀,逐字节匹配才命中;把一切变化赶到末尾。
- 打破缓存的东西几乎都很诱人(时间戳、工具变更、格式漂移),用逐层指纹定位泄漏。
理解了这一层,你就理解了为什么”上下文工程”是 Agent 工程里省钱与提速的核心杠杆。下一部分我们进入记忆与状态——先从短期记忆和上下文压缩讲起。
本文主要整合自
Ch.04(提示词/上下文/缓存),agent开发系列《行为塑造与提示词工程》,hello-generic-agent《上下文》,以及 Hermes Agent / OpenCode 的提示词构建设计。属基于公开教程与开源项目的二次整理。