前言#
在开始讲解什么是 RAG 之前,先讨论讨论现在我们用的这些模型或者说 LLM 有哪些缺点:
- 在没有答案的情况下提供虚假信息。(会胡说八道)
- 当用户需要特定的当前响应时,提供过时或通用的信息。(因为模型有训练的截止日期)
- 从非权威来源创建响应。(因为训练的内容来自于互联网,所以可能会有一些不准确的内容)
- 由于术语混淆,不同的培训来源使用相同的术语来谈论不同的事情,因此会产生不准确的响应。
还有一个重要的也比较现实的原因 —— 我们自己有一些私人的数据,这些数据市面上的一些模型是肯定没有对我们的数据进行训练,所以单我们需要对自己的数据集进行询问就是一个不可能的。
所以 RAG(检索增强生成) 就出现了 —— 它通过修改与大型语言模型(LLM)的交互,使模型在回应用户查询时参考指定的文档集,优先使用这些信息,而不是从自身庞大的静态训练数据中提取信息。这样,LLMs 就可以使用特定领域和 / 最新信息。 用例包括让聊天机器人访问公司内部数据,或只提供来自权威来源的事实信息。
流程#
- 首先,需要将我们的一些知识数据使用向量模型转化为向量,然后保存在向量数据库当中
- 剩下的就是用户输入,然后我们会将用户的数据转化为向量,使用用户输入的向量对现在的向量数据库进行检索(Retrieval),按照相似度匹配找出最接近的几条数据,使用大模型进行将检索出的数据源和用户需求的问题组合(Augmented)一起输入给大模型进行生成(Generation),这样 —— 完整的 RAG 就出来啦。
代码实现#
作为一个前端开发来说,最熟悉的也就是 JavaScript,刚好市面上比较出名的langchain有 js 的版本,使得前端开发进入 AI 开发变得更加的容易,废话不多说现在就开始吧!
简单使用#
接下来,我们通过代码展示如何使用 RAG。为了简单起见,我们采用 LangChain 的 JavaScript 版本,并在本地使用 llama3:3b 模型。你可以根据需要替换其他模型。
import { ChatOllama } from "@langchain/ollama";
import { StringOutputParser } from '@langchain/core/output_parsers'
const llm = new ChatOllama({
model: 'llama3.2:3b-instruct-fp16',
temperature: 0, // 控制生成内容的随机性
});
const res = await llm.invoke('你好');
// 对输出文本进行格式化
const parser = new StringOutputParser();
const parsed = await parser.invoke(res);
console.log(parsed);
// 输出:您好!我是你的AI助手,欢迎来到我们的对话中。有什么需要帮助的吗?
加载文档#
因为我们的目的是对自己的数据进行增强搜索,那么第一步就是需要加载文档,langchain 提供了许多加载不同文档的方法(文档加载),基本上包含了市面上所有的文档,有兴趣的可以去查看一下。本次我们采用的是 Web 远程加载文档。
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
const loader = new CheerioWebBaseLoader("https://goodcheng.wang/web3", {
selector: 'article' // 指定加载内容的区域
});
const doc = await loader.load();
对文档进行分割#
为什么要分割文档呢?直接对整个文档进行嵌入(embedding)看似简单,但实际上这样做并不高效。原因如下:
- 内存与计算限制:大规模文档(如整本书、长篇报告等)通常非常庞大,如果直接将整个文档进行 embedding,计算资源和内存消耗会非常高。有一个显而易见的问题 —— 我们所用到的模型不支持这么庞大的输入。
- 上下文窗口限制:现有的大多数自然语言处理(NLP)模型在处理文本时有一定的上下文窗口大小限制,即模型只能处理一定数量的 token。如果文本超出了该窗口大小,超出部分就会被截断或忽略,导致丢失大量信息。因此,将文档分割成小块,每块的长度适应模型的上下文限制,可以确保模型在生成 embedding 时能够充分利用输入的上下文信息。
- 提升检索与匹配精度: 对文档进行分割还可以提高信息检索的精度。将整个文档 embedding 后,查询特定问题时,embedding 可能会涵盖太多不相关的信息,导致匹配不精确。相反,如果对文档进行分割并对每个分段进行 embedding,查询时可以只检索到与问题最相关的段落,结果更加精准。
- and so on
在这里我们将文本没 500 个字符进行分割,相邻 100 个字符作为一个 “块” 。感兴趣的可以在这个网站上自己摸索Text-splitter demo。
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const splitter = new RecursiveCharacterTextSplitter({
chunkOverlap: 100, // 相邻块的重叠字符数,增强连续性
chunkSize: 500, // 每块的字符数
});
const allSplits = await splitter.splitDocuments(doc);
console.log(allSplits[0]); // 打印分割后的第一个块
将内容 embedding 存储到向量数据库当中#
接下来,将分割后的文档内容进行嵌入,并存储在向量数据库中。在这个示例中,我们使用内存中的向量数据库,实际应用中可以替换为其他存储方案。。VectorStore
import { OllamaEmbeddings } from "@langchain/ollama";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
const embeddings = new OllamaEmbeddings({
model: "mxbai-embed-large", // Default value
baseUrl: "http://localhost:11434", // Default value
});
const vectorStore = await MemoryVectorStore.fromDocuments(allSplits, embeddings)
const vectorStoreRetriever = vectorStore.asRetriever({
k: 5,
searchType: 'similarity',
})
const find = await vectorStoreRetriever.invoke("什么是web3.0")
console.log(find)
整合成 RAG 链#
最后,我们把上述所有步骤整合成一个完整的处理链。首先为生成的链编写提示词模板:
import {ChatPromptTemplate} from '@langchain/core/prompts'
const ragPrompt = ChatPromptTemplate.fromMessages([
[
'human',
`你是问题解答任务的助手。使用以下检索到的上下文回答问题。如果不知道答案,就说不知道。最多使用三句话,答案要简明扼要。
问题: {question}
上下文: {context}
答案:`
]
])
然后,使用 RunnableSequence 对流程进行链接:
import { formatDocumentsAsString } from "langchain/util/document";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
const runnableRagChain = RunnableSequence.from([
{
context: vectorStoreRetriever.pipe(formatDocumentsAsString), // 检索到的上下文
question: new RunnablePassthrough(), // 用户问题
},
ragPrompt, // 生成的提示词
llm, // 大语言模型
new StringOutputParser(), // 解析生成的结果
]);
const res = await runnableRagChain.invoke('web2.0的代表网站有哪些?');
console.log(res);
// 输出:Facebook和Twitter是Web2.0的著名网站。
效果还不错~
总结#
通过 RAG(检索增强生成),我们能够提升现有大语言模型的回答准确性,尤其是在需要最新或特定领域数据的场景中。本文通过 LangChain 的 JavaScript 实现展示了如何从文档加载、分割、嵌入到最终生成的整个流程。