继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

构建基于Node.js服务器的Gen-AI LLM RAG API生态系统

扬帆大鱼
关注TA
已关注
手记 204
粉丝 13
获赞 50

大家好,

在这篇文章中,我将解释如何使用Node.js、LangChain库和RAG技术构建您自己的生成式人工智能大型语言模型API生态系统,以便能够回答关于这些文档的问题。本文的目的是提供构建一个全面的、生产就绪的REST API生态系统的综合知识,您可以轻松地将其与任何产品集成在一起。RAG技术是指检索增强生成。

前提条件
  • 在您的PC上安装Node.js的最新版本
  • 基本的JavaScript知识
  • OpenAI API密钥
    • 热情
什么是检索增强的生成(RAG):

如果你已经对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模型连接,服务器生成与文档上下文相关的响应。

  1. 创建一个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将加载的文档拆分成片段,以便进行嵌入过程。您可以根据需要调整chunkSizechunkOverlap参数。通常,较小的片段可以提供更好的结果,但具体效果取决于实际应用场景。

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生态系统。请记住,适当调整默认设置可以带来更加准确的答案。这只是漫长创新与探索之旅的第一步。

祝你编程愉快…

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP