更新于

LLM 应用缓存层设计:从语义缓存到 Prompt 缓存的完整方案


LLM API 调用成本高、延迟长是每个生产级 AI 应用都绕不开的问题。缓存是最直接的解法——但 LLM 请求的多样性让”缓存”二字远比传统 HTTP 缓存复杂得多。本文系统梳理三大缓存策略,帮你在实际项目中找到合适的落地路径。

一、为什么 LLM 应用特别需要缓存

传统 Web 应用里,缓存命中率取决于请求的重复度。LLM 应用有几个独特性:

  • Token 计费模式:每次调用按输入 + 输出 token 数计费,相同 prompt 重复调用等于重复花钱
  • 延迟敏感:GPT-4o / Claude 3.5 的 P50 延迟普遍在 1–3 秒,缓存命中可降至毫秒级
  • 用户问题高度重叠:客服、问答类产品中,80% 的问题往往集中在 20% 的话题上(典型长尾分布)
  • System Prompt 反复发送:大量 token 花在每次都一样的 system prompt 上

根据不同产品类型,合理的缓存策略可以将 API 费用降低 40%–70%。

二、三大缓存层

LLM 应用缓存可以分三个层次,从浅到深依次是:

层次技术方案适用场景命中率范围
精确匹配缓存Redis KV / 哈希完全相同的请求5%–20%
语义缓存向量相似度同义问法30%–60%
Prompt CachingAPI 原生支持长 System Prompt80%+

三层可以叠加使用,下面逐层展开。

三、精确匹配缓存:最简单的起点

对于完全相同的输入(包括 system prompt + user message),用 MD5/SHA256 哈希作 key,将结果存入 Redis。

import hashlib, json, redis

client = redis.Redis()

def cache_key(messages: list, model: str) -> str:
    payload = json.dumps({"model": model, "messages": messages}, sort_keys=True)
    return "llm:" + hashlib.sha256(payload.encode()).hexdigest()

def llm_call_with_exact_cache(messages, model="gpt-4o"):
    key = cache_key(messages, model)
    cached = client.get(key)
    if cached:
        return json.loads(cached)
    
    response = openai_client.chat.completions.create(
        model=model, messages=messages
    )
    result = response.choices[0].message.content
    client.setex(key, 3600, json.dumps(result))  # 1h TTL
    return result

适合场景

  • 代码生成器里固定模板的调用(如”帮我写单元测试”)
  • 数据提取任务(相同文档 + 相同 prompt)
  • 定时批量任务中的重复数据行

局限:用户措辞稍有变化就无法命中,实际命中率通常低于 20%。

四、语义缓存:同义问法的破局之道

语义缓存的核心思路:把历史问答的 embedding 存入向量数据库,新请求进来先做相似度检索,相似度超过阈值就复用旧答案。

from openai import OpenAI
import numpy as np

openai = OpenAI()

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

class SemanticCache:
    def __init__(self, threshold=0.92):
        self.threshold = threshold
        self.store = []  # 生产中换成 Pinecone / Qdrant / pgvector
    
    def embed(self, text: str) -> list:
        resp = openai.embeddings.create(
            model="text-embedding-3-small", input=text
        )
        return resp.data[0].embedding
    
    def lookup(self, query: str):
        q_emb = self.embed(query)
        for item in self.store:
            sim = cosine_similarity(q_emb, item["embedding"])
            if sim >= self.threshold:
                return item["answer"]
        return None
    
    def save(self, query: str, answer: str):
        self.store.append({
            "embedding": self.embed(query),
            "answer": answer,
            "query": query
        })

阈值选择建议

  • 0.95+:只命中几乎一字不差的问法,误命中率极低
  • 0.90–0.95:同义表达基本都命中,偶有语义漂移
  • < 0.90:命中率高但误判多,慎用

语义缓存的缺点是 embedding 本身也有延迟和费用。text-embedding-3-small 约 $0.02/1M token,换来的收益通常值得。

五、Prompt Caching:API 原生的成本杀手

目前 Claude(Anthropic)和 GPT-4o(OpenAI)都支持 Prompt Caching,本质是在模型提供商端缓存 KV 计算结果。

Claude 的 Prompt Caching(官方支持最完善):

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "你是一个专业的代码审查助手。" + long_context,  # 几千 token 的背景
            "cache_control": {"type": "ephemeral"}  # 标记为可缓存
        }
    ],
    messages=[{"role": "user", "content": user_question}]
)

缓存命中时,cache_read_input_tokens 的计费是正常输入 token 的 10%(claude-opus-4-8)。对于频繁调用同一个长 system prompt 的场景,节省极其显著。

详细的 ROI 分析可以参考 Claude Prompt Caching 使用与收益分析

触发条件

  • Claude:system prompt 超过 1024 token 才有意义,缓存 TTL 5 分钟(活跃调用时自动续期)
  • GPT-4o:prefix 超过 1024 token 自动启用,无需显式标记

六、三层组合的最佳实践

在实际项目中,三层缓存通常按下面的顺序组合:

请求进来

[L1] 精确匹配缓存(Redis)
  命中 → 返回,< 1ms
  未命中 ↓
[L2] 语义缓存(向量数据库)
  命中(相似度 ≥ 0.92)→ 返回,< 50ms
  未命中 ↓
[L3] LLM API 调用(含 Prompt Caching)
  → 写回 L1 + L2 缓存
  → 返回结果,1–5s

这个架构在客服问答类产品中,实测综合命中率可以到 55%–65%。

七、缓存失效策略

缓存最难的部分是失效。LLM 应用常见的失效触发:

  • 知识库更新:RAG 系统里文档变更后,相关 query 的缓存需要作废
  • Prompt 迭代:system prompt 改了,所有缓存应清空(用 prompt 版本号作 key 的一部分)
  • 时效性内容:涉及”今天""最新”等时态词的问题不应缓存,或 TTL 设为几分钟
def cache_key_v2(messages, model, prompt_version):
    """加入 prompt 版本号,Prompt 更新后自动 miss"""
    payload = json.dumps({
        "model": model,
        "messages": messages,
        "prompt_v": prompt_version
    }, sort_keys=True)
    return "llm:v2:" + hashlib.sha256(payload.encode()).hexdigest()

时效性过滤可以用简单的关键词检测:

TEMPORAL_KEYWORDS = ["今天", "现在", "最新", "刚才", "当前", "这周"]

def is_temporal_query(text: str) -> bool:
    return any(kw in text for kw in TEMPORAL_KEYWORDS)

八、监控与调优

上线后需要持续观察几个指标:

  • 缓存命中率(分层):精确命中 / 语义命中 / miss 各占多少
  • 语义误命中率:命中后用户是否追问”这个答案不对”——是的话阈值需要调高
  • 实际节省金额:折算成 token 差价,衡量缓存 ROI

一个简单的埋点模板:

import time

def tracked_llm_call(query, ...):
    start = time.time()
    
    # L1 检查
    if exact := exact_cache.get(query):
        metrics.record("cache_hit", layer="l1", latency=time.time()-start)
        return exact
    
    # L2 检查
    if semantic := semantic_cache.lookup(query):
        metrics.record("cache_hit", layer="l2", latency=time.time()-start)
        return semantic
    
    # 真实调用
    result = llm_api_call(query)
    metrics.record("cache_miss", latency=time.time()-start)
    exact_cache.set(query, result)
    semantic_cache.save(query, result)
    return result

九、相关阅读

如果你在寻找稳定、低成本的 LLM API 接入方案,YoTradeApi 提供国内直连、按量计费的 Claude / GPT-4o / Gemini 中转服务,配合缓存层可以将综合成本压到更低。