RAG(Retrieval-Augmented Generation),中文通常译作“检索增强生成”。
在 LLM 的应用场景中,企业知识库、客服机器人、文档助手等产品背后,基本都能看到 RAG 技术的影子。
为什么需要 RAG
现如今的 LLM 已经非常强大了,为什么还需要 RAG?
LLM 的本质是在预测下一个 Token,这会带来三个常见问题:
- LLM 幻觉:当训练数据中没有答案时,模型未必会直接说“不知道”,而是可能继续预测下一个 Token,直到输出一个看起来合理、但实际并不可靠的答案。
- 知识过时:模型训练数据通常停留在某个时间点。对于训练截止时间之后发生的事情,模型本身并不了解。
- 无法访问私有数据:企业内部数据通常不会公开在公网上,因此模型的训练数据里也不会包含这些知识。此时就需要我们主动把相关内容提供给 LLM,让它基于这些内容进行理解和生成。
这时候我们很容易想到一个方案:直接把整个知识库文档丢给 LLM,让它从里面找我们想要的知识,不就可以了吗?
但这样做又会引出新的问题:
- LLM 的上下文是有限的。当输入内容超出上下文限制时,部分内容可能会被丢弃或压缩,从而丢失关键信息,导致模型给出错误答案。
- 过大的上下文会显著增加每次对话的成本。每次向 LLM 发起请求时,都需要把相关上下文一起提交,如果其中混入大量无关信息,就会造成金钱和性能上的浪费。
- 回复速度会变慢。如果每次提问都把整个知识库塞进去,LLM 就需要完整读取大量内容后才能回答问题,用户可能要盯着白屏等待很久,体验非常差。
所以,更好的方式是:先在知识库中找到最相关的几页或几段内容,再把这些内容交给 LLM。
这就像开卷考试。我们不可能每次都从头到尾翻完整本书再找答案,通常会先定位到大概章节和页码,再从相关内容中找到正确答案。
因此,RAG 的核心思路就是:先检索,再把检索出来的内容交给 LLM 生成答案。这也是“检索增强生成”最直观的含义。
RAG 和核心流程
第一阶段:数据准备
假设我们要给企业做一个问答知识库。
-
企业内部文档通常有各种各样的格式,比如 PDF、Word、Excel、PPT、Markdown 等。这些文档里可能混杂着大量无关内容,看起来比较“脏”。对于这类数据,我们通常需要先进行数据清洗,去掉其中无用的信息。
-
对文档进行切分(chunking),按照章节、段落、字数等规则,把文档切成一个个小块(chunk)。每个 chunk 应尽量表达一个相对完整的语义。比如“张三是一个好男人”,如果切成“张三”和“是一个好男人”,就会破坏原句语义:我们既不知道“是一个好男人”指的是谁,也无法完整理解这句话想表达什么。
具体使用哪种切分方式,需要根据实际场景选择。
为什么需要切分?如果直接按整个文档处理,后续召回的也会是一整篇文档,无法定位到具体信息,RAG 的价值就会大打折扣。
- 由于计算机无法直接理解文字,它更擅长处理数字,因此我们需要对这些 chunk 进行向量化(embedding)。向量化可以简单理解为:通过 embedding 模型,把一句话转换成语义空间中的一组坐标点。
举个例子:
- “张三是男的”,转成坐标点就是(1,1)
- “张三三十岁”,转成坐标点就是(2,1)
- “今天天气很好”,转成坐标点就是(-2,-1)
在这个空间中,语义相近的句子坐标会更接近。比如“张三是男的”和“张三三十岁”都是在描述张三这个人,它们的坐标就会更接近。
在真实的 embedding 模型里,坐标并不是这种二维坐标,而是上百甚至几千维。它们在现实中很难画出来,但在数学上是存在的。
将每一个 chunk 都通过 embedding 模型转换成向量后,我们会把文本内容和对应向量一起存入向量数据库中。一个简化后的例子如下:
| 向量坐标 | 文本 |
|---|---|
| (1,1) | 张三是男的 |
| (2,1) | 张三三十岁 |
| (-2,-1) | 今天天气很好 |
| … | … |
第二阶段:用户提问
用户提出一个问题:张三是谁?
在非 RAG 场景下,LLM 可能会根据训练数据编造出一个并不存在的“张三”。
在 RAG 场景下,当 LLM 识别到需要查询外部知识时,会调用 RAG 工具。
关于 LLM 如何调用工具,请看下集。
在 RAG 工具内部,会先对用户问题进行向量化(embedding)。例如用户提问“张三是谁?”,这个问题可能会被向量化成(1.5,0.5),可以参考上图中的橙色坐标点。
随后,问题向量会与向量数据库中的向量坐标进行相似度计算。常见的计算方式有余弦相似度和欧式距离。
余弦相似度:从(0,0)开始,计算问题向量与其他向量之间的夹角余弦值,例如:
欧式距离:直接计算问题坐标和其他坐标之间的直线距离。
计算完成后,系统会返回最相关的 Top K 个 chunk。K 是一个可配置的数量,可以是 1,也可以是 100。假设这里的 K 是 10,那么系统就会召回 10 个最相关的片段。
需要注意的是,到这里为止只是粗筛。向量检索只能判断召回片段和问题“像不像”,但不能保证这 10 个片段真的回答了这个问题。
因此,这里通常还需要进行重排序(rerank)。重排序会使用更精确的方法,逐一对比用户问题和召回片段,判断这些片段是否真的能回答问题。
经过重排序后,我们可能得到 3 个与用户问题最相关的片段。接下来,就可以把这 3 个片段、用户问题、基础提示词(prompt)等组合成一个新的提示词,交给 LLM 生成答案。
例如:
Answer the following question using only the provided document excerpts.
Question: <用户的问题>
Document excerpts:
1. <重排后的片段 1>
2. <重排后的片段 2>
3. <重排后的片段 3>
Provide a clear, concise answer. Cite excerpt numbers like [1] when referencing sources.
需要注意的是,上面提到的用户问题向量化、查询向量数据库、召回、重排序等步骤,都发生在被调用的 RAG 工具内部。对用户来说,这些过程是透明的。
用户眼中只有:发送问题 -> 获得答案。
至此可以发现,RAG 并没有改变 LLM 本身,而是给 LLM 增加了一个可以查询外部数据的工具。
整个系统分工明确:
- embedding:负责向量化
- 向量搜索:速度快,负责粗筛
- rerank:负责精排和打分,速度相对较慢
- LLM:负责生成最终答案
问题和优化技巧
- 以企业知识库为例,企业私有文档格式复杂,其中往往包含大量无用噪声文本。在对知识库进行 embedding 之前,需要投入大量精力做数据清洗,包括但不限于 OCR、正则匹配、文本重写等技术或非技术手段。
- chunk 的切分大小非常关键。如果 chunk 太大,一个 chunk 中会包含过多内容,导致检索不准确,也可能分散 LLM 的注意力;如果 chunk 太小,又容易造成语义断裂。比如“张三是个好人”被切成“张三”和“是个好人”,就会丢失主语和描述之间的关系。我们需要尽量保证每个 chunk 的语义相对完整,让信息颗粒度刚好适合当前场景。
- 用户提问通常比较口语化,因此可以对输入问题进行 Query Rewrite(问题重写)。比如用户问“张三是谁”,可以重写为“列出张三的性别、年龄、身高、视力、评价、人物关系等信息”。这样做的核心目的是让后续向量搜索更容易召回相关内容。
- RAG 的检索过程也可以继续优化,常见方式是混合检索(关键词检索 + 向量相似度检索)。关键词检索类似查字典,可以根据关键词进行精确匹配;向量检索更擅长召回语义相近的内容。两者结合可以互补盲区。
- 接入 trace,并提供反馈功能。如果用户对某个回答不满意,可以通过反馈按钮提交当前对话的 id。我们就可以根据 trace 追踪问题到底出在 embedding、召回、rerank,还是 LLM 生成阶段,从而有针对性地优化整个流程。
- 模型基座的选择也很重要,通常需要关注:
- 幻觉是否可控
- 输出是否稳定
- 是否能严格遵循指令
- 是否开源,是否方便私有化部署