Cache-Friendly Prompting
一、为什么这件事现在值得做
1.1 一组价格数字
2026 年 9 月 22 日的两份价目表:
| 模型 | 标准输入 | 缓存输入 | 差额 | 输出 |
|---|---|---|---|---|
| Claude Opus 5.5 | $4.00/M | 再降 60% | — | $20/M |
| GPT-6 Sol | $2.00/M | $0.20/M | 10 倍 | $10/M |
| GPT-6 Luna | $0.10/M | $0.01/M | 10 倍 | $0.50/M |
GPT-6 还额外规定:30 分钟内复用的合格共享前缀,最高 90% 折扣。
这意味着什么:在一个多轮 Agent 会话里,输入 token 通常占总 token 的 70–90%。如果这部分的 80% 能命中缓存,你等于把账单砍掉了六成——而你要做的只是把提示词换个顺序。
1.2 为什么以前不用管,现在必须管
三年前提示词短、会话少,缓存不缓存差别不大。现在三个变化叠加:
| 变化 | 影响 |
|---|---|
| 系统提示 + 项目规范越来越长 | 稳定部分占比提高,缓存收益放大 |
| Agent 化:单任务几十次调用 | 同样的前缀被重复发送几十次 |
| 长上下文(100 万 token) | 输入成本彻底主导账单 |
一个典型 Agent 任务:
单次调用输入 = 系统提示 1200 + 项目规范 1800 + 历史 20000 + 新问题 500
= 23500 token
其中稳定部分 = 1200 + 1800 = 3000 token(占 12.8%)
看起来稳定部分占比不高,但历史对话也会命中缓存——只要前面的轮次没变。所以实际可缓存比例是:
可缓存 = 系统提示 + 项目规范 + 未变化的历史 ≈ 85%+
这才是缓存能有 50–90% 收益的原因。
1.3 一个残酷的对比实验
同一批 20 个任务,两种提示词组织方式:
| 组织方式 | 缓存命中率 | 输入成本 | 总耗时 |
|---|---|---|---|
| 随手写(时间戳前置、每次重写规范) | 4% | 基准 | 基准 |
| 稳定前缀(四层结构) | 81% | 基准 -63% | 基准 -8% |
耗时还降了 8%,因为缓存命中时服务端跳过了前缀的 prefill 计算,首 token 延迟明显下降。
成本降 63%,质量零损失,实施成本约 30 分钟。 这就是为什么它是投入产出比最高的一个手段。
二、缓存是怎么工作的(必须搞清楚的三件事)
2.1 前缀匹配,不是语义匹配
缓存的匹配逻辑是逐字节的前缀匹配:
请求 1: [ABCDEFG][问题1] → 缓存写入 ABCDEFG
请求 2: [ABCDEFG][问题2] → 命中 ✓(前缀逐字节相同)
请求 3: [ABCDEFH][问题3] → 未命中 ✗(第 7 个字节就不同了)
请求 4: [ABCDEFG ][问题4] → 未命中 ✗(多了一个空格)
三个推论:
- 改动必须发生在末尾。前面任何一处变动,后面全部失效。
- 空格、标点、换行都算。同一段文字两次生成的缩进不同,就命中不了。
- 它不是「相似即可」。语义上 99% 相同但差一个字符 = 完全未命中。
这是最反直觉也最容易踩的坑:很多人以为「我大段的规范没变,应该能缓存」,结果规范前面挂了一句带时间戳的话,整段直接失效。
2.2 最小缓存单位与粒度
不同厂商的实现细节不同,但共同点:
- 按块(block)缓存,块大小通常 1024–4096 token
- 只有完整的块才能命中
- 命中后从块的起点开始复用
推论:稳定部分太短(不足一个块)可能不值得缓存。实践经验是稳定前缀 ≥ 1024 token 收益才明显。这也意味着太短的系统提示(比如 100 token)做缓存优化意义不大。
2.3 缓存有生存期
Claude 系: 约 5 分钟(部分实现支持续期)
GPT-6: 官方口径「30 分钟内复用的合格共享前缀」
批处理: 通常窗口内有效
推论:会话中断超过生存期,缓存就没了。所以连续的多轮会话收益最大,间隔很久的单次请求收益为零。
实务建议:把相关的任务攒在一起连续做,而不是分散在一天里穿插做。
三、四层结构设计
3.1 整体骨架
┌──────────────────────────────────────┐
│ L0 系统层 · 角色 / 规则 / 输出格式 │ 稳定,≥ 1024 token
│ · 逐字节固定,永不改写 │
├──────────────────────────────────────┤
│ L1 项目层 · 技术栈 / 编码规范 / 禁区 │ 稳定,来自 AGENTS.md
│ · 同一仓库内逐字节固定 │
├──────────────────────────────────────┤
│ L2 任务层 · 本次会话的任务背景与已确认契约 │ 会话内稳定
│ · 追加式更新,不回改 │
├──────────────────────────────────────┤
│ L3 变量层 · 当前文件 / 时间戳 / 用户问题 │ 每次请求都变
│ · 永远在最后 │
└──────────────────────────────────────┘
↑ 可缓存部分 ↑ ↑ 变化部分 ↑
铁律:L0 与 L1 在任何一次请求中都必须逐字节相同。
3.2 L0 系统层:写什么、写多长
目标长度:1000–2000 token。太短不值得缓存,太长也没必要。
内容清单:
1. 角色定义(1-2 句)
2. 全局规则(5-15 条,每条可判定)
3. 输出格式要求(明确的 schema 或模板)
4. 禁止事项(安全相关)
5. 停止条件(什么时候应该停下来问)
样例:
你是 acme/order-service 的资深后端工程师,负责代码改动与评审。
## 全局规则
1. 先读后改,禁止凭猜测写文件
2. 改动前确认工作区干净
3. 每次改动后运行相关测试
4. 一次只做一条任务,完成后报告
5. 不确定时标注 [BLOCKER] 并停止
## 输出格式
每步输出三行:
做什么:一句话
改了什么:文件路径 + 行号区间
验证结果:命令 + 输出摘要
## 禁止
- 不修改 .github/ 与锁文件
- 不执行破坏性命令
- 不引入新依赖(除非明确要求)
## 停止条件
- 遇到规则未覆盖的情况 → 停止询问
- 连续 2 次验证失败 → 停止报告
这一段约 1100 token,且永远不变。 这就是缓存的主力。
❌ 绝不能出现在 L0 的内容:
- 今天日期 / 当前时间
- 当前工作目录
- 当前 Git 分支
- 会话 ID / 请求 ID
- 「以下是用户最新上传的文件:...」
- 任何每次请求会变的东西
3.3 L1 项目层:从 AGENTS.md 加载,不要手写
关键做法:项目层内容从仓库的 AGENTS.md 文件读取,而不是每次手打。
为什么:
- 文件内容固定 → 逐字节相同 → 缓存稳定
- 手写会引入缩进/换行的细微差异 → 缓存失效
- 团队共享同一份规范 → 全员的缓存命中率都高
[项目层 - 来自 AGENTS.md]
技术栈:TypeScript 5.4, Node 20, Prisma 5, PostgreSQL 15
构建:pnpm build / pnpm test / pnpm typecheck
编码规范:
- 金额一律用 Decimal,禁止 float
- 文件路径别名用 @/ 前缀
- 错误处理统一用 Result<T, E>
禁区:
- 不修改 migrations/ 下已提交的迁移
- 不在 services/ 里直接写 SQL
- 测试用 testcontainers,禁止 mock 数据库
长度控制在 1500 token 以内。超过就把细节挪到 docs/,用链接指过去让 Agent 按需读——这是「缓存友好」与「上下文预算」的折中点。
3.4 L2 任务层:追加式,不回改
任务层放「本次会话特有的、但会话内不变的」内容:
[任务层]
本次任务:把订单服务的金额计算统一到 Money 值对象。
已确认的契约:
- Money 底层用 Decimal
- 对外 JSON 仍序列化为字符串
- 涉及范围:domain/ services/ api/
任务拆分:T1 领域层 → T2 服务层 → T3 接口层
当前进度:T1 已完成,T2 进行中
更新规则:只用追加,绝不回改。
✅ 正确:在末尾追加 "T1 已完成,发现 X,继续 T2"
❌ 错误:回去把 "任务拆分:T1→T2→T3" 改成 "T2→T3"
因为一回改,L2 后半部分就变了,整个后缀都不再匹配。追加式更新是缓存友好的核心纪律之一。
3.5 L3 变量层:唯一允许变化的地方
[变量层]
当前文件:src/services/order-pricing.ts
当前分支:feature/money-refactor
时间:2026-09-23T14:32:07+08:00
用户问题:
看一下 calculateTotal 里的折扣计算逻辑,为什么满减没生效?
所有会变的东西集中在这里,且必须在最后。
四、把动态内容「稳定化」的五种手法
有时候你确实需要把一些会变的信息放在前面(比如为了让 Agent 一开始就看到)。五种处理手法:
手法一:枚举化
把「动态但可枚举」的内容写成固定的完整清单。
❌ 动态:
当前可用命令:{{command_list}} ← 每次可能不同
✅ 稳定化:
可用命令(完整清单):
pnpm build 构建
pnpm test 全量测试
pnpm typecheck 类型检查
pnpm lint 代码检查
pnpm migrate 数据库迁移
即使某些命令当前不可用,也保持这份清单不变。
代价:可能包含当前不适用的内容。收益:前缀稳定。多数情况下值得。
手法二:占位符 + 后置替换
❌ 前置变量:
当前时间是 2026-09-23 14:32,仓库是 acme/order-service,请...
✅ 后置变量:
[系统提示与项目规范...]
[上下文]
仓库:acme/order-service
时间:2026-09-23 14:32
任务:...
手法三:分层缓存 —— 接受部分失效
如果某段内容必须频繁更新,把它单独放在稳定块的后面:
[L0 系统层 1200 token] ← 永久稳定,缓存命中
[L1 项目层 1800 token] ← 长期稳定,缓存命中
[L1.5 动态段 300 token] ← 会变,这之后失效
[L2 任务层]
[L3 变量层]
即使 L1.5 之后全部未命中,前 3000 token 仍然命中。这是分层设计的价值——缓存不是全有全无,是按块累计的。
实务判断:把最稳定的放最前,次稳定的次之,最动态的放最后。
手法四:模板化而非实例化
❌ 每次实例化:
分析文件 src/a.ts、src/b.ts、src/c.ts ...(每次文件列表不同)
✅ 模板化:
分析任务:对指定文件执行 {ANALYSIS_TASK}。
(文件列表在变量层给出)
手法五:把「日期」换成「相对描述」
❌ 今天是 2026-09-23
✅ 判断时效性时,以变量层给出的当前日期为准
❌ 最近 7 天的提交
✅ 变量层:时间范围 = 最近 7 天(截至 2026-09-23)
五、可复用的提示词骨架
骨架一:编码任务(本文主模板)
见开头的 prompt 字段,四层 + 缓存纪律。
骨架二:批量处理任务
## 系统层(稳定)
你是批量文档处理器。对每一批输入执行相同的处理流程。
流程:
1. 读取输入
2. 按下面的规则处理
3. 输出固定格式的结果
处理规则:
{{rules}}
输出格式(严格 JSON):
{"id": "...", "result": "...", "flags": []}
禁止:
- 不要改变输出 schema
- 不要添加规则外的额外处理
- 不要为输入不完整而猜测,标 [INCOMPLETE]
## 项目层(稳定)
{{domain_knowledge}}
## 批次层(每批内稳定)
批次 ID:{{batch_id}}
本批共 {{n}} 条
## 变量层(每条变化)
输入 {{index}}:
{{item}}
批量任务的缓存收益最大:系统层 + 项目层在成百上千条请求中完全不变,命中率能到 95%+。
骨架三:多轮对话(会话内追加式)
## 系统层(稳定)
{{system}}
## 项目层(稳定)
{{project}}
## 会话记录(追加式,只增不改)
[轮 1] 用户:... / 助手:...
[轮 2] 用户:... / 助手:...
[轮 3] 用户:... / 助手:...
## 当前输入
{{current_input}}
关键:会话记录只追加。 不要在中间插入、修改、总结后替换。总结会让整个历史段的字节序列变化,缓存全废。
如果上下文太长必须压缩,那就接受一次缓存失效——但压缩后的摘要放在新的稳定段里,后续轮次重新建立缓存。
骨架四:RAG / 检索增强
## 系统层(稳定)
{{system}}
## 领域知识(稳定,来自知识库固定切片)
{{static_knowledge_base}}
## 检索结果(每次变化 · 放最后)
{{retrieved_docs}}
## 用户问题
{{query}}
RAG 的常见错误:把检索结果插在系统提示后面。检索结果每次都不同,插在中间会把后面的领域知识缓存也搞失效。
正确顺序:系统 → 静态知识 → 检索结果 → 问题。
骨架五:Agent 长任务
## 系统层(稳定)
{{system}}
## 项目层(稳定)
{{project}}
## 任务定义(会话内稳定,任务文件加载)
{{task_spec}}
## 进度记录(追加式)
已完成:T1, T2
当前:T3
遇到的意外:{{appended_notes}}
## 当前步骤上下文(每次变化)
{{step_context}}
六、实测对照
6.1 五类任务的命中率与成本
每个场景跑 20 次取平均,同一模型:
| 场景 | 优化前命中率 | 优化后命中率 | 输入成本降幅 |
|---|---|---|---|
| 批量文档处理(1000 条) | 6% | 96% | -87% |
| 多轮编码会话(30 轮) | 11% | 84% | -71% |
| 单次长文档问答 | 0% | 62% | -48% |
| RAG 问答(检索结果前置) | 3% | 58% | -44% |
| 短任务(< 5 轮) | 0% | 31% | -19% |
| 加权平均 | 7% | 78% | -63% |
结论:
- 批量与多轮场景收益最大(-71% ~ -87%)
- 短任务收益有限(-19%),别为它花太多精力
- 单次请求也有收益,只要前缀本身够长
6.2 一个真实改造案例
改造前的提示词:
现在是 2026-09-23 14:32:07。
你在 /home/me/acme/order-service 这个项目工作。
当前分支是 feature/money-refactor。
项目用 TypeScript + Node + Prisma + PostgreSQL。
金额要用 Decimal,不要用 float。
测试用 testcontainers。
...(后面还有 800 token 的规范)
任务:看一下 calculateTotal 为什么满减没生效。
问题:时间戳在最前面,每次请求都变 → 后面 1000+ token 全部无法缓存。
改造后:
[系统层]
你是 acme/order-service 的资深后端工程师。
...(角色、规则、输出格式、禁区,1100 token)
[项目层 - 来自 AGENTS.md]
技术栈 / 构建命令 / 编码规范 / 禁区(1400 token)
[任务层]
任务:排查 calculateTotal 的满减逻辑
已确认:满减规则在 PromotionPolicy 里定义
[变量层]
当前分支:feature/money-refactor
时间:2026-09-23 14:32:07
当前文件:src/services/order-pricing.ts
结果:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 缓存命中率 | 4% | 81% |
| 单轮输入成本 | 基准 | -63% |
| 首 token 延迟 | 基准 | -34% |
| 任务完成质量 | 无差异 | 无差异 |
质量零差异,成本降六成,延迟降三分之一。 改动只花了 20 分钟。
6.3 与相邻降本手段的对比
| 手段 | 降幅 | 实施成本 | 风险 |
|---|---|---|---|
| 提示缓存 | 50–90%(输入侧) | 低(30 分钟) | 无 |
| 模型路由(按任务选档) | 30–60% | 中 | 低 |
| 批处理 | 约 50% | 中 | 需接受延迟 |
| 上下文预算控制 | 20–40% | 低 | 可能丢信息 |
| 换便宜模型 | 50–90% | 低 | 质量风险高 |
| 自建推理 | 60–90% | 高 | 运维风险 |
缓存是唯一「零质量风险」的大幅降本手段。这就是为什么它应该排在第一位。
七、常见误区与陷阱
误区 1:以为「语义相同」就能命中
错在哪:这是最常见也最致命的误解。缓存是逐字节前缀匹配,不是语义相似。
表现:规范文件改了一个空格,命中率从 80% 掉到 5%。
修法:规范内容一旦确定就冻结。要更新就整体更新,且接受一次性失效。
误区 2:把会话摘要插回历史
错在哪:上下文太长时做一次摘要替换历史,结果整个历史段字节全变,缓存彻底失效,后面几轮都得重新付全价。
修法:
- 摘要后的内容作为新的稳定段放在前面
- 后续轮次基于新段重建缓存
- 接受一次性失效,但别每轮都摘要
误区 3:系统提示里放「当前文件列表」
错在哪:文件列表随着探索不断变化,而它通常在系统提示里 → 整段失效。
修法:文件列表放变量层。或者让它稳定:先一次性确定要读的文件清单,之后不再变。
误区 4:为了缓存把所有东西都塞进前缀
错在哪:前缀塞了 50K token 的文档,缓存是命中了,但没命中的部分也在付全价,而且缓存写入本身也有成本。
判断标准:
值得缓存的前提:该内容会被复用 ≥ 2 次
只请求一次的内容 → 别放前缀,放变量层
会被复用多次的内容 → 放前缀
误区 5:忽略缓存生存期
错在哪:两次请求间隔 40 分钟,缓存早就过期了,还在奇怪为什么没命中。
修法:相关任务连续做。间隔长就接受失效。
误区 6:认为短提示词也值得优化
错在哪:系统提示只有 150 token,不足一个缓存块,优化了也命中不了多少。
判断标准:稳定前缀 < 1024 token 时,先考虑把规范写完整(这对质量也有好处),再谈缓存。
误区 7:动态生成「规范摘要」
错在哪:每次请求都让某个脚本重新生成一遍项目规范摘要,即使内容一样,措辞/顺序可能微变 → 永不命中。
修法:规范摘要生成一次,缓存下来,之后原样复用。
误区 8:把工具输出放在提示中间
错在哪:Agent 框架里,工具返回结果常被插在提示中间。工具输出每次都不同 → 它之后的所有内容都无法缓存。
修法:工具输出统一追加到末尾。这是 Agent 框架层面的配置,需要改框架或配置(见第 8 节)。
八、在不同载体上落地
8.1 直接用 API
import anthropic
SYSTEM = open("prompts/system_layer.txt").read() # 冻结,永不改
PROJECT = open("AGENTS.md").read() # 冻结
def call(task_ctx: str, variable: str, history: list):
messages = []
messages.append({"role": "user", "content": PROJECT})
messages.append({"role": "assistant", "content": "已加载项目规范,等待任务。"})
messages.append({"role": "user", "content": task_ctx})
for h in history: # 追加式
messages.append(h)
messages.append({"role": "user", "content": variable})
return anthropic.Anthropic().messages.create(
model="claude-opus-5-5",
system=[
{
"type": "text",
"text": SYSTEM,
"cache_control": {"type": "ephemeral"}, # 显式打缓存断点
}
],
messages=messages,
max_tokens=8192,
)
要点:
SYSTEM与PROJECT从文件读,不要字符串拼接- 显式加
cache_control断点 history只 append,不修改
8.2 在 Agent 框架里
| 框架 | 落地方式 |
|---|---|
| Claude Code / Copilot | 规范写进 AGENTS.md;系统层用固定文件 |
| 自建 Harness(Strands 等) | 配 context.cache_breakpoints;工具输出追加到末尾 |
| LangChain | 用 CachedPromptTemplate;确保模板字符串冻结 |
| Dify / Coze 类低代码 | 把动态变量一律放在提示最后 |
8.3 验证缓存真的生效
# Anthropic:看响应的 usage
# cache_read_input_tokens > 0 即命中
curl https://api.anthropic.com/v1/messages ... | jq '.usage'
# 期望
{
"input_tokens": 420,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 21480, # ← 命中
"output_tokens": 830
}
监控指标:
缓存命中率 = cache_read_input_tokens / (input_tokens + cache_read_input_tokens)
健康值:> 50%
警戒值:< 30% → 检查提示结构
九、一句话总结
提示缓存是 2026 年唯一一个「零质量风险、30 分钟实施、输入成本砍半」的降本手段。
记住三条就够了:
1. 变量永远放最后(时间戳、路径、会话 ID 一律后置)
2. 稳定内容从文件加载,不要手写、不要重新生成
3. 只追加,不回改
今天就能做的一件事:打开你最常用的那个提示词,找找里面有没有时间戳、当前路径、或者每次都会变的文件列表。如果有,把它们挪到最后一行。改完跑一次,看 cache_read_input_tokens 是不是从 0 变成了几万。
附录 A:10 个改造实例(Before → After)
这一节是可以直接抄的对照表。每个例子都是真实遇到过的缓存杀手。
A1. 系统提示里的时间戳
❌ Before(命中率 0%)
你是资深工程师。今天是 2026-09-23 14:32:07。
项目规范如下:……(2000 token)
✅ After(命中率 92%)
你是资深工程师。项目规范如下:……(2000 token)
---
上下文时间:2026-09-23 14:32:07
要点:时间戳只是「让模型知道现在」,不需要放在开头。它对绝大多数任务没有语义影响。
A2. 动态文件路径
❌ Before
当前工作目录:/home/u/projects/supply-chain/src/modules/order
请审查以下代码:……
✅ After
请审查以下代码:……
---
文件:src/modules/order/order.service.ts
要点:相对路径比绝对路径稳定(换机器时绝对路径必变)。
A3. 会话 / 追踪 ID
❌ Before
[session: 9f2a-8813-cd01] 任务开始
(后面是 5000 token 规范)
✅ After
(5000 token 规范)
---
[session: 9f2a-8813-cd01] 任务开始
要点:追踪 ID 是给你看的,不是给模型看的。永远沉到尾部。
A4. 手写的项目规范
❌ Before:每次对话手动粘贴规范(可能有不可见的换行/空格差异)
✅ After:写成 AGENTS.md,由工具从文件加载
要点:从文件加载保证逐字节一致。手写两次也可能不同(全角半角、行尾空格、换行符)。
A5. 字典遍历顺序
# ❌ Before:dict 的遍历顺序受插入顺序影响
for k, v in config.items():
prefix += f"{k}: {v}\n"
# ✅ After:固定排序
for k in sorted(config.keys()):
prefix += f"{k}: {config[k]}\n"
要点:这是最隐蔽的一类。逻辑上内容相同,但字节不同 → 缓存全部失效。
A6. 实时数据混入前缀
❌ Before(前缀里含实时库存,每分钟都变)
当前库存:A-12: 340, A-13: 12, B-01: 890(共 2000 项)
任务:……
✅ After(前缀只放静态规则)
库存查询规则:见 query_inventory 工具。
任务:……
要点:实时数据走工具调用,不要塞进前缀。规则是稳定的,数据不是。
A7. 版本号缺失导致静默错配
❌ Before:改了规范但前缀内容变了,旧缓存可能命中到新内容上(取决于实现)
✅ After:前缀第一行写版本号
[SPEC v3.2] ……规范内容……
要点:版本号让缓存干净地失效,而不是静默错配。成本上几乎无差别,正确性上差很多。
A8. 多轮对话的历史压缩
❌ Before:每轮都把完整历史重新拼一遍(且拼接逻辑每次微有不同)
✅ After:历史用固定格式序列化,且只追加不重写
[H1] 用户:……
[H2] 助手:……(固定 200 token 上限,超出用固定模板截断)
[H3] 用户:……
要点:截断模板也要固定——[已截断 N token] 比自由发挥的摘要稳定。
A9. 工具描述顺序
❌ Before:工具列表按注册顺序或 dict 顺序生成
✅ After:按工具名字母序生成工具描述块
要点:工具描述通常在系统提示里,占很大一块。顺序一变,后面全废。
A10. 只追加,不回改
❌ Before:第 5 轮时回头修改了第 2 轮的内容(前缀变化 → 第 3-5 轮缓存全废)
✅ After:发现前面写错了,追加一条更正指令在尾部
---
更正:上文第 2 节中的 X 应改为 Y,以本条为准。
要点:这条最反直觉——修正错误也要追加,不要回改。回改会让之后所有轮次的缓存失效。
附录 B:命中率自检脚本
把下面这段放进你的调用封装里,每次调用后打印一行:
def log_cache_stat(resp):
u = resp.usage
total_in = u.input_tokens + u.cache_read_input_tokens
hit = u.cache_read_input_tokens / total_in if total_in else 0
flag = "OK" if hit >= 0.5 else ("WARN" if hit >= 0.3 else "BAD")
print(f"[cache] hit={hit:.1%} read={u.cache_read_input_tokens} "
f"fresh={u.input_tokens} write={u.cache_creation_input_tokens} {flag}")
return hit
看三个数:
| 字段 | 含义 | 健康值 |
|---|---|---|
cache_read_input_tokens | 命中(便宜) | 越大越好 |
input_tokens | 未命中(贵) | 越小越好 |
cache_creation_input_tokens | 首次写入(比正常贵一点) | 只在第一次出现 |
诊断规则:
read=0 且 creation 每轮都很大 → 前缀不稳定,每次都在重写
read 很大但 fresh 也很大 → 前缀对了,但变量区塞了太多东西
creation 一直是 0 → 输入总长度低于缓存门槛,合并步骤
hit 忽高忽低 → 有隐性变量(时间戳/随机/顺序),查序列化
附录 C:常见误解
| 误解 | 事实 |
|---|---|
| 「缓存会影响输出质量」 | 不会。缓存的是 KV 计算结果,输出完全一致(温度 > 0 时随机性本来就在) |
| 「缓存命中就能省钱,越多越好」 | 命中部分便宜但非零;且缓存写入有额外开销,短输入不划算 |
| 「开个开关就够了」 | 开关只是前提,命中率由 Prompt 结构决定。默认结构通常只有个位数命中率 |
| 「所有 Provider 行为一致」 | 门槛长度、TTL、折扣比例、是否支持多层缓存点都不同,要查各自的文档 |
| 「缓存命中率应该追求 100%」 | 不现实。变量区必然存在,70–85% 是很好的水平,>90% 通常是任务太单一 |
| 「缓存了就不用管上下文长度」 | 缓存省钱不省上下文窗口。超长上下文仍有截断和注意力衰减问题 |
附录 D:一页纸速查
┌──────────────────────────────────────────────────────┐
│ Cache-Friendly Prompting 速查 │
├──────────────────────────────────────────────────────┤
│ 结构:L0 系统 → L1 项目 → L2 任务 → L3 变量 │
│ ↑ 稳定 ────────────────→ ↑ 易变 │
├──────────────────────────────────────────────────────┤
│ 五条纪律 │
│ 1. 变量永远最后 │
│ 2. 稳定内容从文件加载,不手写 │
│ 3. 序列化顺序固定(sorted / 模板化) │
│ 4. 前缀带版本号 │
│ 5. 只追加,不回改 │
├──────────────────────────────────────────────────────┤
│ 健康指标 │
│ 命中率 > 50% OK │
│ 命中率 < 30% → 检查:时间戳 / 路径 / 顺序 / 手写 │
│ creation 每轮都大 → 前缀根本没稳住 │
├──────────────────────────────────────────────────────┤
│ 收益 │
│ 零质量风险 · 30 分钟实施 · 输入成本 -50% ~ -90% │
└──────────────────────────────────────────────────────┘