13|RAG 通俗讲:给大模型外接一本书

《大模型原理通俗讲》系列第 13 篇,共 16 篇。不需要数学基础,会用大模型聊天就能读。上一篇讲了上下文窗口与提示词:模型只看得见窗口里的“前文”。这一篇讲一个把“前文”用得最聪明的办法:让它先查资料,再回答。

假设你开了一家网店,想做一个“客服机器人”。顾客问:“我不喜欢这件衣服,能退吗?”

你直接问大模型,它会怎么答?它从来没读过你这家店的规则。按第 11 篇的道理,它会按“一般网店常见的退货规则”编一个,听起来很像,但未必是你的规则。

怎么办?有三个办法:

  1. 微调,把你的规则“灌”进去。但第 09 篇说过:微调主要改变的是行为,不太擅长可靠地记住大量新事实,而且规则一改,又得重新训练。
  2. 把所有资料都塞进提示词。 但第 12 篇说过:窗口有限,塞得多又贵又慢,中间的内容还容易被忽略。
  3. 先查,再答:每次提问,先从资料里找出相关的那几段,只把这几段放进提示词,让模型照着答。

第三个办法,就是 RAG(Retrieval-Augmented Generation,检索增强生成)。

读完这一篇,你能回答三个问题:

  1. RAG 到底是怎么运转的?
  2. 它怎么“按意思”找资料?为什么不只是搜关键词?
  3. 它为什么能减少幻觉,又为什么不能根除?

一、先给一个比喻:开卷考试 + 一位图书管理员

回忆第 11 篇:大模型像一个闭卷考试的学生,不会也要写,写得像那么回事。

RAG,就是把闭卷变成开卷:

考场里多了一位图书管理员。你一提问,管理员先去资料库里,翻出最相关的几页,递给学生。学生照着这几页,组织成一段通顺的回答。
在这里插入图片描述

这里面有两个角色,缺一不可:

  • 管理员(检索):负责从海量资料里,把相关的几段找出来;
  • 学生(大模型):负责读懂这几段,用自然的话答出来。

RAG 特别适合三种情况(见上图下方):训练之后才出现的新信息、私有的资料、太多装不下窗口的资料。

而且有一个很大的好处:资料变了,只要更新资料库,不用重新训练模型。


二、RAG 的全流程

把它拆开,一共有两条线:

在这里插入图片描述

① 离线:先把资料准备好(只做一次,资料更新时再重做)

  1. 切块: 把长资料,切成一小段一小段(比如一条规则、一个小节)。
  2. 变成向量: 用第 03 篇的办法,把每一小段变成一串数字(向量),让“意思相近的文字,向量的方向也相近”。
  3. 存进向量库: 每一块文字,配着它的向量,存起来。向量库,就是专门“存向量、并能很快找出最近的几个”的数据库。

② 在线:每次提问

  1. 问题也变成向量。
  2. 找最像的几块: 拿问题的向量,去库里比,找出方向最接近的几块。
  3. 拼进提示词: 把找到的资料和问题,一起拼成一段前文(第 12 篇),并写明“根据下面的资料回答,没有就说不知道”。
  4. 大模型续写: 它读着这份“前文”,往下写回答(第 07 篇)。
  5. 回答 + 出处: 好的做法,是把用了哪几块资料也一并给出,方便人核对。

注意,整个过程里,大模型的参数一个都没改。它之所以“知道”你家的规则,是因为那几段规则,此刻正躺在它的窗口里。

“RAG”这个名字,来自 2020 年的一篇论文。今天的做法有很多变化,但骨架就是上面这几步。


三、关键一步:怎么“找得准”

整个流程里,最关键的是“找最像的几块”:找得准,后面才答得准。

最容易想到的办法是关键词匹配:数一数,问题里的词,哪一块资料里有。

它的问题,用开头的例子就能看到:

  • 顾客问:“我不喜欢,能退吗?”
  • 你的资料写的是:“收到商品七天内可以无理由退货。”

问题里是“退”“不喜欢”,资料里是“退货”“无理由”。一个词都对不上,关键词匹配就找不到了。可是人一眼就知道:这是同一件事。

所以,需要按意思找:把问题和资料,都变成向量,比较向量的方向有多像(第 03 篇讲过)。“退”“退货”“退款”“不喜欢”,它们总在相近的场合出现,向量的方向也就相近。


四、动手:用 37 行代码,做一个迷你 RAG

这段代码做的是:把 6 条“店铺规则”做成向量库,然后问 5 个问题,对比“数相同的词”和“比向量”两种找法,最后把找到的资料拼进提示词。

几点说明(很重要):

  • KB 是这家店的私有资料:大模型训练时不可能读到。
  • BG 是一小批背景句子,只用来让它学“哪些词常一起出现”。真实的向量模型读过海量文字,这里用我手写的几句代替,所以它的“懂”是很有限、也是这里设计出来的,只是为了演示原理。
  • 向量用的是第 03 篇的老办法:数“谁和谁常一起出现”,再压成 5 个数。
  • 最后一步,“让大模型写回答”,迷你模型做不到,所以代码只演示到拼好的提示词。
# mini_rag.py —— 迷你 RAG:资料切块 → 变成向量 → 按"意思"找最相关的块 → 拼进提示词
import numpy as np

KB = ["退货 规则 : 收到 商品 七天 内 可以 无理由 退货 。", "运费 规则 : 订单 满 99 元 免 运费 , 否则 收 8 元 。",
      "发货 时间 : 下单 后 48 小时 内 发货 , 周末 顺延 。", "会员 权益 : 会员 每月 1 号 可以 领 一张 满减 券 。",
      "保修 规则 : 电子产品 保修 一年 , 人为 损坏 不 保修 。", "客服 时间 : 每天 9 点 到 21 点 在线 。"]               # 这家店的"私有资料":大模型训练时不可能读到
BG = ["东西 不 喜欢 , 可以 退 。", "不 喜欢 的 衣服 , 可以 退货 。", "买 错 了 , 可以 退货 , 也 可以 退款 。", "我 想 退货 , 也 想 退款 。",
      "不 想 要 了 , 可以 无理由 退 。", "喜欢 就 留下 , 不 喜欢 就 退 。", "东西 坏 了 , 要 修 。", "手机 坏 了 , 送去 修 , 保修 期 内 免费 。",
      "电脑 坏 了 , 找 师傅 修 。", "坏 了 的 东西 , 保修 期 内 可以 修 。", "快递 要 收 运费 , 满 额 免 运费 。", "运费 多少 钱 , 满 多少 免 。",
      "什么 时候 发货 , 什么 时候 到 。", "几点 上班 , 几点 下班 。", "每天 都 在 线 , 随时 可以 问 。", "会员 有 券 , 有 优惠 。"]  # 背景句子:只用来学"哪些词常一起出现"(真实的向量模型读过海量文字)
STOP = {"不", "了", "吗", "能", "怎么", "谁", "什么", "。", ",", ":", "可以", "的", "也", "都"}                     # 先去掉"的、了、吗"这类没信息的词
sents = [[w for w in s.split() if w not in STOP] for s in KB + BG]
vocab = sorted({w for s in sents for w in s}); idx = {w: i for i, w in enumerate(vocab)}
A = np.zeros((len(vocab), len(sents)))                                   # 词 × 句子 的计数表:哪个词出现在哪句里
for j, s in enumerate(sents):
    for w in s: A[idx[w], j] += 1
idf = np.log(len(sents) / (A > 0).sum(axis=1)) + 0.5                     # 到处都有的词权重低,少见的词权重高
U = np.linalg.svd(A * idf[:, None], full_matrices=False)[0][:, :5]       # 第 03 篇的老办法:压成 5 个数

def embed(words):                                                        # 一段话 → 5 个数 → 长度拉成 1
    c = np.zeros(len(vocab))
    for w in words:
        if w in idx: c[idx[w]] += 1
    v = (c * idf) @ U
    return v / (np.linalg.norm(v) + 1e-9)
docs = np.array([embed(s) for s in sents[:len(KB)]])                     # 每块资料的向量,存起来 = "向量库"

def ask(q):
    w = [x for x in q.split() if x not in STOP]; print(f"\n问:{''.join(q.split())}")
    kw = [len(set(w) & set(s)) for s in sents[:len(KB)]]                  # 做法一:数有几个词一样
    print(f"  关键词:{KB[int(np.argmax(kw))][:4]}(重叠 {max(kw)} 个词)" if max(kw) else "  关键词:一个词都对不上,找不到")
    sc = docs @ embed(w); best = int(np.argmax(sc))                      # 做法二:比向量方向有多像
    print(f"  向量:{KB[best][:4]}(相似度 {sc[best]:.2f})")
    if sc[best] < 0.5: return print("  → 相似度太低:资料里没有,老实说不知道")
    print(f"  → 拼进提示词:根据下面的资料回答,没有就说不知道。\n    资料:{KB[best]}\n    问题:{''.join(q.split())}")

for q in ["运费 怎么 算", "不 喜欢 能 退 吗", "东西 坏 了 谁 修", "今天 天气 怎么样", "我 喜欢 红色"]: ask(q)

运行结果:

问:运费怎么算
  关键词:运费 规(重叠 1 个词)
  向量:运费 规(相似度 1.00)
  → 拼进提示词:根据下面的资料回答,没有就说不知道。
    资料:运费 规则 : 订单 满 99 元 免 运费 , 否则 收 8 元 。
    问题:运费怎么算

问:不喜欢能退吗
  关键词:一个词都对不上,找不到
  向量:退货 规(相似度 0.96)
  → 拼进提示词:根据下面的资料回答,没有就说不知道。
    资料:退货 规则 : 收到 商品 七天 内 可以 无理由 退货 。
    问题:不喜欢能退吗

问:东西坏了谁修
  关键词:一个词都对不上,找不到
  向量:保修 规(相似度 0.98)
  → 拼进提示词:根据下面的资料回答,没有就说不知道。
    资料:保修 规则 : 电子产品 保修 一年 , 人为 损坏 不 保修 。
    问题:东西坏了谁修

问:今天天气怎么样
  关键词:一个词都对不上,找不到
  向量:退货 规(相似度 0.00)
  → 相似度太低:资料里没有,老实说不知道

问:我喜欢红色
  关键词:一个词都对不上,找不到
  向量:退货 规(相似度 0.96)
  → 拼进提示词:根据下面的资料回答,没有就说不知道。
    资料:退货 规则 : 收到 商品 七天 内 可以 无理由 退货 。
    问题:我喜欢红色

在这里插入图片描述

逐问看:

  1. “运费怎么算”:问题里的词,资料里也有,两种做法都找到了。
  2. “不喜欢能退吗”:关键词一个词都对不上,找不到;向量找到了“退货规则”(相似度 0.96)。这就是按意思找的价值。
  3. “东西坏了谁修”:资料里写的是“保修”“损坏”,问题里是“坏”“修”。关键词又失灵了,向量找到了“保修规则”。
  4. “今天天气怎么样”:问题里的词,它一个都没学过,相似度是 0,于是代码判断“资料里没有”,老实说不知道。检索不到相关资料,是一个好信号:别硬答。
  5. “我喜欢红色”:这一行是故意放的,也是最值得看的一行。 问题和店铺规则毫无关系,可是它仍然配上了“退货规则”,相似度高达 0.96(因为“喜欢”这个词,在背景句子里和“退货”常在一起)。向量检索永远会返回“最像的”,哪怕根本不相关。 这和第 11 篇的“模型永远会答”,是同一个道理。

再提醒一次:向量的好坏,取决于怎么训练。 你可以把代码里的“压成 5 个数”,改成 8 个数,再跑一遍:第 2、3 问就都找不到了(相似度掉到 0.06 和 0.17),连“我喜欢红色”的误报也消失了。这个小玩具足够脆弱,也正好说明:检索的质量,是整个 RAG 的命门。


五、切块的学问

“切块”看起来最不起眼,却对效果影响很大。

在这里插入图片描述

  • 块太大:一块里夹杂很多无关内容,占窗口,也不容易找准(整块和问题都有点像,又都不够像)。
  • 块太小:一句话被切断,断章取义。比如“满 99 元免运费”被切成“满 99 元”和“免运费”两块,哪一块都答不了问题。

常见的做法(经验,不是规定):

  • 尽量按段落、标题来切,别在句子中间下刀;
  • 相邻的块留一点重叠,避免关键句被切开;
  • 每块带上标题和出处,既帮助找准,也方便让人核对;
  • 一次取几块(常叫 top-k):取得太少,可能漏掉答案;取得太多,又塞满窗口、混进无关内容。

具体切多大、取几块,要在自己的资料上反复试。


六、RAG 会减少幻觉,但不会消灭幻觉

RAG 最常被拿来对付的,就是第 11 篇的幻觉:把依据直接放在眼前,模型“凭记忆编”的空间就小得多。

但 RAG 不是万事大吉,每一步都可能出问题:

在这里插入图片描述

  • 检索出错:找错了(如“我喜欢红色”也配上一块),或者资料有,却没找到。
  • 资料有问题:过时、写错、互相矛盾,模型照着答,也是错的。(资料错了,RAG 只会让错得更“有依据”。)
  • 模型没照着答:资料里没有的,它仍可能按自己的记忆补一句;或者把资料和记忆混在一起。
  • 资料里藏着指令:网页或文档里,可能有一句“忽略之前的要求,……”。模型读到的都是文字,如果不加防范,它可能把这句话当成前文的一部分去执行。所以,要把资料当成“资料”,而不是“命令”,并限制模型能做的事(后篇讲 Agent 时会再碰到)。

所以,要把“可信”落到实处:

  1. 提示词里写明:只依据资料回答,没有就说不知道;
  2. 设一个相似度门槛,找不到就不答(但如上例,门槛挡不住所有误报);
  3. 要求回答附上出处,并让人能点开核对;
  4. 重要的场合,留一个人把关。

七、RAG、微调、长窗口:什么时候用哪个?

三者经常被拿来比较,这里先给一个粗略的对应(常见做法,不是铁律,下一篇会详细讲):

你想解决的问题常见的选择为什么
它不知道我的资料 / 新信息,资料多、还会更新RAG资料更新,只改资料库,不用重训
想让它换一种固定的风格、格式、做法微调改的是“行为”(第 09 篇)
资料不多,只问一两次直接放进提示词最简单(第 12 篇)

很多真实产品,是几个一起用:比如微调让它学会客服的口吻,RAG 让它查到最新的规则。


八、“图书管理员”这个比喻的说明

  1. 管理员不懂内容,只会“比方向”。 真实的检索,比的是向量的相似程度,它并不理解资料的含义,所以会出现“我喜欢红色”这类误配。
  2. 学生不一定老老实实“照着答”。 模型可能漏看、可能掺进自己的记忆,所以才需要出处和核对。
  3. 管理员递过来的,不一定是最好的几页。 现实里常常需要多取几块再筛选,或者把关键词匹配和向量匹配混着用(关键词擅长精确的专有名词和编号,向量擅长换了说法的问题)。

九、三个常见误解

误解 1:“用了 RAG,模型就学会了我的资料。”
不是。模型的参数一个没改。资料只是在你提问的这一次,被放进了窗口;下次提问,要重新找、重新放。

误解 2:“有了 RAG,就不会有幻觉了。”
不是。检索可能出错、资料可能有误、模型可能没照着答。RAG 是降低幻觉的好办法,但不是保险。

误解 3:“向量检索一定比关键词检索好。”
不一定。向量擅长“换了说法”的问题;但对精确的编号、型号、人名,关键词往往更稳。所以很多系统是两者结合。


十、本篇要点

  1. RAG = 先查,再答:把资料切块、变成向量存起来;提问时找出最相关的几块,拼进提示词,让大模型照着答。模型的参数没改,资料更新也不用重新训练。
  2. 关键在“找得准”:按意思(向量相似)找,才能应对换了说法的问题;但检索永远会返回“最像的”,哪怕不相关,所以要设门槛、带出处、留人核对。
  3. RAG 能明显减少凭记忆瞎编,但不能保证没有错:检索、资料、模型有没有照着答、资料里有没有藏指令,每一环都可能出问题。

下一篇预告: 前面我们说了三种让模型“更适合你”的办法:写提示词、RAG,还有一种:微调。可是全量微调又贵又重,普通人怎么玩得起?下一篇讲微调、LoRA 与选型:什么时候该用哪一种。

系列目录

在这里插入图片描述

文中的图、代码,欢迎自己动手改一改,比如往 KB 里加一条新规则,再问一个相关的问题,看它找不找得到。有哪里没讲明白,欢迎在评论区告诉我。


Logo

更多推荐