RAG缓存

RAG 缓存方案专为高并发低延迟设计,分为两级拦截:

  1. 检索阶段缓存 (Retrieval Cache):
    采用 L1 进程内存(精确 Hash 匹配)和 L2 向量索引(相似度匹配)。目标是吸收 70% 左右的重复或相似查询流量,将 L3 向量数据库的查询延迟从 $10-30\text{ms}$ 降低至 $1-5\text{ms}$,同时解耦核心数据库,保障系统 P99 延迟的稳定性。

  2. 增强/生成阶段缓存 (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. 检索缓存流程

  1. 用户 Query $Q$ 进入。
  2. 计算向量: $V_Q = \text{Embedding}(Q)$。
  3. 向量检索: 使用 $V_Q$ 在向量索引中进行相似度检索。
    • 查询: 查找与 $V_Q$ 相似度 $\ge \tau$(例如 $0.85$)的历史向量 $V_{\text{Hist}}$。
    • 命中: 如果命中 $V_{\text{Hist}}$,取出其对应的 Value(上次检索的文档 ID 列表)。
    • 未命中: $V_Q$ 是新查询,执行 L3 向量数据库查询,并回填缓存。
  4. 召回: 基于缓存的文档 ID 列表,拉取原始文档内容,并将其作为上下文传递给 LLM。

关键技术: 向量索引在 Redis/Milvus 中的低延迟查询,以及合适的相似度阈值 $\tau$。


II. 生成结果缓存(Generation Cache)

这是 LLM 最终生成结果的缓存,目的是避免 LLM 再次进行昂贵的推理。

1. 缓存 Key 设计:复合 Key

生成阶段的结果依赖于 用户 Query检索到的 ContextLLM 的 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. 生成缓存流程

  1. Context 召回后,系统计算 Generation Key $H_G$。
  2. L2 检查: 使用 $H_G$ 检查 L2 缓存。
    • 命中: 直接返回 LLM 答案,跳过推理。
    • 未命中: 执行 LLM 推理。
  3. 回填: 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 就是对这组知识片段进行处理后得到的唯一标识。

技术实现步骤

  1. 收集 Context: 收集 RAG 检索阶段召回的所有文档片段的原始文本($D_1, D_2, D_3, \dots$)。
  2. 标准化与排序:
    • 标准化: 对每个文档进行标准化处理(去除不影响内容的关键符号、空白等)。
    • 排序: 必须按照一个确定的、固定不变的规则对这些文档进行排序(例如,按文档 ID 升序,或按文档内容的 SHA-256 Hash 值升序)。这一步至关重要,因为 LLM 看到 $D_1 \oplus D_2$ 和 $D_2 \oplus D_1$ 得到的结果是一样的,但如果 Key 不同,就会重复推理。
  3. 拼接: 将排序后的文档文本用一个固定分隔符(例如 |$$|)拼接成一个长字符串 $S_{\text{Context}}$。
    $$
    S_{\text{Context}} = D_{\text{sorted}, 1} , || , \text{'|$$|} , || , D_{\text{sorted}, 2} , || , \dots
    $$
  4. 计算 Hash: 对拼接后的长字符串 $S_{\text{Context}}$ 计算 Hash 值。
    $$
    \text{Context Hash} = \text{Hash}(S_{\text{Context}})
    $$

举例

假设用户问 "什么是我们的退货政策?"

文档 ID 文档内容 ($D_i$)
DOC_005 零售商品可在 30 天内退货。
DOC_002 电子产品必须在 7 天内退货。
DOC_010 定制商品不支持退货。
  1. 排序 (假设按 ID 排序): $D_2, D_5, D_{10}$
  2. 拼接: $S_{\text{Context}} = D_2 , || , \text{'|$$|} , || , D_5 , || , \text{'|$$|} , || , D_{10}$
  3. 计算 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:

  1. 角色变化: 从“专业顾问”换成“热情导购”。
  2. 格式变化: 从“输出 JSON 格式”换成“输出 Markdown 列表”。
  3. 约束变化: 从“回复不超过 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 两次推理会得到相同的有效结果,从而安全地使用缓存。

posted @ 2025-12-04 20:54  向着朝阳  阅读(0)  评论(0)    收藏  举报