大家好,
在这篇文章中,我将解释如何使用Node.js、LangChain库和RAG技术构建您自己的生成式人工智能大型语言模型API生态系统,以便能够回答关于这些文档的问题。本文的目的是提供构建一个全面的、生产就绪的REST API生态系统的综合知识,您可以轻松地将其与任何产品集成在一起。RAG技术是指检索增强生成。
前提条件- 在您的PC上安装Node.js的最新版本
- 基本的JavaScript知识
- OpenAI API密钥
-
- 热情
如果你已经对RAG技术有一定了解,你可以跳过这一部分内容。
我们正在从一个指令驱动的时期过渡到一个意图驱动的时代,在这个时代,生成式人工智能大模型(LLMs)正在越来越多地影响软件开发的各个方面,并提升人类体验。然而,LLMs 有一些局限性。
1. 幻觉现象
LLM是在公开可用的数据上接受训练的,可能无法访问独特的或私有的特定领域信息。如果你询问与这些私有知识相关的问题时,LLM可能会提供完全错误的答案,这种现象被称为幻觉现象。
2. 现实世界数据的限制
大型语言模型并不总是能够及时反映现实世界的数据。例如,截至撰写本文件时,OpenAI的ChatGPT 4模型仅包含截至2021年9月的数据,任何在此日期之后发生的事情都超出了它的知识范畴。
为了缓解这些挑战,引入了RAG。通过采用检索技术,我们可以向LLM提供相关上下文,并基于此上下文提出问题。然而,由于提示长度的限制,我们无法直接将全部上下文输入到LLM中。
为克服这个问题,我们处理上下文源,并以适合提示的方式组织数据。例如,如果你有一个包含100万字的文档,我们会将其拆分成每1,000字的片段,例如。通过检索过程,我们识别与问题相关的片段。然后,我们将识别出的相关片段和问题一起提供给大语言模型,要求它根据提供的上下文回答问题。
对于检索部分,我们使用嵌入机制。每个1,000词的块被嵌入到数值向量中并存储在向量数据库中。在向量数据库中,相关的嵌入词彼此靠近。例如,“Apple”和“Phone”可能彼此靠近,但“Orange”和“Phone”则不会。当用户提出新查询时,我们嵌入该问题,并从向量数据库中查找与问题相关的词汇块。
RAG 是一种简单却十分强大的机制,能够克服大型语言模型(LLM,即大型语言模型)的局限性。
增强检索生成(RAG)生态系统
构建 Node.js Express API服务器我们正在构建一个统一的API生态系统,负责所有数据处理任务,该系统可以分为两个部分:数据预处理和数据检索。
数据预处理步骤: 这包括文档分块、嵌入处理以及将分块存储在向量数据库中。
数据检索: 当用户提出问题时,Node服务器从与问题相关的向量数据库中检索相关数据。使用LangChain的RetrievalQAChain与OpenAI的ChatGPT模型连接,服务器生成与文档上下文相关的响应。
- 创建一个Node.js项目并安装所需的模块
要构建一个Node.js Express服务器,你需要创建一个Node.js项目并安装几个必要的依赖项。我将在每个子部分中详细解释每个库的实际用途。每个子部分将详细讨论Node.js Express服务器中的REST API端点。
npm init -y // 初始化一个新的 npm 项目,使用默认设置。
npm i @lancedb/vectordb-linux-x64-gnu // 确定适用于您操作系统的兼容向量数据库。
npm i @langchain/community
npm i @langchain/openai
npm i express
npm i faiss-node
npm i langchain
npm i multer
npm i pdf-parse
npm i vectordb
2. 上传文件
curl --location '{HOST}/upload' \
--form 'file=@"{FILE-LOCATION}"'
为了处理文件,我们首先需要将其上传至Node服务器。上传文档的API使用的是Multer,这是一个用于处理multipart/form-data的Node.js中间件。一旦文件上传成功,API将返回文件名(由服务器使用Date.now()
生成的),该文件名将被保存在服务器上。此文件名将作为未来处理的唯一标识符。
在 index.js
文件中,我们定义了 /upload
端点接口如下所示。
import express from 'express';
import multer from 'multer';
import fs from 'fs';
import path from 'path';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
app.use(bodyParser.json());
/**
* 设置存储引擎用于 multer
* 并创建用于上传文件的目标文件夹
*/
const storage = multer.diskStorage({
destination: function (req, file, cb) {
var folder = './uploads/'
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder, { recursive: true });
}
cb(null, folder);
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname));
}
});
// 初始化 multer
const upload = multer({
storage: storage,
});
/**
* 定义一个路由来处理文件上传
* 如果文件上传成功,则发送成功响应信息
*/
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '没有上传文件' });
}
res.json({ message: '文件上传成功了', fileName: req.file.filename });
});
/**
* 启动服务器
*/
app.listen(port, () => {
console.log(`服务器正在监听 http://localhost:${port} 端口`);
});
3. 文档嵌入
curl --location 'http://{HOST}/embedding-document?document={DOCUMENT-ID}'
现在我们将PDF文档上传到服务器的./uploads/
目录中。接下来所有与服务器的交互都将基于上一步获得的文档ID。
在 index.js
文件中,我们定义了这样的 /embedding-document?document={document-id}
端点,如下所示。
import dataLoader from './util/data-loader.js'
import doc_splitter from './util/doc-splitter.js';
import vectorizer from './util/vectorizer.js';
import express from 'express';
import multer from 'multer';
import bodyParser from 'body-parser';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const app = express();
const port = 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
app.use(bodyParser.json());
/**
* 嵌入文档
*/
app.get('/embedding-document', async (req, res) => {
try{
var filePath = path.resolve(__dirname, "./uploads/"+req.query.document);
const docs = await dataLoader.load_documents(filePath);
const splitted_doc = await doc_splitter.split_documents(docs);
await vectorizer.embed_and_store(req.query.document, splitted_doc);
res.send({status:"SUCCESS"});
}catch (error) {
res.send({status:"FAILED", message:"遇到了未预期的错误。 :)" });
}
});
/**
* 启动服务
*/
app.listen(port, () => {
console.log(`服务正在监听 http://localhost:${port}`);
});
在 Embedding-Document REST 端点中,我们主要执行三项任务:
3.1. 加载文档步骤: 通过文档ID,我们使用PDFLoader LangChain库加载文档,然后将其传递到下一步。
在 util/data-loader.js
文件中,我们如下定义数据加载方法:
从 'langchain/document_loaders/fs/pdf' 导入 { PDFLoader };
const dataLoader = {
loadDocuments: async function(fileLocation){
const 加载器 = new PDFLoader(fileLocation, {
splitPages: true,
});
const docs = await 加载器.load();
return docs;
}
};
export default dataLoader;
2.2. 文档拆分: 使用LangChain的RecursiveCharacterTextSplitter将加载的文档拆分成片段,以便进行嵌入过程。您可以根据需要调整chunkSize
和chunkOverlap
参数。通常,较小的片段可以提供更好的结果,但具体效果取决于实际应用场景。
在 util/doc-splitter.js
文件中,我们如下定义文档拆分方法。
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const doc_splitter = {
分割文档: async function (documents) {
const 分割器 = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 100,
});
const docOutput = await 分割器.splitDocuments(documents);
return docOutput;
}
}
// 导出默认的文档分割器
export default doc_splitter;
3.3. 嵌入并存储于向量数据库中: 分割后的块被传递给嵌入方法。我们使用OpenAIEmbeddings库来嵌入分块文档,并将数据存储在Faiss向量数据库中。该向量数据库以文档ID作为名称,这使得在检索阶段能够轻松地检索与特定文档相关的信息。
在 util/vectorizer.js
文件中,我们如下定义嵌入和存储方式:
import { OpenAIEmbeddings } from "@langchain/openai";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
const 向量化器 = {
embed_and_store: async function (vector_store, split_documents) {
// 将文档加载到向量存储里
const vectorStore = await FaissStore.fromDocuments(
split_documents,
new OpenAIEmbeddings({ openAIApiKey: '{OpenAI-API-KEY}' })
);
// 将向量存储保存到该目录中
const directory = "./vector-db/faiss-store/" + vector_store;
await vectorStore.save(directory);
}
}
export default 向量化器;
4. 您的查询回复
curl --location 'http://{HOST}/?question={QUESTION}&document={DOCUMENT-ID}'
此端点协调检索部分的过程。当用户针对某个文档提出问题时,如果该文档已被处理并已拥有一个文档ID,我们只需将文档ID和问题传递作为查询参数。如果没有文档ID,则需要先执行前两个步骤来获取文档ID。
在 index.js
文件中,我们如下所示定义了 /?document={document-id}&question={question}
这个端点:
import retrieval_qa_chain from './util/retrieval-qa-chain.js';
import express from 'express';
import multer from 'multer';
import bodyParser from 'body-parser';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const app = express();
const port = 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
app.use(bodyParser.json());
/**
* 定义一个 GET 请求处理函数
*/
app.get('/', async (req, res) => {
try{
const documentID = req.query.document;
const answer = await retrieval_qa_chain.ask_question(documentID, req.query.question, []);
res.send(answer);
}catch (error) {
res.send({status:"FAILED", answer : "哎呀,出错了。 :)" });
}
});
/**
* 启动服务器监听
*/
app.listen(port, () => {
console.log(`服务器正在 http://localhost:${port} 监听`);
});
在提问方法中,我们主要执行三个任务:
4.1. 加载向量存储数据库:
使用文档ID,我们通过 FaissStore.load
方法加载相关文档的Faiss Store 向量数据库。
4.2. 创建ConversationalRetrievalQAChain(会话检索问答链):
要定义一个ConversationalRetrievalQAChain
,我们需要两个参数:LLM模型实例和用于处理数据的相关文档内容。此外,我们将returnSourceDocuments
设置为true,以便返回参考文档数据。
4.3. 调用ConversationalRetrievalQAChain:
在调用ConversationalRetrievalQAChain
时,我们可以传递聊天历史,从而提供丰富的对话体验,而不限于单一问题。
一旦所有三个步骤都成功了,你可以将响应发送给用户。为了便于处理和压缩JSON,我对数据进行了一些处理,但这一部分完全由你来处理。
4.1、4.2 和 4.3 相关活动在 util/retrieval-qa-chain.js
文件中定义如下。
import { OpenAIEmbeddings, OpenAI } from "@langchain/openai";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { ConversationalRetrievalQAChain } from "langchain/chains"
import {HumanMessage, AIMessage} from "@langchain/core/messages"
import {ChatMessageHistory} from "langchain/stores/message/in_memory"
const retrieval_qa_chain = {
ask_question : async function(document_id, question, chat_history = [] ){
const directory = "./vector-db/faiss-store/"+document_id;
const model = new OpenAI({openAIApiKey: '{OpenAI-API-KEY}'});
// 从同一目录加载向量存储
const loadedVectorStore = await FaissStore.load(
directory,
new OpenAIEmbeddings({openAIApiKey: '{OpenAI-API-KEY}'})
);
const chain = ConversationalRetrievalQAChain.fromLLM(
model,
loadedVectorStore.asRetriever(),
{
returnSourceDocuments: true,
}
);
const response = await chain.invoke({question: question, chat_history: chat_history});
const history = new ChatMessageHistory();
await history.addMessage(new HumanMessage(question));
await history.addMessage(new AIMessage(response.text));
chat_history.push(history.messages[0]);
chat_history.push(history.messages[1]);
const 回答 = {
answer: response.text,
chat_history: chat_history,
source: response.sourceDocuments
}
return 回答;
}
}
export default retrieval_qa_chain;
恭喜!您已成功在Node服务器上建立了一个全面的RAG生态系统。请记住,适当调整默认设置可以带来更加准确的答案。这只是漫长创新与探索之旅的第一步。
祝你编程愉快…