likes
comments
collection
share

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

作者站长头像
站长
· 阅读数 59

RAG检索增强生成

RAG是什么

RAG是检索增强生成(Retrieval-Augmented Generation),是指对大型语言模型(LLM)输出进行优化,使其能够在生成响应之前引用训练数据来源之外的权威知识库。

为什么需要RAG

为什么需要RAG?我们可以从大语言模型之一的GPT来了解大语言模型的缺陷。

GPT是一种生成式的、预训练的大模型,属于深度学习:

  • G全称为Generative 生成式:GPT能够通过深度学习算法对已有数据库进行学习,再根据输入的指令生成全新的内容。
  • P全称为Pre-tranined预训练:GPT利用海量语料数据进行预先训练、深度学习,从而使得模型能够掌握自然语言的语法、语义和知识等方面的信息,构建一个千亿级参数的知识数据库以供检索。
  • T全称为Transformer转换模型:GPT所用的机器强化学习系统架构、是基于Transformer这个大语言模型,通过神经网络来模拟人脑的学习方式。

大型语言模型例如GPT的本质

通过预训练数据集生成知识数据库,再通过Transformer转换模型理解输入指令,对已有知识数据库检索和学习生成全新内容。

基于大语言模型的本质,我们可以知道它存在以下局限性:

  1. 领域信息不足

    大语言模型基于公开的数据集进行训练,那么意味着它缺乏领域特定或专有的非公开信息训练。

  2. 提供过时的信息

    由于大语言模型需要进行预训练,训练时依赖过去已有的数据。在用户需要实时响应的时候,可能会提供过时或者通用的信息。

  3. 预训练数据不可更改

    当大语言模型使用的预训练数据可能包含错误的信息时,没有办法进行更正或者删除。则模型基于错误的预训练数据给出错误的答案。

  4. 答非所问或者凭空造词

    尽管大语言模型尽力根据已有的数据提供信息和答案,但是在超出其范围的情况下,可能会给出不正确或者是虚构的信息,这种信息也称为大模型的幻觉

这也是GPT模型需要一直更新的原因,尽管GPT4等模型具有联网能力,但仍然存在领域信息不足以及凭空造词的问题。

亚马逊云的解释非常通俗,我们可以将大模型语言看做是一个过于热情的新员工,他拒绝随时了解时事,但总是会绝对自信地回答每一个问题。这种态度会对用户的信任产生负面影响

RAG(检索增强生成技术)是解决其中一些挑战的一种方法。它会重定向LLM, 从权威的、预先确定的知识来源中检索相关信息。更好地控制文本输出,并且用户可以深入了解LLM如何生成响应。

RAG原理

RAG的原理:

  • 创建外部数据(Create external data):LLM原始训练数据集之外的新数据称为外部数据。它可以来自多个数据来源。例如API、数据库、或者文档存储库。数据可能以各种格式存在,例如文件、文本等。一种称为Embedding language model(嵌入语言模型)的AI技术,将数据转换为数字表示形式,并将其存储在向量数据库中(Vector database)。这个过程会创建一个生成式人工智能模型可以理解的知识库。
  • 检索相关信息(Retrieve relevant information):执行相关性搜索。用户查询将转换为向量表示形式,并与向量数据库匹配,召回相关性最高的文档。例如,考虑一个可以回答人力资源问题的智能聊天机器人。如果员工搜索*:“我有多少年假?”*,系统将检索向量数据库,随后年假政策文件以及员工个人过去的休假记录,这些特定信息将被返回,因为它们与员工输入的内容高度相关。相关性是使用数学向量计算和表示法计算和建立的。
  • 增强 LLM 提示(Augument the LLM prompt):接下来,RAG 模型通过在上文中检索到的相关数据来增强用户输入(或提示)。此步骤使用提示工程技术与 LLM 进行有效沟通。增强提示允许大型语言模型为用户查询生成准确的答案。

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

个人理解,简单来说RAG就是让LLM有继续学习的能力,是一种能够向LLM投喂特定知识的技术。

RAG实现流程

RAG的实现流程因研究方法不同而各有差异,但基本流程大致相同:

  • 索引模块(indexing)1-4:将文本数据进行切分,生成不同的文本片段,进行向量化之后存储到向量数据库。
  • 检索模块(retrieval)6-9:将用户的query进行向量化,并从向量数据库中检索 top k 相似向量,然后获取对应的文本片段。
  • 大模型生成模块(generation)10-13:根据文本片段和用户query拼装成提示词,输入到LLM中生成最终答案。

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

  • 索引模块

    对知识进行处理和加工,生成向量模型可以接受长度的文本块(一般最大是512个token)。然后将这些文本块进行向量化之后,输入到向量数据库中进行存储。

    为什么需要对文本数据进行切分?因为目前的向量模型都会基于BERT作为基座模型再进行微调,而这个模型的最大长度仅支持到512.超过512性能会急剧下降,再使用的意义不大,也制约了语义表示的质量。

    常见的文本切分的策略有如下几种方式:

    • 按照固定长度截断,比如256个token位界限进行截断
    • 分段,以L/256的长度分为n段,每段分别去做向量化,然后对所有的向量取最大、平均操作
    • 滑动窗口,把文档分成有重叠的若干段,一般是设定每一个文本块的大小

    一般使用向量数据库取存储生成的向量数据。

    • 向量数据库

      向量数据库是一种专门用于存储、管理、查询和检索向量(Vectors)的数据库,主要应用于人工智能、机器学习、数据挖掘等领域。与传统数据库相比,向量数据库不仅能够完成基本的CURD等操作,还能够对向量数据进行更快速的相似搜索。

      万物皆可Embedding,也就是向量化,只要找到对应的方法

      常见的向量数据库有Chroma、Milvus、Qdrant、Weaviate、Pinecone等,其中Chroma简单易用,并且可以支持直接在内存中调用,无需额外配置服务器和客户端,是做应用开发和demo展示的首选。

  • 检索模块

    • 向量检索

      向量检索的过程就是计算向量之间的相似度,然后返回相似度较高的topK向量返回。一般有如下两种方法:

      • 最近相似检索(K-Nearest Neighbor, KNN)
      • 近似最近邻检索(Approximate Nearest Neighbor,ANN)

      由于 KNN 算法需要对整个空间中的向量都计算相似度,而通常很多时候,我们不需要过于高的精度,只要找到相对比较接近的向量就可以了。因此现在主流的向量数据库都会支持 ANN 算法,这种算法即能保持较高的精度,又可以大幅度缩短召回的时间。

      什么是召回?

      在互联网业务中常见的推荐广告、搜索等场景下, 召回是指从大规模数据集中快速检索出一些候选项,以便进一步进行排序和筛选,召回阶段的目标是尽可能地多捕捉用户可能感兴趣的内容,以提供更准确和和更相关的推荐、广告或者搜索结果。

      在知识库的场景下,召回就是如何找到跟问题最相关的文档片段。

      在向量检索模块中,我们一般关注召回的数量,太少则导致给 LLM 提供的知识不完全,得不到想要的答案。而数量过多,则会导致召回许多不相干的内容,容易误导 LLM。而且 LLM 支持的 token 是有上限,召回太多也会导致LLM 记不住这些内容,因此在真实的场景下,都需要结合实际情况去设置召回的数量。

  • 大模型生成模块

    大模型生成模块,主要是将用户的问题以及向量检索召回的上下文片段,按照固定的提示词模板拼接为提示词,并输入到大模型中,获取最终的回答。

    一个比较常见的提示词模板如下:

    已知信息:
    """
    {context}
    """
    根据上述已知信息,简洁和专业的来回答用户的问题。如果无法从中得到答案,请说 “根据已知信息无法回答该问题” 或 “没有提供足够的相关信息”,不允许在答案中添加编造成分,答案请使用中文。
    问题是:"""{question}"""
    
    

RAG除了Embedding之外还有Fine-tuning微调,他们之间的比较如下,有兴趣的可以试试:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

RAG实践

初始化向量数据库

这里以 chroma 为例,安装 chroma 的 npm 包

pnpm install chromadb chromadb-default-embed 

本地运行 chroma 向量数据库的客户端:

chroma run --path ./chromadb

创建 chroma 连接:

import { ChromaClient } from 'chromadb'
const client = new ChromaClient()

创建Chroma集合

CollectionChroma 的一个概念,是用来存储向量化数据、源文本数据以及其他原数据信息的空间。类似于我们的数据库表的概念。

const collection = await client.createCollection({
	name: 'my_collection'
})

训练数据向量化存储

预训练数据可以由多种来源,这里简单的以一段文本为例:

await collection.add({
    documents: [
        "This is a document about pineapple",
        "This is a document about oranges"
    ],
    ids: ["id1", "id2"],
});

这里 documents 代表预训练数据, ids 是每个 document 必须的唯一关联ID。这里我们直接输入了预训练数据,为什么就能够存储到向量数据库了呢?说好的分块和向量化呢?

这是由于传递 documents 的时候, chroma 会调用默认的 embedding 模型 all-MiniLM-L6-v2 把我们的documents进行向量化后存储**。**

chroma 调用默认模型的时候,会从 huggingface 这个模型仓库远程下载 all-MiniLM-L6-v2 这个模型。

实际使用中,因为 huggingface 的限制,不管有没有使用 VPN ,这段代码都会由于远程下载模型失败而失败。

所以我们只能另辟蹊径,实现向量化之后存储到 chroma ,有两种途径可以实现数据向量化:

  1. 本地化模型,使用 transformer.js 调用模型实现分词和嵌入
  2. 直接调用远程的向量化模型,例如 OpenAI 就有提供 embedding 的方法

分词和嵌入(向量化)

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

预训练数据向量化存储需要经过两个步骤:

第一步是Tokenizer(分词):将输入的字符串分为一些子字符串 token ,再将 token 映射到 id。从字符串到 id 的过程被称为 tokenizer encode ,而从 id 映射会字符串 token 的过程称为 tokenizer decode

分词有多种不同的分词粒度,最直观的分词就是单词分词法(word base)。最详尽的分词发是单字分词法 (character-base)

例如 Today is Sunday 用单词分词法可以分为 ['Today', 'is', 'Sunday']

而使用单字分词法可以分为 ['T', 'o', 'd', ..., 'a', 'y']

那么GPT使用的分词方法是什么呢?从GPT-2到GPT-4,OpenAI一直采用 Byte-Pair Encoding(BPE)分词法,这种方法的分词算法是这样的:

1. 统计输入中所有出现的单词并在每个单词后加一个单词结束符</w> -> ['hello</w>': 6, 'world</w>': 8, 'peace</w>': 2]
2. 将所有单词拆成单字 -> {'h': 6, 'e': 10, 'l': 20, 'o': 14, 'w': 8, 'r': 8, 'd': 8, 'p': 2, 'a': 2, 'c': 2, '</w>': 3}
3. 合并最频繁出现的单字(l, o) -> {'h': 6, 'e': 10, 'lo': 14, 'l': 6, 'w': 8, 'r': 8, 'd': 8, 'p': 2, 'a': 2, 'c': 2, '</w>': 3}
4. 合并最频繁出现的单字(lo, e) -> {'h': 6, 'lo': 4, 'loe': 10, 'l': 6, 'w': 8, 'r': 8, 'd': 8, 'p': 2, 'a': 2, 'c': 2, '</w>': 3}
5. 反复迭代直到满足停止条件

分词过后,通过调用 encode 方法,会将分词的词汇映射成数字ID,关于分词的详细过程,可以查看这篇文章:

zhuanlan.zhihu.com

第二步是Embedding(嵌入):

Embedding就是将tokenizer处理后的数字ID,转换为更低维度的数据,也称为向量数据。

例如使用 T5TokenizerFast 这个分词器来分词 Hello World ,则:

  1. tokenizer会将token序列 ['Hello', 'World', '!'] 编码成数字序列[8774, 1150, 55, 1],也就是['Hello', 'World', '!', '</s>'],然后在句尾加一个</s>表示句子结束。
  2. 这四个数字会变成四个one-hot向量,例如8774会变成[0, 0, ..., 1, 0, 0..., 0, 0],其中向量的index为8774的位置为1,其他位置全部为0。假设词表里面一共有30k个可能出现的token,则向量长度也是30k,这样才能保证出现的每个单词都能被one-hot向量表示。
  3. 也就是说,一个形状为(4)的输入序列向量,会变成形状为(4,30K)  的输入one-hot向量。为了将每个单词转换为一个word embedding,每个单词都需要被送到embedding层进行降维。

使用远程模型实现分词和嵌入(向量化)

既然 chroma向量数据库的默认模型因为各种原因无法下载使用,那么我们可以让 chroma 在往集合添加(add)和查询(query)时调用自定义的 Embedding (向量化)函数。在这个自定义向量化函数里面,我们可以直接调用 openai 提供的 embedding 接口(没有什么是钞能力解决不了的🌝)

关于openai接口调用,其实也是一个坎坷的过程,首先你得拥有chatGPT账号,然后你还得有一张海外信用卡用于绑定openai的账号,因为使用openai的API是需要收费的。我首先尝试了开通海外信用卡,但绑定时被拒绝了。。随后只能找了一个与openai同价位的openai转发地址。

要让 chroma 调用自定义的 Embedding 向量化函数,首先在创建集合时就得指定:

this.client = new ChromaClient()
this.collection = await this.client.getOrCreateCollection({
     name: 'my_collection',
     embeddingFunction: embedder
})

自定义的 embedder 是继承 DefaultEmbeddingFunction 的一个对象实例,需要实现 generate 方法来处理输入的文本,转化为向量数据:

const openai = new OpenAI({
  baseURL: '***/v1',
  apiKey: '***'
})

class RemoteEmbedding extends DefaultEmbeddingFunction {
  async generate(texts: string[]): Promise<number[][]> {
    // 远程调用openai的embedding能力,将文本转化为向量
    const embedding = await openai.embeddings.create({
      model: "text-embedding-ada-002",
      input: texts
    })
    const data = embedding.data.map(item => item.embedding)
    return data
  }
}
const embedder = new RemoteEmbedding()

在指定了自定义的向量化函数之后,往 chroma 添加或者查询之前,都会将文本进行向量化后存储或者查询。

const documents = [
	'蔡徐坤又名坤坤,是中国的superstar,成名曲鸡你太美',
	'中国有一个superstar, 名字叫做成龙'
];
this.collection.add({
  documents,
  // id和文本要一一对应
  ids: documents.map((str, index) => str.slice(0, 2) + index)
})

使用本地模型实现分词和嵌入

上文提到 chroma 在我们添加 add 文本到 collection 集合的时候,会默认调用 embedding 模型 all-MiniLM-L6-v2 把我们的进行向量化后存储**。**

通过参考 chroma 源码中的 DefaultEmbeddingFunction 默认向量化的函数,可以发现,它的核心是调用 chromadb-default-embed 这个库去远程调用模型,然后再执行模型的流水线进行向量化的。

查看 chromadb-default-embed 这个库可以发现,其实际上就是 huggingface 提供的JS机器学习框架 transformer.js 的一个fork而已。

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

那么通过模拟这个chroma的默认向量化函数,我们可以使用本地的 all-MiniLM-L6-v2 模型进行向量化。

import { IEmbeddingFunction } from "chromadb";
import path from "node:path";
import { fileURLToPath } from "node:url";

export default class DefaultEmbedding implements IEmbeddingFunction {
  private pipelinePromise?: Promise<any> | null;
  private model: string;
  private revision: string;
  private quantized: boolean;
  private progress_callback: Function | null;

  /**
   * DefaultEmbeddingFunction constructor.
   * @param options The configuration options.
   * @param options.model The model to use to calculate embeddings. Defaults to 'Xenova/all-MiniLM-L6-v2', which is an ONNX port of `sentence-transformers/all-MiniLM-L6-v2`.
   * @param options.revision The specific model version to use (can be a branch, tag name, or commit id). Defaults to 'main'.
   * @param options.quantized Whether to load the 8-bit quantized version of the model. Defaults to `false`.
   * @param options.progress_callback If specified, this function will be called during model construction, to provide the user with progress updates.
   */
  constructor({
    model = "all-MiniLM-L6-v2",
    revision = "main",
    quantized = false,
    progress_callback = null,
  }: {
    model?: string;
    revision?: string;
    quantized?: boolean;
    progress_callback?: Function | null;
  } = {}) {
    this.model = model;
    this.revision = revision;
    this.quantized = quantized;
    this.progress_callback = progress_callback;
  }

  public async generate(texts: string[]): Promise<number[][]> {
    this.pipelinePromise = new Promise(async (resolve, reject) => {
      try {
        const { pipeline, env } = await import('@xenova/transformers');
        const quantized = this.quantized
        const revision = this.revision
        const progress_callback = this.progress_callback
        const __filename = fileURLToPath(import.meta.url); 

        env.localModelPath = path.resolve(__filename, '../../../pretrained-model/')
        env.allowRemoteModels = false;
        resolve(pipeline("feature-extraction", this.model, {
          local_files_only: true,
          model_file_name: this.model,
          quantized,
          revision,
          progress_callback,
        }))     
      } catch (e) {
        reject(e);
      }
    });

    let pipe = await this.pipelinePromise;
    let output = await pipe(texts, { pooling: "mean", normalize: true });
    return output.tolist();
  }
}

使用 transformer.js 调用模型实现分词和嵌入,同样的默认会从 huggingface 模型库进行下载模型,尚不清楚 huggingface 做了什么,使用代理仍然无法下载模型,所以需要上 huggibgface 将模型下载到本地。

sentence-transformers/all-MiniLM-L6-v2 · Hugging Face

尝试过通过Git方式拉取,依然被拒绝,目前我下载的办法比较笨,是一个个文件从这个模型库上下载下来的:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

完整DEMO

回到我们实现RAG的流程图:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

根据流程图,我们可以写出入口代码:

import { ChatCompletion } from 'openai/resources/index'
import ChromaDBClient from './chromaDBClient.ts'
import { loadDocuments } from './docs/loadDocuments.ts'
import RemoteEmbedding from './embedding/remoteEmbedding.ts'
import { largeLanguageModelChat } from './openai/chat.ts'
import { addPreTrainData } from './utils/addPretrainData.ts'
import { recallQuery } from './utils/recallQuery.ts'
import { ReadableStream } from "node:stream/web"
import LocalEmbedding from './embedding/localEmbedding.ts'

// mode用于区分是本地模式还是远程模式
const mode = process.argv.slice(2)[0]
if (mode === 'local') {
  // 本地模式,读取模型需要使用ReadableStream,这里要做一个polyfill
  Object.defineProperties(globalThis, {
    ReadableStream: { value: ReadableStream },
  })
}

async function init() {
  // 加载vant文件目录
  const docs = await loadDocuments()
  // 自定义embedding函数
  const embedder = mode === 'local' ? new LocalEmbedding() : new RemoteEmbedding()
  // 初始化向量数据库
  const chromaClient = new ChromaDBClient()
  // 创建和加载集合,将自定义embedding类传入
  await chromaClient.load(embedder)
  return {docs, chromaClient}
}

init().then(
  // 加载vant文档、分片存储向量化到向量数据库
  addPreTrainData
).then(
  // 根据用户查询,搜索向量数据库,根据召回的内容进行提示词模板拼接
  recallQuery
).then(
  // 调用大语言模型生成会话,回答用户问题
  largeLanguageModelChat
).then((completion: ChatCompletion) => {
  // 输出大模型回答
  console.log(completion.choices[0])
})

第一阶段预处理:处理训练数据

  1. 准备好预训练数据,这里以 vant 组件库下的所有 md 文件为例,通过 Node 读取所有的 md 文件名,生成对应的文件读取器。

    - vant
    	- README.md
    	- Tag
    		- README.md
    	- Button
    		- README.md
    	...
    
  2. 基于文件名,拿到所有的文件,生成文件读取器对象

  3. 加载文件,将文件的内容基于一个长度进行分块

    /**
     * 读取Zand目录下的所有markdown文件
     */
    export async function loadDocuments() {
      const __filename = fileURLToPath(import.meta.url); 
      const __dirname = path.dirname(__filename);
      // 从vant文件夹里递归读取所有的markdown文件路径
      const files = await extractFiles(path.resolve(__dirname, './vant'), 'md')
      // 将对应路径的文件生成文件读取器
      const fileReaders = files.map(filepath => new FileReader(filepath))
      // 读取文件内容
      const docs = await Promise.all(fileReaders.map(async (curFileReader) => await curFileReader.load()))
      // 拆分文件内容为chunks
      const chunks = docs.reduce<Array<string[]>>((acc, doc) => {
        // 最大size为500
        acc.push(splitChunks(doc, 500))
        return acc
      }, [])
      return chunks
    }
    

第二阶段准备向量数据库:向量数据库增删查改

这里以 chroma 为例:

class ChromaDBClient {
  client: ChromaClient
  collection: Collection
  constructor() {
    this.client = new ChromaClient()
  }
  // 创建或者获取集合
  async load(embedder: IEmbeddingFunction, collectioName: string) {
    this.collection = await this.client.getOrCreateCollection({
      name: `${collectioName}_collection`,
      embeddingFunction: embedder
    })
  }
  // 添加向量化数据
  async add(documents: string[]) {
    await this.collection.add({
      documents,
      ids: documents.map((str, index) => str.slice(0, 2) + index)
    })
  }
  // 基于text查询topK关联度最高的结果
  async query(text: string, topK: number) {
    const results = await this.collection.query({
      queryTexts: [text],
      nResults: topK, // 召回的数量,返回两个最相关的结果
    })
    return results.documents[0]
  }
  // 查询目前集合内的数据
  async getTopCollection(limit: number) {
    // 注意这里include如果不指定Embeddings,那么出于安全考虑,chroma将会返回null
    return await this.collection.get({
      limit,
      include: [IncludeEnum.Embeddings, IncludeEnum.Documents]
    })
  }
}

第三阶段:加载chroma数据库,并设置自定义的向量化函数(Embedding)

远程和本地的Eembedding方法:

import { DefaultEmbeddingFunction } from "chromadb"
import openai from "../openai/openAI.ts"

export default class RemoteEmbedding extends DefaultEmbeddingFunction {
  async generate(texts: string[]): Promise<number[][]> {
    // 远程调用openai的embedding能力,将文本转化为向量
    const embedding = await openai.embeddings.create({
      model: "text-embedding-ada-002",
      input: texts
    })
    const data = embedding.data.map(item => item.embedding)
    return data
  }
}
import { IEmbeddingFunction } from "chromadb";
import path from "node:path";
import { fileURLToPath } from "node:url";

export default class LocalEmbedding implements IEmbeddingFunction {
  ...
  public async generate(texts: string[]): Promise<number[][]> {
    this.pipelinePromise = new Promise(async (resolve, reject) => {
      try {
        const { pipeline, env } = await import('@xenova/transformers');
        const quantized = this.quantized
        const revision = this.revision
        const progress_callback = this.progress_callback
        const __filename = fileURLToPath(import.meta.url); 

        env.localModelPath = path.resolve(__filename, '../../../pretrained-model/')
        env.allowRemoteModels = false;
        resolve(pipeline("feature-extraction", this.model, {
          local_files_only: true,
          model_file_name: this.model,
          quantized,
          revision,
          progress_callback,
        }))     
      } catch (e) {
        reject(e);
      }
    });

    let pipe = await this.pipelinePromise;
    let output = await pipe(texts, { pooling: "mean", normalize: true });
    return output.tolist();
  }
}

加载chroma,并设置自定义的Embedding:

// mode是执行命令时传入的,为local或者remote,本地和远程
const mode = process.argv.slice(2)[0]

async function init() {
  // 加载vant文件目录
  const docs = await loadDocuments()
  // 自定义embedding函数
  const embedder = mode === 'local' ? new LocalEmbedding() : new RemoteEmbedding()
  // 初始化向量数据库
  const chromaClient = new ChromaDBClient()
  // 创建和加载集合,将自定义embedding类传入
  await chromaClient.load(embedder, mode)
  return {docs, chromaClient}
}

第四阶段:将分块后的训练数据,插入到向量化数据库

/**
 * 加载预训练文档
 * @param params 
 * @returns 
 */
export const addPreTrainData: Handler = async (params) => {
  const { docs, chromaClient} = params
  const addPromise = docs.map(async (documents) => {
    await chromaClient.add(documents)
  })
  await Promise.all(addPromise)
  return params
}

第五阶段:将用户查询的数据向量化后(由于自定义了向量化函数,这一步是自动的),查询向量数据库最相关的文本片段(召回),根据召回的文本和用户的查询,组成提示词模板

/**
 * 根据用户查询召回生成提示词
 * @param params 
 * @returns 
 */
export const recallQuery: Handler<{template: string}> = async (params) => {
  const query = await input({message: "请输入您的疑问"});
  const { chromaClient } = params
  // 这里召回数量设置为5条
  const documents = await chromaClient.query(query, 5)
  const template = await generateTemplate(documents, query)
  console.log('召回后的提示词', template)
  return {
    ...params,
    template
  }
}

第六阶段:生成大模型会话,让大模型扮演小助手,回答用户疑问

/**
 * 大语言模型会话
 * @param params 
 */
export const largeLanguageModelChat = async (params: HandlerParams & {template: string}) => {
  const { template } = params;
  const completion = await openai.chat.completions.create({
    model: "gpt-3.5-turbo",
    messages: [
      {
        "role": "system",
        "content": "你是一个专业的前端领域知识库的客服,负责回答用户对于知识库的疑问"
      },
      {
        "role": "user",
        "content": template,
      }
    ]
  })
  return completion
}

RAG发展趋势

上文跟大家介绍了RAG的实践,实际上RAG的应用并没有那么简单。

RAG效果与索引阶段和召回阶段的付出成正比。优化方向也是着重在索引阶段(文档清洗和导入)和召回阶段(用户查询的相似文档搜索)。具体的优化细节可以上网搜索下。

RAG目前可以分为几类:

  • Naive RAG: 朴素RAG,也就是我们上文中介绍的
  • Advanced RAG:高级RAG,在朴素RAG基础上增加数据源和query的前处理,以及对检索结果的后处理。
  • Modular RAG: 模块化RAG,将各个部分区分成固定的模块,更加方便的调用各个组件。同时增加Agent相关的模块,比如路由模块,根据用户的查询转发到对应的知识库。

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

RAG框架

做好一个RAG应用是很复杂的,那么我们能不能使用业界成熟的方案呢?

实际上,RAG目前市面上已经有框架支持,例如 FastGPT

github.com/labring/Fas…

FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景。

支持本地部署,也支持在FastGPT网上体验。

FastGPT

在FastGPT创建应用的时候,可以选择 知识库+对话引导 的方式。

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

我们可以在知识库这先导入知识库相关的数据,可以看到这里FastGPT支持的文件类型不仅仅是纯文本:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

这里我将 vant 相关的 markdown 文件进行上传:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

可以看到我们可以选择文档分片处理的方式:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

当知识库上传后,我们可以在知识库模拟用户查询的query,进行搜索召回测试:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

回到刚刚创建的 知识库+对话引导 的应用,我们可以关联知识库,然后引导模型先对知识库进行搜索。可以看到提示词这里有提示:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

这里我将提示词设置为: 你是vant组件库的小助手,请你搜索vant知识库,并结合知识库的搜索结果进行回答,不允许编造内容 。这里的提示词可以认为是上文中 rolesystem 的信息,用于定义系统的角色。

可以看到右侧有一个调试预览,我这里输入如何使用 virtual-table(virtual-table是我另外上传的markdown,大家可以问vant官方的组件例如如何使用uploader),这个应用就会结合知识库进行回答:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

在回答的最下方,可以看到使用了多少条知识库的引用和上下文:

学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au

在使用FastGPT的时候,可以发现针对索引和召回有非常丰富的方式,这里只是给大家抛砖引玉,有兴趣的可以多尝试交流。当然,不管是远程还是本地化部署,想要使用核心功能,还是得付费。。

示例代码

查看示例代码:github

由于上传了本地模型到 github 上,所以要先配置 git-lfs 指令,安装git-lfs

由于使用了 GPT 的会话接口,所以需要设置 GPT 的 API KEY 。创建 .env 在根目录下,设置 OPENAI_URLopenai的地址、OPENAI_KEY 为自己的 API KEY。

代码执行步骤:

  1. pnpm install
  2. 本地跑chroma向量数据库 pnpm run start:chroma

打开另一个终端:

  • pnpm run local 执行本地模型向量化
  • pnpm run remote 执行远程模型向量化

随后可以看到提示,输入查询后,会打印提示词和会话结果。

结语

在写这个RAG的文章时遇到了很多困难。。。有时候困难不来自于技术,而来自于各种外部限制,例如:

  • 在使用 Openai API 接口时,如何充值openai账户,是一个棘手的问题,最终我还是没充值成功(因为卡被拒了),找了一个同等价位的转发接口。
  • 使用 chroma 时,拉取不到默认的模型,只能使用自定义的模型
  • 使用 HuggingFacetransformer 时,拉取不到模型,只能上网站手动下载
  • 使用Node开发,资料较python来说少一些,只能看着python的资料然后让GPT转成Node代码
转载自:https://juejin.cn/post/7399982698846928937
评论
请登录