跳过正文
  1. 所有文章/

理解 RAG:智能客服背后的检索增强生成

Aaron
作者
Aaron
I only know that I know nothing.
目录

前言
#

公司最近在筹划做智能客服,调研了一圈发现 RAG 这个词出现的频率非常高。企业知识助手、文档问答、智能客服,几乎都绕不开它。既然要用,就得先搞懂底层原理,于是我花时间把 RAG 的完整链路梳理了一遍,从数据准备到最终生成答案,涉及分片、Embedding、向量数据库、召回、重排等环节。这篇文章记录一下我的理解,希望对你也有帮助。

为什么需要 RAG
#

假设你想做一个智能客服,让它回答公司产品相关的问题。最直觉的做法是选一个大模型,然后把产品手册连同用户问题一起发给它。

乍一看没问题,但实际落地时会遇到几个硬伤:

  1. 上下文窗口有限:每个模型能接收的输入长度是有上限的。产品手册动辄上百页,塞不进去就是塞不进去,强行塞只会导致模型读了后面忘了前面,回答质量直线下降。
  2. 推理成本高:输入越长,token 消耗越多。每次提问都带上一整本手册,一天下来账单会很壮观。
  3. 推理速度慢:输入越多,模型需要消化的内容越多,用户等一个回答可能要等半天。

核心矛盾在于:我们希望模型看到所有相关信息,但又不能把所有信息都塞给它。那怎么办?

RAG 的思路是:与其把整个文档都发给模型,不如只发和问题相关的部分。具体来说,先把文档切成多个片段,在用户提问后,从中检索出最相关的几段,再把这些片段连同问题一起交给大模型来生成答案。

RAG 的整体流程
#

RAG 的流程分为两大部分:

  • 数据准备阶段(用户提问前):包含分片(Chunking)和索引(Indexing)两个环节。
  • 回答阶段(用户提问后):包含召回(Retrieval)、重排(Reranking)和生成(Generation)三个环节。

下面逐个拆解。

1. 分片(Chunking)
#

分片就是把一份长文档切分成多个小片段。切分方式有很多:按字数切、按段落切、按章节切、按页码切,甚至还有基于语义的智能切分。

切分粒度是一个需要权衡的问题。片段太大,包含的无关信息就多,相当于把噪音也带进来了;片段太小,可能丢失上下文,模型看到的内容缺乏连贯性。实际项目中往往需要反复试验才能找到合适的粒度。

2. 索引(Indexing)
#

索引是整个数据准备阶段的核心。它做的事情只有两步:

  1. 通过 Embedding 模型把每个片段文本转换成向量。
  2. 把片段文本和对应的向量一起存入向量数据库。

听起来简单,但要真正理解这两步,需要先弄清三个概念:向量、Embedding 和向量数据库。

向量
#

向量是数学中的基本概念,表示一个既有大小又有方向的量。在程序里用一个数组来表示,数组中数字的个数就是向量的维度。

一维向量画在一维坐标轴上,二维向量画在平面坐标系里,三维向量需要三维坐标系。但我们在 RAG 中使用的向量维度通常是几百甚至几千维,无法直观可视化。维度越高,向量能承载的语义信息越丰富,检索的准确性也越高。

Embedding
#

Embedding 是把文本转换成向量的过程。它的关键特性是:语义相近的文本,转换后得到的向量也相近

举个例子,假设用二维向量来表示:

文本 向量
Python 是一门编程语言 (2, 3)
编程语言 Python 简介 (2, 2)
今天适合去爬山 (-3, -1)

前两个句子的向量非常接近,而第三个则离得很远。这正是因为前两个句子语义相近,而第三个完全不相关。

有了这个特性,当用户问「Python 是什么」时,我们先把问题做 Embedding 转换成向量,然后通过向量相似度找到与之接近的其他向量,就能定位到相关的文本片段了。

需要注意的是,Embedding 操作是由专门的 Embedding 模型完成的,不是 GPT-4 Turbo 这类生成式模型。MTEB 排行榜1对各种 Embedding 模型做了评测和排名,可以参考。

向量数据库
#

向量数据库是专门用来存储和查询向量的数据库,针对向量存储做了优化,并提供了计算向量相似度等函数。

值得强调的是,存入向量数据库的不仅有向量,还有原始文本。因为最终要交给大模型的是原始文本,向量只是检索过程中的中间产物。所以向量数据库中通常至少有两列:原始文本和对应向量。

回到索引环节,整个流程就是:对所有分片后的片段逐一做 Embedding,然后把文本和向量一起写入向量数据库,直到所有片段处理完毕。

3. 召回(Retrieval)
#

召回是用户提问后的第一个环节,目的是从所有片段中找出与问题最相关的一批片段。

流程如下:

  1. 把用户的问题发给 Embedding 模型,转换为向量。
  2. 将这个向量发送给向量数据库。
  3. 向量数据库计算该向量与所有已存储片段向量的相似度,返回相似度最高的若干片段(比如 10 个)。

向量相似度计算
#

向量数据库是如何判断哪些片段与用户问题最相关的?答案是计算向量相似度。常见的计算方法有三种:

  • 余弦相似度:计算两个向量之间夹角的余弦值。夹角越小,相似度越高。
  • 欧式距离:计算两个向量之间的直线距离。距离越小,相似度越高。
  • 点积(Dot Product):同时考虑向量的方向和长度。方向一致且长度越大,点积值越大,相似度越高。

向量数据库会把用户问题向量与每个片段向量逐一计算相似度,然后排序,返回前 N 个。

4. 重排(Reranking)
#

重排的作用是从召回阶段返回的片段中,进一步筛选出最相关的几个(比如 3 个)。

你可能会问:既然最终只要 3 个,为什么不在召回阶段就直接取 3 个?为什么要分两步?

原因在于两个阶段使用的相似度计算方式不同,准确率和成本的权衡也不同:

阶段 计算方式 成本 耗时 准确率 作用
召回 向量相似度(余弦、欧式距离等) 较低 粗筛
重排 Cross Encoder 模型 精选

可以类比公司招聘:召回就像 HR 筛简历,从成千上万份简历中快速挑出 10 份看起来不错的;重排就像部门面试,对这 10 个候选人深入考察,最终选出最合适的 3 个。两轮筛选各司其职,效率和准确率都得到了保障。

Cross Encoder2 是一种专门用于文本对相似度判断的模型,它同时接收两个文本作为输入,直接输出相似度分数,准确率比单纯的向量相似度高很多,但计算成本也更高,所以只对少量候选片段使用。

5. 生成(Generation)
#

最后一步就水到渠成了。我们有了用户的问题,也有了经过重排筛选出的 3 个最相关片段,把这两部分组装成一个 Prompt 发给大模型(比如 GPT-4 Turbo),让它根据片段内容来回答用户的问题。

这一步的 Prompt 工程也有一些技巧,比如如何组织上下文、如何引导模型只基于提供的片段来回答、如何处理「找不到答案」的情况等,不过这些属于进阶话题,本文先不展开。

完整流程回顾
#

提问前的数据准备:

  1. 将相关文档进行分片。
  2. 通过 Embedding 模型将每个片段转换为向量。
  3. 将片段文本和向量存入向量数据库。

提问后的回答流程:

  1. 将用户问题通过 Embedding 模型转换为向量。
  2. 在向量数据库中查询与问题最相似的 10 个片段(召回)。
  3. 用 Cross Encoder 对这 10 个片段重新排序,选出最相关的 3 个(重排)。
  4. 将这 3 个片段连同用户问题一起发给大模型,生成最终答案(生成)。

这就是 RAG 的完整工作流程。理解了每个环节的原理,后续不管是选型 Embedding 模型、调优分片策略,还是优化检索效果,都会有更清晰的方向。


  1. MTEB(Massive Text Embedding Benchmark)是一个对 Embedding 模型进行综合评测的排行榜,涵盖分类、聚类、检索等多个维度。 ↩︎

  2. Cross Encoder 是一种将两个文本拼接后 jointly 编码的模型结构,相比 Bi-Encoder(分别编码再计算相似度),它能捕捉更深层的语义关系,但无法预计算向量,因此速度较慢。 ↩︎