一、前言:RAG技术核心认知
随着大语言模型(LLM)的快速发展,其在自然语言处理、智能问答等领域的应用日益广泛,但大语言模型存在两个核心痛点:一是知识时效性不足,训练数据往往滞后于最新信息,无法回答最新出现的内容;二是存在“幻觉”问题,可能会生成与事实不符的内容,尤其在面对专业领域或特定文档相关的问答时,准确性难以保证。
检索增强生成(Retrieval-Augmented Generation,简称RAG)技术的出现,有效解决了上述两个痛点。RAG的核心逻辑是:在让大语言模型生成回答之前,先从外部知识库(如文档、网页、数据库等)中检索出与问题相关的信息,将这些信息作为“参考资料”输入给大语言模型,让模型基于检索到的真实、最新信息生成回答,从而提升回答的准确性、时效性和可信度。
本次学习主要围绕RAG技术的核心环节——文档加载(Loader)、文档分割(Splitter)、向量存储(Vector Store)、检索(Retriever)及问答实现展开,结合参考资料中的完整代码,深入拆解每个环节的原理、实现方式和注意事项,形成一份系统的学习笔记,帮助自己和其他学习者快速掌握RAG技术的基础实现流程,为后续深入学习复杂RAG系统奠定基础。
本次学习所基于的参考资料,提供了一个完整的RAG基础实现案例,涵盖了从网页文档加载、文本分割,到向量数据库创建、相似性检索,再到调用大语言模型生成回答的全流程代码,是入门RAG技术的优质实践素材。本笔记将以该代码为核心,逐步拆解每个模块的功能、代码逻辑,并补充相关扩展知识点,确保内容详实、逻辑清晰,总字数达到4000字以上,兼具理论性和实践性。
二、RAG核心流程总览
在深入学习每个模块之前,先明确RAG技术的核心流程,便于建立整体认知。一个基础的RAG系统,通常包含以下5个核心环节,参考资料中的代码也严格遵循了这个流程:
- 文档加载(Loader) :从外部来源(如本地文件、网页、数据库等)加载原始文档,将其转换为大语言模型可处理的格式(通常是包含文本内容和元数据的Document对象)。参考资料中主要使用了网页Loader,从掘金文章中加载文档内容。
- 文档分割(Splitter) :由于原始文档通常篇幅较长,而大语言模型存在上下文窗口限制(无法处理过长的文本),且过长的文本会增加向量存储和检索的成本,因此需要将加载后的原始文档分割成多个短小、语义完整的文本片段(Chunk)。参考资料中使用了递归字符分割器,结合中文语义特点设置分割规则。
- 向量嵌入(Embeddings) :将分割后的文本片段(Chunk)转换为向量形式(Embedding)。因为计算机无法直接处理文本,向量嵌入可以将文本的语义信息映射到高维向量空间,使得语义相似的文本片段的向量距离更近,为后续的相似性检索提供基础。参考资料中使用了OpenAI的Embeddings模型。
- 向量存储(Vector Store) :将转换后的文本向量存储到专门的向量数据库中,方便后续快速进行相似性检索。向量数据库与传统关系型数据库的核心区别在于,它支持基于向量距离的快速查询,能够高效地找到与用户问题向量最相似的文本片段。参考资料中使用了MemoryVectorStore(内存向量存储),适合快速原型开发和测试。
- 检索与生成(Retrieval & Generation) :用户输入问题后,先将问题转换为向量形式,然后在向量数据库中检索出与问题最相似的若干个文本片段(参考资料);再将这些检索到的文本片段和用户问题一起输入给大语言模型,让模型基于这些参考资料生成准确、相关的回答。
这5个环节环环相扣,每个环节的实现质量都会影响整个RAG系统的性能。接下来,我们将结合参考资料中的代码,逐一深入学习每个环节的细节、原理和实现方式,并补充相关扩展知识点,帮助大家全面理解RAG技术的底层逻辑。
三、文档加载(Loader):RAG的“数据入口”
3.1 Loader的核心作用
Loader是RAG系统的“数据入口”,其核心作用是将外部来源的原始数据(如网页、本地文件、PDF、Excel等)加载到系统中,并转换为统一的Document对象格式。Document对象通常包含两个核心部分:pageContent(文本内容)和metadata(元数据,如文档来源、创建时间、作者等),这种统一格式便于后续的分割、嵌入和检索操作。
在实际应用中,文档来源多种多样,不同来源的文档格式差异较大(如网页是HTML格式,PDF是二进制格式,本地文件有txt、docx等),因此需要针对不同的文档来源,选择对应的Loader。参考资料中主要聚焦于网页文档的加载,使用了LangChain生态中的CheerioWebBaseLoader,这也是实际开发中非常常用的一种Loader。
3.2 参考资料中Loader的代码解析
参考资料中使用CheerioWebBaseLoader加载掘金文章的网页内容,相关代码如下(保留核心代码,补充注释说明):
// 导入依赖包
import "dotenv/config"; // 加载环境变量(如API密钥、模型名称等)
import "cheerio"; // 后端DOM解析工具,用于解析网页HTML
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
// 1. 创建CheerioWebBaseLoader实例,指定要加载的网页URL和解析规则
const cheerioLoader = new CheerioWebBaseLoader(
"https://juejin.cn/post/7233327509919547452?searchId=20260302193603120AE3328025B138C1FB",
{
selector: ".main-area p", // CSS选择器,指定要提取的网页内容区域
}
)
// 2. 加载网页文档,返回Document对象数组
const documents = await cheerioLoader.load();
下面逐行解析这段代码的核心逻辑和注意事项:
-
依赖包导入:
- dotenv/config:用于加载.env文件中的环境变量,如OpenAI的API密钥、模型名称、接口基础URL等。在实际开发中,将敏感信息(如API密钥)放在.env文件中,避免直接硬编码在代码中,提高代码的安全性和可维护性。
- cheerio:一款后端DOM解析工具,其API与前端的jQuery非常相似,可以使用CSS选择器快速查找和提取网页中的特定内容。由于网页文档是HTML格式,直接加载后无法直接获取有效的文本内容,需要通过Cheerio解析HTML结构,提取出我们需要的文本(如文章正文)。
- CheerioWebBaseLoader:来自@langchain/community(LangChain社区模块),是LangChain生态中专门用于加载网页文档的Loader,内部集成了Cheerio解析工具,简化了网页文档的加载和解析流程。
-
CheerioWebBaseLoader实例化:
- 第一个参数是要加载的网页URL,这里选择了一篇掘金文章的URL,作为我们的外部知识库来源。
- 第二个参数是配置对象,其中selector是核心配置项,用于指定要提取的网页内容区域。这里使用的“.main-area p”是CSS选择器,表示提取class为“main-area”的元素下的所有p标签内容,也就是文章的正文段落。这一步非常关键,如果selector配置不当,可能会提取到无关内容(如网页导航、广告、评论等),影响后续的检索和回答质量。
-
加载文档:调用cheerioLoader.load()方法,该方法返回一个Promise,解析后得到一个Document对象数组。这里之所以是数组,是因为Loader可能会将一个网页拆分为多个Document对象(如按段落拆分),但在本案例中,由于我们指定了selector为“p”标签,load()方法会将所有p标签的内容合并为一个Document对象,其pageContent为所有段落的文本拼接,metadata中会包含网页的URL、标题等信息。
补充说明:参考资料中注释了console.log(documents),如果取消注释,运行代码后可以在控制台看到加载后的Document对象结构,便于调试和验证是否成功加载到了目标内容。
3.3 Loader的扩展知识点
参考资料中只使用了网页Loader,但在实际开发中,文档来源多种多样,LangChain生态提供了丰富的Loader类型,满足不同场景的需求。以下是常用的Loader分类及示例,帮助大家扩展知识面:
3.3.1 本地文件Loader
用于加载本地磁盘上的文件,支持txt、docx、pdf、csv等多种格式,常见的有:
- TextLoader:加载txt文本文件,是最基础的本地文件Loader。
- DocxLoader:加载Word文档(.docx格式),需要安装额外的依赖包(如docx)。
- PyPDFLoader:加载PDF文件,支持提取PDF中的文本内容,需要安装pypdf依赖包(注:LangChain的Python版本更常用,JavaScript版本也有对应的实现)。
- CSVLoader:加载CSV文件,适合处理结构化数据,可指定分隔符、编码等参数。
示例(TextLoader加载本地txt文件):
import { TextLoader } from "@langchain/community/document_loaders/fs/text";
const loader = new TextLoader("./docs/study-notes.txt");
const documents = await loader.load();
3.3.2 网页相关Loader
除了CheerioWebBaseLoader,LangChain还提供了其他网页Loader,适用于不同的网页场景:
- WebBaseLoader:通用网页Loader,内部可选择不同的解析器(如Cheerio、JSDOM),功能更灵活。
- PlaywrightWebBaseLoader:适用于动态渲染的网页(如使用React、Vue开发的单页应用),需要使用Playwright模拟浏览器加载,能够提取动态生成的内容(Cheerio只能解析静态HTML,无法处理动态渲染内容)。
3.3.3 数据库Loader
用于从关系型数据库(如MySQL、PostgreSQL)或非关系型数据库(如MongoDB)中加载数据,将查询结果转换为Document对象。例如:
- MySQLLoader:从MySQL数据库中加载数据,需要配置数据库连接信息和查询语句。
- MongoDBLoader:从MongoDB中加载文档,支持指定集合、查询条件等。
3.3.4 Loader使用注意事项
- 内容过滤:加载文档时,要注意过滤无关内容(如广告、导航、冗余信息),可以通过配置selector(网页Loader)、指定提取字段(数据库Loader)等方式实现,避免无关内容影响后续的分割和检索效率。
- 编码处理:加载本地文件时,要注意文件编码(如UTF-8、GBK),如果编码设置错误,可能会出现乱码问题,需要在Loader中指定编码参数。
- 异常处理:网页加载可能会出现网络异常、URL无效等问题,本地文件加载可能会出现文件不存在、权限不足等问题,需要添加异常捕获逻辑(如try-catch),提高系统的稳定性。
- 元数据补充:加载文档时,可以手动补充元数据(如文档类别、作者、创建时间等),元数据有助于后续的检索过滤(如只检索某一类文档)和结果展示(如显示文档来源)。
四、文档分割(Splitter):解决上下文窗口限制的关键
4.1 Splitter的核心作用
在加载完原始文档后,我们得到的Document对象通常包含较长的文本内容(如参考资料中加载的掘金文章,正文可能有几千字)。而大语言模型(如GPT-3.5、GPT-4)都存在上下文窗口限制(例如GPT-3.5的上下文窗口为4096 tokens,GPT-4的基础版为8192 tokens),无法直接处理过长的文本。此外,过长的文本转换为向量后,语义信息会变得模糊,相似性检索的准确性会下降;同时,过长的文本也会增加向量存储的成本和检索的耗时。
因此,文档分割(Splitter)的核心作用是:将加载后的原始文档(长文本)分割成多个短小、语义完整的文本片段(Chunk),每个Chunk的长度控制在大语言模型的上下文窗口范围内,同时保证每个Chunk的语义完整性,避免因分割导致语义断裂,影响后续的检索和回答质量。
文档分割的关键是“语义完整”和“长度合适”。如果分割过细,会导致单个Chunk的语义不完整,无法准确表达原文的含义,检索时可能无法找到与问题相关的片段;如果分割过粗,会导致单个Chunk过长,超出大语言模型的上下文窗口,同时影响检索效率和准确性。
4.2 参考资料中Splitter的代码解析
参考资料中使用了LangChain生态中的RecursiveCharacterTextSplitter(递归字符分割器),这是最常用、最灵活的一种文本分割器,尤其适合中文文本分割。相关代码如下(保留核心代码,补充注释说明):
// 导入递归字符分割器
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
// 1. 创建RecursiveCharacterTextSplitter实例,配置分割参数
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 400, // 每个Chunk的最大长度(单位:字符)
chunkOverlap: 50, // 相邻Chunk之间的重叠长度(单位:字符)
separators: ["。", ",", "!", "?"], // 分割符,优先使用这些符号分割
});
// 2. 对加载后的文档进行分割,返回分割后的Document对象数组
const splitDocuments = await textSplitter.splitDocuments(documents);
下面逐行解析这段代码的核心逻辑、参数含义和注意事项:
4.2.1 核心参数解析
-
chunkSize: 每个Chunk的最大长度,单位是字符(注意:不同的分割器可能有不同的单位,如有的按token计算)。参考资料中设置为400,意味着每个Chunk的文本长度不超过400个字符。这个参数的设置需要结合大语言模型的上下文窗口和文本类型来调整:
- 如果使用的大语言模型上下文窗口较小(如GPT-3.5),chunkSize可以设置小一些(如300-500字符);如果使用上下文窗口较大的模型(如GPT-4 Turbo),chunkSize可以设置大一些(如1000-2000字符)。
- 对于中文文本,每个字符大约对应1个token(英文单词通常对应1个token,中文句子的token数与字符数接近),因此按字符设置chunkSize是比较合理的。
-
chunkOverlap: 相邻两个Chunk之间的重叠长度,单位是字符。参考资料中设置为50,意味着前一个Chunk的最后50个字符,会与后一个Chunk的前50个字符重叠。这个参数的作用是保证分割后的Chunk之间的语义连贯性,避免因分割导致语义断裂。例如,原文是“我喜欢阅读科技类书籍,尤其是人工智能相关的书籍,因为这些书籍能够帮助我了解最新的技术发展趋势。”如果chunkSize设置为30,不设置chunkOverlap,可能会分割为“我喜欢阅读科技类书籍,尤其是人工智能相关的”和“书籍,因为这些书籍能够帮助我了解最新的技术发展趋势。”,导致语义断裂;而设置chunkOverlap后,前一个Chunk的末尾会与后一个Chunk的开头重叠,确保语义连贯。chunkOverlap的取值通常为chunkSize的10%-20%,参考资料中50/400=12.5%,符合这个合理范围。
-
separators: 分割符,用于指定分割文本时优先使用的符号。参考资料中设置了中文常用的标点符号:“。”“,”“!”“?”,这是非常贴合中文语义的设置。RecursiveCharacterTextSplitter的分割逻辑是:优先使用separators中的分割符进行分割,如果分割后的Chunk长度仍然超过chunkSize,就递归地使用更短的分割符(如没有指定的话,会使用默认的分割符,如空格、换行符等),直到Chunk长度符合要求。对于中文文本,使用“。”“,”等标点符号作为分割符,能够更好地保证每个Chunk的语义完整性,因为这些标点符号本身就是中文语义的自然分隔点(参考资料中也提到了“。,? 天然的语义分割点”)。
4.2.2 分割流程解析
textSplitter.splitDocuments(documents)方法的核心流程的是:
- 遍历输入的documents数组(参考资料中只有一个Document对象);
- 提取每个Document对象的pageContent(文本内容);
- 按照设置的separators、chunkSize和chunkOverlap,将文本分割成多个小的文本片段;
- 为每个分割后的文本片段创建一个新的Document对象,其pageContent为该片段的文本,metadata继承自原始Document对象(可手动补充或修改);
- 返回所有分割后的Document对象组成的数组(splitDocuments)。
参考资料中注释了console.log(splitDocuments),取消注释后可以在控制台看到分割后的每个Chunk的内容、长度和元数据,便于验证分割效果。同时,代码中打印了“切分后的文档数量: splitDocuments.length”,可以直观地看到分割后的Chunk数量,例如如果原始文档有1000个字符,chunkSize为400,chunkOverlap为50,那么大约会分割为3个Chunk(400 + 350 + 250,重叠部分不计入重复计数)。
4.3 Splitter的扩展知识点
参考资料中使用的RecursiveCharacterTextSplitter是最常用的分割器,但LangChain生态中还有其他类型的分割器,适用于不同的文本类型和场景。以下是常用的Splitter分类及示例,帮助大家扩展知识面:
4.3.1 按字符分割的Splitter(最常用)
- CharacterTextSplitter:基础的字符分割器,不支持递归分割,只能按照指定的分割符和chunkSize进行一次分割,灵活性不如RecursiveCharacterTextSplitter。适用于文本结构简单、长度均匀的场景。
- RecursiveCharacterTextSplitter:参考资料中使用的分割器,支持递归分割,优先使用指定的分割符,若分割后长度不达标,再使用更细的分割符,灵活性最高,适用于大多数场景(尤其是中文文本)。
4.3.2 按token分割的Splitter
由于大语言模型的上下文窗口是按token计算的,因此按token分割可以更精准地控制Chunk长度,避免因字符数与token数不匹配导致Chunk超出上下文窗口。常用的有:
- TokenTextSplitter:按token数量分割文本,需要指定chunkSize(token数)和chunkOverlap(token数),适用于对Chunk长度要求严格的场景。
- GPT2TokenizerTextSplitter:使用GPT-2的tokenizer进行分割,与OpenAI系列模型的token计算方式一致,精度更高。
示例(TokenTextSplitter):
import { TokenTextSplitter } from "@langchain/textsplitters";
const textSplitter = new TokenTextSplitter({
chunkSize: 500, // 每个Chunk的最大token数
chunkOverlap: 50, // 相邻Chunk的重叠token数
});
const splitDocuments = await textSplitter.splitDocuments(documents);
4.3.3 按结构分割的Splitter
对于有固定结构的文档(如PDF、Markdown、HTML),可以按照文档的结构进行分割,保证Chunk的语义完整性。常用的有:
- MarkdownTextSplitter:按Markdown的标题、段落结构进行分割,例如将每个一级标题、二级标题对应的内容作为一个独立的Chunk,适用于Markdown文档。
- HTMLTextSplitter:按HTML的标签结构进行分割,例如将每个div、p标签对应的内容作为一个Chunk,适用于HTML文档(与网页Loader结合使用效果更好)。
- PDFPlumberTextSplitter:按PDF的页面、段落结构进行分割,适用于PDF文档,能够保留PDF的排版结构信息。
4.3.4 分割的最佳实践
结合参考资料和实际开发经验,总结以下文档分割的最佳实践,帮助大家提升分割效果:
-
结合文本类型选择分割器:中文文本优先使用RecursiveCharacterTextSplitter,设置中文标点符号作为分割符;Markdown、PDF等结构化文档优先使用对应的结构分割器;对token长度要求严格的场景,使用按token分割的Splitter。
-
合理设置chunkSize和chunkOverlap:
- chunkSize:根据大语言模型的上下文窗口设置,通常为模型上下文窗口的1/4
1/2(例如GPT-3.5的上下文窗口为4096 tokens,chunkSize可设置为5001000 tokens),避免Chunk过长或过短。 - chunkOverlap:通常设置为chunkSize的10%~20%,保证语义连贯性,避免分割导致的语义断裂。
- chunkSize:根据大语言模型的上下文窗口设置,通常为模型上下文窗口的1/4
-
保留关键元数据:分割后的每个Chunk都要继承原始文档的元数据,必要时可以补充Chunk的序号、位置等信息,便于后续检索时定位原始文档。
-
过滤无效Chunk:分割后可能会产生一些空文本、冗余文本(如只有标点符号、空格的Chunk),需要过滤这些无效Chunk,避免影响检索效率和准确性。
-
测试分割效果:分割完成后,通过打印Chunk的内容、长度等信息,验证分割效果,确保每个Chunk的语义完整、长度合适,必要时调整分割参数。
五、向量嵌入(Embeddings):文本语义的“数字化转换”
5.1 Embeddings的核心作用
分割后的文本片段(Chunk)仍然是人类可阅读的文本形式,但计算机无法直接处理文本,无法判断两个文本片段之间的语义相似性。因此,需要将文本片段转换为计算机可处理的形式——向量(Embedding),这个过程就是向量嵌入(Embeddings)。
向量嵌入的核心原理是:通过预训练的Embeddings模型,将文本的语义信息映射到一个高维向量空间中,每个文本片段对应一个高维向量。语义相似的文本片段,其对应的向量在向量空间中的距离(如欧氏距离、余弦距离)更近;语义不相关的文本片段,其向量距离更远。这种特性是后续相似性检索的核心基础——用户输入问题后,将问题也转换为向量,然后在向量空间中找到与问题向量距离最近的文本片段,即为与问题最相关的参考资料。
Embeddings模型的选择直接影响向量的质量和检索的准确性。常用的Embeddings模型有OpenAI的Embeddings(如text-embedding-ada-002)、Hugging Face的开源模型(如all-MiniLM-L6-v2)、百度的ERNIE Embeddings等。参考资料中使用的是OpenAI的Embeddings模型,这也是实际开发中非常常用的一种模型,具有精度高、易用性强的特点。
5.2 参考资料中Embeddings的代码解析
参考资料中使用OpenAIEmbeddings创建向量嵌入实例,相关代码如下(保留核心代码,补充注释说明):
// 导入OpenAI相关依赖
import {
ChatOpenAI,
OpenAIEmbeddings
} from "@langchain/openai";
// 1. 创建OpenAIEmbeddings实例,配置模型参数
const embeddings = new OpenAIEmbeddings({
modelName: process.env.EMBEDDINGS_API_NAME, // Embeddings模型名称(从环境变量加载)
apiKey: process.env.OPENAI_API_KEY, // OpenAI API密钥(从环境变量加载)
configuration: {
baseURL: process.env.OPENAI_BASE_URL, // OpenAI API接口基础URL(从环境变量加载)
},
});
下面逐行解析这段代码的核心逻辑、参数含义和注意事项:
5.2.1 核心参数解析
- modelName: 指定使用的Embeddings模型名称。OpenAI提供了多种Embeddings模型,其中最常用的是text-embedding-ada-002,该模型具有精度高、速度快、成本低的特点,适用于大多数RAG场景。参考资料中从环境变量(EMBEDDINGS_API_NAME)加载模型名称,便于灵活切换模型,避免硬编码。补充:OpenAI的Embeddings模型返回的向量维度为1536维,这个维度是固定的,与输入文本的长度无关(只要文本长度不超过模型的限制)。
- apiKey: OpenAI的API密钥,用于调用OpenAI的Embeddings接口。由于API密钥属于敏感信息,参考资料中从.env文件加载(通过dotenv/config),避免直接硬编码在代码中,提高代码的安全性。注意:使用OpenAI的Embeddings服务需要付费,具体费用可参考OpenAI官网的定价标准,建议合理控制文本片段的数量和长度,降低成本。
- configuration.baseURL: OpenAI API接口的基础URL,用于指定调用的API地址。默认情况下,使用OpenAI官方的API地址(api.openai.com/v1),但在某些场景下…
5.2.2 Embeddings的使用场景
在参考资料的代码中,Embeddings主要用于两个场景:
- 向量数据库创建:在创建MemoryVectorStore时,需要将分割后的文本片段(splitDocuments)通过Embeddings转换为向量,然后存储到向量数据库中。相关代码如下:
const vectorStore = await MemoryVectorStore.fromDocuments(
splitDocuments,
embeddings
);
fromDocuments方法会自动遍历splitDocuments中的每个Document对象,提取其pageContent,通过embeddings模型转换为向量,然后将向量和对应的Document对象存储到MemoryVectorStore中。
- 问题向量转换:用户输入问题后,需要将问题转换为向量,才能在向量数据库中进行相似性检索。参考资料中虽然没有直接显示问题向量转换的代码,但在调用retriever.invoke(question)和vectorStore.similaritySearchWithScore(question,2)时,内部会自动将question通过embeddings模型转换为向量。
5.3 Embeddings的扩展知识点
5.3.1 常用的Embeddings模型分类
根据模型的部署方式和来源,常用的Embeddings模型可以分为两类:
-
云服务类Embeddings模型:
- OpenAI Embeddings:如text-embedding-ada-002,精度高、易用性强,但需要付费,且依赖网络连接。
- Azure OpenAI Embeddings:微软Azure云提供的OpenAI Embeddings服务,与OpenAI官方功能一致,适合企业级应用(有更好的稳定性和合规性)。
- 百度ERNIE Embeddings:百度提供的中文Embeddings模型,对中文文本的语义理解更精准,支持免费试用和付费使用。
- 阿里云通义千问Embeddings:阿里云提供的Embeddings模型,支持中文和英文,适合国内场景。
-
开源类Embeddings模型:
- Hugging Face开源模型:如all-MiniLM-L6-v2、all-MiniLM-L12-v2,体积小、速度快、可本地部署,适合对成本敏感、需要离线部署的场景。
- Sentence-BERT系列模型:专门用于句子嵌入的模型,语义理解精度高,支持多语言,开源免费,可本地部署。
- ChatGLM Embeddings:字节跳动开源的中文Embeddings模型,对中文文本的适配性好,可本地部署。
5.3.2 开源Embeddings模型的本地部署
参考资料中使用的是OpenAI的云服务类Embeddings模型,需要网络连接和付费。如果需要离线部署或降低成本,可以选择开源的Embeddings模型(如all-MiniLM-L6-v2),通过LangChain结合Hugging Face的transformers库进行本地部署。示例如下:
import { HuggingFaceEmbeddings } from "@langchain/community/embeddings/huggingface";
// 本地部署开源Embeddings模型
const embeddings = new HuggingFaceEmbeddings({
modelName: "all-MiniLM-L6-v2", // 模型名称
modelPath: "./models/all-MiniLM-L6-v2", // 本地模型路径(如果已下载)
cacheDir: "./models", // 模型缓存目录(自动下载模型到该目录)
});
注意:本地部署开源模型需要安装相关依赖包(如@langchain/community、@xenova/transformers),且需要一定的硬件资源(如CPU足够,若要提高速度可使用GPU)。
5.3.3 Embeddings使用注意事项
- 模型选择:根据场景选择合适的Embeddings模型。如果追求精度和易用性,且不介意付费,优先选择OpenAI Embeddings;如果需要离线部署、降低成本,优先选择开源模型;如果处理中文文本,优先选择对中文适配性好的模型(如百度ERNIE、ChatGLM Embeddings)。
- 文本长度限制:每个Embeddings模型都有输入文本的长度限制(如OpenAI的text-embedding-ada-002的最大输入长度为8191 tokens),因此分割后的Chunk长度必须小于模型的最大输入长度,否则会报错。
- API密钥管理:如果使用云服务类Embeddings模型,必须妥善管理API密钥,避免泄露,建议通过环境变量、配置文件等方式加载,不要硬编码在代码中。
- 成本控制:云服务类Embeddings模型按调用次数和文本长度收费,建议合理控制Chunk的数量和长度,避免不必要的调用,降低成本。
- 向量维度一致性:在同一个RAG系统中,必须使用同一个Embeddings模型,确保文本片段和问题的向量维度一致,否则无法进行相似性计算。
六、向量存储(Vector Store):向量的“专属数据库”
6.1 Vector Store的核心作用
将文本片段转换为向量后,需要将这些向量存储起来,便于后续快速进行相似性检索。传统的关系型数据库(如MySQL、PostgreSQL)主要用于存储结构化数据,支持基于字段的查询,但无法高效地进行向量相似性查询;而向量数据库(Vector Store)是专门为存储和查询向量而设计的数据库,能够高效地计算两个向量之间的距离,快速检索出与目标向量最相似的向量集合,是RAG系统中不可或缺的组成部分。
Vector Store的核心功能包括:
- 向量存储:将文本向量和对应的Document对象存储起来,建立向量与文本的映射关系。
- 相似性检索:根据目标向量(如用户问题的向量),快速检索出与目标向量最相似的若干个向量,并返回对应的Document对象。
- 向量更新和删除:支持向量的新增、修改和删除,便于更新知识库内容。
参考资料中使用的是MemoryVectorStore(内存向量存储),这是一种轻量级的向量存储,适合快速原型开发、测试和小型数据集场景。但需要注意的是,MemoryVectorStore的数据存储在内存中,程序重启后数据会丢失,不适合生产环境。在生产环境中,通常会使用专业的向量数据库(如Pinecone、Chroma、Milvus、FAISS等)。
6.2 参考资料中Vector Store的代码解析
参考资料中使用MemoryVectorStore创建向量数据库,并进行相似性检索,相关代码如下(保留核心代码,补充注释说明):
// 导入MemoryVectorStore
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
// 1. 创建向量数据库,将分割后的文档和Embeddings模型传入
console.log("正在创建向量数据库...");
const vectorStore = await MemoryVectorStore.fromDocuments(
splitDocuments, // 分割后的文本片段(Document对象数组)
embeddings // 向量嵌入模型
);
console.log("向量数据库创建完成");
// 2. 创建检索器(Retriever),用于后续的相似性检索
const retriever = await vectorStore.asRetriever({ k: 2 });
// 3. 相似性检索示例:根据问题检索最相似的2个文本片段
const questions = [ "作者的父亲的去世对作者有什么影响?" ]
for (const question of questions) {
// 方式1:使用检索器检索
const retrievedDocs = await retriever.invoke(question);
// 方式2:直接调用向量数据库的相似性检索方法,返回向量和相似度评分
const scoreResults = await vectorStore.similaritySearchWithScore(question,2);
}
下面逐行解析这段代码的核心逻辑、参数含义和注意事项:
6.2.1 向量数据库创建(fromDocuments方法)
MemoryVectorStore.fromDocuments()是创建内存向量数据库的核心方法,其核心逻辑是:
- 遍历splitDocuments中的每个Document对象,提取其pageContent;
- 通过传入的embeddings模型,将每个pageContent转换为向量;
- 将向量、对应的Document对象存储到内存中,建立映射关系;
- 返回创建好的MemoryVectorStore实例(vectorStore)。
该方法的两个核心参数:
- splitDocuments:分割后的文本片段(Document对象数组),是向量数据库的数据源。
- embeddings:向量嵌入模型,用于将文本转换为向量。
参考资料中通过console.log打印了创建过程,便于观察向量数据库的创建状态,这在开发和测试中非常实用。
6.2.2 检索器(Retriever)的创建
vectorStore.asRetriever({ k: 2 })方法用于创建检索器(Retriever),检索器是RAG系统中用于执行相似性检索的核心组件,其作用是简化检索操作,提供统一的检索接口。
核心参数k:指定每次检索返回的最相似的文本片段(Chunk)的数量,参考资料中设置为2,意味着每次检索会返回与问题最相似的2个Chunk。k的取值需要根据实际场景调整,k值太小可能会遗漏相关信息,k值太大可能会引入无关信息,增加大语言模型的处理成本,通常k的取值为2~5。
6.2.3 相似性检索的两种方式
参考资料中提供了两种相似性检索的方式,适用于不同的场景:
- 使用检索器检索(retriever.invoke(question)) : 这是最常用的检索方式,检索器会自动将问题(question)转换为向量,然后在向量数据库中检索出k个最相似的Document对象,返回一个Document对象数组(retrievedDocs)。这种方式简化了检索流程,不需要手动处理向量转换,适合大多数场景。
- 直接调用向量数据库的检索方法(similaritySearchWithScore) : 该方法不仅会返回最相似的Document对象,还会返回每个Document对象与问题向量的相似度评分(score)。参考资料中设置为检索2个最相似的片段(第二个参数为2),返回的scoreResults是一个数组,每个元素是一个包含[Document对象, 相似度评分]的数组。相似度评分(score)的取值范围通常为0~1(不同的向量数据库评分规则可能不同),score越接近0,说明相似度越高;score越接近1,说明相似度越低。参考资料中通过计算“1 - score”得到相似度(similarity),使结果更直观(相似度越接近1,说明越相关)。
6.3 Vector Store的扩展知识点
6.3.1 常用的Vector Store分类
根据部署方式和适用场景,常用的Vector Store可以分为三类:
-
内存型向量存储:
- 代表:MemoryVectorStore、Chroma(可配置为内存模式)。
- 特点:轻量级、速度快、部署简单,数据存储在内存中,程序重启后数据丢失。
- 适用场景:快速原型开发、测试、小型数据集(如参考资料中的场景)。
-
本地文件型向量存储:
- 代表:Chroma(本地文件模式)、FAISS(Facebook开源,可存储到本地文件)。
- 特点:数据存储在本地文件中,程序重启后数据不丢失,不需要依赖外部服务,部署简单。
- 适用场景:中小型应用、离线部署场景。
-
云服务型向量存储:
- 代表:Pinecone、Weaviate、Milvus Cloud、Chroma Cloud。
- 特点:支持大规模数据存储、高并发检索、自动扩容,不需要自己维护服务器,易用性强。
- 适用场景:生产环境、大规模数据集、高并发场景。
6.3.2 生产环境Vector Store的选择建议
参考资料中的MemoryVectorStore不适合生产环境,以下是生产环境中Vector Store的选择建议:
- 中小规模应用、低成本需求:选择Chroma(本地文件模式)或FAISS,部署简单、成本低,支持中等规模的向量存储和检索。
- 大规模应用、高并发需求:选择Pinecone或Milvus Cloud,支持大规模数据存储、高并发检索,具有完善的运维和扩容机制,适合企业级应用。
- 开源、可定制需求:选择Milvus或Weaviate,开源免费,可本地部署和定制,适合对数据隐私和定制化要求高的场景。
6.3.3 Vector Store使用注意事项
-
数据持久化:生产环境中,必须选择支持数据持久化的Vector Store(如Chroma、Pinecone、Milvus),避免程序重启后数据丢失。
-
检索性能优化:
- 合理设置k值,避免k值过大导致检索速度下降。
- 对向量进行索引优化(如FAISS的IVF_FLAT索引、Milvus的HNSW索引),提高检索速度。
- 控制向量的维度,维度越高,检索速度越慢,可选择维度适中的Embeddings模型(如1536维的OpenAI Embeddings)。
-
数据更新:知识库内容更新时,需要及时更新Vector Store中的向量(如新增、删除、修改Chunk对应的向量),确保检索结果的准确性。
-
多模态支持:如果需要处理图片、音频等多模态数据,需要选择支持多模态向量存储的Vector Store(如Weaviate、Milvus)。
七、检索与生成(Retrieval & Generation):RAG的“核心输出环节”
7.1 检索与生成的核心逻辑
检索与生成是RAG系统的最终输出环节,其核心逻辑是:将用户的问题通过检索器从向量数据库中获取相关的参考资料(文本片段),然后将参考资料和用户问题一起输入给大语言模型,让模型基于参考资料生成准确、相关的回答。这个过程既利用了向量检索的高效性(快速找到相关信息),又利用了大语言模型的语言生成能力(生成自然、流畅的回答),从而解决了大语言模型的知识时效性和幻觉问题。
参考资料中的代码完整实现了这一环节,涵盖了检索、相似度评分展示、Prompt构建和模型调用四个核心步骤,是非常典型的基础RAG问答实现。
7.2 参考资料中检索与生成的代码解析
参考资料中,检索与生成环节的代码紧密衔接前文的向量存储和检索器创建,完整实现了“检索-评分- Prompt构建-模型调用-回答输出”的全流程,相关核心代码如下(保留核心逻辑,补充详细注释,与前文代码风格保持一致):
// 导入ChatOpenAI(用于调用大语言模型生成回答)
import { ChatOpenAI } from "@langchain/openai";
// 1. 创建大语言模型实例,配置模型参数
const model = new ChatOpenAI({
modelName: process.env.OPENAI_API_MODEL, // 大语言模型名称(从环境变量加载,如gpt-3.5-turbo)
apiKey: process.env.OPENAI_API_KEY, // OpenAI API密钥(与Embeddings共用)
configuration: {
baseURL: process.env.OPENAI_BASE_URL, // API接口基础URL(与Embeddings共用)
},
temperature: 0.3, // 生成回答的随机性,0~1之间,值越小回答越精准、越固定
});
// 2. 定义用户问题(实际场景中可由用户输入,此处为示例)
const questions = [ "作者的父亲的去世对作者有什么影响?" ]
// 3. 遍历问题,执行检索与生成流程
for (const question of questions) {
console.log(`\n当前问题:${question}`);
// 步骤1:使用检索器检索最相似的2个文本片段(Chunk)
const retrievedDocs = await retriever.invoke(question);
console.log("检索到的相关参考资料:");
retrievedDocs.forEach((doc, index) => {
console.log(`参考资料${index+1}:`, doc.pageContent);
});
// 步骤2:直接调用向量数据库的检索方法,获取相似度评分
const scoreResults = await vectorStore.similaritySearchWithScore(question, 2);
console.log("检索结果相似度评分:");
scoreResults.forEach(([doc, score], index) => {
const similarity = (1 - score).toFixed(4); // 转换为相似度(越接近1越相关)
console.log(`参考资料${index+1} - 相似度:${similarity},内容:${doc.pageContent}`);
});
// 步骤3:构建Prompt(提示词),将问题和检索到的参考资料传入模型
// 核心思路:明确告知模型“基于参考资料回答,不编造信息,参考资料不足时说明”
const prompt = `请基于以下参考资料,准确回答用户问题,不要添加参考资料中没有的信息。如果参考资料不足以回答问题,请直接说明“参考资料不足,无法回答该问题”。
参考资料:${retrievedDocs.map(doc => doc.pageContent).join("\n\n")}
用户问题:${question}`;
// 步骤4:调用大语言模型,基于Prompt生成回答
const response = await model.invoke(prompt);
console.log("模型生成的回答:", response.content);
}
下面逐模块解析这段代码的核心逻辑、参数含义和注意事项,确保与前文代码解析的深度和风格一致,帮助大家完整理解检索与生成的实现流程:
7.2.1 大语言模型(ChatOpenAI)实例化
大语言模型是RAG系统中“生成”环节的核心,参考资料中使用了OpenAI的ChatOpenAI(对应Chat模型,如gpt-3.5-turbo、gpt-4),其核心参数解析如下:
- modelName:指定使用的大语言模型名称,从环境变量(OPENAI_API_MODEL)加载,便于灵活切换模型。常用的模型有gpt-3.5-turbo(性价比高,适合大多数基础场景)、gpt-4(精度高,适合复杂场景)、gpt-4-turbo(上下文窗口大,支持更长文本输入)。
- apiKey和configuration.baseURL:与前文Embeddings模型的配置一致,复用OpenAI的API密钥和接口地址,避免重复配置,同时保证代码的简洁性和可维护性。
- temperature:生成回答的随机性参数,取值范围为0~1。参考资料中设置为0.3,属于低随机性,目的是让模型生成的回答更精准、更贴合参考资料,避免出现“幻觉”和无关内容。如果需要更具创造性的回答,可以适当提高该值(如0.7);如果需要固定、严谨的回答(如专业领域问答),可降低该值(如0.1)。
补充说明:ChatOpenAI与OpenAI的Completion模型(如text-davinci-003)的核心区别在于,ChatOpenAI是基于对话式的模型,更适合处理问答场景,生成的回答更自然、流畅,且支持更长的上下文输入,是RAG系统中生成环节的首选。
7.2.2 检索结果的获取与展示
代码中使用了前文创建的检索器(retriever)和向量数据库(vectorStore),实现了两种检索结果的获取方式,并添加了日志打印,便于调试和观察检索效果:
- 通过检索器获取检索结果(retriever.invoke(question)) : 该方式返回的是最相似的k个Document对象数组(retrievedDocs),每个Document对象包含pageContent(文本内容)和metadata(元数据)。代码中通过forEach遍历,打印每个参考资料的内容,方便直观查看检索到的相关信息。
- 通过向量数据库获取带评分的检索结果(similaritySearchWithScore) : 该方式返回的是包含[Document对象, 相似度评分]的数组(scoreResults),其中评分(score)是向量之间的距离(如余弦距离),值越接近0,说明文本片段与问题的语义相似度越高。代码中通过“1 - score”将距离转换为相似度(取值0~1),让结果更直观,便于判断检索结果的相关性。这一步的核心作用是:不仅能获取参考资料,还能通过相似度评分筛选出最相关的内容,避免引入低相关性的Chunk,提高模型生成回答的准确性。在实际开发中,可以根据相似度评分设置阈值(如相似度≥0.7才作为参考资料),进一步优化检索效果。
7.2.3 Prompt构建:RAG生成质量的关键
Prompt(提示词)是大语言模型生成回答的“指令”,其构建质量直接影响回答的准确性和相关性。参考资料中构建的Prompt遵循了RAG场景的最佳实践,核心设计思路如下:
- 明确指令:告知模型“基于参考资料回答,不添加参考资料中没有的信息”,从根本上避免模型编造信息(幻觉问题)。
- 补充边界条件:明确说明“如果参考资料不足以回答问题,请直接说明无法回答”,避免模型在信息不足时强行生成错误回答。
- 清晰组织内容:将参考资料和用户问题分开呈现,参考资料用换行符分隔,便于模型快速识别和提取相关信息,降低模型理解成本。
补充说明:Prompt的构建需要结合具体场景优化。例如,在专业领域(如医疗、法律),可以在Prompt中添加“回答需严谨,引用参考资料中的关键语句”;在多轮问答场景,可以添加“结合历史对话内容和参考资料回答”。合理的Prompt设计,能让模型更好地发挥作用,提升RAG系统的回答质量。
7.2.4 模型调用与回答输出
调用model.invoke(prompt)方法,将构建好的Prompt传入大语言模型,模型会基于Prompt中的参考资料和问题,生成对应的回答。该方法返回一个包含回答内容(content)、模型信息、使用token数等信息的对象,代码中通过response.content获取最终的回答内容,并打印输出。
注意事项:
- token数控制:Prompt的总token数(参考资料+问题+指令)不能超过大语言模型的上下文窗口限制,否则会报错。因此,需要合理控制检索的Chunk数量(k值)和每个Chunk的长度(chunkSize),确保Prompt的总token数在模型限制范围内。
- 异常处理:模型调用可能会出现API调用失败、网络异常等问题,实际开发中需要添加try-catch异常捕获逻辑,例如:
try {
const response = await model.invoke(prompt);
console.log("模型生成的回答:", response.content);
} catch (error) {
console.error("模型调用失败:", error.message);
}
7.3 检索与生成的扩展知识点
参考资料中的检索与生成实现是基础版本,适用于简单的单轮问答场景。在实际开发中,需要根据具体需求进行优化和扩展,以下是常用的扩展知识点和最佳实践,帮助大家提升RAG系统的性能和用户体验:
7.3.1 检索策略的优化
基础版本中使用的是“相似性检索”(基于向量距离),适用于简单场景,但在复杂场景中,检索精度和效率可能不足。常用的优化策略如下:
- 混合检索:结合“关键词检索”和“相似性检索”,先通过关键词检索筛选出大致相关的文档,再通过相似性检索获取最相关的Chunk,既保证检索效率,又提升检索精度。例如,使用Elasticsearch(关键词检索)结合Milvus(向量检索),实现混合检索。
- 检索重排序:对检索到的Chunk进行二次排序,除了向量相似度,还可以结合Chunk的相关性分数、元数据(如文档更新时间)、用户历史交互数据等,提升检索结果的相关性。常用的重排序算法有BM25、Cross-BERT等。
- 多轮检索:针对复杂问题(如多步骤、多维度问题),通过多轮检索逐步获取相关信息。例如,先将复杂问题拆解为多个子问题,分别检索每个子问题的相关资料,再将所有资料整合,输入模型生成回答。
7.3.2 Prompt工程的优化
Prompt的质量直接影响回答质量,除了参考资料中的基础Prompt,以下是常用的Prompt优化技巧:
- 添加角色设定:在Prompt中给模型设定明确的角色,例如“你是一名RAG问答助手,擅长基于参考资料准确回答用户问题,语言简洁、严谨,不编造信息”,让模型更贴合场景需求。
- 结构化Prompt:使用固定的格式组织Prompt,例如分“指令、参考资料、问题、输出要求”四个部分,让模型更容易理解和遵循。例如:
【指令】请基于以下参考资料,准确回答用户问题,严格遵循以下要求: `` 1. 只使用参考资料中的信息,不添加任何额外内容; `` 2. 回答简洁明了,分点说明(如果适用); `` 3. 参考资料不足时,直接说明“参考资料不足,无法回答”。 ```` 【参考资料】 `` {参考资料内容} ```` 【用户问题】 `` {用户问题} ```` 【输出要求】 ``简洁、严谨、准确,不冗余。 - few-shot Prompt:在Prompt中添加少量示例(问题+参考资料+正确回答),让模型更清楚如何基于参考资料回答问题,尤其适用于专业领域或复杂场景。例如:
【示例1】 `` 参考资料:RAG技术的核心环节包括Loader、Splitter、Embeddings、Vector Store、Retrieval & Generation。 `` 用户问题:RAG技术的核心环节有哪些? `` 回答:RAG技术的核心环节包括文档加载(Loader)、文档分割(Splitter)、向量嵌入(Embeddings)、向量存储(Vector Store)、检索与生成(Retrieval & Generation)。 ```` 【指令】请基于以下参考资料,按照示例格式准确回答用户问题... `` 【参考资料】{参考资料内容} ``【用户问题】{用户问题}
7.3.3 大语言模型的选择与优化
除了OpenAI的模型,实际开发中还可以根据场景选择其他大语言模型,同时进行针对性优化:
- 模型选择: 云服务类模型:除了OpenAI,还可以选择百度文心一言、阿里云通义千问、字节跳动火山方舟等,这些模型对中文的适配性更好,且部分支持国内部署,解决网络访问问题。
- 开源类模型:如Llama 2、ChatGLM 3、Qwen等,可本地部署,避免网络依赖和API费用,适合对数据隐私和成本敏感的场景。
- 模型优化: 上下文窗口扩展:对于长文本参考资料,选择上下文窗口大的模型(如GPT-4 Turbo、Llama 2 70B),或使用文本压缩技术,减少参考资料的冗余内容,确保Prompt能完整输入模型。
- 模型微调:如果RAG系统针对特定领域(如医疗、金融),可以使用该领域的数据集对模型进行微调,让模型更熟悉领域知识,提升回答的专业性和准确性。
7.3.4 多轮问答的实现
参考资料中的实现是单轮问答(用户提出一个问题,系统返回一个回答),但实际场景中,用户可能会进行多轮对话(基于上一轮的回答继续提问)。多轮问答的核心是“保留对话历史”,并将对话历史与参考资料、当前问题一起传入模型,实现流程如下:
// 初始化对话历史(数组形式,存储每一轮的问题和回答)
let chatHistory = [];
// 多轮问答示例
const multiRoundQuestions = [
"作者的父亲的去世对作者有什么影响?",
"这种影响具体体现在哪些方面?",
"作者是如何应对这种影响的?"
];
for (const question of multiRoundQuestions) {
// 1. 检索相关参考资料
const retrievedDocs = await retriever.invoke(question);
// 2. 构建Prompt,包含对话历史、参考资料和当前问题
const historyText = chatHistory.map((item) => `用户:${item.question}\n助手:${item.answer}`).join("\n");
const prompt = `请基于以下对话历史和参考资料,准确回答用户当前的问题,不要添加无关信息。
对话历史:${historyText || "无"}
参考资料:${retrievedDocs.map(doc => doc.pageContent).join("\n\n")}
当前用户问题:${question}`;
// 3. 调用模型生成回答
const response = await model.invoke(prompt);
const answer = response.content;
// 4. 更新对话历史
chatHistory.push({ question, answer });
// 5. 输出结果
console.log(`用户:${question}`);
console.log(`助手:${answer}\n`);
}
多轮问答的关键是合理管理对话历史,避免对话历史过长导致Prompt超出模型上下文窗口。可以通过以下方式优化:
- 对话历史压缩:只保留最近几轮的对话(如最近3轮),或对对话历史进行摘要压缩,减少token占用。
- 结合检索:每一轮问答都重新检索相关参考资料,确保回答的准确性,避免依赖上一轮的回答(可能存在误差)。
7.4 检索与生成环节的常见问题及解决方案
在实际开发中,检索与生成环节容易出现一些问题,影响RAG系统的性能和用户体验。结合参考资料和实际开发经验,总结以下常见问题及解决方案:
7.4.1 问题1:检索结果不相关,导致模型回答偏离问题
表现:检索到的Chunk与用户问题语义相关性低,模型基于这些无关资料生成的回答偏离用户需求。
解决方案:
- 优化文档分割:调整chunkSize、chunkOverlap和separators,确保每个Chunk的语义完整,避免语义模糊。
- 优化Embeddings模型:选择对中文语义理解更精准的模型(如百度ERNIE、ChatGLM Embeddings),或更换维度更高的模型,提升向量的语义表达能力。
- 添加检索阈值:设置相似度阈值(如相似度≥0.7),只将符合阈值的Chunk作为参考资料,过滤低相关性的Chunk。
- 优化检索策略:采用混合检索(关键词+向量检索),提升检索精度。
7.4.2 问题2:模型生成回答时,未完全基于参考资料,出现幻觉
表现:模型生成的回答中包含参考资料中没有的信息,或与参考资料内容不符。
解决方案:
- 优化Prompt:明确告知模型“只能使用参考资料中的信息,不编造内容”,添加惩罚机制(如“编造信息将被扣分”)。
- 降低temperature值:将temperature设置为0.1~0.3,减少模型的随机性,让回答更贴合参考资料。
- 添加参考资料引用:让模型在回答中引用参考资料的关键语句,便于验证回答的准确性,同时约束模型不编造信息。
- 过滤无效参考资料:删除空文本、冗余文本等无效Chunk,避免模型从无效信息中“编造”内容。
7.4.3 问题3:模型调用速度慢,用户等待时间长
表现:调用大语言模型生成回答时,响应时间过长(超过5秒),影响用户体验。
解决方案:
- 选择轻量型模型:如gpt-3.5-turbo替代gpt-4,或使用开源的轻量型模型(如ChatGLM 3-6B),提升响应速度。
- 优化检索速度:使用性能更好的向量数据库(如Pinecone、Milvus),添加向量索引,减少检索耗时。
- 减少参考资料数量:合理设置k值(如2~3),避免过多的Chunk传入模型,减少模型处理时间。
- 添加缓存机制:将常见问题的检索结果和模型回答缓存起来,当用户再次提出相同问题时,直接返回缓存结果,无需重新检索和调用模型。
7.4.4 问题4:Prompt超出模型上下文窗口,导致报错
表现:参考资料过多、对话历史过长,导致Prompt的总token数超出模型的上下文窗口限制,出现报错。
解决方案:
- 优化文档分割:减小chunkSize,让每个Chunk的token数更少,减少参考资料的总token数。
- 限制参考资料数量:降低k值,只保留最相关的几个Chunk(如2~3个)。
- 压缩对话历史:只保留最近几轮对话,或对对话历史进行摘要压缩。
- 选择上下文窗口大的模型:如GPT-4 Turbo(上下文窗口可达128k tokens),支持更长的Prompt输入。
八、完整RAG系统的测试与调试
8.1 测试的核心目的
参考资料中的代码实现了一个基础的RAG系统,但在实际部署前,需要进行充分的测试与调试,确保系统的稳定性、准确性和易用性。测试的核心目的是:
- 验证每个环节的功能是否正常:确保Loader能正确加载文档、Splitter能正确分割文本、Embeddings能正确转换向量、Vector Store能正确存储和检索、检索与生成能正确返回回答。
- 提升系统的准确性:减少检索结果的相关性误差和模型回答的幻觉问题,确保回答贴合参考资料和用户需求。
- 优化系统的性能:降低检索和模型调用的耗时,提升系统的响应速度。
- 排查潜在问题:如网络异常、API调用失败、参数配置错误等,确保系统在实际场景中能稳定运行。
8.2 测试的主要内容和方法
8.2.1 各环节功能测试
- Loader测试: 测试方法:更换不同的文档来源(如网页、本地txt、PDF),调用Loader加载文档,打印Document对象,验证是否能正确提取文本内容和元数据,是否过滤了无关信息。测试要点:验证不同格式文档的加载效果、编码处理是否正确、异常情况(如无效URL、文件不存在)的处理是否合理。
- Splitter测试: 测试方法:使用不同长度、不同类型的文本(如中文长文本、英文文本、结构化文本),调用Splitter进行分割,打印分割后的Chunk,验证Chunk的长度、语义完整性和重叠情况。测试要点:验证chunkSize、chunkOverlap、separators参数的设置是否合理,分割后的Chunk是否符合预期。
- Embeddings测试: 测试方法:将不同语义的文本片段转换为向量,计算向量之间的余弦距离,验证语义相似的文本向量距离是否更近,语义不相关的文本向量距离是否更远。测试要点:验证Embeddings模型的语义表达能力,向量维度是否正确,API调用是否稳定。
- Vector Store测试: 测试方法:将分割后的Chunk存储到Vector Store中,输入不同的问题,进行相似性检索,验证检索结果的相关性和评分的合理性,测试数据的新增、删除、更新功能是否正常。测试要点:验证检索速度、检索精度,数据持久化(非内存型Vector Store)是否正常。
- 检索与生成测试: 测试方法:准备不同类型的测试问题(简单问题、复杂问题、专业问题、无关问题),调用检索与生成流程,验证回答的准确性、相关性和流畅性,排查幻觉问题。测试要点:验证Prompt的有效性、模型参数的合理性,回答是否完全基于参考资料,无关问题是否能正确提示“无法回答”。
8.2.2 性能测试
性能测试主要关注系统的响应速度和并发处理能力,测试方法如下:
- 响应速度测试:记录每个环节的耗时(Loader加载耗时、Splitter分割耗时、Vector Store检索耗时、模型调用耗时),计算总响应时间,确保总响应时间控制在用户可接受范围内(如≤3秒)。
- 并发测试:模拟多个用户同时提出问题,测试系统的并发处理能力,验证是否会出现卡顿、报错、响应延迟增加等问题,确保系统能支持一定量的并发请求。
优化方向:针对耗时较长的环节进行优化(如优化检索速度、使用更快的模型、添加缓存),提升系统的整体性能。
8.2.3 异常测试
异常测试主要排查系统在异常情况下的表现,确保系统的稳定性,测试场景如下:
- 网络异常:模拟网络中断、网络延迟,测试系统是否能正确捕获异常,给出友好提示,避免崩溃。
- API调用异常:模拟OpenAI API密钥错误、API接口不可用,测试系统是否能正确处理,给出错误提示。
- 输入异常:模拟用户输入空问题、超长问题、无意义问题,测试系统是否能正确处理,避免报错。
- 数据异常:模拟文档内容为空、文档格式错误、Chunk长度超出Embeddings模型限制,测试系统是否能正确处理。
8.3 调试技巧
在测试过程中,遇到问题时,可以使用以下调试技巧快速排查问题:
- 添加日志打印:在每个环节的关键位置添加console.log,打印相关数据(如Document对象、分割后的Chunk、检索结果、Prompt、模型回答等),直观查看数据流转过程,定位问题所在。
- 逐步排查:如果系统出现问题,不要直接排查整个流程,而是逐步排查每个环节。例如,先验证Loader是否加载正确,再验证Splitter是否分割正确,依次排查,缩小问题范围。
- 参数调试:针对性能和准确性问题,通过调整参数(如chunkSize、k值、temperature、相似度阈值),对比不同参数的效果,找到最优参数组合。
- 简化测试场景:将复杂的测试场景简化为简单场景(如使用短文本、简单问题),先确保简单场景能正常运行,再逐步增加场景复杂度。
九、学习总结与后续展望
9.1 学习总结
通过本次学习,结合参考资料中的完整代码,我系统掌握了RAG技术的核心流程、每个环节的原理、实现方式和注意事项,完成了一份完整的学习笔记,主要总结如下:
- RAG技术核心认知:RAG技术通过“检索+生成”的模式,解决了大语言模型的知识时效性和幻觉问题,核心是将外部知识库的相关信息作为参考,让模型基于真实信息生成回答,提升回答的准确性和可信度。
- 核心流程拆解:RAG系统的核心流程包括5个环节——文档加载(Loader)、文档分割(Splitter)、向量嵌入(Embeddings)、向量存储(Vector Store)、检索与生成(Retrieval & Generation),每个环节环环相扣,缺一不可。 Loader:作为数据入口,负责加载外部文档并转换为统一的Document对象,需根据文档来源选择合适的Loader。
- Splitter:解决大语言模型的上下文窗口限制,将长文本分割为语义完整、长度合适的Chunk,核心是平衡“语义完整”和“长度合适”。
- Embeddings:将文本转换为高维向量,实现文本语义的数字化,向量的质量直接影响检索精度,需选择合适的Embeddings模型。
- Vector Store:专门存储向量,支持高效的相似性检索,需根据场景选择内存型、本地文件型或云服务型向量数据库。
- Retrieval & Generation:核心输出环节,通过检索获取参考资料,构建Prompt并调用大语言模型生成回答,关键是优化检索精度和Prompt质量。
- 代码实践收获:参考资料中的代码实现了一个基础的RAG系统,通过逐行解析代码,我掌握了LangChain生态的使用方法(如Loader、Splitter、Embeddings、Vector Store的调用),学会了如何结合OpenAI的API实现检索与生成,同时掌握了代码的调试和优化技巧。
- 常见问题与解决方案:总结了每个环节的常见问题(如检索结果不相关、模型幻觉、响应速度慢等),并给出了对应的解决方案,为后续实际开发提供了参考。
本次学习的核心收获是:RAG技术的关键在于“检索的精度”和“生成的准确性”,每个环节的优化都会影响系统的整体性能。基础RAG系统的实现难度不大,但要构建一个高性能、高准确性的RAG系统,需要结合场景进行针对性的优化,同时不断积累实践经验。
9.2 后续展望
本次学习主要聚焦于基础RAG系统的实现,后续可以从以下几个方面进一步深入学习和实践,提升RAG技术的应用能力:
- 复杂RAG系统的构建:学习构建更复杂的RAG系统,如多模态RAG(支持图片、音频等多模态文档)、跨知识库RAG(整合多个不同来源的知识库)、多轮对话RAG(优化对话历史管理,提升多轮问答体验)。
- 高级检索策略的学习:深入学习混合检索、检索重排序、多轮检索等高级检索策略,提升检索精度和效率;学习向量数据库的高级特性(如索引优化、数据分片),适应大规模数据集场景。
- 模型优化与本地化部署:学习开源大语言模型(如Llama 2、ChatGLM 3)和开源Embeddings模型的本地化部署,掌握模型微调技巧,解决网络依赖和API费用问题,适应数据隐私要求高的场景。
- 实际项目实践:结合具体场景(如企业知识库问答、个人学习助手、客服问答系统),构建完整的RAG项目,将理论知识转化为实际应用,积累项目开发经验。
- RAG技术的前沿动态:关注RAG技术的前沿发展(如RAG与Agent的结合、自适应检索、动态知识库更新等),不断学习新的技术和方法,提升自身的技术竞争力。
总之,RAG技术是大语言模型应用的重要方向,具有广泛的应用场景和发展前景。通过本次系统学习,我奠定了RAG技术的基础,后续将通过持续学习和实践,不断提升自己的技术能力,更好地应用RAG技术解决实际问题。