跳至主要内容

LLM缓存详解

 可以把“大模型缓存”理解成:把已经算过的结果(或中间结果)存下来,下次尽量复用。但这里面其实分几层,不只是简单的“问题→答案”缓存。


1️⃣ 常见的几种缓存类型

(1)KV Cache(推理内部缓存)

Transformer 在生成时,会把前面 token 的 Key/Value 向量缓存下来。

  • 本质:避免重复计算 attention

  • 作用:同一请求内部加速

  • 特点:
    👉 只对“同一上下文继续生成”有效
    👉 不跨用户、不跨请求

这类缓存是你体感“流式输出越来越快”的原因之一。


(2)Prompt Cache(提示词缓存)

缓存的是:

相同(或高度相似)的 prompt → 对应的中间表示 / 输出

典型场景:

  • 系统提示词(system prompt)很长

  • 多轮对话里前文基本不变

👉 这里能省掉前缀计算成本(prefill)


(3)Embedding / 语义缓存(Semantic Cache)

这个才是你问题的关键 👇

不是按“字符串完全一致”,而是:

把问题转成向量 → 找“语义相似”的历史问题 → 直接复用答案


2️⃣ 为什么命中缓存成本低很多?

因为大模型推理成本主要在两块:

(1)Prefill(吃 prompt)

  • 复杂度 ~ O(n²)

  • 很贵(尤其长 prompt)

(2)Decode(逐 token 生成)

  • 每个 token 都要算一遍模型


而缓存命中后:

  • KV cache:不用重复 attention

  • Prompt cache:不用重新 encode

  • 语义缓存:直接跳过模型推理

👉 相当于从:

几十~几百毫秒 + GPU算力

变成:

一次向量检索(毫秒级)+ 直接返回

所以成本差一个数量级是正常的。


3️⃣ “每个人问法不同,怎么命中缓存?”

这是核心难点,也是工程重点👇

❌ 不能靠字符串匹配

比如:

  • “今天天气怎么样”

  • “今天外面热不热”

字符串完全不同 → 必须 miss


✅ 用语义相似度(Embedding)

流程一般是:

  1. 把问题转 embedding(向量)

  2. 在向量数据库里找 TopK 相似问题

  3. 如果相似度 > 阈值(比如 0.9)

  4. 直接返回缓存答案


一个简单示意

Q1: 北京天气怎么样
→ embedding A

Q2: 北京今天热吗
→ embedding B

cosine(A, B) ≈ 0.95 ✅ 命中

4️⃣ 实际工程里会更复杂(关键细节)

(1)不仅缓存“问题”,还要缓存“上下文结构”

例如:

系统提示 + 用户问题

否则同一句话在不同系统角色下答案完全不同。


(2)要做归一化(Normalization)

比如:

  • 时间标准化(“今天”→具体日期)

  • 数字归一

  • 同义词处理


(3)分层缓存(很关键)

一般会这样设计:

L1:精确缓存(完全命中)
L2:语义缓存(embedding)
L3:模型推理

(4)风险控制(不能乱复用)

语义缓存有坑:

  • 用户问:“苹果股票能买吗”

  • 另一个问:“特斯拉股票能买吗”

embedding可能很接近 ❗

👉 但答案绝不能复用

所以需要:

  • 加实体识别(NER)

  • 加关键字段校验

  • 或直接限制只缓存低风险问题(FAQ类)


5️⃣ 为什么很多场景缓存命中率依然很高?

因为现实中:

👉 用户问题是“高度重复的”

典型场景:

  • 客服(退款、发票)

  • 编程问题(报错)

  • 搜索类问题

哪怕表达不同:

语义空间其实是“高度聚集”的


6️⃣ 一句话总结

大模型缓存本质是三层优化:

  • KV Cache → 避免重复算 attention(单请求内)

  • Prompt Cache → 复用前缀计算

  • Semantic Cache → 用 embedding 做“语义命中”

而你关心的“问法不同也能命中”,核心就是:

把“文本匹配”问题,变成“向量空间相似度”问题


此博客中的热门博文

Elasticsearch 读写原理指南

### 1. 什么是 segment,里面装了什么? 在 Lucene(也是 Elasticsearch)里,索引被切分成若干 **segment(段)**,每个 segment 是一个完整的、只读的倒排索引单元。一个 segment 包含: * **倒排词典** —— 用 **FST(Finite‑State Transducer)** 以高度压缩的形式保存每个字段出现的所有 term 以及 term→ord 的映射。对应的磁盘文件是 `*.tim`(新版)或 `*.tis/*.tii`(旧版)。 * **倒排列表(postings)** —— 保存每个 term 出现的文档 ID、频次、位置信息等,文件名通常是 `*.doc`、`*.pos`、`*.pay`。 * **存储字段**(_source、store:true 的字段)—— 以二进制块的形式写入 `*.fdt` / `*.fdx`。 * **doc‑values、norms、向量** 等辅助结构,分别保存在 `*.dv`、`*.norm`、`*.tv` 等文件里。 * **deleted‑docs bitmap**(`*.del`),标记哪些文档已被删除或被更新。 所有这些文件在 segment **写入磁盘后即成为只读**,后续的查询只能读取,永远不会在原文件上进行增删改。 --- ### 2. 原始文档和 FST 为什么都在 segment 里? * **原始文档**:Elasticsearch 默认把完整的 JSON(_source)以及任何 `store:true` 的字段写入 segment 的 `*.fdt/*.fdx` 文件。每个 segment 保存自己的那部分文档,旧的 segment 在合并前仍然保留,直到合并后被删除。 * **FST**:每个字段的词典在每个 segment 中单独维护,采用 FST 进行前缀共享和字节压缩。这样即使同一个 term 在多个 segment 中出现,也会在每个 segment 里拥有独立的映射,查询时只需要在对应 segment 的 FST 中定位即可。 --- ### 3. 查询时到底是怎么遍历 segment 的? 1. **请求入口**      客户端的搜索请求先到达 **协调节点**,协调节点把请求 ...

事务的ACID是什么

 事务的 ACID 是数据库事务必须满足的四个基本性质,用来保证在并发和故障情况下数据的正确性与可靠性: A(Atomicity,原子性) 一个事务中的操作要么 全部成功 ,要么 全部失败回滚 ,不存在“只做了一半”的中间状态。 C(Consistency,一致性) 事务执行前后,数据库都必须处于 一致的合法状态 ,满足约束(如主键、外键、唯一性、业务规则等)。 I(Isolation,隔离性) 并发执行的多个事务之间 相互隔离 ,一个事务未提交的中间结果对其他事务不可见(具体强弱由隔离级别决定)。 D(Durability,持久性) 一旦事务提交成功,其结果会被 永久保存 ,即使系统崩溃也不会丢失(通常依赖 WAL/redo log 等机制)。 一句话记忆: 要么全做完、前后不破坏规则、互不干扰、做完不丢。