在优化大型分布式 RAG 系统以获取最佳性能时,实现最小端到端延迟 Ltotal 和最大每秒查询数 QPS 成为一个核心目标。缓存作为系统设计中的一种成熟技术,通过将常用数据或计算结果存储在更接近需要它们的地方,提供一种有效方法来实现这些目标,从而减轻后端组件的负担并缩短响应时间。在分布式 RAG 架构的各个层策略性地实施缓存,不仅仅是优化,它通常也是构建能处理生产规模工作负载的响应迅速、成本效益高的系统所必需的。
核心思想很简单:如果一段数据或一个计算结果很可能很快再次被请求,就将其存储在快速访问层。然而,在具有多个互通服务的分布式 RAG 系统中,缓存的“缓存什么、在哪里缓存、以及如何缓存”变成了多方面的决策,对性能、一致性和操作复杂性都有重要影响。
RAG 管线中的缓存
在分布式 RAG 系统中,有效的缓存需要在多个阶段识别机会,从初始查询处理和检索,到语言模型生成和最终响应组装。
检索系统缓存
检索组件负责获取相关文档片段,是缓存的主要候选。重复查询或共享公共子部分的查询可以大幅受益。
-
查询到文档缓存: 这可能是检索管线中最直接的缓存。
- 缓存内容: 对于给定用户查询,是检索到的文档ID集合,甚至是完整的文档片段。
- 键策略: 缓存键通常源自用户查询。精确字符串匹配很简单,但更复杂的方法涉及查询规范化(例如,小写化、删除停用词、词干提取),甚至使用查询嵌入本身(或其哈希值)作为键来捕捉语义相似性。
- 影响: 通过绕过昂贵的向量搜索或混合搜索操作,大幅减少了流行或重复查询的延迟。
- 考量: 缓存大小会快速增长。“流行”查询的定义可能会变化,需要自适应淘汰策略。
-
嵌入缓存: 尽管文档嵌入通常是预先计算并存储的,但查询嵌入是即时生成的。
- 缓存内容: 针对频繁提交的查询的查询嵌入。
- 影响: 节省了通过嵌入模型处理查询的计算成本。如果嵌入模型推理是瓶颈,这特别有益。
- 考量: 如果嵌入生成与搜索本身相比已经高度优化且快速,则影响较小。
-
重排序器输出缓存: 如果您的 RAG 系统采用带有重排序器的多阶段检索过程,缓存其输出可能会有益。
- 缓存内容: 对于来自初始检索器的给定输入列表,是重排序后的文档ID/片段列表。
- 键策略: 策略可以是输入文档ID和查询的哈希值。
- 影响: 避免了重排序器模型对相同的中间结果进行重新计算。
这些检索缓存通常使用分布式键值存储(如 Redis 或 Memcached)来实现,以便在检索服务实例之间共享访问,甚至可以在单个服务实例内部作为内存缓存使用,以实现对非常热门项目的极低延迟访问(L1 缓存)。
LLM 和生成缓存
生成组件,通常是 LLM,往往是 RAG 系统中计算最密集且导致延迟的部分。在此处进行缓存可以带来显著的性能提升。
实施 LLM 缓存通常涉及与检索缓存类似的技术,但对数据陈旧的敏感性可能需要更短的生存时间(TTL)值或更激进的失效策略,尤其是在支持上下文的检索文档高度动态的情况下。
数据管线和静态资产缓存
尽管数据摄取管线(第四章)侧重于处理和嵌入,但服务方面也可以从缓存中受益。
- 常用元数据缓存: 文档元数据(例如,源 URL、创建日期、作者)经常与检索到的内容一起请求,可以被缓存以减少数据库负载。
- 静态资产缓存(客户端/边缘): 对于带有网页界面的 RAG 应用程序,标准网页缓存技术适用。
- 缓存内容: 用户界面组件、JavaScript 库、CSS 文件、图像。
- 实现: 浏览器缓存和内容分发网络(CDN)。
- 影响: 改善页面加载时间并减少应用程序服务器的负载。
应用和编排层缓存
协调 RAG 流程的层也可以对完全组装的响应实施缓存。
- 完全组装响应缓存:
- 缓存内容: 经过所有检索、生成和后处理步骤后,发送给用户的最终完整响应。
- 键策略: 基于初始用户查询和可能的其他请求参数(例如,用于个性化 RAG 的用户 ID)。
- 影响: 为重复的相同请求提供最快的可能响应。
- 考量: 这是最高层缓存;失效必须考虑任何底层组件(检索到的文档、LLM、后处理逻辑)的变化。
以下图表显示了分布式 RAG 系统架构中的潜在缓存点:
缓存在分布式 RAG 系统中各个阶段的放置,从查询处理到 LLM 生成和应用程序编排。
高级缓存策略与管理
实施缓存只是成功了一半。有效管理它们,尤其是在分布式环境中,需要仔细考量失效、一致性、淘汰策略和监控。
缓存失效
确保缓存数据保持适当的新鲜度非常重要。陈旧的缓存条目可能导致不正确或过时的响应,损害 RAG 系统的效用。
- 生存时间(TTL): 每个缓存条目都被分配一个过期时间。这实现起来简单,但可能是一个粗略的工具。短 TTL 减少陈旧性但也会降低命中率。长 TTL 增加命中率但增加了提供陈旧数据的风险。
- 直写式缓存: 对底层数据存储的写入通过缓存同步进行。缓存会立即更新或失效。这确保了一致性但增加了写入操作的延迟。
- 回写式缓存: 写入首先到达缓存,然后异步传播到数据存储。这提供了低写入延迟,但如果数据持久化之前缓存失败,则存在数据丢失风险。这也意味着缓存与数据源之间存在一段不一致时期。
- 事件驱动失效: 对于动态 RAG 系统,这通常是最有效的方法。当底层数据源发生变化时(例如,文档更新,嵌入重新生成),会发布事件(例如,通过像 Kafka 这样的消息队列,或如第四章所述的数据库变更数据捕获 (CDC))。缓存服务订阅这些事件并主动失效或更新相关条目。这使得数据能够接近实时保持新鲜,同时保持未更改数据的高命中率。
分布式环境中的缓存一致性
当服务的多个实例维护各自的缓存,或在使用分布式缓存集群时,确保一致性(所有缓存都看到数据的一致视图,或不一致性有界)成为一个挑战。
- 分布式失效消息: 常见的策略是使用发布/订阅机制。当一个项目在一个缓存中或在源头被更新或失效时,一条失效消息会广播到分布式缓存中的所有其他缓存实例或节点。
- 版本控制: 将版本号与缓存项目关联。请求可以指定最低可接受版本,或者缓存可以在检测到有新版本可用时主动刷新。
缓存淘汰策略
由于缓存存储是有限的,当缓存满时,需要策略来决定丢弃哪些项目。
- LRU(最近最少使用): 丢弃最长时间未被访问的项目。适用于通用缓存,其中最近的访问可以预测未来的访问。
- LFU(最不常用): 丢弃访问次数最少的项目。适用于某些项目持续流行而其他项目很少被访问的情况。需要更多开销来跟踪频率。
- FIFO(先进先出): 丢弃最旧的项目。简单,但通常不是最优的。
- 基于大小的淘汰: 优先淘汰较大的项目,或具有更高存储成本比的项目。
淘汰策略的选择很大程度上取决于被缓存特定数据的访问模式。例如,针对突发新闻查询的 LLM 响应可能与针对一般知识查询的响应具有不同的访问模式。
分层缓存
多级缓存层次结构可以同时优化速度和容量:
- L1 缓存: 服务进程内部的内存缓存。访问速度最快,但容量有限且局限于服务实例。
- L2 缓存: 共享分布式缓存(例如,Redis、Memcached)。比 L1 慢但容量大得多,并且所有服务实例均可访问。
- L3 缓存(或持久存储): 实际数据源(例如,向量数据库、文档存储、LLM 服务)。
请求首先检查 L1,然后是 L2,如果都未命中,最终才访问 L3。
缓存预热
为避免初始“冷启动”时期(此时缓存为空,所有请求都命中后端,导致高延迟),可以预加载或“预热”缓存。
- 策略: 用最受欢迎查询的数据、或来自先前操作窗口的最近访问项目填充缓存,或基于预测可能请求的分析来填充。
- 使用场景: 在新部署、服务重启或扩缩容事件之后。
监控缓存性能
为了了解缓存层的有效性并对其进行调整,严格的监控是必要的。指标包括:
- 命中率 (H): 由缓存服务的请求百分比。H=缓存命中数/(缓存命中数+缓存未命中数)。高命中率通常是期望的。
- 未命中率 (M): M=1−H。
- 缓存延迟 (Tcache): 从缓存中检索一个项目所需的时间。
- 源延迟 (Torigin): 缓存未命中时,从后端源检索/计算一个项目所需的时间。
- 有效访问时间 (Teff): 访问项目的平均时间,考虑命中和未命中。这可以建模为:
Teff=(H⋅Tcache)+((1−H)⋅Torigin)
目标是最小化 Teff。
- 缓存大小/内存使用: 用于管理成本和容量。
- 淘汰率: 每单位时间淘汰的项目数量。高淘汰率可能表明缓存太小或 TTL 设置过于激进。
分析这些指标有助于确定缓存大小、调整 TTL、选择合适的淘汰策略,并最终证明在缓存基础设施上的投资是合理的。
缓存的安全与成本影响
尽管缓存提升性能,但它带来其他考量:
- 安全: 如果缓存敏感数据(例如,检索到的文档或 LLM 响应中的个人身份信息(PII)),缓存本身就成为一个敏感数据存储。对缓存数据进行静态和传输中的加密,以及对缓存实例进行严格的访问控制,这些都是必要的。要特别小心提示到响应的缓存,因为提示可能包含敏感的用户输入。
- 成本: 缓存基础设施(例如,托管的 Redis/Memcached 服务、CDN 费用)增加了运营开支。必须权衡此成本与因减少在更昂贵资源(如 LLM 推理端点或大型数据库集群)上的计算而节省的成本,以及获得的性能优势。配置不当的缓存(例如,命中率非常低)可能产生费用但未提供显著效益。
策略性实施且管理良好的缓存层,对于构建功能强大、同时在生产中高效、响应迅速且经济可行的大型分布式 RAG 系统是不可或缺的。每个缓存决策都应以数据为依据,并基于性能分析和对 RAG 应用程序中特定访问模式的了解。