AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


用JavaScript快速实现低成本RAG知识库
发布日期:2024-08-21 20:01:03 浏览次数: 1727 来源:唐霜


很久没有写文章了,过去两个多月我研发了一套系统,以辅助开发者们更便捷的获得AI服务。在过程中,我想提供给用户一个FAQ,但又不想做的像传统的问答编辑系统一样,于是想借助RAG来实现问答系统。初步想法是把站内所有的wiki向量化,作为知识库的一部分,另外,当用户提出自己的问题时,运营同学可以在后端进行答复,把这些答复的内容也向量化作为知识库的一部分。通过RAG来做,可以防止类似github issue沉底的问题,避免已经出现过的问题和答复无法实时的让新用户在有疑问时获得了解。
同时,在这个过程中,我浏览了各个云平台,以及各家大模型服务商,试图找到一个可以直接用于实现RAG的后端服务,但没有找到。对于FAQ这一需求而言,我认为一个精简的产品形态就是几个接口即可:
  • 用于提交知识(文本)的接口,该接口用于把网站或客户端的知识集中到后端服务中向量化存储

  • 用于提问/查询的接口,该接口用于从知识库中获得结果

  • 用于列出所有提问的接口

  • 用于对单一提问进行人工回答的接口
通过4个接口,屏蔽了有关向量化、向量存储、检索增强等一系列的技术性问题,变成了无论是前端还是后端同学都可以使用的纯粹接口。当然,我们可以在云服务的后台,对所使用的大模型、向量模型、向量存储等更底层的服务进行配置,不同的配置所需的花费不同,从而让不同资金需求的用户都可以获得各自想要的RAG服务效果。
抱着这样的心态,我开始自己实现这一需求。

什么是RAG知识库?

相信你已经对RAG已经有所了解了,为了使得文章更充实(凑字数),我还是从我个人的角度来聊一聊什么是RAG知识库。
作为本科和研究生阶段都对信息管理有深入研究的从业者,脱离学术,我们讲知识管理,本质上是在讲人类利用信息的高效通道。传统知识库我们称为“图书馆”,通过类目方式对知识进行索引,并提供类目检索工具让我们快速找到对应的图书,从而可以获得知识,但当我们要找粒度很细的知识时,犹如大海捞针。现代知识库我们称之为“搜索引擎”,传统知识库获取知识的通道效率太低,想要精准的获取某个知识点,可能还需要借助对应的人才能完成,而搜索引擎时代,通过新的索引方式,向人们提供了寻找知识的捷径。当代知识库我们我们称之为“智能助手”,搜索引擎时代虽然相较于传统图书馆已经有了极大的提升,但是随着信息泛滥,搜索引擎只能提供获取知识的“线索”,而要精准获得知识,仍然有赖于搜索者对搜索结果的总结,而借助于AI的智能助手,则更近一步,不仅主动完成用户对信息的搜索,还对信息进行提炼,将人们对知识的需求,以最直接的方式呈现在面前,也就是“所问既所答”。
RAG(检索增强生成)知识库就是当代AI背景下的知识助手,LLM-Based已经成为AI的新范式,可以说没有LLM也就没有RAG知识库。
知识库本身存在知识范围的限制,人类知识过于浩瀚,而真正在企业中发挥经济价值的知识,往往限定在与企业生产生活相关的范畴中。因此,从知识范围的角度,RAG知识库可以分为宏观行业知识库、企业单位生产知识库、自然个人学习生活知识库。对于使用RAG知识库的用户而言,应该明确自己所需的知识范围,避免因为知识范围的不同导致往知识库中加入错误的知识来源。
对于AI行业而言,RAG又是有效的应用形态,弥补了LLM训练数据局限的问题。
总而言之,RAG知识库是当下AI技术发展到LLM-Based背景下,可以提供给人类最先进的知识获取的方式。

RAG知识库通用技术架构

不同企业在实现RAG知识库时,技术细节上各有优化,但是总体而言,RAG有着通用的技术架构。
从我个人的角度,我把RAG的技术分为两大部分:1.向量化检索;2.增强生成。
什么是向量化检索呢?举个例子,我们规定了3个维度,分别是:高度、体积、颜色。现在任何一个物体,我们都能推算出它在这三个维度上的值。我们把这个物体存入到仓库中时,标注了它的这3个维度。现在,我们的仓库中充满了物体,我们看到一个新物体,想要找出仓库中与之最为接近的物体,就只需要找出这3个维度的值与新物体最接近的哪些物体即可。这种方法可以大大提高检索的效率。这就是向量化检索,其中这个物体在3个维度上的表示,就是一个向量,而通过向量坐标,在数学上进行相似性运算是很方便的。同时,在进行向量化时,我们还会做一个隐藏技能,就是对原本文档进行分片,通过分片,既让知识向量化,又让知识体积变小,这样对后面把知识交给LLM去整合又有帮助。
增强生成,简单讲就是让LLM结合向量化检索的结果,直接返回知识点的内容(而非参考条目)给用户。在我的博客 www.tangshuang.net 中,我提供了有关LLM应用的更多内容,你可以通过博客阅读更多。
如果对技术的发展做不恰当的比方,RAG非常简单,就是在上一代知识技术(搜索引擎)的基础上,用LLM对搜索结果进行总结生成。但是,由于技术的局限,我们无法真的在现实中,直接通过LLM对搜索引擎搜索结果进行处理,这里面涉及到LLM本身的上下文长度限制、搜索引擎结果的质量等等问题。在企业内,我们有可能会自己构建一套搜索引擎,例如基于ES来实现权重查询。限定在企业内的知识,搜索结果少、质量高,理论上应该是可以实现的,但是,现实中,企业知识文件体积大,要从巨大的文件中只挖掘小的知识点,有点大刀小用。况且,搜索结果的质量还依赖于搜索词。
带着以上总总的问题思考,我们来看看当代典型的RAG知识库的技术架构是怎么设计的,它是怎么解决上述的这些问题的。
RAG知识库通用技术架构
如图,在数据准备阶段,我们将不同的文件内容,进行embedding,此时,我们需要依赖一个Embedding Model(嵌入模型),目前很多大模型的服务商或者云服务商都提供了独立的Embedding Model服务,它的作用是将文件内容进行向量化。经过embedding之后,我们便得到了一堆向量,这里我们称之为Semantic Vector(语义向量),它们可存储可计算,对AI友好。之后我们将这些向量存储到一个向量数据库中,一般而言,向量数据库支持检索功能。不同的向量数据库特性和能力不同,因此,市面上有许多付费的向量数据库。
从这里可以看出,嵌入模型、向量数据库、大模型,这些底层服务对于厂商而言都是利润,而对于开发者而言,则是成本。当然,我们可以用自己的技术,在自己的服务器部署这些底层服务,从而降低成本。
数据检索阶段,用户输入一个问题,这个问题,搜索词经过嵌入模型向量化,称为Query Vector(查询向量),再该向量去向量数据库进行检索,获得结果。这里得到的结果就是我们需要的知识点,它们来自各种原始数据中,但此处它们并不是原始数据,而是原始数据的向量碎片,虽然它们关联了某些文件,但是它们现在的状态就是一个个的碎片,不利阅读。接下来,我们要把它们交给大模型去处理,我们需要构造一个合适的prompt,并把它们嵌入到该prompt中,由LLM来对该prompt进行响应。当LLM完成该prompt的响应后,我们就获得了较为精炼的知识内容。
以上是RAG的通用技术架构,我们可以在该架构基础上进行优化,例如对prompt进行优化,对向量数据库的召回率进行优化,对大模型的结果进行更深一层的智能逻辑处理(可以利用Agent技术)等等。但是,无论怎么变体,RAG的技术本质可以通过该架构体现。

基于LangChain在NodeJS中实现RAG

我的技术栈是JS,因此,我更多的是在nodejs中实现各种想法。langchain官方提供了RAG的引导文档,你几乎可以在理解了架构和langchain的设计基础上,无需辅助,照着文档完成。但作为开发者,我们希望更低成本的实现它,因此,我们要找到免费的技术选型。
通过上面技术架构我们可以知道,实际上,要构建最小的RAG,我们所依赖的服务,就3个:LLM、Embedding Model、Vector Database。有没有免费的替代呢?当然有,LLM和Embedding Model我们都可以通过Ollama来获得,向量数据库就使用本地化的faiss。
首先看下ollama中的嵌入模型
还不错,目前为止,有3个可以选。
接下来,就让我们一步一步的完成我们的代码。
ChatModel
import { ChatOllama } from "@langchain/ollama";
export function initOllamaChatModel() {const llm = new ChatOllama({model: "llama3",temperature: 0,maxRetries: 2,// other params...});return llm;}
当然,这里,你需要先用ollama把llama3跑起来。
Embedding Model
import { OllamaEmbeddings } from "@langchain/ollama";
epxort function initOllamaEmbeddings() {const embeddings = new OllamaEmbeddings({model: "mxbai-embed-large", // Default valuebaseUrl: "http://localhost:11434", // Default value});return embeddings;}
同样的道理,ollama把对应的模型跑起来。
TextLoader
我打算把所有的内容,都以纯文本的形式进行载入。你也可以使用其他的loader载入如txt、pdf、docx等文件或网页。
import { Document } from "@langchain/core/documents";
export async function loadPureText(text, metadata) {const docs = [new Document({pageContent: text,metadata,}),];return docs;}
TextSplitter
对大文本进行分片。
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
export async function splitTextDocuments(docs) {const textSplitter = new RecursiveCharacterTextSplitter({chunkSize: 1000,chunkOverlap: 200,});return await textSplitter.splitDocuments(docs);}
Vector Store
使用faiss作为向量数据库,节省成本,可本地存储,方便部署和迁移。
import { FaissStore } from "@langchain/community/vectorstores/faiss";
export function createFaissVectorStore(embeddings) {return new FaissStore(embeddings, {});}
export async function searchFromFaissVectorStore(vectorStore, searchText, count) {const results = await vectorStore.similaritySearch(searchText, count);return results;}
export async function addDocumentsToFaissVectorStore(vectorStore, docs, ids) {return await vectorStore.addDocuments(docs, { ids });}
export async function deleteFromFaissVectorStore(vectorStore, ids) {return await vectorStore.delete({ ids });}
/** 备份 */export async function saveFaissVectorStoreToDir(vectorStore, dir) {await vectorStore.save(dir);}
/** 恢复 */export async function loadFaissVectorStoreFromDir(dir, embeddings) {const vectorStore = await FaissStore.load(dir, embeddings);return vectorStore;}
Prompt
构建常用的RAG prompt模板。
import { ChatPromptTemplate } from "@langchain/core/prompts";
export function createRagPromptTemplate() {const promptTemplate = ChatPromptTemplate.fromTemplate(`You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.Question: {question}Context: {context}Answer:`);return promptTemplate;}
RAGApplication
最后,是把上面的这些部分组合起来。
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";import { StringOutputParser } from "@langchain/core/output_parsers";import path from 'path';import fs from 'fs';
export async function createRagApplication() {const llm = initOllamaChatModel();const embeddings = initOllamaEmbeddings();
const faissStoragePath = path.join(__dirname, 'faiss');const isFaissExist = fs.existsSync(faissStoragePath);const vectorStore = isFaissExist ? await loadFaissVectorStoreFromDir(faissStoragePath, embeddings) : createFaissVectorStore(embeddings);const saveTo = () => {if (!isFaissExist) {fs.mkdirSync(faissStoragePath, { recursive: true });}return saveFaissVectorStoreToDir(vectorStore, faissStoragePath);};
const retriever = vectorStore.asRetriever(options.retriever);const prompt = createRagPromptTemplate();
const chain = await createStuffDocumentsChain({llm,prompt,outputParser: new StringOutputParser(),});
/** * * @param {string} text * @param {{ * type: string; // 类型 * id: string | number; // 标识 * }} meta */const addText = async (text, meta) => {const docs = await loadPureText(text, meta);const docsToAdd = await splitTextDocuments(docs);const { type, id } = meta;const ids = docsToAdd.map((_, i) => `${type}_${id}_${i}`);await addDocumentsToFaissVectorStore(vectorStore, docsToAdd, ids);await saveTo(); // 每次新增向量之后,自动保存到目录中return ids;};
/** * @param {string[]} ids */const remove = async (ids) => {await deleteFromFaissVectorStore(vectorStore, ids);await saveTo();};
const query = async (question) => {const context = await retriever.invoke(question);const results = await chain.invoke({question,context,})return results;};
const stream = async (question) => {const context = await retriever.invoke(question);const results = await chain.stream({question,context,})return results;};
return {addText,remove,query,stream,};}
当然,这里你需要把前面的所有函数都引入进来。
Example
最后,我们来写一个例子,用以测试它是否正常工作:
const rag = await createRagApplication();
await rag.addText(`RAG 检索的底座:向量数据库,我的博客 www.tangshuang.net 中有专门的内容对向量数据库做介绍。在业界实践中,RAG 检索通常与向量数据库密切结合,也催生了基于 ChatGPT + Vector Database + Prompt 的 RAG 解决方案,简称为 CVP 技术栈。这一解决方案依赖于向量数据库高效检索相关信息以增强大型语言模型(LLMs),通过将 LLMs 生成的查询转换为向量,使得 RAG 系统能在向量数据库中迅速定位到相应的知识条目。这种检索机制使 LLMs 在面对具体问题时,能够利用存储在向量数据库中的最新信息,有效解决 LLMs 固有的知识更新延迟和幻觉的问题。尽管信息检索领域也存在选择众多的存储与检索技术,包括搜索引擎、关系型数据库和文档数据库等,向量数据库在 RAG 场景下却成为了业界首选。这一选择的背后,是向量数据库在高效地存储和检索大量嵌入向量方面的出色能力。这些嵌入向量由机器学习模型生成,不仅能够表征文本和图像等多种数据类型,还能够捕获它们深层的语义信息。在 RAG 系统中,检索的任务是快速且精确地找出与输入查询语义上最匹配的信息,而向量数据库正因其在处理高维向量数据和进行快速相似性搜索方面的显著优势而脱颖而出。以下是对以向量检索为代表的向量数据库与其他技术选项的横向比较,以及它在 RAG 场景中成为主流选择的关键因素分析:`, { type'text'id0 });
const results = await rag.stream('RAG检索的底座是什么?');for await (const chunk of results) {process.stdout.write(chunk);}

在命令行中运行上面这个js,就可以看到效果。如果运行正常,说明我们的编码没有问题。

需要注意的点
上面实现中,只提供了addText纯文本写到向量库中,你可以根据你自己的实际需求,利用langchain提供的loader,实现各种形式的载入。
另外,上面的实现中,只从RAG的通用架构角度进行了技术实现,而没有从应用出发,去进行缓存、去重等一系列的应用层设计。它相当于是一个通用的代码,任何nodejs项目都可以拿去用,再在它的上层进行更深度的业务封装。我自己就是在这样的基础上,封装了FAQ系统,当用户发起提问的时候,从知识库中捞取知识进行回答,同时,在业务上,我又增加了运营人员手工回答来覆盖AI回答,同时,又把人工回答再载入到知识库中,从而使得将来类似问题可以被更好的回答。

结语

虽然我在去年就在腾讯内网发布了如何用nodejs来实现RAG的文章,但是随着langchain的进步,以及市场上各类底层服务的完善,现在构建RAG已经变得轻而易举。本文算是我闲暇时,保持博客更新的一篇总结吧,毕竟对于不少朋友来说,还是需要有一篇类似的文章引导的。我在翻看langchain文档时,发现不少工具是nodejs独享的,python没有,这说明js社区的强大。而随着AI技术的不断普及化,以及LLM上下文长度的提升,在不考虑tokens的费用的情况下,或许我们真的可以做到不需要向量化来实现RAG知识库。对了,文章开头说的FAQ接口服务,我会在近期放出,你只需要持续关注我,适当的时候,它就会出现。



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询