主题
03.5 Prompt Caching 与成本工程
同一份上下文,布局不同,成本能差一个数量级。
读完你能做什么:设计出高缓存命中率的提示词结构,并知道哪些操作会意外击穿缓存。
一、缓存的基本原理
大模型推理时,处理输入 token 需要计算并存储中间状态(KV cache)。Prompt Caching 的核心思想:
如果这次请求的前缀和上次完全一样,就复用上次算好的中间状态,不用重算。
关键词是前缀(prefix)。
text
请求 1:[A][B][C][D] → 全量计算
请求 2:[A][B][C][E] → A、B、C 命中缓存,只算 E ✅ 省
请求 3:[X][A][B][C] → 前缀从第一个 token 就不同 ❌ 全量重算推论:只要在上下文前面插入任何内容,后面所有内容的缓存全部失效。
二、缓存友好的上下文布局
2.1 基本原则:稳定的在前,变化的在后
text
┌──────────────────────────────────┐
│ 【永不变化】 │ ← 缓存命中率 100%
│ · 系统提示词 │
│ · 工具定义 │
├──────────────────────────────────┤
│ 【很少变化】 │ ← 高命中率
│ · CLAUDE.md / 项目规范 │
│ · 长参考文档 │
│ · Few-shot 示例 │
├──────────────────────────────────┤
│ 【每轮变化】 │ ← 不缓存
│ · 对话历史 │
│ · 本轮问题 │
└──────────────────────────────────┘2.2 一个常见的反模式
text
❌ 每轮都在最前面注入当前时间戳
系统提示词:「当前时间:2026-07-29 14:32:15」
→ 每一轮时间戳都不同 → 整个缓存永远命中不了修复:把时间戳移到上下文末尾,或者只精确到日期。
Claude Code 提供了一个专门的 flag 解决这类问题:
bash
claude -p --exclude-dynamic-system-prompt-sections "your query"它把「工作目录、环境信息、记忆路径、git 状态」这些每台机器都不同的段落,从系统提示词移到第一条用户消息里。这样不同用户、不同机器跑同一个任务时,系统提示词部分的缓存可以共享。在多用户的脚本化工作流中,这个 flag 能显著降本。
三、什么会击穿缓存
| 操作 | 是否击穿 | 说明 |
|---|---|---|
| 在末尾追加新消息 | ❌ 不击穿 | 前缀不变,理想情况 |
| 修改系统提示词 | ✅ 全部击穿 | 前缀第一段就变了 |
编辑历史消息([Edit] (编辑)) | ✅ 从该消息起击穿 | 该消息之后全部重算 |
/compact | ✅ 击穿 | 历史被重写成摘要 |
/clear | ✅ 击穿 | 但之后重新建立新缓存 |
| 新增/移除 MCP server | ✅ 击穿 | 工具定义在前部 |
| 修改 CLAUDE.md | ✅ 击穿 | 下次会话生效时前缀改变 |
| 切换模型 | ✅ 击穿 | 缓存不跨模型 |
| 在文档开头插入内容 | ✅ 击穿 | 所以要追加到末尾 |
3.1 实践建议
批量操作时,不要中途改配置。
bash
# ❌ 每次都改 system prompt
for f in *.md; do
claude -p --append-system-prompt "处理文件 $f" "..."
done
# ✅ system prompt 保持一致,变化的信息放在用户消息里
for f in *.md; do
claude -p --append-system-prompt "$(cat ./common-rules.txt)" "处理文件 $f:..."
done四、成本的三个乘数
理解成本,需要理解它是三个因素的乘积:
text
总成本 ≈ 上下文长度 × 轮数 × 模型单价系数乘数 1:上下文长度
这是最容易被忽略、也最容易优化的。
一个 5 万 token 上下文的会话,跑 20 轮,处理的输入 token 总量大致是:
text
5万 + 5.2万 + 5.4万 + ... ≈ 110 万 token(不算缓存)而如果你把上下文控制在 1 万:
text
1万 + 1.2万 + ... ≈ 24 万 token差 4.5 倍。 这就是 /clear 和精简 CLAUDE.md 的真实价值。
乘数 2:轮数
减少轮数的方法:
- 一次说清需求,而不是挤牙膏式追问
- 用结构化提示词减少澄清轮次
- 让它一次并行做完多个独立操作,而不是一件一件问
text
✅ 请同时做三件事(它们互不依赖,可以并行):
1. 读 src/api/routes.ts
2. 跑 npm run typecheck
3. 查 git log 最近 10 条提交
全部完成后一起汇报。乘数 3:模型单价
text
Haiku : Sonnet : Opus ≈ 1 : 5 : 25(数量级参考)混合模型策略是最大的省钱杠杆:
markdown
---
name: scanner
description: 在大型代码库中快速定位相关文件。用于需要广泛搜索但不需要深度分析的场景。
model: haiku
tools: Glob, Grep, Read
---
你只做定位,不做分析。返回文件路径 + 匹配到的行 + 一句话说明。主会话用 Opus 做决策,扫描交给 Haiku。同样的任务,成本可能降到 1/5。
五、Claude Code 的成本控制工具
5.1 查看消耗
text
> /usage在 Pro / Max / Team / Enterprise 计划上,会按 skill、subagent、plugin、MCP server 分类展示 —— 直接告诉你哪个自动化在烧钱。
5.2 设置硬性上限
bash
# 单次运行的美元上限(含子代理开销)
claude -p --max-budget-usd 3.00 "重构 src/legacy"
# 最大轮数
claude -p --max-turns 15 "修复 CI"v2.1.217+ 在达到预算上限后会停止仍在运行的后台子代理,避免"主任务停了但子代理还在烧"。
5.3 在 Pro / Max 上设置支出限制
text
> /usage-credits打开 CLI 内的对话框,可以购买额度、设置月度上限。
六、成本优化 Checklist
按收益从高到低排序:
| 优先级 | 动作 | 预期收益 |
|---|---|---|
| 🔴 高 | 把 CLAUDE.md 压到 200 行以内 | 每次会话省数千 token |
| 🔴 高 | 启用 MCP tool search(工具多时) | 可能省数万 token |
| 🔴 高 | 默认模型从 Opus 降到 Sonnet | 约省 80% |
| 🔴 高 | 切换任务时用 /clear 而不是继续 | 避免上下文复利 |
| 🟡 中 | 扫描类子代理改用 Haiku | 该部分省 80% |
| 🟡 中 | 工具输出加管道过滤 | 每次省数百到数千 token |
| 🟡 中 | 长文档改成索引 + 按需读取 | 大幅降低单轮成本 |
| 🟡 中 | 用 --exclude-dynamic-system-prompt-sections | 提升多用户场景缓存命中 |
| 🟢 低 | 减少无效 [Retry] (重新生成) | 视习惯而定 |
| 🟢 低 | 用 --bare 跑简单脚本任务 | 跳过 hooks/skills/MCP 加载 |
6.1 --bare 模式
对于简单的脚本化调用,跳过所有自动发现:
bash
claude --bare -p "把这段 JSON 转成 YAML: $(cat input.json)"它会跳过 hooks、skills、plugins、MCP servers、auto memory 和 CLAUDE.md 的加载,只保留 Bash、文件读、文件编辑工具。启动更快,上下文更小。
七、一份成本友好的默认配置
~/.claude/settings.json:
json
{
"model": "sonnet",
"fallbackModel": "haiku",
"effortLevel": "medium",
"autoMemoryEnabled": true
}项目级 .claude/settings.json:
json
{
"claudeMdExcludes": [
"**/monorepo/other-team/**"
]
}配合一份精简的 CLAUDE.md(< 200 行)和按需加载的 Skills。
八、一个真实的对比
同一个任务「为一个中型项目补充单元测试」:
| 做法 | 上下文均值 | 轮数 | 模型 | 相对成本 |
|---|---|---|---|---|
| 把整个 src 读进来,Opus 一路聊到底 | 12 万 | 40 | Opus | 100× |
| 精简 CLAUDE.md + 按文件分批 + Sonnet | 2 万 | 45 | Sonnet | 4× |
| 上面 + 用 Haiku 子代理做文件扫描 | 1.5 万 | 45 | 混合 | 3× |
| 上面 + 每个模块一个独立短会话 | 8 千 | 60(分散) | 混合 | 1× |
结论:优化上下文长度的收益,通常远大于优化模型选择。