大模型"知识"的外挂:RAG检索增强生成详解
引言
读者朋友们,欢迎回到我们的大模型探索之旅。
我们的模型,就像一位博览群书的学者,脑中装满了人类语言的规律与世界的无数事实。
但不知道你是否发现了一个有趣的现象?这位"学者"虽然博学,但他的知识却被永远地封印在了"毕业"的那一刻------也就是模型训练完成的那一天。如果你问他昨天发生了什么新闻,或者让他查询一下你公司内部刚刚发布的最新季度财报,他恐怕只能抱歉地摇摇头,因为这些知识都不在他当年"阅读"过的"书籍"里。更糟糕的是,他有时为了回答问题,甚至会一本正经地"编造"一些看似合理却完全错误的信息,我们称之为"幻觉"。
这该怎么办呢?难道我们每掌握一点新知识,就要把这位"学者"请回学校,花费巨大的代价重新"深造"一遍(也就是重新训练模型)吗?
当然不用!今天,我们就来学习一种极其巧妙的技术,它就像是为我们这位博学的"学者"配备了一个可以实时上网的"平板电脑"和一位专业的"图书管理员"。这项技术,就是检索增强生成(Retrieval-Augmented Generation) ,简称RAG。
在本章中,我们将一起:
- 理解为什么LLM需要RAG,它解决了哪些核心痛点。
- 通过生动的比喻,直观地感受RAG的工作原理。
- 一步步拆解RAG的技术流程,从数据准备到最终生成,洞悉其内部机制。
- 亲手用Python代码,从零开始搭建一个简单的RAG问答机器人,让你体验"外挂"知识库的威力。
准备好了吗?让我们一起为我们的模型,插上名为"RAG"的翅膀,让它突破知识的壁垒,连接真实、动态的世界。
正文
为什么需要RAG?LLM的"知识困境"
要理解RAG的价值,我们首先要深入理解当前大语言模型面临的两个核心局限:
-
知识的静态性:LLM的知识,本质上是其在训练阶段从海量数据中学到的模式和信息的"参数化压缩"。3 这意味着,一旦训练完成,模型的知识就被固定了下来。它无法得知训练截止日期之后发生的任何新事件、新知识。这就像一本2023年出版的百科全书,永远也查不到2024年的新闻。
-
知识的通用性:预训练LLM接触的是公开的、广泛的互联网数据。对于特定领域(如医疗、法律)或特定组织(如你的公司内部知识库)的私有、非公开信息,它一无所知。直接让它回答这些问题,无异于缘木求鱼。
为了克服这些问题,人们最初想到的方法是微调(Fine-tuning)。微调就像是针对特定主题给模型"开小灶",用特定领域的数据继续训练模型,让它掌握新的知识。这种方法确实有效,但缺点也同样明显:
- 成本高昂:微调依然需要消耗大量的计算资源和时间。
- 更新不便:如果知识需要频繁更新(比如每天都有新文档),频繁地微调整个模型是不现实的。
- "灾难性遗忘":在学习新知识的过程中,模型可能会忘记一部分旧的通用知识,就像一个只专注于专业课而忘记了基础课的学生。
正是在这样的背景下,RAG应运而生。它提出了一种全新的、更轻量级的思路:我们能不能不把新知识"塞进"模型的大脑里,而是在模型需要回答问题的时候,把相关的知识"递"给它参考呢?
这种"开卷考试"的模式,就是RAG的核心思想。它将模型的"记忆"分为两部分:
- 内部知识(参数化记忆):模型在预训练阶段学到的通用世界知识,存储在模型的参数中。
- 外部知识(非参数化记忆):一个可以随时更新的、外置的知识库,模型可以在需要时进行查询。
通过这种方式,我们无需对模型本身进行伤筋动骨的修改,就能让它具备了实时获取和利用新知识的能力,既经济又高效。
RAG的直观比喻:一位聪明的"开卷"考生
为了让你更直观地理解RAG,我们来打个比方。
想象一下,你正在参加一场历史考试。考题是:"请详细论述2024年巴黎奥运会的历史意义。"
-
一个没有RAG的LLM :它就像一个记忆力超群但只能"闭卷"考试的学生。他的大脑里记满了从古代到2023年的所有历史知识。对于这道题,他可能会因为知识库里没有"2024年奥运会"的信息而无法回答,或者更糟,他会根据自己学到的关于"奥运会"和"巴黎"的通用知识,编造一个听起来很合理的答案(产生幻觉)。
-
一个有RAG的LLM :它就像一个同样聪明但被允许"开卷"考试 的学生。考试时,他不仅可以依赖自己脑中的知识,桌上还放着几本最新的、关于2024年奥运会的参考资料(外部知识库)。
当他看到题目时,他会做以下几件事:
-
检索(Retrieval) :他不会立刻动笔,而是先快速地在参考资料中查找与"2024年巴黎奥运会"相关的章节和段落。这个查找过程,就是RAG中的检索环节。他可能会找到关于会徽设计、比赛项目、场馆介绍等好几段相关信息。
-
增强(Augmentation) :他将找到的这些具体信息,和他自己脑中关于"奥运会历史"、"国际关系"等通用知识结合起来,形成一个更丰富、更全面的"答案腹稿"。这个过程,就是增强环节。
-
生成(Generation) :最后,他基于这个增强了的"腹稿",用自己流畅的语言和强大的逻辑组织能力,写下一段详实、准确、有深度的答案。这个过程,就是生成环节。
通过这个"开卷考试"的流程,RAG让LLM的回答不再仅仅依赖于其固化的内部记忆,而是能够引用外部的、最新的、权威的信源,从而大大提高了回答的准确性和时效性。1 2
RAG的工作原理:两阶段详解
现在,让我们从"比喻"走向"现实",深入RAG的技术内部,看看这个"开卷考试"系统在计算机里是如何实现的。RAG的整个工作流程可以分为两个主要阶段:知识库构建(Indexing) 和 检索生成(Retrieval & Generation)。
第一阶段:知识库构建(Indexing)
这个阶段是准备"开卷考试"的"参考资料"的过程,通常是离线完成的。
步骤1:加载与切割(Load & Chunk) 首先,我们需要将我们的知识源(比如,一系列的PDF文档、网页、公司的Markdown格式的内部文档等)加载到系统中。由于LLM的处理能力(即上下文窗口)有限,我们不能把一篇长达几万字的文档直接扔给它。因此,我们需要将这些长文档切割成更小的、有意义的文本块(Chunks)。
- 为什么需要切割? 想象一下,如果参考资料是一整本厚厚的书,让你在几秒钟内找到某个知识点,你会非常低效。但如果把书拆解成一张张带有明确小标题的卡片,查找起来就快多了。切割就是这个目的,它能让后续的检索更精确。
步骤2:文本嵌入(Embedding) 计算机不理解文字,只理解数字。为了让计算机能够判断"哪两段文字在语义上是相似的",我们需要一种将文字"翻译"成数字的方法。这个"翻译官"就是嵌入模型(Embedding Model)。
嵌入模型可以将任何一段文本转换成一个由数百个数字组成的列表,我们称之为向量(Vector)。这个向量就像是文本在"语义空间"中的坐标。语义上相近的文本,它们的向量在空间中的距离也更近。
- 举个例子:"今天天气真好"和"今天阳光明媚"的向量,会比"今天天气真好"和"我晚饭吃了汉堡"的向量在空间上离得更近。
我们会遍历所有切割好的文本块,用嵌入模型将它们一一转换成向量。
步骤3:存入向量数据库(Store in Vector Database) 现在我们有了一大堆文本块和它们对应的向量"坐标"。我们需要一个地方来高效地存储和检索它们。这个地方就是向量数据库。
向量数据库是一种专门为高效处理向量数据而设计的数据库。它能够以极快的速度接收一个查询向量,并返回与之最相似的N个向量。
至此,我们的"参考资料"就准备好了。所有的知识都已经被转换成向量,并整齐地存放在向量数据库中,等待着被随时检索。
第二阶段:检索与生成(Retrieval & Generation)
这个阶段是"开卷考试"的现场,是当用户提出问题时实时发生的。
步骤A:嵌入用户查询 当用户输入一个问题(例如,"RAG技术有什么好处?")时,我们使用与步骤2中完全相同的嵌入模型,将这个问题也转换成一个向量。
步骤B:在向量数据库中搜索 我们拿着这个"问题向量",去向量数据库中进行搜索。数据库会计算问题向量与库中所有文本块向量的相似度(通常使用余弦相似度等算法),然后返回最相似的几个文本块。这些被找回来的文本块,就是我们认为与问题最相关的"参考资料"。
步骤C:增强提示词(Augment Prompt) 现在我们有了两样东西:
- 用户原始的问题。
- 从知识库中检索到的相关文本块。
我们会将这两者组合成一个新的、更丰富的提示词(Prompt)。这个过程就是"增强"。通常,我们会构建一个类似下面这样的模板:
"请根据以下提供的上下文信息,来回答用户的问题。如果上下文中没有足够的信息,请回答你不知道。
上下文信息: [这里插入从数据库中检索到的文本块1] [这里插入从数据库中检索到的文本块2] ...
用户的问题: [这里插入用户的原始问题]"
步骤D:生成答案 最后,我们将这个"增强提示词"发送给大语言模型(LLM)。LLM会像一个真正的"开卷考生"一样,阅读我们提供的上下文信息,并基于这些信息来生成对用户问题的回答。由于有了明确的参考资料,LLM"编造"答案的可能性被大大降低,回答的准确性和相关性也得到了极大的保障。1
代码实战:从零搭建你的第一个RAG问答机器人
理论讲了这么多,是时候动手实践了!我们将使用Python,并借助强大的transformers
和scikit-learn
库,来构建一个极简的RAG系统。这个系统将能回答关于我们第8章内容("大力出奇迹"的哲学:大语言模型的核心技术揭秘)的问题。
场景 :为我们教程的某一章,创建一个问答机器人。 问题定义 :当用户提问时,机器人能根据该章节的内容,而不是其通用知识来回答。 解决思路:我们将遵循上面讲解的RAG流程,手动实现各个步骤。
第一步:环境准备
首先,请确保你已经安装了必要的库。
bash
pip install transformers torch scikit-learn
transformers
: 来自Hugging Face的库,我们将用它来加载预训练的嵌入模型。torch
:transformers
库的依赖。scikit-learn
: 一个强大的机器学习库,我们将用它来计算向量的余弦相似度,以实现一个最简单的"向量数据库"。
第二步:编写代码
python
import torch
from transformers import BertModel, BertTokenizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# --- 1. 知识库构建阶段 (Indexing) ---
# 步骤1: 加载与切割 (这里我们手动创建一个简单的知识库)
# 在真实场景中,你会从文件或数据库加载这些文本
knowledge_base_texts = [
"大语言模型(LLM)的核心技术之一是其巨大的规模。这包括了数千亿甚至万亿级别的参数数量,以及TB级别的海量训练数据。",
"LLM的"大力出奇迹"哲学,指的是通过显著增加模型参数量、数据量和计算量,来实现模型性能的质的飞跃,涌现出新的能力。",
""涌现能力"是指在小模型上不存在,但在模型规模达到一定阈值后突然出现的能力,例如上下文学习、逻辑推理等。",
"训练LLM需要巨大的算力,通常使用数千个高性能GPU进行长达数周甚至数月的并行计算。",
"尽管LLM功能强大,但它们也面临着如幻觉、知识过时、以及潜在的偏见和安全问题等挑战。"
]
print("知识库已加载")
# 步骤2 & 3: 文本嵌入并存储 (使用BERT作为嵌入模型)
# 加载预训练的BERT模型和分词器
# 我们选择一个中文的BERT模型
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') # 用于将文本转换为模型认识的ID
model = BertModel.from_pretrained('bert-base-chinese') # 用于获取文本的向量嵌入
print("BERT嵌入模型已加载")
# 创建一个函数来将文本转换为向量
def get_embedding(text):
# 1. 分词并转换为ID
inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=512)
# 2. 不计算梯度,以节省计算资源
with torch.no_grad():
# 3. 获取模型输出
outputs = model(**inputs)
# 4. 我们取[CLS]标志对应的输出作为整个句子的向量表示
embedding = outputs.last_hidden_state[:, 0, :].numpy()
return embedding
# 将知识库中的所有文本转换为向量
knowledge_base_embeddings = np.vstack([get_embedding(text) for text in knowledge_base_texts])
print(f"知识库已嵌入,向量维度为: {knowledge_base_embeddings.shape}")
# --- 2. 检索与生成阶段 (Retrieval & Generation) ---
def answer_question(question):
print(f"用户问题: {question}")
# 步骤A: 嵌入用户查询
question_embedding = get_embedding(question)
print("已将问题转换为向量...")
# 步骤B: 在向量数据库中搜索
# 使用余弦相似度计算问题向量与知识库中所有向量的相似度
similarities = cosine_similarity(question_embedding, knowledge_base_embeddings)
# 找到最相似的文本块的索引
most_similar_index = np.argmax(similarities)
retrieved_chunk = knowledge_base_texts[most_similar_index]
print(f"检索到的最相关信息: {retrieved_chunk}")
# 步骤C: 增强提示词
# 在这个简化版中,我们不调用真正的LLM,而是直接展示检索结果
# 在一个完整的RAG中,你会把问题和检索到的信息组合成一个prompt
augmented_prompt = f"""
请根据以下信息回答问题:
---
上下文: {retrieved_chunk}
---
问题: {question}
"""
print("已构建增强提示词 (在完整RAG中会发送给LLM):")
print(augmented_prompt)
# 步骤D: 生成答案 (简化版)
# 真实场景是: final_answer = llm.generate(augmented_prompt)
# 这里我们直接返回检索到的信息作为答案
final_answer = retrieved_chunk
print("生成答案 (简化版):")
return final_answer
# --- 测试我们的RAG系统 ---
user_question = "模型规模变大后,会发生什么现象?"
answer = answer_question(user_question)
print(f"
机器人回答: {answer}")
print("-" * 20)
user_question_2 = "训练大模型需要什么?"
answer_2 = answer_question(user_question_2)
print(f"
机器人回答: {answer_2}")
预期输出:
makefile
知识库已加载
BERT嵌入模型已加载
知识库已嵌入,向量维度为: (5, 768)
用户问题: 模型规模变大后,会发生什么现象?
已将问题转换为向量...
检索到的最相关信息: "涌现能力"是指在小模型上不存在,但在模型规模达到一定阈值后突然出现的能力,例如上下文学习、逻辑推理等。
已构建增强提示词 (在完整RAG中会发送给LLM):
请根据以下信息回答问题:
---
上下文: "涌现能力"是指在小模型上不存在,但在模型规模达到一定阈值后突然出现的能力,例如上下文学习、逻辑推理等。
---
问题: 模型规模变大后,会发生什么现象?
生成答案 (简化版):
机器人回答: "涌现能力"是指在小模型上不存在,但在模型规模达到一定阈值后突然出现的能力,例如上下文学习、逻辑推理等。
--------------------
用户问题: 训练大模型需要什么?
已将问题转换为向量...
检索到的最相关信息: 训练LLM需要巨大的算力,通常使用数千个高性能GPU进行长达数周甚至数月的并行计算。
已构建增强提示词 (在完整RAG中会发送给LLM):
请根据以下信息回答问题:
---
上下文: 训练LLM需要巨大的算力,通常使用数千个高性能GPU进行长达数周甚至数月的并行计算。
---
问题: 训练大模型需要什么?
生成答案 (简化版):
机器人回答: 训练LLM需要巨大的算力,通常使用数千个高性能GPU进行长达数周甚至数月的并行计算。
代码逐行解释:
knowledge_base_texts
: 我们手动定义了一个包含5句话的列表,作为我们的"知识库"。BertTokenizer.from_pretrained(...)
: 加载一个中文BERT模型的分词器,它知道如何把汉字切分成模型认识的最小单元。BertModel.from_pretrained(...)
: 加载中文BERT模型的主体,我们将用它来计算文本的嵌入向量。get_embedding(text)
: 这是我们定义的核心函数,用于将任何文本字符串转换为768维的向量。tokenizer(...)
: 分词器处理输入文本。with torch.no_grad()
: 一个重要的优化,告诉模型我们只是在做前向计算(获取嵌入),不需要为反向传播计算梯度,可以节省大量内存和计算。outputs.last_hidden_state[:, 0, :]
: BERT模型会为输入的每个词都生成一个向量。我们采取一种常见的策略,即使用第一个特殊标记[CLS]
对应的向量来代表整个句子的语义。np.vstack([...])
: 将所有知识库文本的向量堆叠成一个大的NumPy数组,这就是我们最简单的"向量数据库"。cosine_similarity(...)
:scikit-learn
库提供的函数,方便地计算一个向量和一组向量之间的余弦相似度。np.argmax(...)
: 找到相似度得分最高的那个文本块的索引。answer_question(question)
: 封装了整个检索和生成流程的函数。
Q&A: 你可能会问......
-
Q: 为什么我们的代码没有真正调用一个像GPT一样的大模型来"生成"答案,而是直接返回了检索到的内容?
- A: 非常好的问题!这主要是为了简化教学和避免复杂的API调用。在这个例子里,我们把重点放在了**检索(Retrieval)**这一步,让你能清晰地看到系统是如何根据问题找到最相关的知识的。在一个生产级的RAG系统中,最后一步确实会把我们构建的
augmented_prompt
发送给一个强大的生成模型(如GPT-4),让它用更自然、更流畅的语言来组织和回答问题,而不是生硬地复述原文。
- A: 非常好的问题!这主要是为了简化教学和避免复杂的API调用。在这个例子里,我们把重点放在了**检索(Retrieval)**这一步,让你能清晰地看到系统是如何根据问题找到最相关的知识的。在一个生产级的RAG系统中,最后一步确实会把我们构建的
-
Q: 为什么选择BERT作为嵌入模型?我可以用别的吗?
- A: 当然可以!我们选择BERT是因为它是一个经典且效果不错的嵌入模型,尤其是在处理中文方面。事实上,选择哪个嵌入模型对RAG系统的效果至关重要。现在有许多专门为"语义检索"任务优化的嵌入模型(如
m3e-base
,bge-large-zh
等),它们通常比通用的BERT模型效果更好。你可以很方便地在Hugging Face上找到它们,并替换掉我们代码中的bert-base-chinese
。
- A: 当然可以!我们选择BERT是因为它是一个经典且效果不错的嵌入模型,尤其是在处理中文方面。事实上,选择哪个嵌入模型对RAG系统的效果至关重要。现在有许多专门为"语义检索"任务优化的嵌入模型(如
-
Q: 每次都重新计算知识库的嵌入向量,是不是太慢了?
- A: 是的,你说到了关键点。在我们的简单代码里,每次运行都会重新计算知识库的嵌入。在真实应用中,**知识库构建(Indexing)**是一个一次性的、离线的步骤。你会提前计算好所有文本块的向量,并将它们连同原文一起存储在专门的向量数据库(如FAISS, Milvus, Pinecone等)中。这样,在用户提问时,你只需要计算问题的向量,然后直接在数据库中进行高效搜索,无需重复计算整个知识库。
总结与预告
在本章中,我们深入探讨了检索增强生成(RAG)这一强大的技术。我们了解到:
- 核心痛点:RAG旨在解决大语言模型知识静态、无法访问私有或实时信息的根本问题。
- 核心思想:它将"闭卷考试"变成了"开卷考试",通过从外部知识库检索信息来增强模型的回答能力。
- 工作流程 :RAG主要包括两个阶段:
- 知识库构建:将文档进行切割、嵌入,并存入向量数据库。
- 检索与生成:将用户问题嵌入后进行搜索,用检索到的信息增强提示词,最后由LLM生成答案。
- 实践意义:RAG是一种成本效益极高的方法,它无需重新训练或微调,就能显著提升LLM在知识密集型任务上的准确性和可靠性。
我们不仅理解了理论,还亲手实现了一个迷你的RAG系统。虽然它很简单,但"麻雀虽小,五脏俱全",它完整地展示了RAG的核心逻辑。
今天,我们为模型配备了外置的"知识大脑"。但是,仅仅给予知识就足够了吗?我们如何确保模型在使用这些知识时,能够遵循我们设定的特定"规矩"或"契约"?例如,如何让模型在扮演"客服"角色时,严格按照公司的服务章程来回答,而不是自由发挥?
在下一章,我们将探讨一个同样有趣的话题:模型的"契约"------MCP模型上下文协议剖析。我们将学习如何为模型的行为设定更精细的"游戏规则",引导它成为一个更可靠、更可控的AI助手。敬请期待!
课后练习
- 动手修改 :将我们代码中的
knowledge_base_texts
替换为你感兴趣的一段长文本(比如,一篇新闻报道,或者你自己的笔记)。先手动将其拆分成几个句子或段落,然后运行代码,看看它是否能正确回答你基于这段文本提出的问题。 - 边界测试 :尝试向你的RAG机器人问一个知识库中完全没有的问题。观察它的反应。在我们的简化版代码中,它可能会返回一个毫不相关的信息。思考一下,如何修改代码,让它在找不到相关信息时,能够诚实地回答"我不知道"?(提示:可以设置一个相似度阈值)。
- 思考题:RAG的检索环节是整个系统的关键。如果检索环节出错了(比如,没找到最相关的信息,或者找到了错误的信息),会对最终的生成结果产生什么影响?你认为有什么方法可以提高检索的准确率吗?