缓存 Key 规范与 Claude 动态/静态 Prompt Cache 机制详解

缓存 Key 规范与 Claude 动态/静态 Prompt Cache 机制详解

适用对象:Agent 工程落地 / 毕设技术章节 / 团队知识库 主线:本地缓存 Key → 云端动静边界 → {memory} 归属 → 两层缓存对比 校订说明:本文在原始笔记基础上,按 Anthropic 官方《Prompt caching》《How Claude Code uses prompt caching》《Modifying system prompts》逐条核对,修正三处:①「静态缓存永不失效」不成立,缓存有 TTL, inactivity 会过期;② 动静边界并非在所有环境下都生效 ,Bedrock / Vertex / Foundry / LLM 网关下整块发送;③ 动态段要分清楚"system prompt 内 boundary 之后"与"system prompt 之外的 messages",{memory} 属于前者,而 SideQuery 召回片段、工具结果、对话历史属于后者。其余结构与结论保留。

缓存这个词在 Agent 工程里至少指两件完全不同的事:一件是你自己代码里的本地缓存 (省 CPU),一件是Claude API 的 Prompt Cache(省 Token 和钱)。这两层经常被混为一谈,于是就会出现"我加了缓存,为什么账单没降"这类问题。

这篇把两层彻底分开讲清楚,并给出 {memory} 归属的定论。


一、本地缓存 Key 设计规范(工程层)

1.1 为什么不能用 Python 内置 hash()

hash() 有两个致命缺陷,决定了它只能用于运行期的数据结构(如 dict 查找),不能用于工程化的缓存索引。

缺陷一:进程级哈希种子随机化。

Python 默认开启哈希随机化(PEP 456,PYTHONHASHSEED)。同一个字符串、字典、列表,在不同进程、不同次启动中 hash() 值完全不同。

python 复制代码
# 进程 A
hash("hello")   # 比如 3134078793093316572
# 进程 B(重启后)
hash("hello")   # 变成完全不同的另一个值

缓存索引的第一要求是可复现:这次进程写入、下次进程还能命中。随机化种子直接否掉了这一点。

缺陷二:不支持可变结构化类型。

list / dict 是不可哈希类型,直接调用会报错:

python 复制代码
hash({"model": "claude", "temp": 0.7}}
# TypeError: unhashable type: 'dict'

而 Agent 场景里要缓存的恰恰是结构化上下文------工具参数、状态字典、消息列表。

1.2 工程标准方案:json.dumps

统一用序列化字符串作为缓存 Key:

python 复制代码
cache_key = json.dumps(data, sort_keys=True, ensure_ascii=False)
参数 作用 不带的后果
sort_keys=True 递归排序字典键 内容相同、键顺序不同的两个字典会生成两个 Key,缓存永远命中不了
ensure_ascii=False 保留中文与特殊字符原样 中文被转义成 \uXXXX,Key 可读性差,且差异化排查困难

三个落地增强(可选,但强烈建议):

python 复制代码
import hashlib, json

def make_cache_key(data, prefix="ctx") -> str:
    raw = json.dumps(
        data,
        sort_keys=True,
        ensure_ascii=False,
        separators=(",", ":"),   # 压缩空白,Key 更短
        default=str,             # 兜住 datetime / Enum / 自定义对象
    )
    if len(raw) > 512:           # 超长 Key 二次摘要,避免撑爆内存与日志
        raw = hashlib.sha256(raw.encode("utf-8")).hexdigest()
    return f"{prefix}:{raw}"

要点:

  • separators 去掉默认空格,Key 体积更小;
  • default=str 让不可 JSON 序列化的对象不至于抛异常;
  • 超长 Key 用 hashlib.sha256 二次摘要 ------这是稳定哈希,与 Python 内置 hash() 的随机化哈希是两回事,不要混淆;
  • 加 prefix 命名空间,避免不同业务域的 Key 撞车。

1.3 本层缓存的真实作用(重点)

该缓存 ≠ Claude API 官方 Prompt Cache。

它属于应用内存级缓存,唯一目的是省本地开销:

  • ✅ 避免重复拼接超长 Prompt 字符串;
  • ✅ 减少重复的序列化与字符串拼接 CPU 开销;
  • ✅ 加速本轮上下文组装;
  • ❌ 不会减少 API Token;
  • ❌ 不会触发云端缓存;
  • ❌ 不会降低计费。

一句话:它是性能优化,不是成本优化。 生命周期就是单次进程内存,进程退出即失效。


二、Claude 官方云端 Prompt Cache(模型 API 层)

2.1 核心原理:动静边界分割

Claude Code / Agent SDK 提供了一个边界常量:

typescript 复制代码
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'

它解决的问题非常具体:如果你的 system prompt 是"永远不变的指令"+"每请求都变的上下文"拼成的一个字符串,那么上下文一变,整个 system prompt 都变了,前面那些不变的指令也跟着一起 miss 缓存。

用数组形式传入,两侧各自成为一个独立的 text block,各自带自己的 cache breakpoint,边界标记本身会被 SDK 移除、不会送达模型:

typescript 复制代码
import { query, SYSTEM_PROMPT_DYNAMIC_BOUNDARY } from "@anthropic-ai/claude-agent-sdk";

const instructions = await readFile("triage-instructions.md", "utf8");   // 每次都一样
const ticketContext = "Customer plan: Enterprise. Open tickets: 3.";      // 每次都不同

for await (const message of query({
  prompt: "Triage ticket 4821",
  options: { systemPrompt: [instructions, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, ticketContext] }
})) { /* ... */ }

几个实现细节(官方口径):

  • SDK 会把边界两侧的字符串用空行连接 ,并移除标记本身;
  • 标记出现多次时,只有第一个生效,其余被移除;
  • 用 CLI 的 --system-prompt / --system-prompt-file 时是单字符串,改为插入单独一行 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 来分割(需 v2.1.275+);
  • 两个断点都命中时,usage 里会出现 cache_read_input_tokens(命中)与 cache_creation_input_tokens(新建)。

2.2 静态段与动态段各装什么

静态段(boundary 之前)------每请求恒定:

内容 说明
Agent 身份与系统角色 "You are Claude Code..." 类固定表述
工作流 / 任务执行规则 做事方式、风险与确认规则
ReAct 循环定义、工具使用偏好 如优先用 Read 而不是 cat
语气与风格约束 固定输出规范

动态段(boundary 之后)------随会话或随轮次变化:

内容 变化频率
memory ------ 从记忆目录加载的记忆提示 会话级
env_info ------ CWD、git status、平台、模型、shell 会话/轮次级
language / output_style / token_budget 会话级
MCP 服务器指令 每轮重算(MCP 服务器可能在轮次之间连接或断开)
SideQuery / subagent 相关段落 会话级

说明:上述分段清单来自 Claude Code 实现对系统提示词分节(section)的注册方式,社区已有逆向整理;各 section 的归属会随版本调整,以你本地 claude -p 的实际输出与官方文档为准。不变的是机制本身:boundary 之前恒定、之后可变。

2.3 生效条件(最容易被忽略的限定)

动静切分不是在所有环境下都发生。 官方明确:

Claude Code 只在直接调用 Claude API 或运行在 Claude Platform on AWS 时才切分 system prompt。

在其他配置下,它把整个 prompt 作为一个 block 发送,等同于传单个字符串------边界不生效,缓存粒度退化为整个前缀:

  • Amazon Bedrock
  • Google Cloud 的 Agent Platform
  • Microsoft Foundry
  • 任何 LLM 网关
  • 以及设置了 CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 时

写论文或汇报时,这句话必须带上:"动静边界是 1P API 通道上的优化,跨云与网关场景下不适用。"

2.4 TTL 与失效(原笔记最大的一处硬伤)

❌ 原表述:"后续动态内容怎么变,静态缓存永不失效 。" ✅ 官方口径:缓存前缀在一段不活动期之后过期(cached prefixes expire after a period of inactivity)。每次命中都会重置计时器,所以只要持续工作缓存就是热的;闲置足够久之后,下一次请求会重算完整输入并重建缓存------这就是为什么离开一会儿回来后的第一轮明显更慢。

关键事实:

项 官方口径
TTL 选项 5 分钟(默认)与 1 小时(额外费用)
计时方式 每次命中重置;从写入开始计时,不是从最后一次生成结束
计费 写入 1.25×(5m)/ 2×(1h)基础输入价;读取 0.1×
命中条件 完整前缀完全一致;任意一处字节变化,该处及其之后全部 miss
已知失效因素 tool_choice 变化、prompt 中图片的有无 、thinking 配置、output_config.effort、工具 tool_use 内容块里键顺序不稳定(Go / Swift 的 JSON 转换会随机化键序,直接打断缓存)
断点数量 单请求最多 4 个 breakpoint
最小长度 存在最小可缓存前缀长度要求,过短则静默不缓存 (cache_creation_input_tokens 为 0),具体阈值随模型不同,见官方文档
调试手段 cache diagnostics 让 API 对比相邻请求并报告从哪里开始分叉

TTL 的两个桶(Claude Code 侧):

请求桶 Claude 订阅(额度内) API key / 用量计费 / 云厂商
主对话(交互轮次、-p、Agent SDK turns 及其内联 helper) 1 小时 5 分钟
其他一切(subagent、workflow、teammate、fork、压缩、会话标题) 5 分钟 5 分钟

注意第二行:subagent 的请求默认只有 5 分钟 TTL ,除非显式设置 subagentPromptCacheTtl / CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL。这跟上一篇讲的"subagent 有独立上下文、无法共享主会话前缀"是同一个道理------它在缓存层面也是另一套账。

还有一个官方建议:频繁使用的 prompt 不要盲目换 1 小时 TTL。如果请求间隔本来就小于 5 分钟,5 分钟缓存会被免费持续刷新,换成 1 小时只是白付更高的写入单价。1 小时适合"间隔在 5 分钟到 1 小时之间"的场景(例如一个 side-agent 要跑超过 5 分钟)。

2.5 {memory} 归属:定论与精度修正

结论:{memory} 100% 属于动态段。

依据是分节注册:memory 就是动态区里注册的一个 section("loaded memory prompt from memdir"),它随会话/轮次加载的内容而变,天然落在 boundary 之后。

但这里有一个必须澄清的精度问题------"动态段"这个词其实指两个不同的位置:

位置 包含什么 与边界的关系
system prompt 内、boundary 之后 memory 记忆提示、env 信息(CWD/git/平台/模型/shell)、语言与输出风格、MCP 指令 在 system prompt 里,位于边界之后
system prompt 之外的 messages 流 当前会话历史对话、用户每轮最新提问、文件读取结果、Shell 输出、工具返回内容、SideQuery 召回的记忆片段、本轮临时状态 根本不在 system prompt 里,更谈不上属于"动态段"

也就是说:

  • ✅ {memory} 记忆提示 → system prompt 的动态段(boundary 之后);
  • ✅ 会话历史、用户输入、工具输出、SideQuery 召回片段 → messages 流,位于 system prompt 之后,连"静态/动态段"这个划分都不适用;
  • 一致的结论是:它们都不可缓存为静态前缀。

原因:每一轮召回的记忆不一样、用户输入不一样、工具结果不一样,属于典型逐轮可变数据。把它们放进静态段,等于每轮主动打碎自己的缓存。


三、两层缓存彻底对比

维度 应用层本地缓存 Claude 云端 Prompt Cache
技术 json.dumps(sort_keys=True) 作 Key(超长再 sha256) __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ 动静分割 + cache_control 断点
实现位置 你自己代码里的进程内 dict / LRU Anthropic API 侧
作用 避免重复拼接超长 Prompt 字符串 复用不变前缀,减少云端重复计算
收益 降低本地 CPU 与序列化开销 显著降本提速:读取 0.1×、写入 1.25×/2×
影响 Token 无任何节省 直接减少计费 Token
生命周期 单次进程内存 云端缓存,5 分钟 / 1 小时 TTL,命中即续期
失效条件 进程退出、Key 内容变化 前缀字节变化、TTL 到期、tool_choice/图片/thinking 配置变化、键序不稳定
适用环境 任何环境 仅 1P API 与 Claude Platform on AWS 支持动静切分;Bedrock / Vertex / Foundry / 网关下退化为整块前缀
排查指标 命中率(自己埋点) cache_read_input_tokens / cache_creation_input_tokens

一句话区分:本地缓存省的是"你机器的 CPU",云端缓存省的是"你的账单"。


四、工程落地 Checklist

本地 Key

  1. 禁用 hash() 作持久化/跨进程 Key(随机化种子 + 不支持可变类型);
  2. json.dumps(sort_keys=True, ensure_ascii=False) 打底,补 separators 与 default=str;
  3. 超长 Key 用 hashlib.sha256 二次摘要,并加业务前缀命名空间;
  4. 明确标注该层不省 Token,别拿它去解释账单。

云端缓存

  1. 稳定内容放前、易变内容放后:tools → system 静态段 → 【边界】→ system 动态段 → messages;
  2. 每请求变化的上下文(客户、工单、memory、env)一律放边界之后;
  3. 确认你的通道是否支持动静切分(跨云与 LLM 网关不支持);
  4. 保证 tool_use 内容块的键顺序稳定(Go / Swift 的 JSON 转换要特别处理);
  5. 别把 tool_choice、图片有无、thinking 配置做成"随机漂移"的开关;
  6. 断点最多 4 个,前缀过短会静默不缓存;
  7. 用 cache diagnostics 定位分叉点,盯 cache_read_input_tokens 比例;
  8. TTL 按间隔选:小于 5 分钟用默认;5 分钟~1 小时才上 1 小时 TTL;subagent 默认只有 5 分钟。

五、总结

  1. 工程层缓存 Key 禁用 hash() ,必须用 json.dumps(sort_keys=True, ensure_ascii=False)(超长再 sha256),保证全局稳定且支持字典、列表结构。
  2. 本地缓存只优化本地字符串拼接性能,与 Claude 云端 Prompt Cache 无关,不省 Token、不影响计费。
  3. Claude 通过动静边界分割实现云端缓存 :固定系统规则为静态可缓存段,{memory}、env 信息、MCP 指令,以及 messages 流里的对话历史、工具输出、SideQuery 召回片段,全部逐轮变化、不可静态缓存。
  4. 动态段变更不会破坏其之前的前缀缓存 ,但缓存不是永久的 ------TTL 5 分钟(默认)或 1 小时,命中续期、闲置过期。真正的表述是"规则长期可缓存、内容实时更新、缓存按 TTL 续期",而不是"永不失效"。

附:80 字核心摘要(PPT / 论文小结用)

本地缓存用 json.dumps(sort_keys=True) 生成稳定 Key,只省 CPU 不省 Token;Claude 云端靠 __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ 切分静态规则与动态内容({memory}、工具输出、对话历史皆属后者),静态前缀可跨轮复用,但受 5 分钟 / 1 小时 TTL 约束,命中续期、闲置过期。

相关推荐
人生百态,人生如梦1 天前
Ubuntu在低版本下使用ccswitch+claudecode(CCSwitch+Docker+Claude)连调教程
linux·ubuntu·docker·claudecode·ccswitch
三水写代码13 天前
手写一个 Claude Code(1):从 Agent Loop 到工具、权限、Hooks 与任务规划
python·ai编程·claude·ai agent·claudecode
带刺的坐椅13 天前
什么样的编码智能体值得信任?——SolonCode 的设计取舍
codex·claudecode·traecn·soloncode·zcode
2601_962296511 个月前
Claude Code:缓存优先 Agent Harness
compaction·claudecode·promptcaching·缓存优先agentharness·工具建模
小七-七牛开发者1 个月前
拆解 dsh 系列:从源码和版本变化看 DeepSeek Harness 的设计取舍
ai·大模型·claude·token·工作流·skill·claudecode·ai coding
小七-七牛开发者1 个月前
Agent 小知识 | Skill 的设计与生命周期:从工具接口到能力模块
ai·大模型·agent·token·工作流·claudecode·ai coding
小七-七牛开发者1 个月前
拆解 DeepSeek Harness:Profile 与 Bundle 如何装配运行时
ai·大模型·agent·token·工作流·claudecode·ai coding
小七-七牛开发者2 个月前
61 亿次请求背后:LLM Serving 的 Cache 与调度难题
ai·大模型·agent·token·工作流·claudecode·ai coding
Roadinforest2 个月前
Claude Code 架构深度解析:从 Agent Loop 到 Tool、MCP 与 Context
ai·架构·llm·agent·anthropic·claudecode