RAG缓存
RAG 缓存方案专为高并发低延迟设计,分为两级拦截:
-
检索阶段缓存 (Retrieval Cache):
采用 L1 进程内存(精确 Hash 匹配)和 L2 向量索引(相似度匹配)。目标是吸收 70% 左右的重复或相似查询流量,将 L3 向量数据库的查询延迟从 $10-30\text{ms}$ 降低至 $1-5\text{ms}$,同时解耦核心数据库,保障系统 P99 延迟的稳定性。 -
增强/生成阶段缓存 (Generation Cache):
采用复合 Hash Key ($\text{Query} \oplus \text{Context} \oplus \text{Prompt}$) 进行精准匹配。目标是跳过最昂贵的 LLM 推理环节。只有当用户意图、知识背景和系统指令完全一致时才命中,确保缓存答案的 100% 精确性,并显著降低 LLM Token 成本和推理时间。
RAG 缓存技术实现框架
RAG 流程分为 检索(Retrieval) 和 生成(Generation) 两大阶段,缓存也应针对这两阶段进行优化。
I. 检索结果缓存(Retrieval Cache)
这是 RAG 缓存的核心,目的是避免对向量数据库进行重复或相似的查询。
1. 缓存 Key 设计:向量相似度匹配
由于用户查询(Query)总是略有不同,不能使用精确匹配。Key 必须是向量。
| 组成部分 | 描述 | 技术细节 |
|---|---|---|
| 主 Key | 用户 Query 的 Embedding 向量 | 使用与 RAG 系统相同的 Embedding 模型计算向量 $V_Q$。 |
| 索引结构 | 向量数据库索引 | 将历史 Query 的向量 $V_Q$ 存储在高性能向量数据库(如 Milvus, Vespa, Redis Stack/Vector Search, ElasticSearch)中,建立 ANN(近似最近邻)索引。 |
2. 缓存 Value 存储
| 组成部分 | 描述 | 存储位置 |
|---|---|---|
| Value | 上次检索到的 Top-K 文档 ID 列表 | Redis 或专用缓存数据库。 |
| Context | Top-K 文档的原始文本内容 | 可以直接存储在 Redis/缓存中,或只存 ID,然后从文档存储(如 S3, PostgreSQL)中快速拉取。 |
3. 检索缓存流程
- 用户 Query $Q$ 进入。
- 计算向量: $V_Q = \text{Embedding}(Q)$。
- 向量检索: 使用 $V_Q$ 在向量索引中进行相似度检索。
- 查询: 查找与 $V_Q$ 相似度 $\ge \tau$(例如 $0.85$)的历史向量 $V_{\text{Hist}}$。
- 命中: 如果命中 $V_{\text{Hist}}$,取出其对应的 Value(上次检索的文档 ID 列表)。
- 未命中: $V_Q$ 是新查询,执行 L3 向量数据库查询,并回填缓存。
- 召回: 基于缓存的文档 ID 列表,拉取原始文档内容,并将其作为上下文传递给 LLM。
关键技术: 向量索引在 Redis/Milvus 中的低延迟查询,以及合适的相似度阈值 $\tau$。
II. 生成结果缓存(Generation Cache)
这是 LLM 最终生成结果的缓存,目的是避免 LLM 再次进行昂贵的推理。
1. 缓存 Key 设计:复合 Key
生成阶段的结果依赖于 用户 Query、检索到的 Context 和 LLM 的 Prompt/配置。Key 必须包含所有这些影响最终结果的要素。
$$
\text{Generation Key} = \text{Hash}(\text{标准化}(Q_{\text{final}}) \oplus \text{Context Text} \oplus \text{System Prompt})
$$
| 组成部分 | 描述 | 关键作用 |
|---|---|---|
| Query Hash | 上下文重写后的 Query Hash。 | 确保用户意图匹配。 |
| Context Hash | 所有召回文档内容拼接后的文本 Hash。 | 确保 LLM 看到的基础知识是一致的。这是最关键的差异点。 |
| Prompt Hash | LLM 的 System Prompt (角色、约束等) Hash。 | 确保 LLM 的推理约束是一致的。 |
2. 缓存 Value 存储
| 组成部分 | 描述 | 存储位置 |
|---|---|---|
| Value | LLM 最终生成的文本答案 | Redis 或 Key-Value 存储。 |
| Metadata | 答案引用来源(Citation),Token 计数等。 | 存储在同一个 Value 中。 |
3. 生成缓存流程
- Context 召回后,系统计算 Generation Key $H_G$。
- L2 检查: 使用 $H_G$ 检查 L2 缓存。
- 命中: 直接返回 LLM 答案,跳过推理。
- 未命中: 执行 LLM 推理。
- 回填: LLM 完成生成后,使用 $(H_G, \text{Answer})$ 回填缓存。
III. 整体技术栈与部署
| 环节 | 推荐技术栈 |
|---|---|
| L1 进程内存缓存 | Java/Python 进程内 LRU 缓存 (如 Caffeine, Guava, Python 的 functools.lru_cache) |
| L2/L3 缓存存储 | Redis Cluster (提供 Key-Value 和向量搜索能力) 或 Milvus/Vespa (作为专用的向量检索层) |
| Hash 算法 | SHA-256 或 MurmurHash (后者更快,但安全性略低,适合作为纯缓存 Key) |
| 向量检索加速 | HNSW (Hierarchical Navigable Small World) 索引算法 |
关键的工程挑战:过期策略
RAG 缓存的挑战在于知识更新:
- 检索缓存 (Retrieval Cache) TTL: 应当与底层知识库的更新频率挂钩。如果知识库每天更新,TTL 就不能超过一天,甚至要采用 基于事件的淘汰机制。
- 生成缓存 (Generation Cache) TTL: 答案通常更持久,但仍需设置 TTL(例如 1-7 天),以应对 LLM 模型自身的更新(即使 Prompt 和 Context 不变,新模型也可能给出更好的答案)。
FAQ
1 Query Hash 是经过rewrite后的query吗?2 Context Hash 是什么,举个例子 3 Prompt Hash 是对提示词文件做hash吗
您提出的这三个问题非常重要,它们直击 RAG 缓存精确性 的核心。
确实,要让 生成结果缓存(Generation Cache) 有效工作,Key 必须包含所有影响最终答案的元素。
下面,我将针对这三个 Hash Key 组成部分进行详细解释和举例:
RAG 缓存 Generation Key 详解
1. Query Hash 是否是经过 Re-write 后的 Query?
回答:是的,必须是。
| Hash Key | 目的 | 举例 |
|---|---|---|
| Query Hash | 确保缓存 Key 反映的是用户的真实、完整的意图,而不是简短的指代词。 | 用户输入 (Q_raw): "换一组数据" $\rightarrow$ Re-write 输出 (Q_final): "再推荐一组 4000 元以下的手机" $\rightarrow$ Query Hash Key: $\text{Hash}(Q_{\text{final}})$ |
为什么必须是 Re-write 后的?
如果使用 $Q_{\text{raw}}$ ("换一组数据"),那么无论是哪种上下文,只要用户说 "换一组数据",都会命中同一个 Hash Key。这显然是错误的。
只有使用了 $Q_{\text{final}}$(语义完整的 Query),才能确保:当且仅当完整意图完全相同时,才触发缓存。
2. Context Hash 是什么?请举例
回答:Context Hash 是所有被召回用于生成答案的知识片段的指纹。
定义
在 RAG 流程中,LLM 生成答案需要依赖一组检索到的文档(通常是 $K=3$ 或 $K=5$ 个知识片段)。Context Hash 就是对这组知识片段进行处理后得到的唯一标识。
技术实现步骤
- 收集 Context: 收集 RAG 检索阶段召回的所有文档片段的原始文本($D_1, D_2, D_3, \dots$)。
- 标准化与排序:
- 标准化: 对每个文档进行标准化处理(去除不影响内容的关键符号、空白等)。
- 排序: 必须按照一个确定的、固定不变的规则对这些文档进行排序(例如,按文档 ID 升序,或按文档内容的 SHA-256 Hash 值升序)。这一步至关重要,因为 LLM 看到 $D_1 \oplus D_2$ 和 $D_2 \oplus D_1$ 得到的结果是一样的,但如果 Key 不同,就会重复推理。
- 拼接: 将排序后的文档文本用一个固定分隔符(例如
|$$|)拼接成一个长字符串 $S_{\text{Context}}$。
$$
S_{\text{Context}} = D_{\text{sorted}, 1} , || , \text{'|$$|} , || , D_{\text{sorted}, 2} , || , \dots
$$ - 计算 Hash: 对拼接后的长字符串 $S_{\text{Context}}$ 计算 Hash 值。
$$
\text{Context Hash} = \text{Hash}(S_{\text{Context}})
$$
举例
假设用户问 "什么是我们的退货政策?"
| 文档 ID | 文档内容 ($D_i$) |
|---|---|
DOC_005 |
零售商品可在 30 天内退货。 |
DOC_002 |
电子产品必须在 7 天内退货。 |
DOC_010 |
定制商品不支持退货。 |
- 排序 (假设按 ID 排序): $D_2, D_5, D_{10}$
- 拼接: $S_{\text{Context}} = D_2 , || , \text{'|$$|} , || , D_5 , || , \text{'|$$|} , || , D_{10}$
- 计算 Hash: $\text{Context Hash} = \text{Hash}(S_{\text{Context}})$
目的: 只有当 Query Hash 和 Context Hash 都相同时,才意味着 LLM 面临的问题和知识背景完全一致,可以安全地命中缓存。
3. Prompt Hash 是对提示词文件做 Hash 吗?
回答:是的,是对 LLM 的 System Prompt(系统提示词)做 Hash。
| Hash Key | 目的 | 举例 |
|---|---|---|
| Prompt Hash | 确保 LLM 的角色、约束和输出格式没有发生变化。 | System Prompt (P): "你是一个专业的金融顾问,请用客观且简洁的语言回复,并严格引用来源。" $\rightarrow$ Prompt Hash Key: $\text{Hash}(P)$ |
为什么需要 Prompt Hash?
在 Agent 系统中,我们可能会动态地改变 LLM 的 Prompt:
- 角色变化: 从“专业顾问”换成“热情导购”。
- 格式变化: 从“输出 JSON 格式”换成“输出 Markdown 列表”。
- 约束变化: 从“回复不超过 50 字”换成“回复不超过 100 字”。
即使 Query 和 Context 完全相同,如果 LLM 的 Prompt 变了,它给出的答案也会不同。因此,Prompt 变化后,旧的缓存结果就失效了。
实现方式: 将 LLM 在这次调用中使用的完整、最终的 System Prompt 文本提取出来,进行标准化(去除额外的换行和空格)后,计算其 Hash 值。
总结:Generation Cache Key 的完整性
Generation Cache Key 是一个三重校验,确保了缓存结果的准确性:
| 校验对象 | 对应元素 | 解决问题 |
|---|---|---|
| Query Hash | 问了什么? | 意图一致性 |
| Context Hash | 知识基础是什么? | 知识环境一致性 |
| Prompt Hash | LLM 的人设和指令是什么? | 推理约束一致性 |
只有三者皆相同,才能保证 LLM 两次推理会得到相同的有效结果,从而安全地使用缓存。

浙公网安备 33010602011771号