初探AI开发之RAG

前言

在开始讲解什么是 RAG 之前，先讨论讨论现在我们用的这些模型或者说 LLM 有哪些缺点：

在没有答案的情况下提供虚假信息。（会胡说八道）

当用户需要特定的当前响应时，提供过时或通用的信息。（因为模型有训练的截止日期）

从非权威来源创建响应。（因为训练的内容来自于互联网，所以可能会有一些不准确的内容）

由于术语混淆，不同的培训来源使用相同的术语来谈论不同的事情，因此会产生不准确的响应。

还有一个重要的也比较现实的原因 —— 我们自己有一些私人的数据，这些数据市面上的一些模型是肯定没有对我们的数据进行训练，所以单我们需要对自己的数据集进行询问就是一个不可能的。

所以 RAG（检索增强生成） 就出现了 —— 它通过修改与大型语言模型（LLM）的交互，使模型在回应用户查询时参考指定的文档集，优先使用这些信息，而不是从自身庞大的静态训练数据中提取信息。这样，LLMs 就可以使用特定领域和 / 最新信息。 用例包括让聊天机器人访问公司内部数据，或只提供来自权威来源的事实信息。

流程

首先，需要将我们的一些知识数据使用向量模型转化为向量，然后保存在向量数据库当中

剩下的就是用户输入，然后我们会将用户的数据转化为向量，使用用户输入的向量对现在的向量数据库进行检索（Retrieval），按照相似度匹配找出最接近的几条数据，使用大模型进行将检索出的数据源和用户需求的问题组合（Augmented）一起输入给大模型进行生成（Generation），这样 —— 完整的 RAG 就出来啦。

代码实现

作为一个前端开发来说，最熟悉的也就是 JavaScript，刚好市面上比较出名的langchain有 js 的版本，使得前端开发进入 AI 开发变得更加的容易，废话不多说现在就开始吧！

简单使用

接下来，我们通过代码展示如何使用 RAG。为了简单起见，我们采用 LangChain 的 JavaScript 版本，并在本地使用 llama3:3b 模型。你可以根据需要替换其他模型。

Copy
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 远程加载文档。

Copy
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。

Copy
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

Copy
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 链

最后，我们把上述所有步骤整合成一个完整的处理链。首先为生成的链编写提示词模板：

Copy

import {ChatPromptTemplate} from '@langchain/core/prompts'

const ragPrompt = ChatPromptTemplate.fromMessages([
[
'human',
`你是问题解答任务的助手。使用以下检索到的上下文回答问题。如果不知道答案，就说不知道。最多使用三句话，答案要简明扼要。
问题: {question}
上下文: {context}
答案:`
]
])

然后，使用 RunnableSequence 对流程进行链接：

Copy
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 实现展示了如何从文档加载、分割、嵌入到最终生成的整个流程。