RAG 的基本原理

Caret Up

RAG(Retrieval-Augmented Generation),中文通常译作“检索增强生成”。

在 LLM 的应用场景中,企业知识库、客服机器人、文档助手等产品背后,基本都能看到 RAG 技术的影子。

为什么需要 RAG

现如今的 LLM 已经非常强大了,为什么还需要 RAG?

LLM 的本质是在预测下一个 Token,这会带来三个常见问题:

  1. LLM 幻觉:当训练数据中没有答案时,模型未必会直接说“不知道”,而是可能继续预测下一个 Token,直到输出一个看起来合理、但实际并不可靠的答案。
  2. 知识过时:模型训练数据通常停留在某个时间点。对于训练截止时间之后发生的事情,模型本身并不了解。
  3. 无法访问私有数据:企业内部数据通常不会公开在公网上,因此模型的训练数据里也不会包含这些知识。此时就需要我们主动把相关内容提供给 LLM,让它基于这些内容进行理解和生成。

这时候我们很容易想到一个方案:直接把整个知识库文档丢给 LLM,让它从里面找我们想要的知识,不就可以了吗?

但这样做又会引出新的问题:

  1. LLM 的上下文是有限的。当输入内容超出上下文限制时,部分内容可能会被丢弃或压缩,从而丢失关键信息,导致模型给出错误答案。
  2. 过大的上下文会显著增加每次对话的成本。每次向 LLM 发起请求时,都需要把相关上下文一起提交,如果其中混入大量无关信息,就会造成金钱和性能上的浪费。
  3. 回复速度会变慢。如果每次提问都把整个知识库塞进去,LLM 就需要完整读取大量内容后才能回答问题,用户可能要盯着白屏等待很久,体验非常差。

所以,更好的方式是:先在知识库中找到最相关的几页或几段内容,再把这些内容交给 LLM。

这就像开卷考试。我们不可能每次都从头到尾翻完整本书再找答案,通常会先定位到大概章节和页码,再从相关内容中找到正确答案。

因此,RAG 的核心思路就是:先检索,再把检索出来的内容交给 LLM 生成答案。这也是“检索增强生成”最直观的含义。

RAG 和核心流程

第一阶段:数据准备

假设我们要给企业做一个问答知识库。

  1. 企业内部文档通常有各种各样的格式,比如 PDF、Word、Excel、PPT、Markdown 等。这些文档里可能混杂着大量无关内容,看起来比较“脏”。对于这类数据,我们通常需要先进行数据清洗,去掉其中无用的信息。

  2. 对文档进行切分(chunking),按照章节、段落、字数等规则,把文档切成一个个小块(chunk)。每个 chunk 应尽量表达一个相对完整的语义。比如“张三是一个好男人”,如果切成“张三”和“是一个好男人”,就会破坏原句语义:我们既不知道“是一个好男人”指的是谁,也无法完整理解这句话想表达什么。

具体使用哪种切分方式,需要根据实际场景选择。

为什么需要切分?如果直接按整个文档处理,后续召回的也会是一整篇文档,无法定位到具体信息,RAG 的价值就会大打折扣。

  1. 由于计算机无法直接理解文字,它更擅长处理数字,因此我们需要对这些 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:负责生成最终答案

问题和优化技巧

  1. 以企业知识库为例,企业私有文档格式复杂,其中往往包含大量无用噪声文本。在对知识库进行 embedding 之前,需要投入大量精力做数据清洗,包括但不限于 OCR、正则匹配、文本重写等技术或非技术手段。
  2. chunk 的切分大小非常关键。如果 chunk 太大,一个 chunk 中会包含过多内容,导致检索不准确,也可能分散 LLM 的注意力;如果 chunk 太小,又容易造成语义断裂。比如“张三是个好人”被切成“张三”和“是个好人”,就会丢失主语和描述之间的关系。我们需要尽量保证每个 chunk 的语义相对完整,让信息颗粒度刚好适合当前场景。
  3. 用户提问通常比较口语化,因此可以对输入问题进行 Query Rewrite(问题重写)。比如用户问“张三是谁”,可以重写为“列出张三的性别、年龄、身高、视力、评价、人物关系等信息”。这样做的核心目的是让后续向量搜索更容易召回相关内容。
  4. RAG 的检索过程也可以继续优化,常见方式是混合检索(关键词检索 + 向量相似度检索)。关键词检索类似查字典,可以根据关键词进行精确匹配;向量检索更擅长召回语义相近的内容。两者结合可以互补盲区。
  5. 接入 trace,并提供反馈功能。如果用户对某个回答不满意,可以通过反馈按钮提交当前对话的 id。我们就可以根据 trace 追踪问题到底出在 embedding、召回、rerank,还是 LLM 生成阶段,从而有针对性地优化整个流程。
  6. 模型基座的选择也很重要,通常需要关注:
    1. 幻觉是否可控
    2. 输出是否稳定
    3. 是否能严格遵循指令
    4. 是否开源,是否方便私有化部署