确保RAG系统在生产环境中的持续运行是首要目标。高可用性(HA)是指系统设计旨在尽可能长时间地不间断运行,即使面对组件故障也能最大限度地减少停机时间。对于RAG应用来说,它们经常处理交互式用户查询或重要的业务流程,因此可用性直接影响用户信任和运行稳定性。构建满足严格可用性要求的RAG系统的架构模式和策略将加以阐述。典型的RAG流程涉及多个独立服务:API网关或编排层、检索组件(嵌入模型、重排序器)、向量数据库和生成式大型语言模型(LLM)。其中任何一个发生故障都可能导致整个系统无响应。设计高可用性意味着在每个层引入弹性。RAG高可用性原则实现高可用性依赖于几个核心原则:消除单点故障(SPOFs): 识别任何一个组件的故障都可能导致整个系统崩溃的点。每个单点故障都必须实现冗余。可靠的切换/故障转移: 当主组件发生故障时,系统必须自动检测到并切换到冗余(备用或活动)组件。故障检测: 部署机制以检测故障发生时的情况,甚至主动预测它们。RAG组件的冗余策略冗余是高可用的基础。它涉及为组件提供多个实例,以便在一个实例发生故障时,其他实例可以接管其工作负载。具体策略通常取决于组件是无状态还是有状态的。无状态组件: 这些组件在请求之间不存储任何客户端或会话数据。RAG中的例子包括API编排层(如果设计为无状态)、嵌入模型服务、重排序器服务和LLM推理端点。无状态服务更容易实现高可用:你可以运行多个相同的实例,并使用负载均衡器在它们之间分配流量。如果一个实例发生故障,负载均衡器会将流量重定向到健康的实例。有状态组件: 这些组件维护状态。RAG系统中最突出的有状态组件是向量数据库,它存储文档嵌入和可能的元数据。如果LLM经过微调且自托管,并带有每次请求不传递的特定会话上下文或用户历史记录,它们也可能具有有状态方面,尽管常用LLM API从客户端角度来看,每次API调用都是无状态的。有状态服务需要更复杂的冗余策略,例如数据复制和共识机制。API网关和编排层RAG系统的入口点,通常是API网关或自定义编排服务,必须高可用。部署: 在不同的可用区(AZ)甚至区域部署API网关/编排器的多个实例,以实现更高层级的高可用性。负载均衡: 在这些实例前放置一个负载均衡器,以分配传入请求并管理故障转移。配置健康检查,以便负载均衡器可以自动从路由池中移除不健康的实例。检索服务(嵌入模型、重排序器)嵌入生成和重排序服务通常是无状态的计算任务。多个实例: 运行嵌入模型服务器和重排序器服务的多个实例。负载均衡: 使用负载均衡器分配请求。如果嵌入服务实例发生故障,新请求会被路由到剩余的健康实例。可以配置自动扩缩组以保持所需数量的健康实例。向量数据库向量数据库是一个重要有状态组件。它的可用性确保RAG系统能够检索相关上下文。复制: 大多数生产级的向量数据库(例如Pinecone、Weaviate、Qdrant、Milvus)提供内置的复制支持。主-副本(领导者-跟随者): 写入操作发送到主节点,主节点随后将数据复制到一个或多个副本节点。读取操作通常可以由副本提供,从而分配负载并在主节点故障时提供故障转移。如果原始主节点故障,系统需要一种机制来将副本提升为主状态。分片与复制: 对于非常大的数据集,向量数据库通常使用分片(将数据分区到多个节点)结合每个分片的复制。这提供了可扩展性和可用性。托管服务: 使用托管向量数据库服务通常可以减轻设置和维护复制以及故障转移机制的复杂性。这些服务通常提供服务水平协议(SLA)以保证正常运行时间。自托管方案: 如果自托管开源向量数据库,你负责配置和管理复制、故障转移和备份流程。这需要仔细规划和测试。生成式LLM端点访问生成式LLM对于生成最终答案非常重要。托管LLM API(例如OpenAI、Anthropic、Google): 这些提供商通常在他们自己的服务端管理高可用性。然而,API中断仍然可能发生。请考虑:重试机制: 在你的客户端代码中部署智能重试逻辑(带有指数退避和抖动)。区域端点: 一些提供商提供区域端点。使用地理上更接近你的应用的端点可以减少延迟,并可能提供与其它区域中断的隔离。回退策略: 对于极高的可用性需求,你可能考虑拥有一个次要LLM提供商或一个更小的、自托管模型作为回退。这会增加复杂性,但可以防止整个提供商中断。系统将需要逻辑来检测主LLM故障并切换到回退方案。自托管LLM: 如果你正在托管自己的LLM(例如使用开源模型),请应用与其他无状态服务相似的原则:在不同的硬件或可用区部署多个推理服务器实例。使用带有健康检查的负载均衡器。考虑不同的服务框架(如vLLM、TGI、Triton Inference Server),它们可能具有支持分布式推理或更易于管理多个副本的功能。下图展示了一个RAG架构,其中冗余应用于其各个组件:digraph G { bgcolor="transparent"; rankdir=TB; node [shape=box, style="filled,rounded", fillcolor="#e9ecef", fontname="Helvetica", fontsize=10]; edge [fontname="Helvetica", fontsize=9]; subgraph cluster_user { label = "用户交互"; style=dashed; user [label="用户查询", shape=ellipse, fillcolor="#a5d8ff"]; } subgraph cluster_lb_api { label = "API 网关 / 编排(冗余)"; style=dashed; lb_api [label="负载均衡器\n(API/编排)", shape=cylinder, fillcolor="#74c0fc"]; api1 [label="API/编排器 1", fillcolor="#a5d8ff"]; api2 [label="API/编排器 2", fillcolor="#a5d8ff"]; api_n [label="API/编排器 N", fillcolor="#a5d8ff"]; lb_api -> api1; lb_api -> api2; lb_api -> api_n [style=dashed]; } subgraph cluster_retrieval { label = "检索服务(冗余)"; style=dashed; lb_retriever [label="负载均衡器\n(检索器)", shape=cylinder, fillcolor="#74c0fc"]; retriever1 [label="检索器 1\n(嵌入器 + 重排序器)", fillcolor="#96f2d7"]; retriever2 [label="检索器 2\n(嵌入器 + 重排序器)", fillcolor="#96f2d7"]; retriever_n [label="检索器 N\n(嵌入器 + 重排序器)", fillcolor="#96f2d7", style="filled,dashed"]; lb_retriever -> retriever1; lb_retriever -> retriever2; lb_retriever -> retriever_n [style=dashed]; } subgraph cluster_vectordb { label = "向量数据库(复制)"; style=dashed; vdb_primary [label="向量数据库主节点", fillcolor="#ffc9c9"]; vdb_replica1 [label="向量数据库副本 1", fillcolor="#ffa8a8"]; vdb_replica2 [label="向量数据库副本 2", fillcolor="#ffa8a8", style="filled,dashed"]; vdb_primary -> vdb_replica1 [label="复制", style=dotted, dir=both]; vdb_primary -> vdb_replica2 [label="复制", style=dotted, dir=both]; } subgraph cluster_generation { label = "生成服务(冗余)"; style=dashed; lb_llm [label="负载均衡器\n(LLM)", shape=cylinder, fillcolor="#74c0fc"]; llm1 [label="LLM 端点 1", fillcolor="#bac8ff"]; llm2 [label="LLM 端点 2", fillcolor="#bac8ff"]; llm_fallback [label="回退 LLM\n(可选)", fillcolor="#d0bfff", style="filled,dashed"]; lb_llm -> llm1; lb_llm -> llm2; lb_llm -> llm_fallback [style=dashed, label="如果主LLM故障"]; } user -> lb_api [label="HTTPS"]; api1 -> lb_retriever; api2 -> lb_retriever; api_n -> lb_retriever; retriever1 -> vdb_primary [label="查询(写入/读取)"]; retriever2 -> vdb_primary [label="查询(写入/读取)"]; retriever_n -> vdb_primary [label="查询(写入/读取)"]; retriever1 -> vdb_replica1 [label="查询(读取)", style=dashed]; retriever2 -> vdb_replica1 [label="查询(读取)", style=dashed]; api1 -> lb_llm; api2 -> lb_llm; api_n -> lb_llm; // 从LLM返回API/编排器的连接 llm1 -> api1 [label="响应", dir=back, style=invis]; // 为了布局整洁而使用隐形连接,假设连接存在 // 实际响应路径是负载均衡器LLM -> 发出请求的编排器实例 }该图展示了RAG系统常见的高可用配置,特点是为无状态服务(API网关、检索器、LLM)配置了负载均衡器,以及为有状态数据存储配置了复制的向量数据库。回退LLM和额外的实例(N)表明了可扩展性和增强的弹性。负载均衡负载均衡器是高可用的基础。它们将传入流量分配到你的多个服务实例,防止任何单个实例过载,并将流量从故障实例中移开。健康检查: 配置你的负载均衡器对后端实例执行定期健康检查。健康检查可以是对服务上/health端点的简单HTTP GET请求。如果一个实例的健康检查失败,负载均衡器会停止向其发送流量。类型:应用负载均衡器(ALB)或L7负载均衡器: 在应用层(HTTP/HTTPS)运行。它们可以根据请求内容(例如URL路径、头部)做出路由决策。适合将流量分配到API网关、编排器以及LLM/检索微服务。网络负载均衡器(NLB)或L4负载均衡器: 在传输层(TCP/UDP)运行。它们通常更快,可以以极低的延迟处理每秒数百万个请求。适用于高吞吐量场景或不需要L7功能时。会话亲和性(粘性会话): 在一些罕见的RAG场景中,编排器可能维护与多轮对话相关的短生命周期状态(尽管为了高可用性理想情况下应避免),可以使用会话亲和性来确保来自特定客户端的请求总是路由到相同的后端实例。然而,这会使故障转移和负载分配复杂化。尽可能争取无状态服务。自动化故障转移当主组件发生故障时,系统必须自动切换到备份或冗余组件,且中断最小化。无状态服务: 故障转移通常由负载均衡器处理,将故障实例从其路由池中移除。自动扩缩组随后可以启动一个新实例来替换故障实例。有状态服务(向量数据库): 故障转移更为复杂。对于主-副本设置,这包括:检测主节点故障(例如,通过健康检查、心跳丢失)。将一个副本提升为新的主节点。这可能涉及共识算法或选举过程。重新配置应用程序和其他副本以连接到新的主节点。可能启动一个新的副本以保持所需的冗余水平。 托管数据库服务自动化了这一点。对于自管理系统,像Patroni(用于一些向量数据库可能用于元数据的PostgreSQL系统)或向量数据库自身的内置集群功能可以处理这一点。定期测试故障转移流程。地理冗余(多区域和多可用区)为了最高级别的可用性,特别是为了防止大规模中断,例如整个数据中心或可用区(AZ)故障,考虑在区域内跨多个可用区,甚至跨多个地理区域部署RAG系统。多可用区: 在同一云区域内跨不同可用区部署冗余组件是一种常见做法。可用区是物理上分离的数据中心,拥有独立的电源、散热和网络。有状态服务(如向量数据库)之间的数据复制通常是同步或接近同步的,以相对较低的延迟开销提供良好保护。多区域: 跨多个区域部署可以防止区域性中断。这会显著增加复杂性:数据复制: 跨区域复制向量数据库内容可能因延迟而具有挑战性。异步复制很常见,这意味着在一个区域写入的数据可能需要一段时间才能在另一个区域出现。这可能导致最终一致性。流量路由: 需要全球负载均衡器或基于DNS的故障转移(例如Amazon Route 53、Azure Traffic Manager)来将用户引导到适当的活动区域。成本: 在多个区域运行活跃的基础设施成本更高。根据你的应用的正常运行时间要求和预算选择地理冗余级别。对于许多应用来说,在单个区域内精心设计的多可用区部署提供了足够的可用性。为故障而设计:确保可靠性仅仅拥有冗余组件,一个真正的高可用系统遵循“为故障而设计”的理念。这意味着预测故障并构建机制来优雅地处理它们。断路器RAG流程中不同服务之间的相互影响(例如,编排器调用LLM服务)是潜在的故障点。如果下游服务变得缓慢或无响应,重复调用会耗尽调用服务的资源,导致连锁故障。断路器模式包装受保护的函数调用。它监控故障,如果故障数量超过阈值,就会“打开”电路。这意味着后续调用会自动快速失败(例如,返回错误或缓存/回退响应),而不会尝试联系故障服务。在超时期限后,断路器进入“半开”状态,允许有限数量的测试请求。如果这些请求成功,电路关闭并恢复正常运行。如果它们失败,电路重新打开。digraph CircuitBreaker { bgcolor="transparent"; rankdir=LR; node [shape=Mrecord, style="filled", fontname="Helvetica", fontsize=10]; edge [fontname="Helvetica", fontsize=9]; CLOSED [label="{CLOSED(关闭)|请求通过服务\n监控故障}", fillcolor="#b2f2bb", width=2.5]; OPEN [label="{OPEN(打开)|请求立即失败\n(不调用服务)\n计时器激活}", fillcolor="#ffc9c9", width=2.5]; HALF_OPEN [label="{HALF_OPEN(半开)|允许有限测试请求\n如果成功 → CLOSED\n如果失败 → OPEN}", fillcolor="#ffe066", width=3]; CLOSED -> OPEN [label=" 故障阈值\n 超过", color="#f03e3e"]; OPEN -> HALF_OPEN [label=" 超时\n 到期", color="#1c7ed6"]; HALF_OPEN -> CLOSED [label=" 测试请求\n 成功", color="#37b24d"]; HALF_OPEN -> OPEN [label=" 测试请求\n 失败", color="#f03e3e"]; }断路器的状态。这种模式防止客户端重复尝试调用可能失败的服务。可以使用Hystrix(Java,现已进入维护模式)、Resilience4j(Java)、Polly(.NET)等库或自定义实现来部署断路器。超时和重试超时: 为RAG组件之间的所有网络调用设置严格的超时。缓慢的下游服务不应无限期地阻塞上游服务。重试: 对瞬时故障(例如,临时网络故障、速率限制)部署重试机制。使用指数退避(增加重试之间的等待时间)并添加抖动(在退避期内加入随机性),以避免许多客户端同时重试导致的“惊群效应”问题。不要无限期重试或对非瞬时错误进行重试。可用性监控和告警持续监控对于确保高可用性策略的有效性非常重要,并在问题导致显著停机之前检测到它们。指标:正常运行时间/可用性: 系统运行的时间百分比(例如,99.9%、99.99%)。错误率: 监控每个组件和端到端请求的错误率。延迟: 跟踪重要操作的P50、P90、P99延迟。延迟增加可能是故障的前兆。健康检查状态: 监控负载均衡器报告的健康检查状态。资源使用率: 所有组件的CPU、内存、网络、磁盘I/O。告警: 为重要阈值突破设置告警(例如,错误率飙升、延迟增加、组件故障、向量数据库节点磁盘空间不足)。告警应可操作并路由到适当的值班人员。成本与复杂性权衡实现高可用性并非没有成本。它会带来金钱成本和系统复杂性的增加:基础设施成本: 运行服务器、数据库和负载均衡器的冗余实例会产生额外基础设施开销。多区域高可用性尤其昂贵。复杂性: 管理分布式、复制的系统比管理单实例部署更为复杂。这包括部署、配置管理、监控和故障排除。性能考量: 数据复制,特别是同步复制或跨区域复制,可能引入延迟。你实现的高可用性级别应该是一个业务决策,平衡停机成本与高可用性解决方案的成本和复杂性。为可用性定义明确的服务级别目标(SLO),并设计你的系统以满足它们。通过周到地应用这些架构原则和模式,你可以构建不仅智能而且有弹性的RAG系统,能够抵御生产环境中不可避免的故障,并为用户提供一致、可靠的体验。随后的关于容错和管理更新的章节将在此基础上进一步介绍系统运行。