从缓存命中率 7% 到 84%这是 ProjectDiscovery 用三个月跑出来的真实数字。他们的 Neo 安全测试平台每次任务要走 26 个步骤、40 次工具调用系统提示塞了 2500 行 YAML——超过 20K token。在没有认真对待缓存之前每天的 LLM 账单是一条持续攀升的曲线。优化之后累计命中了 98 亿 token成本降了 59%。这篇文章不讲「Prompt Caching 是什么」——这个问题谷歌五分钟能解决。我想讲的是为什么你的缓存命中率停在 20% 左右以及怎么把它推过 70%。一、KV Cache 的底层逻辑在开始之前先把「为什么前缀必须完全一样才能命中」这件事说清楚否则后面所有的工程决策都会显得莫名其妙。Transformer 在做 attention 计算时会为每个 token 生成一对 key-value 张量。这个计算过程很重——它是 LLM 推理延迟和费用的大头。KV Cache 的核心思路是如果下一次请求的前缀部分和上次完全相同那就可以直接复用上次算好的 KV 张量不用重算。复用 省时间 省钱。关键词是「完全相同」。不是语义相似而是字节级别的精确匹配。一个多余的空格、一个不同的时间戳全盘 miss。二、两家主要 Provider 的实现对比Anthropic Claude手动打标Claude 采用显式标记的方式。你需要在内容块上加cache_control字段importanthropic clientanthropic.Anthropic()responseclient.messages.create(modelclaude-sonnet-4-5,max_tokens1024,system[{type:text,text:你是一个代码审查专家...,cache_control:{type:ephemeral}}],messages[{role:user,content:帮我审查这段代码}])usageresponse.usageprint(f写入缓存:{usage.cache_creation_input_tokens})print(f从缓存读取:{usage.cache_read_input_tokens})硬约束最小 1024 token、最多 4 个断点、TTL 5 分钟起频繁访问可延至 1 小时。定价逻辑Claude Sonnet 4.5类型价格每百万 token标准输入$3.00缓存写入首次$3.7525%缓存读取命中$0.30-90%Break-even 在1.4 次命中只要一个 prompt 前缀会被使用 2 次以上缓存就是合算的。OpenAI自动模式OpenAI 无需任何代码改动自动缓存所有超过 1024 token 的 prompt 前缀fromopenaiimportOpenAI clientOpenAI()responseclient.chat.completions.create(modelgpt-4o,messages[{role:system,content:你是一个代码审查专家...很长的系统提示},{role:user,content:审查这段代码...}])usageresponse.usage cachedusage.prompt_tokens_details.cached_tokens total_promptusage.prompt_tokens hit_ratecached/total_promptiftotal_prompt0else0print(f缓存命中率:{hit_rate:.1%})缓存按 128 token 粒度命中TTL 约 5-10 分钟。价格命中享 50% 折扣无写入溢价。三、Prompt 结构即缓存架构核心原则只有一句话越静态的内容越靠前越动态的内容越靠后。┌─────────────────────────────────────┐ │ System Prompt所有请求共享 │ ← 最稳定最靠前打缓存断点 ├─────────────────────────────────────┤ │ 工具定义工具集固定时 │ ← 次稳定打缓存断点 ├─────────────────────────────────────┤ │ 检索文档 / 上下文资料 │ ← 对话内共享 ├─────────────────────────────────────┤ │ 对话历史随对话增长 │ ← 动态增长 ├─────────────────────────────────────┤ │ 当前用户消息 │ ← 每次不同最靠后 └─────────────────────────────────────┘五个最贵的习惯1. 在 system prompt 里注入时间戳# ❌ 每次前缀都不同永远 misssystemf当前时间{datetime.now()}。你是一个助手...# ✅ 时间放到 user messagesystem你是一个助手...userf[当前时间{datetime.now()}]\n用户问题{question}2. 注入用户 ID 或请求 ID# ❌ 每个用户前缀不同systemf用户ID:{user_id}。你是一个专属助手...# ✅ 用户信息后置system你是一个专属助手...userf[用户:{user_name}]\n{question}3. 随机化 few-shot examples 顺序— 固定顺序按质量排序后保持不变。4. 每次动态生成工具定义— 用sort_keysTrue序列化后缓存字符串直接复用。5. 在 system prompt 开头放用户配置— 静态部分前置用户配置后置。四、Agent 系统的三断点架构这是 ProjectDiscovery 把命中率从 20% 推到 84% 的核心技巧。importjsondefbuild_agent_messages_v2(system_prompt,tool_definitions,conversation_history,working_memory,user_message):三断点架构最大化缓存命中率# 断点1静态系统提示最稳定TTL ~1小时system[{type:text,text:system_prompt,cache_control:{type:ephemeral}}]# 断点3工具定义工具集不变时极稳定tool_defs_textjson.dumps(tool_definitions,ensure_asciiFalse,sort_keysTrue)messages[{role:user,content:[{type:text,text:ftools\n{tool_defs_text}\n/tools,cache_control:{type:ephemeral}# 断点3},{type:text,text:[对话开始]}]},{role:assistant,content:好的我准备好了。}]# 对话历史断点2 最近N轮ifconversation_history:messages.extend(conversation_history[:-1])last_histconversation_history[-1].copy()ifisinstance(last_hist.get(content),str):last_hist[content][{type:text,text:last_hist[content],cache_control:{type:ephemeral}# 断点2}]messages.append(last_hist)# 工作内存 当前用户消息动态不打断点放最后current_contentifworking_memory:current_contentfworking_memory\n{working_memory}\n/working_memory\n\ncurrent_contentuser_message messages.append({role:user,content:current_content})returnsystem,messagesRelocation Trick把工作内存从 system prompt 末尾移到 user message 末尾。工作内存每步都在变化如果放在 system prompt 尾部会让整个 20K token 的系统提示缓存每步失效。移到 user message 之后system prompt 缓存就能稳定命中了。仅此一个改动命中率从 20% → ~74%。五、并发陷阱与冷启动并发 Race ConditionT0ms: 请求A → 缓存 miss开始写入 T2ms: 请求B → 缓存还在写miss T5ms: 请求C → 命中服务启动预热asyncdefwarm_up_cache(client,system_prompt,tool_definitions):responseawaitclient.messages.create(modelclaude-sonnet-4-5,max_tokens1,system[{type:text,text:system_prompt,cache_control:{type:ephemeral}}],messages[{role:user,content:ping}])print(f预热完成写入{response.usage.cache_creation_input_tokens}token)六、监控指标体系fromdataclassesimportdataclassdataclassclassCacheMetrics:cache_creation_tokens:int0cache_read_tokens:int0regular_input_tokens:int0request_count:int0defrecord(self,usage):self.request_count1self.cache_creation_tokensgetattr(usage,cache_creation_input_tokens,0)self.cache_read_tokensgetattr(usage,cache_read_input_tokens,0)self.regular_input_tokensgetattr(usage,input_tokens,0)propertydefhit_rate(self)-float:totalself.cache_creation_tokensself.cache_read_tokensself.regular_input_tokensreturnself.cache_read_tokens/totaliftotal0else0defreport(self):print(f命中率:{self.hit_rate:.1%})ifself.request_count100andself.hit_rate0.5:print(⚠️ 命中率低于 50%建议检查 prompt 结构)elifself.hit_rate0.7:print(✅ 命中率健康70%)目标基线生产系统命中率70%。总结三条核心原则静态内容前置越稳定的越靠前动态内容后置越易变的越靠后持续监控命中率把它当成产品指标5 分钟自检表System prompt 里有没有时间戳或用户 ID工具定义是不是每次动态生成System prompt 超过 1024 token 了吗有没有监控缓存命中 tokenAgent 系统里工作内存放在 prefix 中间还是末尾参考资料How We Cut LLM Costs by 59% With Prompt Caching — ProjectDiscovery, 2026Prompt Caching Infrastructure — Introl, 2026Prompt Caching 201 — OpenAI, 2026
PromptCaching 工程实践:把LLM 调用成本砍掉80%
从缓存命中率 7% 到 84%这是 ProjectDiscovery 用三个月跑出来的真实数字。他们的 Neo 安全测试平台每次任务要走 26 个步骤、40 次工具调用系统提示塞了 2500 行 YAML——超过 20K token。在没有认真对待缓存之前每天的 LLM 账单是一条持续攀升的曲线。优化之后累计命中了 98 亿 token成本降了 59%。这篇文章不讲「Prompt Caching 是什么」——这个问题谷歌五分钟能解决。我想讲的是为什么你的缓存命中率停在 20% 左右以及怎么把它推过 70%。一、KV Cache 的底层逻辑在开始之前先把「为什么前缀必须完全一样才能命中」这件事说清楚否则后面所有的工程决策都会显得莫名其妙。Transformer 在做 attention 计算时会为每个 token 生成一对 key-value 张量。这个计算过程很重——它是 LLM 推理延迟和费用的大头。KV Cache 的核心思路是如果下一次请求的前缀部分和上次完全相同那就可以直接复用上次算好的 KV 张量不用重算。复用 省时间 省钱。关键词是「完全相同」。不是语义相似而是字节级别的精确匹配。一个多余的空格、一个不同的时间戳全盘 miss。二、两家主要 Provider 的实现对比Anthropic Claude手动打标Claude 采用显式标记的方式。你需要在内容块上加cache_control字段importanthropic clientanthropic.Anthropic()responseclient.messages.create(modelclaude-sonnet-4-5,max_tokens1024,system[{type:text,text:你是一个代码审查专家...,cache_control:{type:ephemeral}}],messages[{role:user,content:帮我审查这段代码}])usageresponse.usageprint(f写入缓存:{usage.cache_creation_input_tokens})print(f从缓存读取:{usage.cache_read_input_tokens})硬约束最小 1024 token、最多 4 个断点、TTL 5 分钟起频繁访问可延至 1 小时。定价逻辑Claude Sonnet 4.5类型价格每百万 token标准输入$3.00缓存写入首次$3.7525%缓存读取命中$0.30-90%Break-even 在1.4 次命中只要一个 prompt 前缀会被使用 2 次以上缓存就是合算的。OpenAI自动模式OpenAI 无需任何代码改动自动缓存所有超过 1024 token 的 prompt 前缀fromopenaiimportOpenAI clientOpenAI()responseclient.chat.completions.create(modelgpt-4o,messages[{role:system,content:你是一个代码审查专家...很长的系统提示},{role:user,content:审查这段代码...}])usageresponse.usage cachedusage.prompt_tokens_details.cached_tokens total_promptusage.prompt_tokens hit_ratecached/total_promptiftotal_prompt0else0print(f缓存命中率:{hit_rate:.1%})缓存按 128 token 粒度命中TTL 约 5-10 分钟。价格命中享 50% 折扣无写入溢价。三、Prompt 结构即缓存架构核心原则只有一句话越静态的内容越靠前越动态的内容越靠后。┌─────────────────────────────────────┐ │ System Prompt所有请求共享 │ ← 最稳定最靠前打缓存断点 ├─────────────────────────────────────┤ │ 工具定义工具集固定时 │ ← 次稳定打缓存断点 ├─────────────────────────────────────┤ │ 检索文档 / 上下文资料 │ ← 对话内共享 ├─────────────────────────────────────┤ │ 对话历史随对话增长 │ ← 动态增长 ├─────────────────────────────────────┤ │ 当前用户消息 │ ← 每次不同最靠后 └─────────────────────────────────────┘五个最贵的习惯1. 在 system prompt 里注入时间戳# ❌ 每次前缀都不同永远 misssystemf当前时间{datetime.now()}。你是一个助手...# ✅ 时间放到 user messagesystem你是一个助手...userf[当前时间{datetime.now()}]\n用户问题{question}2. 注入用户 ID 或请求 ID# ❌ 每个用户前缀不同systemf用户ID:{user_id}。你是一个专属助手...# ✅ 用户信息后置system你是一个专属助手...userf[用户:{user_name}]\n{question}3. 随机化 few-shot examples 顺序— 固定顺序按质量排序后保持不变。4. 每次动态生成工具定义— 用sort_keysTrue序列化后缓存字符串直接复用。5. 在 system prompt 开头放用户配置— 静态部分前置用户配置后置。四、Agent 系统的三断点架构这是 ProjectDiscovery 把命中率从 20% 推到 84% 的核心技巧。importjsondefbuild_agent_messages_v2(system_prompt,tool_definitions,conversation_history,working_memory,user_message):三断点架构最大化缓存命中率# 断点1静态系统提示最稳定TTL ~1小时system[{type:text,text:system_prompt,cache_control:{type:ephemeral}}]# 断点3工具定义工具集不变时极稳定tool_defs_textjson.dumps(tool_definitions,ensure_asciiFalse,sort_keysTrue)messages[{role:user,content:[{type:text,text:ftools\n{tool_defs_text}\n/tools,cache_control:{type:ephemeral}# 断点3},{type:text,text:[对话开始]}]},{role:assistant,content:好的我准备好了。}]# 对话历史断点2 最近N轮ifconversation_history:messages.extend(conversation_history[:-1])last_histconversation_history[-1].copy()ifisinstance(last_hist.get(content),str):last_hist[content][{type:text,text:last_hist[content],cache_control:{type:ephemeral}# 断点2}]messages.append(last_hist)# 工作内存 当前用户消息动态不打断点放最后current_contentifworking_memory:current_contentfworking_memory\n{working_memory}\n/working_memory\n\ncurrent_contentuser_message messages.append({role:user,content:current_content})returnsystem,messagesRelocation Trick把工作内存从 system prompt 末尾移到 user message 末尾。工作内存每步都在变化如果放在 system prompt 尾部会让整个 20K token 的系统提示缓存每步失效。移到 user message 之后system prompt 缓存就能稳定命中了。仅此一个改动命中率从 20% → ~74%。五、并发陷阱与冷启动并发 Race ConditionT0ms: 请求A → 缓存 miss开始写入 T2ms: 请求B → 缓存还在写miss T5ms: 请求C → 命中服务启动预热asyncdefwarm_up_cache(client,system_prompt,tool_definitions):responseawaitclient.messages.create(modelclaude-sonnet-4-5,max_tokens1,system[{type:text,text:system_prompt,cache_control:{type:ephemeral}}],messages[{role:user,content:ping}])print(f预热完成写入{response.usage.cache_creation_input_tokens}token)六、监控指标体系fromdataclassesimportdataclassdataclassclassCacheMetrics:cache_creation_tokens:int0cache_read_tokens:int0regular_input_tokens:int0request_count:int0defrecord(self,usage):self.request_count1self.cache_creation_tokensgetattr(usage,cache_creation_input_tokens,0)self.cache_read_tokensgetattr(usage,cache_read_input_tokens,0)self.regular_input_tokensgetattr(usage,input_tokens,0)propertydefhit_rate(self)-float:totalself.cache_creation_tokensself.cache_read_tokensself.regular_input_tokensreturnself.cache_read_tokens/totaliftotal0else0defreport(self):print(f命中率:{self.hit_rate:.1%})ifself.request_count100andself.hit_rate0.5:print(⚠️ 命中率低于 50%建议检查 prompt 结构)elifself.hit_rate0.7:print(✅ 命中率健康70%)目标基线生产系统命中率70%。总结三条核心原则静态内容前置越稳定的越靠前动态内容后置越易变的越靠后持续监控命中率把它当成产品指标5 分钟自检表System prompt 里有没有时间戳或用户 ID工具定义是不是每次动态生成System prompt 超过 1024 token 了吗有没有监控缓存命中 tokenAgent 系统里工作内存放在 prefix 中间还是末尾参考资料How We Cut LLM Costs by 59% With Prompt Caching — ProjectDiscovery, 2026Prompt Caching Infrastructure — Introl, 2026Prompt Caching 201 — OpenAI, 2026