AI API 降本教程:哪些内容该缓存,怎样提高缓存命中率
很多人听过“缓存能省钱”,但一落地就变成两种极端:要么什么都不缓存,要么什么都想缓存。真正有效的缓存策略没有那么玄,核心就一句话:把重复、稳定、够长的前缀放前面,别让每次请求都从零开始。本文会讲清楚什么值得缓存、什么不值得缓存,并给出可复制的 Python 代码片段。
正文
这篇教程适合谁
适合:
- 你有很长的 system prompt
- 你每次都会重复传工具定义、业务规则、知识库说明
- 你在做文档问答、代码仓分析、长上下文助手
不太适合:
- 每次请求都很短
- 每次请求内容完全不同
- 你连 prompt 结构都还没稳定下来
先讲人话:缓存到底在省什么
缓存不是“把回答缓存起来”,而是把重复出现的输入前缀缓存起来。
也就是说,真正能省钱的通常是这些内容:
- 固定 system prompt
- 固定工具定义
- 固定输出格式说明
- 大段重复背景知识
- 同一份长文档、同一份代码仓说明
真正不适合缓存的通常是:
- 每次都变的用户提问
- 带用户私有变量的动态段落
- 很短的 prompt
各平台现在怎么做缓存
按 2026-04-13 可查到的官方文档,主流平台大致是这样:
- OpenAI:自动 prompt caching,支持
prompt_cache_key - Anthropic:显式
cache_control,适合把可复用内容标出来 - Gemini:同时有 implicit caching 和 explicit caching
这里最重要的共同点只有一个:
要想命中缓存,前缀必须稳定,而且最好放在最前面。
第一原则:静态内容放前面,动态内容放后面
这是缓存命中率的核心。
错误写法:
用户问题
系统说明
工具定义
输出格式更合理的写法:
系统说明
工具定义
输出格式
用户问题因为缓存命中的本质就是“前缀相同”。
一个最简单、最实用的 Python 写法
下面这段代码不依赖任何平台特性,先把 prompt 结构整理对。
from dataclasses import dataclass
@dataclass
class PromptParts:
system_rules: str
tool_spec: str
output_schema: str
user_input: str
def build_prompt(parts: PromptParts) -> str:
# 静态内容放前面,动态内容放最后
return f"""
【系统规则】
{parts.system_rules}
【工具定义】
{parts.tool_spec}
【输出格式】
{parts.output_schema}
【用户输入】
{parts.user_input}
""".strip()
parts = PromptParts(
system_rules="你是企业客服助手,必须使用正式语气,不要编造政策。",
tool_spec="你可以使用 order_lookup(order_id) 工具查询订单状态。",
output_schema='返回 JSON:{"status": "", "reply": ""}',
user_input="用户问:订单 12345 为什么还没发货?",
)
print(build_prompt(parts))这段代码看起来很朴素,但它决定了你后面能不能吃到平台缓存红利。
第二原则:先缓存“最稳定的长前缀”
不是所有内容都值得缓存。 最优先考虑的是这三类:
1. 很长,而且几乎不变的 system prompt
比如企业助手规范、品牌语气规则、审核规则。
2. 很长的工具定义
如果你工具很多,工具 schema 本身就会吃掉很多 token。
3. 重复使用的大文档
比如:
- 产品手册
- 合同模板
- 代码仓结构说明
第三原则:缓存是有成本的,不是默认全开
这件事一定要讲清楚。
- OpenAI 的 prompt caching 是自动的,官方文档说明无需额外代码就可能命中
- Anthropic 的 cache write 和 cache read 有不同价格倍率
- Gemini 的 explicit caching 还涉及 TTL 和 storage 成本
所以缓存不是“白送魔法”,而是“在重复足够多时更划算”。
一个本地辅助缓存的 Python 示例
这段代码不是替代平台缓存,而是帮你在应用层避免重复拼装和重复上传静态内容。
import hashlib
import json
from pathlib import Path
CACHE_DIR = Path(".cache/prompts")
CACHE_DIR.mkdir(parents=True, exist_ok=True)
def make_cache_key(system_rules: str, tool_spec: str, output_schema: str) -> str:
raw = json.dumps(
{
"system_rules": system_rules,
"tool_spec": tool_spec,
"output_schema": output_schema,
},
ensure_ascii=False,
sort_keys=True,
)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def save_static_prefix(cache_key: str, prefix_text: str) -> Path:
path = CACHE_DIR / f"{cache_key}.txt"
path.write_text(prefix_text, encoding="utf-8")
return path
def load_static_prefix(cache_key: str) -> str | None:
path = CACHE_DIR / f"{cache_key}.txt"
if not path.exists():
return None
return path.read_text(encoding="utf-8")
system_rules = "你是售后支持助手,回答必须引用工单规则。"
tool_spec = "你可以调用 ticket_lookup(ticket_id) 和 refund_policy()。"
output_schema = '{"risk_level": "", "answer": ""}'
cache_key = make_cache_key(system_rules, tool_spec, output_schema)
prefix = load_static_prefix(cache_key)
if prefix is None:
prefix = f"{system_rules}\n\n{tool_spec}\n\n{output_schema}"
save_static_prefix(cache_key, prefix)
user_input = "用户说:退款多久到账?"
final_prompt = f"{prefix}\n\n{user_input}"
print(final_prompt)这段代码解决的是一个很现实的问题: 你在自己的应用层,也应该把“重复前缀”和“动态输入”分开管理。
如果你要接 OpenAI,可以怎么写
按当前 OpenAI 文档,prompt caching 会自动生效;如果你想让共享前缀更稳定地路由到同一类请求,可以使用 prompt_cache_key。
import os
import httpx
API_KEY = os.getenv("OPENAI_API_KEY", "")
payload = {
"model": "gpt-5.1",
"input": [
{
"role": "system",
"content": "你是企业知识库助手,必须先遵守下面的审核规则......"
},
{
"role": "user",
"content": "请总结这份文档里关于退款条件的部分。"
}
],
"prompt_cache_key": "kb_assistant_v1",
}
with httpx.Client(timeout=60) as client:
response = client.post(
"https://api.openai.com/v1/responses",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json=payload,
)
response.raise_for_status()
data = response.json()
print(data["usage"])这里真正值得关注的是 usage 里的缓存命中情况,而不是“我觉得应该命中了”。
如果你要接 Anthropic,可以怎么写
Anthropic 的缓存思路更显式。你要把可复用内容标出来。
payload = {
"model": "claude-sonnet-4-20250514",
"max_tokens": 512,
"system": [
{
"type": "text",
"text": "你是客服质检助手,必须先遵守下面的审核准则。",
"cache_control": {"type": "ephemeral"},
}
],
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "请检查这段客服回复有没有违规承诺。"}
]
}
],
}这个例子最重要的不是具体字段,而是思路: 把可复用内容明确标成缓存边界。
如果你要接 Gemini,可以怎么写
Gemini 的 explicit caching 适合重复用长文档或大段上下文。
from google import genai
from google.genai import types
client = genai.Client()
cache = client.caches.create(
model="gemini-2.5-flash",
config=types.CreateCachedContentConfig(
display_name="refund-policy-v1",
system_instruction="你是退款政策助手,必须严格按文档回答。",
contents=["这里放长文档或上传后的文件引用"],
ttl="3600s",
),
)
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="请总结退款到账时间。",
config=types.GenerateContentConfig(cached_content=cache.name),
)
print(response.usage_metadata)最容易踩的 5 个坑
1. 把变量内容塞进前缀里
比如用户 ID、订单号、时间戳。 这些内容一变,缓存命中率就会明显下降。
2. prompt 顺序老变
今天把工具定义放前面,明天又换位置,缓存基本就白做了。
3. 短 prompt 硬做缓存
太短的请求,本来就吃不到多少收益。
4. 只看“开了缓存”,不看命中率
缓存不是布尔值,而是效果问题。 你要看:
- 命中率
- cached tokens
- 延迟下降是否明显
5. 缓存了不该缓存的敏感动态内容
不要把用户强相关、一次性、敏感变量混进可复用前缀。
总结
- 哪些 system prompt 固定
- 哪些工具定义固定
- 哪些文档值得缓存
- 哪些内容绝对不能放进缓存前缀
最后一句话
缓存真正省钱的前提,不是你“打开了缓存”,而是你把 prompt 结构整理成了“静态在前,动态在后”。
如果结构没理顺,缓存就只是一个听起来很美的功能。
官方资料
- OpenAI Prompt Caching
https://developers.openai.com/api/docs/guides/prompt-caching
- Anthropic Prompt Caching
https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- Gemini Context Caching
https://ai.google.dev/gemini-api/docs/caching