系列 《Prompt 与上下文工程:模型这一步该看到什么》 第 5 / 5 篇
系列总览 — 为什么这样组织、读它需要什么、读完能做什么
- 上下文的解剖——一次请求里模型看到的一切
- prompt 设计——稳定的模式与不稳定的措辞
- 结构化输出——约束解码、schema 设计与失败修复
- 上下文预算与压缩——给每一部分定配额,超了怎么办
- prompt caching 与上下文的排列
L1 第四篇算过缓存的账:命中读价 0.1×(Fable 5.1 0.025×、DeepSeek 0.02×),写入 1.25×,读一次就回本,多轮对话的二次增长被乘上 0.1。这一篇讲怎样让它命中——这是一个排列问题:缓存的是逐字节相同的前缀,所以上下文里每一段的位置、每一个字节是否稳定、每一次修改发生在哪,直接决定命中率。Manus 把 KV-cache 命中率列为”生产 agent 最重要的单一指标”,理由是它同时决定成本与 TTFT,而 agent 的输入输出比是 100 : 1——几乎全部成本都在可缓存的那一侧。
前四篇讲的每一种上下文操作都与缓存有关:第一篇的分层决定断点放哪;第三篇的 schema 稳定性影响前缀;第四篇的清理与压缩必然让缓存失效一次。这一篇把它们收拢成一套排列规则、四家的机制差异、几条反直觉的做法,以及命中率的监控与排查。
本篇要回答的核心问题是:
四家的缓存各要什么条件,断点怎么分层放?1 哪些操作会破坏前缀,为什么”屏蔽工具”比”删除工具”好?2 命中率低于多少要查什么、怎么查?3
一、总览
1. 一条规则
前缀逐字节相同才命中;顺序是 tools → system → messages;改动从改动处起全部失效。 其余一切都是这条规则的推论。
2. 排列的原则
┌──────────────────────────────────────────────┐
│ tools(工具定义) │ ← 静态:发布时变 断点 ①
├──────────────────────────────────────────────┤
│ system:指令 · 规则 · 格式(含 schema 注入) │ ← 静态 断点 ②
├──────────────────────────────────────────────┤
│ system 末尾 / 首条消息:few-shot · 用户画像 · 记忆 │ ← 半静态:会话间变 断点 ③
├──────────────────────────────────────────────┤
│ messages:历史(只追加) │ ← 每轮追加,前面的不变 断点 ④(最后一条消息)
├──────────────────────────────────────────────┤
│ 动态段:日期 · 环境状态 · 本轮检索 · 当前输入 │ ← 每轮变,放最后,不缓存
└──────────────────────────────────────────────┘
变化频率从上到下递增;断点放在每一层的末尾;动态内容放在所有断点之后。Anthropic 最多 4 个断点,正好对应四层;OpenAI 与 DeepSeek 自动缓存不需要你放断点,但排列规则相同——自动缓存也只缓存前缀。
3. 本文的章节安排
第二章四家的机制;第三章断点的分层与放置;第四章破坏前缀的操作清单与 Manus 的三条规则;第五章反直觉的做法(屏蔽工具、保留失败、不删历史);第六章命中率的监控与排查;第七章与清理、压缩、effort 的配合;第八章实践建议。
二、四家的机制
| Anthropic | OpenAI | Google(Gemini) | DeepSeek | |
|---|---|---|---|---|
| 声明 | 显式:cache_control: {type: ephemeral} 放在具体的内容块上;或顶层一个 cache_control 自动放到最后一个可缓存块 |
自动(无声明);prompt_cache_key 可选,帮路由到同一缓存 |
隐式(自动,短 TTL)+ 显式:创建缓存对象、指定 TTL、请求引用它 | 自动,磁盘级前缀缓存 |
| 最小长度 | 按模型 1,024 token 起,不足静默不缓存 | 1,024 token;按 128 的倍数向下取整 | 模型相关 | 64 token 粒度 |
| 断点 | 最多 4 个;20 块回看窗口(系统在最后一个断点之前 20 块内找最长命中) |
|
显式缓存对象即断点 | 无需 |
| TTL | 5 分钟(命中免费刷新);1 小时(写 2×) | 分钟级;部分模型支持延长保留 | 显式缓存按设定 TTL,按小时收存储费(3.8 Flash 引入价 0.50 美元 / 百万 token · 小时) | 数小时到数天 |
| 价格 | 读 0.1×(Fable 5.1 0.025×);写 1.25× / 2× | 读 0.1×;写 1.25×(GPT-5.6 起) | 读 0.1× + 存储费 | 读约 0.02×;无写入费 |
| 用量字段 | cache_read_input_tokens、cache_creation_input_tokens |
input_tokens_details.cached_tokens |
cached_content_token_count |
prompt_cache_hit_tokens、prompt_cache_miss_tokens |
| 特点 | 控制最细;缓存键含顶层参数(effort、thinking 配置)——逐消息 effort(beta)是特意做成不失效的例外 | 最省事;prompt_cache_key 让同一用户 / 会话的请求落到同一缓存 |
显式缓存适合”一份长文档反复问”:存一次、按小时付存储、每次问只付读价 | 最便宜;粒度最细(64) |
两个设计差别值得注意。显式 vs 自动:Anthropic 的显式断点让你精确控制”缓存到哪”,代价是要理解规则(最小长度、20 块回看)并在代码里维护;自动缓存零维护但你不知道它缓存了哪一段——只能从 usage 反推。存储费:Gemini 的显式缓存是唯一按时间收费的,它的模型是”你租一块 KV 存储”,适合长文档问答(把 500 页手册存一小时、问二十个问题),不适合高频短会话。
三、断点的分层
1. 四层四个断点
以 Anthropic 为例(自动缓存的供应商按同样顺序排列即可):
| 断点 | 放在 | 覆盖 | 变化频率 | 命中场景 |
|---|---|---|---|---|
| ① | 最后一个工具定义 | 全部 tools | 发布时 | 所有请求(任何用户、任何会话) |
| ② | system 的静态部分末尾 | tools + 静态 system | 发布时 | 所有请求 |
| ③ | 半静态部分末尾(用户画像、few-shot、记忆) | + 半静态 | 会话间 / 用户间 | 同一用户的所有请求 |
| ④ | 最后一条历史消息 | + 全部历史 | 每轮 | 同一会话的下一轮 |
四个断点的意义是分级命中:一个新用户的第一轮命中 ①②(跨用户共享的前缀),第二轮起命中 ④。20 块回看窗口的含义:系统从最后一个断点向前找最多 20 个内容块,寻找最长的已缓存前缀——所以断点 ③④ 之间不要隔太多块。
顶层自动断点(Anthropic 2026 年加的 cache_control 顶层写法)等价于只放断点 ④ 并让它随对话前移——多轮对话的最简写法,但失去了 ①②③ 的跨会话共享。两者可以同时用(自动断点占 4 个槽位之一)。
%% 图:四个断点的分级命中——新用户第一轮命中 ①②(所有请求共享的 tools + 静态 system);同一用户的新会话命中到 ③(加上用户画像与记忆);同一会话的下一轮命中到 ④(加上全部历史);请求越「熟」,命中的前缀越长
flowchart TB
subgraph P["前缀(按变化频率排)"]
direction LR
T["tools"] --> S["静态 system"] --> U["半静态:画像 · few-shot · 记忆"] --> H["历史(只追加)"] --> D["动态段:日期 · 检索 · 当前输入"]
end
T -.- B1["断点 ①"]
S -.- B2["断点 ②"]
U -.- B3["断点 ③"]
H -.- B4["断点 ④"]
R1["新用户第一轮"] -->|"命中到 ②"| B2
R2["老用户新会话"] -->|"命中到 ③"| B3
R3["同一会话下一轮"] -->|"命中到 ④"| B4
classDef pre fill:#eefaf0,stroke:#4d9a5c,color:#222
classDef dyn fill:#fdecea,stroke:#c0392b,color:#222
classDef bp fill:#eef6ff,stroke:#5b8fd6,color:#222
classDef req fill:#fff7e0,stroke:#c98a00,color:#222
class T,S,U,H pre
class D dyn
class B1,B2,B3,B4 bp
class R1,R2,R3 req
2. 最小长度的陷阱
不足最小长度的块静默不缓存——没有报错,usage 里 cache_creation_input_tokens 是 0。一个 800 token 的 system prompt 永远不会被缓存;一个团队为此困惑了一周的情况并不少见。对策:把 tools 与 system 合起来看是否过线(Anthropic 的断点覆盖的是到该块为止的全部前缀,所以 ② 覆盖 tools + system 的总长);或者接受它——800 token 每轮全价也不贵,二次增长的主体在历史。
3. 半静态层的判断
用户画像、few-shot、长期记忆放在 ② 与 ③ 之间还是 ③ 之后,取决于它们多久变一次:用户画像会话间变、会话内不变——放 ③;每轮都在更新的记忆(Claude Code 的自动记忆在会话中会写)——它的更新会让 ③ 之后全部失效,要么放到动态段、要么批量更新(会话结束时写)。
四、破坏前缀的操作
1. 清单
从最常见到最隐蔽:
| 操作 | 后果 | 修法 |
|---|---|---|
| system 开头写当前时间(精确到秒 / 分) | 每次请求前缀不同,全部失效 | 日期放动态段末尾;精度到天 |
| 请求间隔超过 TTL | 缓存过期,下一次全量写入 | 会话型产品用 1 小时 TTL(Anthropic);接受低频用户不命中 |
| 修改历史中间的内容(清理旧返回、换摘要、修正一条消息) | 从修改处起失效 |
|
| 工具集动态增删 | tools 在最前,改它让一切失效 | 保留全部工具、用屏蔽控制可用(第五章) |
| 序列化不确定 | JSON 键序、浮点格式、空白不同 → 字节不同 | 固定键序(sort_keys)、固定格式 |
| schema 动态生成 | 注入的 schema 文本每次不同 | schema 稳定(第三篇) |
| 顶层参数改动 | Anthropic 缓存键含 effort / thinking 配置 | 会话内不改;用逐消息 effort(beta) |
| 多实例路由到不同缓存 | OpenAI 的缓存按前缀哈希路由,负载均衡可能把同一会话打到不同节点 | 传 prompt_cache_key(如会话 id) |
| 半静态层每轮更新 | ③ 之后失效 | 批量更新或移到动态段 |
| 检索结果放在历史前面 | 每轮检索不同 → 历史全部失效 | 检索结果放动态段(历史之后) |
2. Manus 的三条规则
Manus 的博客把它们压成三句,值得原样记:
- 保持 prompt 前缀稳定。 由于自回归的性质,一个 token 的差异让其后的缓存全部失效——常见的错误是在 system 开头放精确时间戳。
- 上下文只追加。 不修改之前的动作与观察;序列化要确定性——很多框架不保证 JSON 键序,一次序列化的差异就静默地毁掉缓存。
- 需要时显式标记断点。 有些供应商或框架不做自动增量缓存,要手工插断点;放断点时至少要覆盖 system prompt 的末尾。
他们给的数字:Claude Sonnet 缓存与未缓存输入价差 10 倍(0.30 vs 3.00 美元 / 百万,2025 年价目),一个 50 步的 agent 任务里这个差异是成本的主体。
%% 图:一个 50 步 agent 任务的输入成本(美元,Sonnet 每百万输入 3 美元、缓存读 0.3 美元,2025 年价目;常驻 15K,每步 +3.3K)——前缀稳定时约 2 美元,system 开头有精确时间戳时每步全价重读,约 14 美元;差的那 12 美元全是被时间戳毁掉的缓存
%%{init: {"xyChart": {"width": 760, "height": 340, "plotReservedSpacePercent": 60}, "themeVariables": {"xyChart": {"plotColorPalette": "#c0392b, #5b8fd6, #4d9a5c"}}}}%%
xychart-beta
title "50 步 agent 任务的累计输入成本(美元)"
x-axis "步数" [10, 20, 30, 40, 50]
y-axis "美元" 0 --> 16
line [0.9, 2.8, 5.7, 9.5, 14.4]
line [0.26, 0.56, 0.96, 1.46, 2.06]
红线是前缀每步都变(时间戳、不确定序列化),蓝线是前缀稳定、只追加。两条线的差就是 Manus 说的「成本的主体」。
五、反直觉的做法
1. 屏蔽工具,不删除工具
agent 在不同阶段可用的工具不同(规划阶段不该写文件、确认前不该付款)。直觉做法是按阶段增删 tools——这让最前面的前缀失效,且历史里引用了已删除工具的调用会让模型困惑甚至报错。Manus 的做法是保留全部工具定义,用屏蔽控制可用性:自托管时在解码阶段屏蔽不允许的工具名的 logits(工具名有统一前缀如 browser_、shell_,屏蔽一个前缀即屏蔽一类);用 API 时用 tool_choice 约束(auto / required / 指定 / none)。前缀不变,可用性变了。
OpenAI 与 Anthropic 的 tool search 是另一条路:工具定义不常驻、按需加载。它解决的是”工具太多占预算”(第一篇),代价是按需加载的定义在历史中间、不可缓存;对”阶段性可用”这个问题,屏蔽仍然更好。
%% 图:按阶段控制工具可用性的两种做法——按阶段增删 tools 让最前面的前缀失效、历史里引用了已删工具的调用会让模型困惑;保留全部定义、用 logits 屏蔽(自托管,按 browser_ / shell_ 前缀屏蔽一类)或 tool_choice 约束(API)控制可用性,前缀一个字节不变
flowchart TB
subgraph A["增删 tools(直觉做法)"]
direction LR
A1["规划阶段:tools = [search, read]"] --> A2["执行阶段:tools = [search, read, write, shell]"] --> A3["确认前:tools = [search, read]"]
A1 -.- AX["每次切换 → tools 变 → 前缀全部失效<br/>历史里的 write 调用引用了已删的工具"]
end
subgraph B["屏蔽(Manus 做法)"]
direction LR
B1["全部阶段:tools = [search, read, write, shell](不变)"] --> B2["规划阶段:屏蔽 write_ / shell_ 前缀的 logits<br/>或 tool_choice 限定"] --> B3["执行阶段:全部放开"]
B1 -.- BX["前缀逐字节不变 → 命中"]
end
A ~~~ B
classDef bad fill:#fdecea,stroke:#c0392b,color:#222
classDef ok fill:#eefaf0,stroke:#4d9a5c,color:#222
classDef n fill:#fff7e0,stroke:#c98a00,color:#222
class A1,A2,A3 n
class AX bad
class B1,B2,B3,BX ok
2. 不删失败
第四篇已讲:失败的工具返回、错误信息保留在上下文里是模型调整行为的证据。从缓存角度它还有一个理由:删除就是修改历史,让前缀失效。两个理由指向同一个做法。
3. 不”整理”历史
工程师的本能是让上下文整洁:合并连续的用户消息、把冗长的工具返回换成一句总结、纠正模型上一轮的一个错字。每一项都是修改历史、都让前缀失效、都可能破坏推理状态(L1 第三篇)。整洁的正确时机只有一个:压缩(第四篇),一次做完、接受一次失效。
4. 长 TTL 的账
Anthropic 的 1 小时 TTL 写入价 2×(vs 5 分钟的 1.25×),L1 第四篇算过要读两次才回本。什么场景值得:用户思考时间长的会话(客服里用户查资料再回来、编程助手里用户去改代码再回来)——5 分钟内没有下一轮,缓存过期,下一轮全量重写;1 小时 TTL 让这类会话的第二轮起仍然命中。判断方法:从 trace 里统计轮次间隔的分布,间隔 5–60 分钟的轮次占比高于两成就值得。
六、命中率的监控与排查
1. 定义与目标
\[\text{命中率} = \frac{n_{\text{hit}}}{n_{\text{in}} + n_{\text{hit}} + n_{\text{write}}}\]按请求类型分开看:多轮对话与 agent 的稳态命中率应在 70–90%;单轮问答(每次不同用户、无历史)只有 ①② 能命中,命中率 = 静态前缀 / 总输入,通常 30–60%,这是结构决定的不是问题。把命中率与未缓存 token 的绝对数一起看——第一篇自测 5 的例子:渐进披露让命中率下降但总成本下降。
2. 排查表
| 症状 | 最可能的原因 | 检查 |
|---|---|---|
| 命中率长期为 0 | 前缀不足最小长度;或 Anthropic 没放断点 | 看 cache_creation_input_tokens 是否为 0;数 tools + system 的 token |
| 每轮都是写入、没有读取 | 前缀每次不同:时间戳、随机 id、不确定序列化 | diff 连续两次请求体的前 N 个字节 |
| 命中率随会话长度下降 | 半静态层在会话中更新;或检索结果放在历史前 | 检查 ③ 之后的内容是否每轮变 |
| 某些用户命中、某些不命中 | 请求间隔超 TTL | 统计轮次间隔分布 |
| 命中率在某天骤降 | 部署改了 system / tools / schema;或模型升级换了 tokenizer | 对照部署时间线;换模型会一次性失效(正常) |
| 多实例部署命中率低于单实例 | 路由到不同缓存节点 | OpenAI 传 prompt_cache_key |
| 清理 / 压缩后命中率归零 | 预期行为 | 频率是否过高 |
3. 从 trace 计算
L1 第二篇的中间层记录了四类 token;每个请求算命中率,按会话、按请求类型、按模型聚合;再加两个派生指标——未缓存输入 token / 请求(绝对量)与缓存节省的美元 / 天(\(n_{\text{hit}} \times (p_{\text{in}} - p_{\text{hit}})\) 减去写入加价)。第二个数字通常是让团队认真对待排列规则的那一个。
%% 图:命中率排查的决策树——先看 cache_creation 是否为 0(前缀不足最小长度或没放断点),再看是否每轮都在写入(前缀每次不同:时间戳、随机 id、序列化不确定),再看是否随会话长度下降(半静态层每轮更新或检索结果放在历史前),再看是否只有部分用户不命中(间隔超 TTL),最后看是否某天骤降(部署改了前缀或换了模型)
flowchart TB
Q["命中率低于预期"] --> A{"cache_creation 也是 0?"}
A -->|"是"| A1["前缀不足最小长度(1,024)<br/>或 Anthropic 没放断点"]
A -->|"否"| B{"每轮都是写入、没有读取?"}
B -->|"是"| B1["前缀每次不同:<br/>时间戳 · 随机 id · JSON 键序不定<br/>→ diff 连续两次请求体的前 N 字节"]
B -->|"否"| C{"随会话长度下降?"}
C -->|"是"| C1["半静态层每轮更新<br/>或检索结果放在历史前面"]
C -->|"否"| D{"只有部分用户不命中?"}
D -->|"是"| D1["请求间隔超 TTL<br/>→ 统计间隔分布,考虑 1 小时 TTL"]
D -->|"否"| E{"某天骤降?"}
E -->|"是"| E1["部署改了 system / tools / schema<br/>或换模型(一次性失效,正常)"]
E -->|"否 · 多实例"| F1["路由到不同缓存节点<br/>→ 传 prompt_cache_key"]
classDef dec fill:#eef6ff,stroke:#5b8fd6,color:#222
classDef fix fill:#fff7e0,stroke:#c98a00,color:#222
class A,B,C,D,E dec
class A1,B1,C1,D1,E1,F1 fix
七、与其他操作的配合
- 清理(第四篇):修改历史 → 失效。批量做、在清理线做,让两次清理之间的前缀稳定;服务端清理(Anthropic 的 context editing)由供应商处理缓存关系,优于客户端自己删。
- 压缩:必然失效一次,之后以短前缀重建。压缩线离硬线要有距离,否则每几步一次压缩,缓存永远在重建。
- effort:Anthropic 的缓存键含顶层 effort,会话中改 effort 会失效——逐消息 effort(beta 头
mid-conversation-output-config-2026-07-01)是不失效的例外;OpenAI 的 Responses 每次请求独立设reasoning.effort,服务端状态下不影响前缀。 - 子 agent:子 agent 有自己的上下文与前缀;它的 system 与工具定义也应稳定,多个子 agent 共享同一前缀时能互相命中(同一时间窗口内)。
- 多供应商 fallback(L1 第六篇):切到另一家,缓存从零开始;fallback 的成本要把这一项算进去。
八、实践建议
- diff 两次连续请求体:取同一会话相邻两轮的完整请求体,找第一个不同的字节在哪——它就是你的有效前缀边界。多数团队第一次做会在 system 前几百字节里找到一个时间戳或 id。
- 按四层放断点(Anthropic)或按四层排列(其他家),动态段一律放最后。
- 固定序列化:
sort_keys、固定浮点格式、去掉无意义空白;把”序列化确定性”写进代码审查清单。 - 工具全量保留 + 屏蔽,不按阶段增删。
- 统计轮次间隔,决定 TTL;多实例部署传
prompt_cache_key。 - 命中率、未缓存 token / 请求、缓存节省美元 / 天三个指标上仪表盘,按请求类型分开;命中率骤降接部署时间线告警。
九、本文小结
- 一条规则:前缀逐字节相同才命中,顺序 tools → system → messages,改动处起失效;排列按变化频率分四层——静态工具、静态 system、半静态(画像 / few-shot / 记忆)、只追加的历史——动态段放最后。
- 四家:Anthropic 显式断点(最多 4 个、20 块回看、1,024 起静默不缓存、5 分钟 / 1 小时)或顶层自动;OpenAI 自动(1,024、128 倍数、
prompt_cache_key路由);Gemini 隐式 + 显式缓存对象(按小时存储费,适合长文档反复问);DeepSeek 自动 64 粒度。 - 破坏前缀的十种操作:时间戳、超 TTL、改历史、增删工具、不确定序列化、动态 schema、改顶层参数、多实例路由、半静态层每轮更新、检索放历史前。Manus 三条:前缀稳定、只追加、显式断点。
- 反直觉:屏蔽工具不删除(logits 或
tool_choice)、不删失败、不整理历史(整理只在压缩时)、用户思考时间长的会话用 1 小时 TTL。 - 命中率 = hit / 总输入,多轮与 agent 稳态 70–90%,单轮 30–60% 是结构决定;与未缓存 token 绝对量、节省美元一起看;排查表按症状定位。
十、自测
-
一个客服应用的请求体:system 开头是”当前时间:2026-10-10 14:23:07”,之后是 3,000 token 的规则、工具定义、历史。命中率接近 0。为什么?改法?
答案
时间戳精确到秒放在最前面,每次请求前缀的第一行就不同,其后全部失效。改法:把时间移到动态段末尾(历史之后、当前输入之前),精度到天;tools 与 system 按静态 → 半静态排列并放断点。详见第四章。
-
Anthropic 上一个 700 token 的 system prompt 加 200 token 的工具定义,放了断点,
cache_creation_input_tokens始终为 0。原因?值得修吗?答案
到断点为止的前缀 900 token 不足该模型的最小可缓存长度(1,024 起),静默不缓存。修的价值取决于历史长度:常驻层每轮全价 900 token 不贵,二次增长的主体在历史——在最后一条历史消息上放断点让历史命中才是收益所在;若想让常驻层也命中,可把 few-shot 等半静态内容一起纳入使前缀过线。详见第三章。
-
agent 分”规划 / 执行 / 确认”三阶段,团队按阶段传不同的 tools 列表。命中率与错误率各会怎样?Manus 的做法是什么?
答案
tools 在前缀最前,换列表让每个阶段切换时缓存全部失效;且历史里有对已删工具的调用记录,模型会困惑、API 可能因引用不存在的工具而报错。Manus:全部工具始终在定义里,用 logits 屏蔽(自托管,按工具名前缀屏蔽一类)或
tool_choice约束(API)控制当前阶段可用的工具——前缀不变。详见第五章。 -
一个会话第 2–10 轮命中率 85%,第 11 轮起变成每轮都是写入。看 trace 发现第 11 轮起每轮请求体里历史之前多了一段”用户最近浏览记录”,每轮不同。诊断与修法?
-
从 trace 统计得到轮次间隔:60% 在 5 分钟内,30% 在 5–60 分钟,10% 超过 1 小时。Anthropic 上该用哪个 TTL?算一下 30% 那部分的账(写 1.25× vs 2×,读 0.1×,假设每个间隔后至少还有 3 轮)。
答案
30% 的轮次在 5 分钟 TTL 下会过期、下一轮全量重写;1 小时 TTL 覆盖它们。账:5 分钟 TTL 下这些前缀被写两次(间隔前后各一次 1.25×)加后续读;1 小时 TTL 写一次 2× 加读。间隔后有 3 轮时:5 分钟方案 1.25 + 0.3 + 1.25 + 0.3 = 3.1×,1 小时方案 2 + 0.6 = 2.6×——1 小时更省,且 TTFT 更好;占比三成足以让 1 小时 TTL 划算。超过 1 小时的 10% 两种方案都不命中。详见第五章。
-
条件都是前缀逐字节相同、顺序 tools → system → messages。Anthropic:显式
cache_control放在内容块上(最多 4 个断点、20 块回看窗口、按模型 1,024 token 起不足静默不缓存、5 分钟或 1 小时 TTL、缓存键含顶层参数)或顶层一个自动断点;OpenAI:自动(1,024 起、128 倍数、GPT-5.6 起断点在最近可缓存消息末尾),prompt_cache_key帮多实例路由;Gemini:隐式自动 + 显式缓存对象(按小时收存储费,适合长文档反复问);DeepSeek:自动磁盘缓存、64 token 粒度、无写入费。断点按变化频率分四层:① 工具定义末尾(跨用户共享)、② 静态 system 末尾、③ 半静态(画像 / few-shot / 记忆)末尾(同一用户共享)、④ 最后一条历史消息(同一会话下一轮命中);动态段(日期、环境、本轮检索、当前输入)放所有断点之后。详见第二章、第三章。 ↩ -
破坏前缀:system 开头的精确时间戳、请求间隔超 TTL、修改历史中间内容(清理、换摘要、纠错)、工具集动态增删(tools 在最前,一切失效)、不确定的序列化(JSON 键序 / 浮点格式)、动态生成的 schema、会话中改顶层参数(Anthropic 缓存键含 effort)、多实例路由到不同缓存节点、半静态层每轮更新、检索结果放在历史前。Manus 三条:前缀稳定、只追加(序列化确定性)、显式断点。屏蔽优于删除:按阶段增删 tools 让最前面的前缀失效且历史里引用了已删工具会让模型困惑或报错;保留全部定义、用 logits 屏蔽(自托管,按名字前缀屏蔽一类)或
tool_choice(API)控制可用性,前缀不变。同理不删失败、不整理历史——整理只在压缩时一次做完。详见第四章、第五章。 ↩ -
命中率 = \(n_{\text{hit}} / (n_{\text{in}} + n_{\text{hit}} + n_{\text{write}})\),多轮与 agent 稳态应 70–90%,单轮问答 30–60% 是结构决定;要与未缓存 token 绝对量、节省美元一起看。排查:长期为 0 → 前缀不足最小长度或没放断点(看
cache_creation_input_tokens);每轮都写没有读 → 前缀每次不同(diff 连续两次请求体前 N 字节找时间戳 / 随机 id / 序列化差异);随会话长度下降 → 半静态层会话中更新或检索放在历史前;部分用户不命中 → 间隔超 TTL(统计间隔分布,三成在 5–60 分钟就换 1 小时 TTL);某天骤降 → 部署改了 system / tools / schema 或换模型;多实例低于单实例 → 传prompt_cache_key;清理 / 压缩后归零 → 预期,看频率。详见第六章。 ↩
系列 《Prompt 与上下文工程:模型这一步该看到什么》 第 5 / 5 篇
系列总览 — 为什么这样组织、读它需要什么、读完能做什么
- 上下文的解剖——一次请求里模型看到的一切
- prompt 设计——稳定的模式与不稳定的措辞
- 结构化输出——约束解码、schema 设计与失败修复
- 上下文预算与压缩——给每一部分定配额,超了怎么办
- prompt caching 与上下文的排列
本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/prompt-caching-and-context-layout.html)的前提下,欢迎各种形式的转载、翻译或商业引用。
COMMENTS
评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。