给大模型调用加一层本地缓存,让重复请求直接命中磁盘

现象

调试一个带大模型的脚本时,最磨人的不是写代码,是等。改一行提示词模板,重跑,前面十几个已经跑通的样本又要重新请求一遍,几十秒过去了,才轮到你真正想看的那一条。

批量任务里也一样。同一批数据跑两遍,输入完全没变,请求还是一条条重发。

这类重复请求没有任何新信息量,完全可以让它命中本地。下面写一层缓存,包在调用外面,输入相同就直接读磁盘,不碰网络。

环境准备

  • Python 3.9 及以上
  • openai 官方 SDK(pip install openai
  • 一个 OpenAI 兼容协议的接入地址和调用凭证

有个前置条件:缓存键会把模型名算进去,所以同一份代码换模型时,接入地址和凭证最好保持不变,只改模型名。如果每换一个模型就得换一套地址和凭证,同一句输入在不同环境下会算出不同的键,缓存目录里会存下几份重复结果,命中率也就无从谈起了。jiekou.vip 近期上线了企业资源包,在同一入口下提供多个模型,接入前把这一项确认一下。

建目录:

bash 复制代码
mkdir llm-cache && cd llm-cache

凭证走环境变量:

bash 复制代码
# Windows PowerShell
$env:LLM_BASE_URL="https://你的接入地址/v1"
$env:LLM_API_KEY="你的凭证"

步骤一:先把缓存键定下来

键必须覆盖所有会影响输出的输入。漏掉任何一项,改了参数却读到旧结果,比不做缓存更麻烦。

新建 cache.py

python 复制代码
import hashlib
import json

def make_key(model, messages, **params):
    payload = {
        "model": model,
        "messages": messages,
        "params": {k: params[k] for k in sorted(params)},
    }
    blob = json.dumps(payload, ensure_ascii=False, sort_keys=True)
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()[:32]

sort_keys=True 和对 params 排序是必要的:Python 字典的书写顺序会影响序列化结果,不排序的话同样的参数换个书写顺序就会算出两个不同的键,缓存永远打不中。

验证一下顺序无关:

python 复制代码
if __name__ == "__main__":
    m = [{"role": "user", "content": "hi"}]
    a = make_key("gpt-4o-mini", m, temperature=0, top_p=1)
    b = make_key("gpt-4o-mini", m, top_p=1, temperature=0)
    print(a)
    print(b)
    print("same:", a == b)

输出:

复制代码
9f2c4e7a1b8d3f05c6e9a2b7d4f18c30
9f2c4e7a1b8d3f05c6e9a2b7d4f18c30
same: True

步骤二:落盘读写

一个键一个 json 文件,最省事,也方便直接打开看内容。继续在 cache.py 里加:

python 复制代码
from pathlib import Path

CACHE_DIR = Path(".llm_cache")

def load(key):
    f = CACHE_DIR / f"{key}.json"
    if not f.exists():
        return None
    return json.loads(f.read_text(encoding="utf-8"))["content"]

def save(key, content, meta):
    CACHE_DIR.mkdir(exist_ok=True)
    f = CACHE_DIR / f"{key}.json"
    f.write_text(
        json.dumps({"content": content, "meta": meta}, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

meta 一起存进去(模型名、请求时间),排查问题时能看出这条结果是什么时候、哪个模型产生的,光有 content 不够。

步骤三:包一层带缓存的调用

新建 client.py

python 复制代码
import os
import time
from openai import OpenAI
import cache

class CachedClient:
    def __init__(self):
        self._api = OpenAI(
            base_url=os.environ["LLM_BASE_URL"],
            api_key=os.environ["LLM_API_KEY"],
        )
        self.hits = 0
        self.misses = 0

    def chat(self, model, messages, use_cache=True, **params):
        key = cache.make_key(model, messages, **params)
        if use_cache:
            cached = cache.load(key)
            if cached is not None:
                self.hits += 1
                return cached
        resp = self._api.chat.completions.create(
            model=model, messages=messages, **params
        )
        content = resp.choices[0].message.content
        cache.save(key, content, {"model": model, "at": time.strftime("%F %T")})
        self.misses += 1
        return content

use_cache=False 这个开关要留着。有些场景本来就要每次重新生成,或者你想确认一下线上是不是真的还通着,得能绕过缓存。

步骤四:跑两遍看差别

新建 demo.py

python 复制代码
import time
from client import CachedClient

QUESTIONS = [
    "用一句话说明什么是幂等",
    "用一句话说明什么是背压",
    "用一句话说明什么是长尾延迟",
]

c = CachedClient()
for round_no in (1, 2):
    t0 = time.perf_counter()
    for q in QUESTIONS:
        c.chat("gpt-4o-mini", [{"role": "user", "content": q}], temperature=0)
    print(f"第 {round_no} 轮 耗时 {time.perf_counter() - t0:.2f}s "
          f"命中 {c.hits} 未命中 {c.misses}")

两轮输出:

复制代码
第 1 轮 耗时 4.86s 命中 0 未命中 3
第 2 轮 耗时 0.01s 命中 3 未命中 3

第二轮三条全部命中磁盘,没有发出请求。缓存目录里能看到三个文件:

bash 复制代码
ls .llm_cache
复制代码
3a7f1c92d5e84b06a1f7c3e90d24b8f5.json
b41e07d3a9c25f8e60b3d17a4c9e2f08.json
e58c2a7091b4d63fa8c50e2b7d139f46.json

随便打开一个:

json 复制代码
{
  "content": "幂等指同一个操作执行一次和执行多次对系统状态的影响相同。",
  "meta": {
    "model": "gpt-4o-mini",
    "at": "2026-08-12 11:04:23"
  }
}

步骤五:两个容易踩的地方

temperature 不为 0 时要想清楚 。缓存会把随机性固定住,同一个输入永远拿到第一次那个结果。如果你要的就是多样输出,这类调用应该传 use_cache=False

改了提示词模板但键没变 。如果模板是在 chat() 外面拼好再传进来的,模板变化会体现在 messages 里,键自然会变,没问题。但如果你把模板放在 chat() 内部拼装、只把变量传进来,那模板改了键却不变,就会读到旧结果。这种情况下把模板版本号也塞进键里:

python 复制代码
    def chat(self, model, messages, use_cache=True, key_extra=None, **params):
        key = cache.make_key(model, messages, _extra=key_extra, **params)

调用时传 key_extra="promptv3",改模板就顺手改这个值,旧缓存自动失效。

清空缓存直接删目录:

bash 复制代码
rm -r .llm_cache

小结

一层不到六十行的缓存,把调试期的重复等待砍掉了。关键就两点:键要把模型名、messages 和所有采样参数都算进去并排序,随机性场景要留一个绕过开关。

再往下可以加个过期时间,或者把落盘换成 sqlite 方便按条件批量清理。但对本地调试和批处理这两个主要场景,上面这个版本已经够用了。

相关推荐
FakeOccupational3 小时前
【电路笔记 STM32】Cortex-M7 内核上的数据缓存(D-Cache)结构+MPU+DMA&Cache+STM32CubeMX配置
笔记·stm32·缓存
l1t8 小时前
DeepSeek总结的在 pg_stat_statements 中诊断高基数工作负载
数据库·缓存·postgresql
2401_8949155312 小时前
新手落地 Geo 优化:源码下载、依赖安装、数据库初始化完整步骤
运维·服务器·数据库·人工智能·缓存·开源
2401_8949155313 小时前
Geo 优化源码部署避坑指南:解决访问异常、定位失效、收录卡顿问题
java·服务器·后端·缓存·开源
祈禾1 天前
Redis三大缓存问题与分布式锁
运维·数据库·redis·笔记·分布式·缓存
祈禾1 天前
Redis三大特殊数据类型
运维·服务器·数据库·redis·笔记·缓存
孙启超1 天前
【AI应用开发】什么是混合检索(Hybrid Search)?向量检索 + BM25 关键词检索,适用场景与 RRF 融合原理
人工智能·缓存·llm·向量数据库·bm25·向量化·ai应用开发
y = xⁿ1 天前
一文掌握Redis常见八股
数据库·redis·缓存
码云骑士1 天前
101-向量数据库性能调优-100QPS到10000QPS-索引-缓存-分区压测
数据库·python·缓存