在人工智能领域,大语言模型(LLM)虽然强大,但也面临着"知识幻觉"(即模型会编造事实)和知识时效性不足的挑战。RAG(Retrieval-Augmented Generation,检索增强生成)技术应运而生,它如同为模型配备了一个外部的知识库,使其在回答问题时能够参考实时、准确的外部信息。本文将通过一个实际的代码示例,为您剖析 RAG 的核心机制。
1. RAG 的核心思想:知识外挂
传统的 LLM,其知识完全来源于训练时的数据集。一旦训练完成,其内部知识就固定了,且容易产生幻觉。RAG 技术巧妙地将"检索"(Retrieval)与"生成"(Generation)两个步骤结合起来:
- 检索 (Retrieval) : 当用户提出一个问题时,RAG 系统首先不在模型内部寻找答案,而是去一个预先准备好的、庞大的"知识库"中进行搜索,找出与问题最相关的几段信息。
- 增强 (Augmentation) : 系统将检索到的相关信息作为上下文(Context),与原始问题一同构造成一个新的、信息更丰富的提示词(Prompt)。
- 生成 (Generation) : 这个增强后的 Prompt 被发送给大语言模型。模型基于这些真实的、相关的上下文信息,生成最终的答案。
这样,模型的回答就有了事实依据,大大降低了幻觉的风险,并且能够回答关于特定知识库内容的问题。
2. 理解 Chunking:让大模型"消化"长文档的艺术
大语言模型都有一个"上下文窗口"的限制,即一次只能处理有限长度的文本。例如,GPT-3.5 的最大上下文约为 4096 个 token。面对动辄上万字的 PDF 文档或长网页,直接喂给模型是不可行的。这时,"文本分割"(Chunking)就派上了用场。
文本分割的目的:
- 适应模型限制: 将长文档切成模型能够处理的小块(Chunks)。
- 保持语义连贯: 分割时不仅要考虑大小,还要保证每个块内部的语义完整性。
代码示例分析:
javascript
import { RecursiveCharacterTextSplitter } from "@langchain/community/text_splitters";
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400, // 每个块的最大字符数
chunkOverlap: 50, // 相邻块之间重叠的字符数
separators: ['。', ',', '!', '?'] // 优先在此处分割
});
chunkSize: 400: 每个文本块最多包含 400 个字符。这确保了单个块不会超出模型的处理能力。separators: 定义了分割的优先级。代码会优先尝试在句号、逗号等标点符号处分割,因为这些地方通常是语义的自然断点,有助于保持内容的完整性。chunkOverlap: 50: 这是一个非常巧妙的设计。相邻的两个块会有 50 个字符的重叠。例如,Chunk 1 的结尾是"...去公园散步。",Chunk 2 的开头可能是"...去公园散步。公园里有很多花..."。这种重叠确保了即使关键信息恰好横跨两个块,也能在至少一个块中被完整检索到,有效防止了信息丢失。
3. 从网页爬取到向量存储的完整流程
让我们跟随代码,看看一个网页是如何被一步步处理,最终服务于 RAG 系统的。
第一步:网页加载与内容提取
javascript
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
const cheerioLoader = new CheerioWebBaseLoader(
"https://juejin.cn/post/7233327509919547452?...",
{ selector: '.main-area p' } // CSS 选择器,定位到正文段落
);
const documents = await cheerioLoader.load(); // 加载网页内容
系统使用 CheerioWebBaseLoader 这个工具,像浏览器一样打开指定 URL,并利用 CSS 选择器(.main-area p)精准地抓取页面中的正文 <p> 标签内容,过滤掉导航栏、广告等无关信息。
第二步:文本分割
csharp
const splitDocuments = await textSplitter.splitDocuments(documents);
// splitDocuments 现在是一个包含多个小块的数组
上一步加载的长文档被送入 RecursiveCharacterTextSplitter,根据我们设定的规则(400 字符大小,50 字符重叠)被切割成一系列 Document 对象。
第三步:文本向量化与存储
javascript
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "@langchain/community/vectorstores/memory";
const embeddings = new OpenAIEmbeddings({...}); // 初始化嵌入模型
const vectorStore = await MemoryVectorStore.fromDocuments(
splitDocuments, // 传入分割好的文档
embeddings // 传入嵌入模型
);
这是 RAG 的核心技术之一。系统使用 OpenAIEmbeddings 模型将每一个文本块转换成一个高维向量(例如,一个包含 1536 个数字的数组)。这个向量是文本内容的数学表示,蕴含了其语义信息。
这些向量化的文本块被存储在一个特殊的数据库------向量数据库 (此处代码使用的是内存版 MemoryVectorStore,实际应用中会用 Milvus、Pinecone 等)中。这个数据库不仅能存储向量,还能高效地进行"相似度搜索"。
4. 问答环节:问题如何匹配知识
当用户提出问题时,RAG 系统的响应流程如下:
- 问题向量化 : 用户的问题(如"父亲的去世对作者的人生态度产生了怎样的根本性逆转?")同样被
OpenAIEmbeddings模型转换成一个向量。 - 相似度检索: 系统在向量数据库中,使用某种算法(如 Cosine Similarity,余弦相似度)快速计算用户问题向量与数据库中所有文档块向量的相似度。
- 召回相关文档 : 系统找出与问题向量最相似的 Top-K 个文档块(代码中
k: 2表示取出最相关的 2 个块),这些块包含了回答问题所需的上下文信息。 - 构造 Prompt: 系统将检索到的 2 个文档块内容与原始问题拼接成一个完整的 Prompt。
- 生成答案 : 这个包含丰富上下文的 Prompt 被发送给
ChatOpenAI模型,模型基于这些真实信息生成最终、准确、且有据可循的回答。
通过以上步骤,RAG 系统巧妙地将外部知识与强大的语言生成能力结合在一起,实现了更准确、更可靠的信息检索与问答。文本分割(Chunking)作为其中的基石,确保了海量信息能够被有效地处理和利用。