目录

LLM API 缓存实战:三层缓存把大模型调用成本打下来

为什么你的 LLM 账单越烧越高

接入大模型三个月,账单比想象中高出一大截。翻日志发现一个扎心的事实:大量请求是重复的

  • 定时任务每天早上用同一份模板汇总数据,prompt 一字不差,却每次全价调用;
  • 用户提问"帮我解释下这段代码"和"这段代码什么意思",语义相同,模型却要重新推理一遍;
  • 长 system prompt(几千 token 的角色设定 + 知识库)每次请求都重复计费。

LLM 按 token 计费,重复推理就是纯浪费。本文分享一套实战验证过的三层缓存方案,从上到下依次是:精确缓存 → 语义缓存 → 供应商 Prompt Cache,三层叠加通常能把成本降 50% 以上,顺带把响应延迟从秒级降到毫秒级。

第一层:精确缓存(Exact Match Cache)

最简单、命中率最低但零风险的一层:prompt 完全相同就直接返回缓存结果,压根不调模型。

适合场景:固定模板的批处理(日报生成、定时翻译、批量摘要)、A/B 测试的对照组、代码审查的固定规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import hashlib
import json
import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def exact_cache_key(model: str, messages: list, temperature: float) -> str:
    """把请求参数序列化后取哈希,作为缓存键"""
    payload = json.dumps({
        "model": model,
        "messages": messages,
        "temperature": temperature,
    }, ensure_ascii=False, sort_keys=True)
    return "llm:exact:" + hashlib.sha256(payload.encode()).hexdigest()

def call_llm_with_exact_cache(model, messages, temperature=0.7):
    key = exact_cache_key(model, messages, temperature)
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached)  # 命中,直接返回,0 token 消耗

    # 未命中,调用真实 API
    result = real_llm_call(model, messages, temperature)
    r.setex(key, 3600, json.dumps(result))  # TTL 1 小时
    return result

注意:temperature 必须进缓存键。如果 prompt 相同但 temperature=1.0 时命中了 temperature=0 的缓存,就失去了随机性,生成的内容会异常雷同。

第二层:语义缓存(Semantic Cache)

精确缓存救不了开放式输入——用户提问千变万化,一个字不同就 miss。语义缓存的思路:先算 embedding,相似度超过阈值就返回缓存结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
from openai import OpenAI

client = OpenAI()

def embed(text: str) -> list[float]:
    resp = client.embeddings.create(
        model="text-embedding-3-small", input=text
    )
    return resp.data[0].embedding

def cosine_sim(a: list[float], b: list[float]) -> float:
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

def semantic_get(user_query: str, threshold: float = 0.95):
    """遍历缓存池找语义相近的历史问题"""
    query_vec = embed(user_query)
    for entry in r.hgetall("llm:semantic:pool").values():
        item = json.loads(entry)
        if cosine_sim(query_vec, item["embedding"]) >= threshold:
            return item["answer"]
    return None

实战要点:

  • 阈值要按业务调。0.95 以上近乎复读(“你好” vs “你好呀”),0.85 可能把"推荐餐厅"和"推荐咖啡店"混为一谈。先跑一批真实 query 画相似度分布再定。
  • 缓存池要限容。用 Redis hash + 定时淘汰(LRU 或按 TTL),不然 embedding 存储和遍历开销会反噬成本。
  • embedding 本身也花钱。embedding 模型便宜但非免费,高频短 query 场景要算清楚:省下的生成 token 钱 > embedding 开销才划算。
  • 生产环境建议直接用现成方案,比如 GPTCache(开源,支持 Redis 后端 + 多种 embedding),别自己造轮子。

第三层:供应商 Prompt Cache(白嫖的缓存)

前面两层都是"应用层"缓存。还有一层是模型供应商内置的:Anthropic 的 Prompt Caching、OpenAI 的 Automatic Prompt Caching 等。它们对重复的前缀(长 system prompt、对话历史、few-shot 示例)自动缓存 KV 状态,命中部分按远低于正常价的缓存读取价计费,输入 token 成本可降 90%,延迟也能降低。

1
2
3
4
5
6
7
8
# OpenAI 侧:长 system prompt 自动命中,无需额外参数
response = client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[
        {"role": "system", "content": LONG_SYSTEM_PROMPT},  # 复用同一前缀即命中
        {"role": "user", "content": user_query},
    ],
)

关键认知:前缀越长、复用得越频繁,命中收益越大。所以实践上要:

  1. 把静态内容放前面:system prompt、知识库上下文、few-shot 示例放 messages 开头,用户输入放最后。对话历史的顺序被打乱会破坏前缀缓存。
  2. 不要拼接随机内容:每次请求往 system prompt 里塞时间戳、随机 ID,等于主动摧毁缓存。
  3. 固定 model 版本:换模型版本(如 gpt-5.4 → gpt-5.5)缓存全部失效。

三层怎么组合:决策树

1
2
3
4
请求进来
├─ 精确缓存命中 → 直接返回(0 成本,毫秒级)
├─ 语义缓存命中   → 直接返回(1 次 embedding 成本)
└─ 全部 miss     → 调 LLM,结果回填两层缓存

一个常见的坑:缓存了不该缓存的结果。写代码、算数这类"一次生成终身有效"的任务适合缓存;但"根据最新股价写分析"这类时效性强的任务,必须给缓存键加时间维度(比如把日期拼进缓存键),或者直接跳过缓存。缓存键里也应该带用户/租户维度,避免 A 用户拿到 B 用户的私有数据。

效果与取舍

层级 成本节省 延迟收益 实现成本 风险
精确缓存 高(命中场景) 秒级→毫秒级 极低(几十行) 几乎无
语义缓存 中高 秒级→毫秒级 中(embedding+相似度) 误命中
Prompt Cache 中(输入侧) 零(供应商内置)

我的建议落地顺序:先开 Prompt Cache(一行代码不用改,白赚);再给固定模板类任务加精确缓存;最后给高频开放式 query 上语义缓存。三层都上齐后,同一套业务,token 账单能砍掉一半以上——这笔优化比换便宜模型更划算,而且不影响输出质量。