深入讲解RAG核心工程决策链:文档处理、表示、检索、排序、上下文、生成、评估、安全、延迟与成本。
大型语言模型在生成回答方面表现出色。让它解释一个复杂的概念、总结一份文档、编写代码或推理一个问题,它往往能在几秒钟内完成。
但在这种流畅性的背后隐藏着一个根本性的局限性:模型的知识与你此刻需要访问的信息并不相同。
模型可能对一个主题了解很多,却对你公司的最新政策、昨天的支持工单、本月的内部报告或最新版本的法规一无所知。当所需信息缺失时,它也可能给出一个自信满满的答案。
检索增强生成(Retrieval-Augmented Generation,简称 RAG)是解决这一问题最实用的方法之一。
其核心思想很简单:
不要让模型记住一切,而是在它需要回答问题的时刻,把相关信息提供给它。
这句话看似简单,实则暗藏玄机。一个生产级别的 RAG 系统,涉及一系列关于文档、表示、搜索、排序、上下文、生成、评估、安全、延迟和成本的决策。大多数有意思的工程工作都发生在这些决策之中。
本文从零开始构建 RAG,然后顺着系统探究当一个简单的原型变成真实应用时会遇到哪些问题。
想象一位博学的顾问,他读过大量的书籍、论文、手册和网站。他能够极好地进行推理、解释复杂概念、快速综合信息。
但有一个问题:他在某个特定日期之后就被关在了一个房间里。
他不知道之后发生了什么。
还有一个问题。当他不知道某件事时,他可能仍然会试图给你一个答案。因为他被优化为生成合理的语言,即使错了,听起来也可能令人信服。
这是理解 LLM 的一个有用的心智模型。
现在想象给这位顾问配一个研究助手。助手可以搜索档案柜,找到相关页面,在顾问回答之前把它们放到顾问的桌上。
顾问仍然负责推理。助手提供证据。
这就是 RAG 的核心思想。
检索找到相关的外部信息。增强(Augmentation)将那些信息放入模型的上下文。生成(Generation)利用该上下文产生回答。
模型的推理能力与其外部知识已经被分离了。
RAG 将模型能够推理的内容与它当前能够访问的内容解耦了。
这个区别比记住某个特定的 RAG 框架或向量数据库更重要。

一个常见的错误是把 RAG 和微调当作做同一件事的竞争关系。
RAG 主要是一种在推理时为模型提供信息访问能力的方式。微调则改变模型的学习行为。更大的上下文窗口给模型更多的接收信息空间,但本身并不创建一个检索机制。
一个有用的简写是:
微调教模型如何行为;RAG 给模型需要知道的内容。
在实践中,这两种技术可以结合使用。一个专业化模型可以通过微调学习领域词汇、格式或沟通风格,同时由 RAG 提供当前的私有知识。
长上下文再次改变了权衡。如果一个任务所需的完整信息足够小,可以轻松放入上下文,那么检索可能就不必要了。当语料库庞大、动态变化、私有或每次提示都塞入成本高昂时,RAG 就变得更加有用。
没有放之四海而皆准的赢家。选择取决于应用的信息特性、延迟、成本、更新频率和可靠性要求。
最简单的理解,RAG 可以看作四个阶段:
Index → Retrieve → Augment → Generate
第一个阶段准备知识库。其他阶段在用户提问时发生。

在任何入提问之前,系统需要将其源材料转化为可搜索的形式。
一个典型的索引流程如下:
Load → Parse → Chunk → Embed → Store
数据源可能是几乎任何东西:
代码或技术文档
重要的一点是,RAG 并不要求知识原本就是整齐的结构化文本。
原始文档很少能直接用于检索。
PDF 可能每页都重复页眉和页脚。网页可能包含导航菜单。表格可能以错误的顺序被提取。OCR 可能引入错误。
如果解析器把一份有用的文档变成了糟糕的文本,后续每个阶段都会继承这种损害。
这是 RAG 中最不光鲜的部分之一,却也是最有影响力的:
垃圾进,检索出来仍是垃圾。
数据摄取还需要处理一些实际问题,例如去重、删除、文档版本、权限,以及嵌入模型发生变化时会发生什么。这些并非独立于 RAG 质量之外,而是 RAG 质量的一部分。
一份 200 页的文档很少是一个有用的检索单元。
相反,它会被分割成更小的段落,称为 chunk(块)。
目标是找到一个有用的平衡点。
太小的块可能会丢失理解一个陈述所需的上下文。太长的块包含太多不相关的信息,会成为更不精确的检索单元。
一个常见的起点是几百个 token 并带有一些重叠,但没有放之四海而皆准的块大小。法律合同、支持工单、代码文件和研究论文可能都需要不同的结构。
重叠的存在有一个简单的原因:边界是人为的。
如果一个句子从一块的末尾开始,在另一块的开头结束,那么单独检索其中任何一块都可能让模型得到一个不完整的想法。
更高级的方法包括父子检索(parent-child retrieval),其中小块(child chunk)被索引用于精确检索,但在找到匹配后,向模型提供的是更大的父段落(parent passage)。
重要的原则是:
为检索而分块,而不仅仅是为存储而分块。
一旦块存在,系统需要一个用数值表示其含义的方式。
嵌入(embedding)是一个向量:由嵌入模型生成的一串数字。语义相关的文本在嵌入空间中通常位于相近的区域。
例如,用户可能会问:
"我可以居家办公吗?"
而文档说的是:
"员工每周最多可以远程工作三天。"
措辞不同,但含义相关。密集语义检索可以识别这种关系。
嵌入模型与生成式 LLM 并不是同一个东西。嵌入模型为搜索创建表示。LLM 读取上下文并生成语言。
由此得出一个重要的工程规则:
查询和被索引的文档需要兼容的表示。
如果文档使用一个模型嵌入,而查询使用不兼容的模型嵌入,向量之间不一定能进行有意义的比较。
密集嵌入在语义相似性方面表现出色,但它们并非唯一有用的表示。
像 BM25 这样的稀疏检索方法与文本中出现的术语紧密相关。这使得它们在精确名称、产品代码、错误信息、稀有技术标识符以及其他字面词语重要的场景中特别有用。
因此,密集检索和稀疏检索具有互补的优势。
这就是生产系统通常将两者结合使用的原因。
一旦块有了嵌入,它们需要存在于一个能够高效搜索的地方。
向量数据库或向量索引将向量与原始文本以及有用的元数据一起存储,例如:
当用户提问时,查询被嵌入到相同的表示空间中,并与存储的向量进行比较。
一种常见的相似度度量是余弦相似度(cosine similarity),它关注向量之间的角度。点积(dot product)是另一个常见选项,而 L2 距离则直接度量几何距离。归一化很重要,因为它改变了这些度量之间的关系。
重要的思想不在于记住某个公式,而在于理解系统在做什么:
它试图在选定的相似度函数下,找到与查询接近的存储表示。
随着语料库的增长,精确搜索每个向量变得代价高昂。近似最近邻(ANN)方法用一定的精确度换取更快的搜索速度。
关键词是"近似"。精确最近邻搜索要求系统将查询与每个候选项进行比较,并找出真正的最近向量。在大规模场景下,这种方式可能变得过于昂贵。ANN 方法转而组织搜索空间,使系统只需探索一小部分有希望的候选。
HNSW(Hierarchical Navigable Small World,分层可导航小世界)是一种流行的基于图的 ANN 方法。
一个有用的直觉是一张有多层级的城市地图。上层包含少量长距离连接,让你快速穿越地图。下层包含越来越精细的局部连接。
查询从高层开始,向有潜力的区域移动,然后逐层下降,直到导航到包含最近候选的详细邻域。
结果是系统不需要检查每个存储的向量。
HNSW 还通过 ef_search 等参数暴露了一个重要的工程权衡。更大的搜索努力意味着算法探索更多候选节点。这可以提高召回率,但也会增加搜索工作量和延迟。更小的值可以使搜索更快,但增加错过真正好邻居的概率。
因此,即使在同一个向量数据库中,ANN 质量也不是简单的"开"或"关"。你是在选择速度与召回率曲线上所处的位置。
另一种常见方法是倒排文件索引(IVF)。
IVF 不是将整个向量空间作为一个巨大的搜索区域,而是将向量划分到聚类中。在索引期间,每个向量被分配到适当的聚类。
在查询时,系统首先确定哪些聚类最接近查询,然后只搜索那些区域。
重要的调参概念是 nprobe:查询应该检查多少个聚类?
低的 nprobe 意味着更少的工作和更低的延迟,但可能错过位于搜索从未检查过的聚类中的相关向量。更高的 nprobe 搜索更多区域,可以提高召回率,但代价是额外的计算。
因此 HNSW 和 IVF 使用不同的结构来做相同的大致权衡:在可接受地保持错过有用邻居的概率低的同时,避免穷举搜索。
在大规模时还有一个问题:内存。
大型语料库可能包含数亿个向量,每个向量有数百或数千个维度。以完整精度保存每个向量可能变得昂贵。
产品量化(PQ)将向量压缩成更小的表示。系统不再直接存储每个原始值,而是用紧凑码表示向量的各个部分。
好处是大幅降低内存使用,并可能加快搜索。代价是压缩引入了近似,这可能降低检索准确性。
这是同一个 RAG 工程原则的又一个例子:你是在用一种资源换取另一种。
将 ANN 简单地描述为"不太准确的搜索"很诱人。这太简化了。
真正的问题是,随着语料库增长,在固定的延迟和内存预算下保持非常高的召回率变得越来越难。系统有更多的可能邻居要考虑,有更多的索引结构要维护,而且有更大的压力来限制每个查询执行的工作量。
这就是为什么 ANN 系统暴露调参参数,以及为什么检索应该通过实证评估。
在实践中,你关心的问题包括:
因此有用的心智模型不是"ANN 是一种近似"。
ANN 是搜索成本与找到最佳邻居的概率之间的受控权衡。
其他 ANN 方法包括倒排文件方法和向量压缩技术(如产品量化)。具体选择取决于语料库大小、延迟要求、可用的内存以及你需要的召回率。

元数据过滤是另一个重要能力。你可能只希望在法律部门的文档中进行语义相似性搜索,或者只在新于某个日期的文档中搜索,或者只搜索当前用户有权查看的文档。
最后一个要求在多租户和权限敏感系统中变得至关重要。一个找到正确答案但暴露了用户无权查看的信息的检索系统,仍然是一个有缺陷的系统。
一个基本的 RAG 原型可能会嵌入查询、执行相似性搜索、取前五个块,然后将它们发送给 LLM。
有时这很有效。
有时它会以难以诊断的方式失败。
有几个反复出现的原因。
用户可能说"heart attack",而文档说"myocardial infarction"(心肌梗塞)。
语义检索可以帮助弥合这一差距,但没有检索方法是完美的。
现在假设查询包含产品 ID(如 XR-4729)、法律条款编号或特定错误代码。
语义相似性不一定是处理这些的最佳工具。
这正是稀疏关键词检索非常有价值的地方。
用户查询可能是六个词,而检索到的块可能有几百个词。比较这两者并不完全等同于比较两个大小相似的文档。
前十个结果可能都在说本质上相同的事情。
向 LLM 发送十个版本的同一个想法并不一定会使答案更好。
一个常见的生产基线是混合搜索。
并行运行密集语义搜索和稀疏词法搜索,然后合并它们的排名。
BM25 是一种经典的稀疏检索方法。它奖励有用的词匹配,同时考虑词在整个集合中的常见程度。
密集检索贡献语义匹配。
BM25 贡献精确的词法匹配。
结果可以使用互惠排名融合(RRF)等排名方法合并,文档根据其在每个排名列表中出现的位置获得贡献。
重要的洞察不是公式本身,而是两个不完美的检索机制可以互补。
用户原始问题并不总是最佳搜索查询。
一个会话式问题如:
"What about the policy for contractors?"(承包商的政策是什么?)
可能依赖于前面的几个轮次。
查询重写步骤可以将会话语言转换为独立的检索查询。
更复杂的问题也可以分解。
假设有人问:
"How did our Q3 revenue compare with the previous quarter, and what were the main reasons for the change?"(我们第三季度的收入与上一季度相比如何,变化的主要原因是什么?)
这可能需要对数字和解释分别进行独立检索。
系统可以将整个问题作为一个搜索查询来处理,也可以创建子查询、检索每个的证据,然后合并结果。
这增加了复杂性,有时还有额外的模型调用,所以应该只在查询确实从中受益时使用。
HyDE(Hypothetical Document Embeddings,假设文档嵌入)采用了另一种方法。
系统不是直接嵌入用户简短的问题,而是首先让 LLM 生成一个假设的答案或文档。然后嵌入该假设文本并用于检索。
假设文本不一定要事实正确。它的目的是提供一个更丰富的相关文档可能看起来像什么的表示。
权衡很简单:另一个模型调用意味着额外的延迟和成本。
多查询检索在概念上更简单。系统生成同一问题的几个替代表述,对每个进行检索,合并和去重结果,然后对候选进行排名。
当原始查询是对系统实际需要的信息的不良表示时,这些技术很有用。
它们不是每个 RAG 系统的必备成分。
实用 RAG 中最有用的模式之一是将候选生成与精确排名分开。
第一个检索阶段应该很快。它可能返回 20、30 或 50 个候选。
然后更昂贵的交叉编码器重排序器将查询和每个候选一起检查,并评分它们的匹配程度。
这是因为两个阶段有不同的职责。
双编码器或向量搜索系统之所以高效,是因为文档表示可以预先计算。
交叉编码器可以做出更详细的查询-文档比较,但针对数百万个文本块这样做成本太高。
因此架构变成:
大规模语料库 → 快速检索 → 候选集缩小 → 昂贵的重排序 → 最终小上下文
这是 RAG 工程中最重要的模式之一。

多样性同样重要
有时排名最高的五个文本块都在重复相同的信息。
最大边际相关性(MMR)提供了一种在相关性与多样性之间取得平衡的方式。
从概念上讲,MMR 奖励与查询相关且已被选中文本块相似度较低的文本块。
结果是最终上下文包含几条互补的证据,而不是五个近乎重复的内容。
更重要的教训是:
检索不是要找到最相似的文本。而是要构建最有用的证据集。
生成阶段:上下文是证据,而非装饰
一旦系统检索到证据,它需要组装 LLM 将看到的提示词。
一个有效的 RAG 提示词通常包含:
清晰的系统指令
带标签的来源文本块
必要时定义输出格式
结构很重要,因为现在要求模型基于证据进行推理。
一条好的指令可以说:
仅使用提供的上下文回答。如果答案不被上下文支持,说明信息不足。不要超出来源进行推测。
这并不会神奇地消除幻觉。但它给模型一个明确的操作规则。
更多上下文并不自动意味着更好
很容易认为:
"如果五个文本块有用,二十个一定更好。"
通常,事情并没有那么简单。
每个额外的文本块都会增加 token 使用量和延迟。不相关的文本块可能引入噪声。相互冲突的文本块可能使模型产生不确定感。长上下文可能导致注意力问题。
这与中间丢失现象有关:当相关信息埋在一长串上下文中间时,模型可能更有效地使用开头和结尾的信息,而不是中间的信息。
因此一个实用策略是:
检索更多,发送更少。
检索广泛的候选集。对其进行重排序。选择一个小而高质量的上下文。

RAG 的实际优势之一是系统知道它检索了哪些来源。
引用可以以内联方式呈现:
员工每周最多可以远程工作三天 [来源 1]。
来源:HR 政策,第 4 页;经理手册,第 12 页。
在生产环境中,结构化的引用数据甚至更有用。系统可以返回答案以及来源 ID、页码、声明和链接,以便界面可以使证据可点击和可审计。
当答案不在文档中时会发生什么?
一个可信的 RAG 系统需要对此问题有明确的答案。
如果检索器没有找到相关内容,模型不应该觉得有义务编造答案。
一个简单的设计是将基础指令与检索阈值相结合。如果检索到的候选内容都没有通过适当的相关性阈值,系统可以拒绝从知识库回答。
确切的阈值是应用特定的。来自一个嵌入系统的分数不能自动被视为通用的相关性度量。
更深层的原则更重要:
一个好的 RAG 系统必须有优雅的失败模式。
"我在检索到的来源中没有足够的信息"往往比一个自信的虚构答案要好得多。
对话式 RAG:查询并不总是查询
聊天使检索变得更难。
"退款政策是什么?"
"数字产品呢?"
"如果我用礼品卡呢?"
最后一条消息不是一个完整的检索查询。
一个常见的解决方案是查询压缩:在检索前使用对话历史将最新消息重写为一个独立问题。
"用礼品卡购买的数字产品的退款政策是什么?"
现在检索系统有了有效搜索所需的信息。
这增加了另一个处理步骤,但它解决了人类对话方式与检索系统搜索方式之间的根本不匹配。
思考 RAG 最有用的方式之一是:一个糟糕的答案并不一定意味着 LLM 是问题。
失败可能发生在多个层面:
摄取失败 — 相关信息被错误解析、遗漏、重复或索引错误。
分块失败 — 信息存在,但被分割成破坏有用上下文的检索单元。
检索失败 — 正确的信息存在于索引中但从未被检索到。
排序失败 — 正确的信息被检索到了,但埋在了用处较小的候选内容下面。
上下文组装失败 — 找到了正确的证据但排序不佳、过长或与冲突内容混合。
生成失败 — 模型收到了有用的证据但未能遵循它或正确回答问题。
这给你一个实用的调试顺序:
检查来源 → 检查检索到的证据 → 检查排序 → 检查组装的上下文 → 检查生成。
当真正的问题是检索时,不要立即更换 LLM。
RAG 系统可能产生听起来很优秀但底层存在问题的答案。
评估需要将管道分离成不同的部分来验证。