译:设计生产级 RAG 架构

原文:levelup.gitconnected.com/designing-a...

示例代码(开源,可本地运行):github.com/matt-bentle...

核心主题:用免费、开源的技术栈搭建一套完整的、生产级的 RAG(检索增强生成)架构,以便把大语言模型(LLM)"接地"到你自己的私有知识库上。文中以完整的 Kubernetes 官方文档(PDF 版)作为示例知识库,逐步走过你会遇到的关键设计决策。

大语言模型很强大,但在被迫"猜测"时也出了名地不可靠:它们会以极高的自信给出回答,并以惊人的流畅度编造事实(幻觉)。RAG 的存在,正是为了用从你私有知识库中检索到的、可验证的数据来"接地"LLM 的输出,从而消除这种猜测。过去几年里,RAG 已经成熟为任何严肃 AI 系统的核心构件------从智能体框架到开发者副驾。


一、高层架构

RAG 由两个独立的流程组成:

1. 摄取管线(Ingestion Pipeline)

负责从知识库中抽取文本,并把它存成可检索的格式。这是 RAG 系统中最重要也最复杂的部分------这一步做不好,整个系统就废了。难点在于:可用的技术非常多,而你必须根据数据类型在大量配置之间做出选择。

2. 检索 + 生成(Retrieval + Generation)

数据变成可检索格式后,你需要:

  • 当用户输入提示时,检索出相关片段;
  • 以尽可能好用的格式把它注入 LLM 作为上下文,用于生成

两个流程就绪后,用户通过基于 LLM 的应用提问时,回答前会先被知识库中的相关信息所"增强"。

你的数据,你的系统

RAG 有大量技巧,但并不意味着你需要应该 全用上。也不存在对所有数据都可靠的"完美设计"。架构决策最重要的驱动因素永远是你的数据本身,以及你希望它如何被消费。要先问清楚:

  • 数据是什么格式?(PDF、HTML、JSON、自由文本等)
  • 是否高度结构化?(章节/子章节、层级、表格、法律引用等)
  • 是否有需要索引的额外元数据?
  • 是否有非文本内容?(图像、视频、音频)
  • 数据的规模与体量?
  • 用户将如何检索它?

二、采用哪种检索方式?

这是第一个也是最重要的决策,其余一切都由它衍生。

关键词检索(稀疏向量 Sparse Vectors)

搜索引擎多年来一直使用关键词检索。查询中的词/词元被抽取出来,与知识库中每篇文档的词进行匹配。匹配通过稀疏向量完成------基于词表为查询或文档中的每个词建立索引。

匹配方法可以从简单词频,到更复杂的 BM25(Best Match 25)。BM25 用在整个语料上计算的**逆文档频率(IDF)**给词元赋权,降低那些在大量文档中频繁出现的词带来的噪声,从而给出更可靠的匹配。大多数全文数据库(如 Lucene、ElasticSearch)都使用 BM25 的某种变体。

嵌入相似度(稠密向量 Dense Vectors)

如果查询中的确切词不在知识库里,关键词检索就会失效。嵌入(Embedding)通过把文本表示成数值向量 来解决这个问题。嵌入由专门的机器学习模型生成,能够捕捉词的语义含义

之所以叫稠密向量 ,是因为模型对每段文本生成的向量都具有相同的维度数。语义相近的词在向量空间中彼此更接近。检索时使用余弦相似度(Cosine Similarity)来查找语义相近的文本,因此这类检索常被称为语义检索(Semantic Search)。向量数据库专为存储数值向量并原生支持余弦相似度检索;关系型数据库(如 PostgreSQL、SQL Server)也开始原生支持向量。

混合检索(Hybrid Search)

稠密向量与稀疏向量检索可以合并成混合检索,往往能得到最好的效果。

但开箱即用支持混合检索的数据库不多,通常需要复杂的自定义工作来融合不同的检索结果。

决策:混合检索

本案例采用混合检索,因为它在 Kubernetes 文档上最可能表现最佳。权重设置为:

  • 嵌入(语义)70%------预期表现最好;
  • 关键词(BM25)30%------支持按特定条件/代码检索。

这是 RAG 中常用的比例。

数据库选型:Qdrant

选数据库时要考虑:

  • 支持的检索功能类型
  • 预算:开源还是商业
  • 隐私:数据是否需要自托管
  • 流行度:更好的支持和社区
  • SDK 支持
  • 基于需求的性能

支持混合检索的数据库有几个。BM25 比较麻烦,因为它需要维护跨所有文本的词频索引,所以建议选一个能替你做这件事的向量库。

Qdrant 能同时存储稠密向量和稀疏向量,并对两者执行混合查询,结果用 倒数排名融合(Reciprocal Rank Fusion, RRF) 合并------这是 RAG 的标准做法。Qdrant 开源、可在 Docker 中运行、托管极其简单,且对 .NET 支持出色(原作者使用 .NET)。

其他支持混合检索的优秀选项:Weaviate、Pinecone、Milvus


三、摄取管线(Ingestion Pipeline)

确定要做混合检索后,摄取管线就需要同时生成并存储稠密向量嵌入稀疏向量

Kubernetes 源文档相当长(RAG 知识库很常见),这对检索策略尤其是基于嵌入的策略不友好。嵌入试图提炼文本的语义含义,但长文本会覆盖很多不同主题。嵌入在语义内聚良好的较小文本块 上效果最好。而且我们也很难把整篇长文档塞进 LLM 上下文------我们想根据用户查询取最相关的部分。RAG 系统通过在生成嵌入和存储前把文档**切块(Chunking)**来解决这个问题。

一个高效的摄取管线(尤其是切块策略)是 RAG 系统实现卓越检索召回率最重要的环节。

标准摄取管线的关键步骤:

  1. 抽取(Extract)
  2. 预处理(Pre-Process)
  3. 切块(Chunk)
  4. 生成元数据(Generate Metadata)
  5. 生成稠密与稀疏向量(Generate Dense and Sparse Vectors)
  6. 存储(Store)

1. 抽取(Extraction)

第一步是从源文档中抽取文本。任何可用于检索或丰富回答的元数据也应一并抽取,例如文档名、页码、摘要。

如果文档有明确结构并被拆成章节,最好按章节抽取文本。这有助于后续切块------可以保证一个块不跨越多个章节。

章节按页存储,便于之后拼接还原并理解块属于哪些页。作者通常把子章节扁平化成一个"章节路径(Section Path)",并指定结构层级应嵌套多深。

由于文档布局/结构各异,通常需要多种抽取策略。示例仓库提供了:

  • SimplePdfExtractor:把整个 PDF 的所有文本抽取为单个章节。
  • BookmarkPdfExtractor:搜索 PDF 中的书签,用来识别章节和嵌套章节。
  • FormatBasedPdfExtractor:分析文本格式来推断章节,假设标题更大或加粗,越深层的章节字号/字重越小。

Kubernetes 文档的标题格式非常一致,因此 FormatBasedExtractor 完美适用。下图展示了一个基于标题格式抽取出的章节与子章节的示例。

2. 切块(Chunking)

把文档抽取成章节/子章节后,你已经完成了高效切块的一半。下一步是把过长的章节切成更小的块,以生成高质量嵌入。

块长度(Chunk Length)

  • 一般 200--300 词元 效果不错,但务必用你的数据测试不同长度来比较召回率。
  • 对复杂的政策类或法律类文档,最高到 600 词元可能更好。
  • 要注意嵌入模型的限制:许多开源模型有 512 词元 的硬上限。

块切分点(Chunk Breaks)

  • 不要用严格的固定块大小,以免切断句子。
  • 应优先在段落分隔 处切块,至少也要在句末切。
  • 更高级的做法:用嵌入模型或 LLM 在语义明显不同的文本处切块。

块重叠(Chunk Overlap)

  • 无论切块多复杂,总会有不理想的边界。
  • 因此在块之间制造 10--20% 的重叠 通常能提升召回率,而不损害精度或延迟。

针对 Kubernetes 文档中一些较大的章节,作者发现以下配置效果好:

  • 最大块长度:400 词元
  • 块重叠:50 词元

3. 生成元数据(Generating Metadata)

给块附加额外元数据有两个目的:

  1. 丰富 LLM 的生成,例如添加引用和页码;
  2. 在检索时做自定义检索或过滤

存哪些元数据取决于你的数据,示例:

  • 源文档名
  • 页码
  • 章节 与 章节路径(Section Path)
  • 章节内块索引 与 块总数(Chunk Index / Total Chunks)
  • 引用 ID、条款号、规则/错误/产品代码
  • 块内容摘要(由 LLM 生成)

4. 生成稠密向量(语义嵌入)

嵌入由所选嵌入模型基于块文本生成。如果有助于语义相关性,也可以把源文档名和/或章节路径加入嵌入文本。

模型选择很多,不同模型对不同数据类型表现各异。更强的模型通常产出维度更高的向量,但需要更多算力和 GPU。

作者选用 BAAI/bge-small-en-v1.5 ,因为它足够小、能在 CPU 上较快运行,产出 384 维 向量。通过一个 Python FastAPI 应用托管 Hugging Face 模型。

5. 生成稀疏向量(BM25)

稀疏向量只包含块中出现词项的词频。不存词项文本,而是给每个词项分配整数 ID。两种分配方式:

  1. 用一份词表做"词项 → ID"映射;
  2. 对词项做哈希得到整数。

作者一般偏好简单哈希 ,避免维护词表。像 xxHash 这样的算法极快,且几乎不会发生哈希冲突。

摄取调度(Ingestion Schedule)

如何运行、多久运行摄取管线取决于数据和场景:

  • 手动:数据极少变化时可以手动跑。
  • 定时任务:文档变化较频繁时用定时/cron 任务,常见做法是每日运行。
  • 实时:如果有文档管理层且文档频繁变化,可用异步事件驱动流程,在源文档变化时触发管线。

四、检索 + 生成(Retrieval + Generation)

大部分决策和复杂度都在摄取阶段解决了。接下来只需一个根据用户提示检索块的流程。

检索(Retrieval)

选 Qdrant 是因为它开箱即用地同时执行 BM25 关键词检索和嵌入余弦相似度检索。建议给混合检索的不同策略加权,RAG 常用:

  • 稠密向量(嵌入)70%
  • 稀疏向量(关键词/BM25)30%

嵌入捕捉语义、通常表现最好,所以一般最依赖它。但理想权重取决于你的数据以及关键词查询是否非常重要。

检索前,先把用户查询走一遍摄取管线中的部分步骤:

  1. 预处理(Pre-Process)
  2. 生成稠密与稀疏向量
  3. 检索(Search)

检索时应返回多个块,因为可能有多个块匹配查询------这通常叫 "Top k" 结果。Top k 取 5--10 通常够用,取决于数据和切块策略。块越小,通常需要越大的 Top k

这套做法不错但并非万无一失。混合检索不完美,尤其当用户查询很短时。可以在检索之后叠加一些技术来提升召回率和精度。

相邻块(Adjacent Chunks)

我们是按章节抽取并在章节内切块的,这意味着检索结果的相邻块极可能也相关(如果它们没被检索直接返回的话)。

推荐的第一个后续步骤是用相邻块 丰富检索结果。一般取检索块前后 1--2 个索引 内的块效果好。这正是为什么要把文档名、块索引、块总数、章节路径存为元数据------以便高效查询相邻块。

如果向量库支持索引(Qdrant 支持),应在这些字段上建索引,以高效查询相邻块。

重排序(Reranking)

检索常会取到完全不相关的块,关键词检索尤其严重。重排序 可按相关性对结果排序并过滤掉不相关的块。一个强力技巧:检索时返回 Top k 的 2--3 倍 结果,再用重排序器筛到最相关的 Top k。

示例(Top k = 5):

  1. 混合检索返回 Top k × 2 = 10 块
  2. 加入相邻块 → 18 块
  3. 重排序器按相关性排序并返回 Top k → 5 块

这种方式能显著提升检索精度。

重排序器是接受一对文本 作为输入、输出相关性分数 的专用模型。把每个检索结果连同用户查询喂给重排序器即可。还可以用相关性分数设定一个最低相关度阈值,而不是只取 Top k。

交叉编码器(Cross Encoders) :专为相关性打分训练的机器学习模型,处理文本比 LLM 快得多。更高质量的重排序器在生产系统中通常仍需 GPU。本例用免费开源的 BAAI/bge-reranker-base,表现不错且在 CPU 上够快,同样通过 Python FastAPI 托管 Hugging Face 模型。

LLM 重排序(LLM Reranking) :目前云托管的重排序模型不多,另一种方式是用 LLM 配合定制的系统消息(System message)来重排序。建议用轻量 LLM(如 GPT-4.1 Mini),以免显著拖慢 RAG 系统的延迟。原文为此提供了一个可用于 LLM 重排序的系统消息模板(见原作者 GitHub 仓库)。

生成(Generation)

检索搞定后,剩下的就是把块送进 LLM 做总结。

上下文格式(Context Format) :如果 LLM 支持,注入块的最佳方式是作为工具消息(Tool message)。在系统消息中加入 LLM 应如何解释检索结果、以及找不到匹配时如何反应的规则与护栏。

清晰的角色划分:

  • 系统消息(System message)→ 规则、行为、护栏
  • 工具消息(Tool message)→ 检索到的上下文(块)
  • 用户消息(User message)→ 问题

工具消息中要包含你希望 LLM 引用的任何元数据,连同块内容一起。用户消息示例:"Pod 是如何被调度的?" (原文给出了 LLM 重排序系统消息、RAG 系统消息、RAG 工具上下文 JSON 三个模板片段,完整内容见原作者 GitHub 仓库。)

编排(Orchestration)

更易管理的方式是用编排器(Orchestrator)来生成工具调用和输出,例如 Semantic KernelLangChain。用编排器还有两个额外好处:

  1. 发往 RAG 系统的查询先由 LLM 解释,可在需要时被改进/纠正;
  2. 知识库可作为更大的 AI 智能体/副驾的一部分被使用。

用编排器时,你只需关心系统消息和用户消息。编排器负责在需要时调用 RAG 检索,并把数据作为工具消息(通常是 JSON)加入上下文。

示例应用中,作者用 Semantic Kernel 插件集成了 OpenAI 风格的工具调用,以支持基于提示的索引和 RAG 查询。

LLM 模型选择

  • 如果系统只有 RAG 这一个功能,便宜的 LLM 就够用------只需对检索到的文档做基本总结。
  • 如果用编排器,则需要支持工具调用 的模型。作者通过 Ollama 使用了 Llama 3.2 3B(小语言模型 SLM),以便在 CPU 上运行;但更大的模型会可靠得多。

五、托管(Hosting)

作者把 Semantic Kernel RAG 助手打包成了 CLI 控制台应用,简单场景够用。若要面向更广泛的用户,最好在上面写个小的 Web UI(如 React 或 Angular)。

每个服务(重排序器、嵌入器等)作为独立进程运行。独立托管每个服务可以独立扩缩,并为各服务使用不同的基础设施或资源配置。例如重排序器可能很吃资源,需要扩容到大量副本;而 LLM 很可能需要在 GPU 基础设施上运行。

容器非常适合这类进程------把各服务隔离打包成容器镜像,在不同托管环境中获得可靠运行。推荐用容器编排器以便快速扩缩,Kubernetes 就很理想

自托管 vs 云服务

示例方案全用开源技术拼成,因此你可以全部自托管,或在 CPU 上本地运行。但作者一般不推荐走纯自托管路线,除非你公司有重大数据风险且不接受云服务。要注意,大多数云服务(如 Azure OpenAI)面向企业用途且完全无状态------即不存储你提示或回答中的任何数据。

混合方式也可行:把较轻量的模型自托管。嵌入和重排序模型通常不像 LLM 那么吃算力。理想情况下,生成用的 LLM 需要 GPU 才能可靠运行,而在这方面云服务通常比自建基础设施便宜得多。

市面上有很多通用的开箱即用 RAG 方案,但自建架构能让一切都贴合你特定的数据需求,往往带来更优的召回率和精度。即便你决定用外部 RAG 服务,这里讨论的很多技术(不同切块策略、重排序等)也能与之协同使用。

完整代码见原作者 GitHub:matt-bentley/LLM-RAG-Architecture


关键参数速查

决策点 推荐值 / 选择
检索方式 混合检索(Hybrid Search)
混合权重 稠密 70% / 稀疏 30%
结果融合 倒数排名融合(RRF)
向量数据库 Qdrant(备选:Weaviate / Pinecone / Milvus)
块长度 一般 200--300 词元;复杂文档可到 600;K8s 案例用 400
块重叠 10--20%;K8s 案例用 50 词元
嵌入模型 BAAI/bge-small-en-v1.5(384 维,CPU 友好)
稀疏向量词项 ID 偏好哈希(如 xxHash),免维护词表
Top k 5--10(块越小,k 越大)
重排序召回 检索 Top k 的 2--3 倍,再筛到 Top k
重排序模型 BAAI/bge-reranker-base(交叉编码器)或轻量 LLM(如 GPT-4.1 Mini)
相邻块扩展 前后 1--2 个索引
编排器 Semantic Kernel / LangChain
生成 LLM 纯 RAG 用便宜模型;需工具调用用 Llama 3.2 3B 等
部署 各服务独立容器化,用 Kubernetes 编排
相关推荐
怕浪猫7 小时前
领域特定语言(Domain-Specific Language, DSL)
设计模式·程序员·架构
怕浪猫7 小时前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
Jack2014 小时前
HarmonyOS APP事件驱动大揭秘
架构
Colin草率地做慢慢地改14 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构
candyTong1 天前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
唐某人丶1 天前
从画架构图开始:架构分析与进阶指南
架构
只会cv的前端攻城狮2 天前
DSL 领域模型架构设计:消灭 CRUD 重复工作
前端·架构
禅思院3 天前
路由性能优化终极指南:从懒加载漏洞到边缘渲染的架构跃迁
前端·架构·前端框架