AI Native 应用缓存架构实战:语义缓存、Prompt 缓存与响应缓存的三层设计
当 LLM 账单增速超过用户增速,"再调调模型"往往不是第一解。对大多数生产级 AI 应用而言,设计一套分层缓存架构才是成本与延迟的最高 ROI 杠杆。
一、为什么缓存是 AI Native 架构的必修课
大模型推理的成本结构与传统服务完全不同:每一次请求都要为输入 token 付费,长上下文还会线性放大延迟。更麻烦的是,用户查询天然具有高度重复性——客服机器人里 30% 以上的问题只是换了个问法,RAG 应用里同一份文档被反复加载,Agent 的工具定义在每个请求里都要重传一遍。
缓存的价值因此变得异常直接:
- 响应缓存能直接省掉一次完整的 LLM 调用;
- 语义缓存把"同一个意思的不同表达"也纳入命中范围;
- **Prompt 缓存(前缀缓存)**则让模型厂商帮你复用 system prompt、工具目录和长文档的 KV 计算。
三类缓存不是互斥的。生产系统里它们通常以漏斗状分层组合:先查精确匹配,再查语义相似,最后依赖厂商前缀缓存。本文将逐层拆解原理、实现与选型决策。
二、三层缓存架构总览
一个典型的生产级 LLM 缓存流水线如下:
┌─────────────────────────────────────────────────────────────┐
│ 用户请求 │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Layer 1: 响应缓存(精确匹配) │
│ 键:SHA-256(prompt + model + params) │
│ 命中:直接返回,延迟 < 5ms │
└──────────────┬─────────────────────────┘
│ 未命中
▼
┌────────────────────────────────────────┐
│ Layer 2: 语义缓存(向量相似) │
│ 键:query embedding │
│ 命中:返回相似响应,延迟 20–50ms │
└──────────────┬─────────────────────────┘
│ 未命中
▼
┌────────────────────────────────────────┐
│ Layer 3: Prompt 缓存(厂商前缀缓存) │
│ 复用 system prompt / tools / docs 的 KV │
│ 命中:节省 50–90% 输入 token 成本 │
└──────────────┬─────────────────────────┘
│ 未命中
▼
┌────────────────────────────────────────┐
│ 完整 LLM 推理调用 │
└────────────────────────────────────────┘
关键设计原则:越靠近入口的层越"Lossy",越靠近模型的层越"Lossless"。响应缓存和语义缓存可能返回旧答案,必须接受正确性权衡;Prompt 缓存只复用计算状态,输出仍由模型生成,因此是无损的。
三、Layer 1:响应缓存——把重复的请求拦截在网关层
响应缓存是最简单、收益最确定的一层。它的核心假设是:完全相同的请求应该得到完全相同的回答。在客服 FAQ、文档问答、自动化测试等场景,这一层能命中 5%–15% 的流量。
3.1 缓存键设计
不要用原始 prompt 字符串当 key。一个稳定的 key 必须包含:
- 模型名称与版本
- 完整消息列表(按顺序)
- temperature、top_p、max_tokens 等影响输出的参数
- 工具定义(如果使用 function calling)
常见错误是把请求 ID、时间戳、用户昵称塞进 system prompt,导致缓存永远失效。
import hashlib, json
from typing import Any
def make_cache_key(model: str, messages: list[dict], params: dict) -> str:
canonical = json.dumps({
"model": model,
"messages": messages,
"params": {k: params[k] for k in sorted(params) if k != "stream"}
}, sort_keys=True, ensure_ascii=False)
return hashlib.sha256(canonical.encode()).hexdigest()
3.2 最小实现
import redis, json
class ResponseCache:
def __init__(self, redis_url: str, ttl: int = 3600):
self.r = redis.from_url(redis_url)
self.ttl = ttl
def get(self, key: str):
data = self.r.get(f"llm:resp:{key}")
return json.loads(data) if data else None
def set(self, key: str, response: dict):
self.r.setex(f"llm:resp:{key}", self.ttl, json.dumps(response))
async def call_with_cache(self, llm_call, key: str):
cached = self.get(key)
if cached:
return {"from_cache": True, **cached}
resp = await llm_call()
self.set(key, resp)
return {"from_cache": False, **resp}
响应缓存的后端通常选 Redis 或 Memcached,追求亚毫秒级查询和低成本。它适合温度参数为 0、答案稳定的场景;一旦 temperature > 0,就要谨慎缓存或按温度分层。
四、Layer 2:语义缓存——让"换种问法"也能命中
语义缓存是 AI 应用区别于传统缓存的最大创新点。它不再比较字符串,而是比较向量空间中的语义距离。
4.1 工作原理
- 对进入的请求做 embedding;
- 在向量数据库中搜索最近邻;
- 若余弦相似度超过阈值(通常 0.92–0.97),返回缓存响应;
- 否则调用 LLM,并将 (query_embedding, response) 写入缓存。
用户:"怎么重置密码?"
用户:"忘记密码了怎么办?"
│
▼ embedding
[0.12, -0.05, 0.33, ...]
│
▼ ANN 搜索
top1: "如何重置账户密码?" similarity=0.96
│
▼ 命中 → 返回缓存答案
4.2 生产实现:基于 Redis Vector Search
Redis 8 已原生支持向量索引,适合不想额外引入专用向量数据库的团队。
import redis
from redis.commands.search.query import Query
from redis.commands.search.field import TextField, VectorField
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
r = redis.Redis(host="localhost", port=6379)
INDEX = "semantic_cache_idx"
DIM = 1536 # text-embedding-3-small
def create_index():
schema = (
TextField("query"),
TextField("response"),
VectorField("vec", "HNSW", {
"TYPE": "FLOAT32",
"DIM": DIM,
"DISTANCE_METRIC": "COSINE"
}),
)
try:
r.ft(INDEX).create_index(schema, definition=IndexDefinition(prefix=["cache:"], index_type=IndexType.HASH))
except Exception:
pass # 已存在
def cache_lookup(query_vec: bytes, threshold: float = 0.95):
q = (Query("*=>[KNN 1 @vec $vec AS score]")
.return_fields("response", "score")
.dialect(2).paging(0, 1))
res = r.ft(INDEX).search(q, query_params={"vec": query_vec})
if not res.docs:
return None
top = res.docs[0]
sim = 1 - float(top.score) # Redis 返回距离,转换为相似度
if sim >= threshold:
return top.response
return None
4.3 阈值是唯一的调参杠杆
相似度阈值是语义缓存的核心权衡点:
| 阈值 | 命中率 | 误命中风险 | 适用场景 |
|---|---|---|---|
| 0.99 | 低 | 极低 | 法律、医疗、合规问答 |
| 0.95 | 中 | 低 | 通用客服、文档问答 |
| 0.90 | 高 | 中 | 内部工具、非关键推荐 |
不要凭直觉设阈值。正确做法是:准备一批已知"应该命中"和"不应该命中"的 query pair,做阈值扫描,选择误命中率可接受前提下的最大命中率。
4.4 多轮对话的上下文陷阱
同一个问题在不同上下文中可能应有不同答案。简单做法:
- 上下文无关缓存:只对首轮用户问题做语义缓存;
- 上下文感知缓存:把最近 N 轮对话也嵌入到 key 中,命中率下降但正确性提升。
对于 Agent 场景,更安全的模式是:语义缓存只用于事实型查询,状态型/操作型查询跳过缓存。
五、Layer 3:Prompt 缓存——让厂商替你复用 KV
Prompt 缓存(Prefix Caching)是 2025–2026 年各大厂商重点推出的能力。它不缓存最终回答,而是缓存 prompt 前缀的 KV 状态,让后续请求跳过重复的前向计算。
5.1 三大厂商实现对比
| 厂商 | 模式 | 最小 token | 折扣 | 控制粒度 |
|---|---|---|---|---|
| Anthropic Claude | 显式 cache_control | 1024–4096 | 读:-90%,写:+25% | 高,最多 4 个断点 |
| OpenAI | 自动前缀缓存 | 1024 | -50% | 低,无 API 开关 |
| Google Gemini | 显式 cachedContent | 1024/4096 | -75% | 中,按小时存储计费 |
5.2 Anthropic 显式缓存示例
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是企业 SaaS 产品的技术支持专家。",
},
{
"type": "text",
"text": LONG_PRODUCT_DOCS, # 几千到几万 token 的文档
"cache_control": {"type": "ephemeral"} # ← 缓存断点
}
],
messages=[{"role": "user", "content": user_question}]
)
首次请求会多付 25% 的写成本;从第二次请求开始,该前缀按 0.1 倍价格计费。盈亏平衡点约为同一前缀被调用 2 次。
5.3 Prompt 缓存的架构原则
想要高命中率,prompt 结构必须遵循"静态前置、动态后置":
[系统提示 — 静态] ← 缓存
[工具定义 — 静态] ← 缓存
[长文档/RAG 上下文 — 静态] ← 缓存
[对话历史 — 半静态] ← 部分缓存
[用户当前输入 — 动态] ← 不缓存
常见翻车点:
- 在 system prompt 里塞
datetime.now(); - 工具定义用非确定性字典序序列化;
- 把用户 ID、会话 ID 放在缓存前缀之前;
- A/B 测试版本号随机注入。
建议做法:在服务端定期计算并监控 prompt 前缀的哈希,缓存命中率暴跌时第一时间告警。
六、三层组合架构的完整代码骨架
import hashlib, json
import openai
from gptcache import cache as gpt_cache
from gptcache.adapter import openai as cached_openai
from gptcache.embedding import Onnx
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation
# 1) 响应缓存:Redis 精确匹配
redis_cache = ResponseCache(redis_url="redis://localhost:6379")
# 2) 语义缓存:GPTCache 默认 ONNX 本地嵌入
gpt_cache.init(
embedding_func=Onnx().to_embeddings,
similarity_evaluation=SearchDistanceEvaluation(),
)
async def chat(messages, model="gpt-4o", params=None):
params = params or {}
# Layer 1: 精确匹配
exact_key = make_cache_key(model, messages, params)
hit = redis_cache.get(exact_key)
if hit:
return hit
# Layer 2: 语义缓存(只对单轮、事实型查询启用)
last_user_msg = messages[-1]["content"] if messages[-1]["role"] == "user" else ""
if len(messages) <= 2:
semantic_hit = cached_openai.semantic_search(last_user_msg, top_k=1)
if semantic_hit and semantic_hit.score > 0.95:
return semantic_hit.response
# Layer 3 + 完整调用:把静态内容前置以命中厂商前缀缓存
response = await cached_openai.ChatCompletion.acreate(
model=model,
messages=messages,
temperature=params.get("temperature", 0),
)
# 回填响应缓存
redis_cache.set(exact_key, response)
return response
七、基础设施选型对比
| 维度 | Redis / KeyDB | 专用向量数据库(Qdrant/Milvus) | GPTCache |
|---|---|---|---|
| 适用层 | 响应缓存 | 语义缓存 | 语义缓存封装 |
| 查询延迟 | < 5ms | 10–30ms(含 embedding) | 依赖底层存储 |
| 容量与扩展 | 中等,适合热数据 | 十亿级向量,分布式 | 中等 |
| 运维成本 | 低 | 中–高 | 低 |
| 混合搜索 | Redis Search 支持 | 原生支持 | 需配置 |
一个务实的起步方案:
- 响应缓存:Redis(或 Upstash 用于 serverless);
- 语义缓存:小规模用 GPTCache + FAISS/SQLite,生产用 Redis Vector Search 或 Qdrant;
- Prompt 缓存:直接利用厂商能力,无需自建基础设施。
八、性能优化与 ROI 量化
8.1 成本模型
假设某客服系统日均 10 万次请求,平均 prompt 4000 token,输出 800 token,使用 Claude Sonnet(输入 15/MTok):
| 方案 | 日成本估算 | 备注 |
|---|---|---|
| 无缓存 | $1,920 | 全额计费 |
| 仅响应缓存(10% 命中) | $1,728 | 省 $192 |
| + 语义缓存(额外 40% 命中) | $921 | 省 $1,000 |
| + Prompt 缓存(前缀 3000 token,80% 命中) | ~$450 | 省近 $1,500 |
8.2 关键优化建议
- 先做响应缓存,再做语义缓存。响应缓存实现成本最低,且不误命中。
- 语义缓存只缓存确定性回答。对价格、政策、实时数据设置短 TTL 或跳过缓存。
- Prompt 缓存的静态前缀越大越划算。把工具定义、RAG 文档、few-shot 示例都往前放。
- 监控三个命中率。每一层都要独立计数,不要只看总命中率。
- 建立缓存质量评估循环。定期抽样 LLM-as-a-judge 或人工评审语义缓存命中的正确性。
九、常见陷阱与安全边界
- 数据隔离:不要把用户 A 的个性化响应缓存给用户 B。按
user_id或tenant_id分区。 - PII 与合规:缓存中可能存储敏感对话,需加密、设置 TTL,并满足数据驻留要求。
- 缓存投毒:恶意用户可能通过构造相似 query 让系统返回错误缓存。结合输入过滤与阈值保守策略。
- 陈旧性:文档更新后,旧缓存可能给出过期答案。关键领域使用 tag-based 失效或短 TTL。
十、结语
AI Native 应用的缓存不是传统缓存的简单迁移,而是围绕"token 经济性"重新设计的一套分层体系。响应缓存解决重复,语义缓存解决同义,Prompt 缓存解决前缀冗余。三者叠加,才能在不牺牲体验的前提下,把 LLM 成本真正压下来。
最值得记住的一点:缓存是一项正确性工程,不是纯基础设施工程。每一层缓存都意味着"有时不调用模型",而模型的缺席必须由严格的评估、监控和失效策略来兜底。只有把成本、延迟、正确性三者的关系量化到仪表盘上,缓存才算真正落地。