学习RAG!前端也能打造自己的AI知识库RAG检索增强生成 RAG是什么 RAG是检索增强生成(Retrieval-Au
RAG检索增强生成
RAG是什么
RAG是检索增强生成(Retrieval-Augmented Generation),是指对大型语言模型(LLM)输出进行优化,使其能够在生成响应之前引用训练数据来源之外的权威知识库。
为什么需要RAG
为什么需要RAG?我们可以从大语言模型之一的GPT来了解大语言模型的缺陷。
GPT是一种生成式的、预训练的大模型,属于深度学习:
- G全称为Generative 生成式:GPT能够通过深度学习算法对已有数据库进行学习,再根据输入的指令生成全新的内容。
- P全称为Pre-tranined预训练:GPT利用海量语料数据进行预先训练、深度学习,从而使得模型能够掌握自然语言的语法、语义和知识等方面的信息,构建一个千亿级参数的知识数据库以供检索。
- T全称为Transformer转换模型:GPT所用的机器强化学习系统架构、是基于Transformer这个大语言模型,通过神经网络来模拟人脑的学习方式。
大型语言模型例如GPT的本质
通过预训练数据集生成知识数据库,再通过Transformer转换模型理解输入指令,对已有知识数据库检索和学习生成全新内容。
基于大语言模型的本质,我们可以知道它存在以下局限性:
-
领域信息不足
大语言模型基于公开的数据集进行训练,那么意味着它缺乏领域特定或专有的非公开信息训练。
-
提供过时的信息
由于大语言模型需要进行预训练,训练时依赖过去已有的数据。在用户需要实时响应的时候,可能会提供过时或者通用的信息。
-
预训练数据不可更改
当大语言模型使用的预训练数据可能包含错误的信息时,没有办法进行更正或者删除。则模型基于错误的预训练数据给出错误的答案。
-
答非所问或者凭空造词
尽管大语言模型尽力根据已有的数据提供信息和答案,但是在超出其范围的情况下,可能会给出不正确或者是虚构的信息,这种信息也称为大模型的幻觉
这也是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就是让LLM有继续学习的能力,是一种能够向LLM投喂特定知识的技术。
RAG实现流程
RAG的实现流程因研究方法不同而各有差异,但基本流程大致相同:
- 索引模块(indexing)1-4:将文本数据进行切分,生成不同的文本片段,进行向量化之后存储到向量数据库。
- 检索模块(retrieval)6-9:将用户的query进行向量化,并从向量数据库中检索 top k 相似向量,然后获取对应的文本片段。
- 大模型生成模块(generation)10-13:根据文本片段和用户query拼装成提示词,输入到LLM中生成最终答案。
-
索引模块
对知识进行处理和加工,生成向量模型可以接受长度的文本块(一般最大是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实践
初始化向量数据库
这里以 chroma
为例,安装 chroma
的 npm 包
pnpm install chromadb chromadb-default-embed
本地运行 chroma
向量数据库的客户端:
chroma run --path ./chromadb
创建 chroma 连接:
import { ChromaClient } from 'chromadb'
const client = new ChromaClient()
创建Chroma集合
Collection
是 Chroma
的一个概念,是用来存储向量化数据、源文本数据以及其他原数据信息的空间。类似于我们的数据库表的概念。
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
,有两种途径可以实现数据向量化:
- 本地化模型,使用
transformer.js
调用模型实现分词和嵌入 - 直接调用远程的向量化模型,例如
OpenAI
就有提供embedding
的方法
分词和嵌入(向量化)
预训练数据向量化存储需要经过两个步骤:
第一步是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,关于分词的详细过程,可以查看这篇文章:
第二步是Embedding(嵌入):
Embedding就是将tokenizer处理后的数字ID,转换为更低维度的数据,也称为向量数据。
例如使用 T5TokenizerFast
这个分词器来分词 Hello World
,则:
- tokenizer会将token序列 ['Hello', 'World', '!'] 编码成数字序列[8774, 1150, 55, 1],也就是['Hello', 'World', '!', '
</s>
'],然后在句尾加一个</s>
表示句子结束。 - 这四个数字会变成四个one-hot向量,例如8774会变成[0, 0, ..., 1, 0, 0..., 0, 0],其中向量的index为8774的位置为1,其他位置全部为0。假设词表里面一共有30k个可能出现的token,则向量长度也是30k,这样才能保证出现的每个单词都能被one-hot向量表示。
- 也就是说,一个形状为(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而已。
那么通过模拟这个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方式拉取,依然被拒绝,目前我下载的办法比较笨,是一个个文件从这个模型库上下载下来的:
完整DEMO
回到我们实现RAG的流程图:
根据流程图,我们可以写出入口代码:
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])
})
第一阶段预处理:处理训练数据
-
准备好预训练数据,这里以
vant
组件库下的所有md
文件为例,通过Node
读取所有的md
文件名,生成对应的文件读取器。- vant - README.md - Tag - README.md - Button - README.md ...
-
基于文件名,拿到所有的文件,生成文件读取器对象
-
加载文件,将文件的内容基于一个长度进行分块
/** * 读取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框架
做好一个RAG应用是很复杂的,那么我们能不能使用业界成熟的方案呢?
实际上,RAG目前市面上已经有框架支持,例如 FastGPT
FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景。
支持本地部署,也支持在FastGPT网上体验。
在FastGPT创建应用的时候,可以选择 知识库+对话引导
的方式。
我们可以在知识库这先导入知识库相关的数据,可以看到这里FastGPT支持的文件类型不仅仅是纯文本:
这里我将 vant
相关的 markdown
文件进行上传:
可以看到我们可以选择文档分片处理的方式:
当知识库上传后,我们可以在知识库模拟用户查询的query,进行搜索召回测试:
回到刚刚创建的 知识库+对话引导
的应用,我们可以关联知识库,然后引导模型先对知识库进行搜索。可以看到提示词这里有提示:
这里我将提示词设置为: 你是vant组件库的小助手,请你搜索vant知识库,并结合知识库的搜索结果进行回答,不允许编造内容
。这里的提示词可以认为是上文中 role
为 system
的信息,用于定义系统的角色。
可以看到右侧有一个调试预览,我这里输入如何使用 virtual-table(virtual-table是我另外上传的markdown,大家可以问vant官方的组件例如如何使用uploader),这个应用就会结合知识库进行回答:
在回答的最下方,可以看到使用了多少条知识库的引用和上下文:
在使用FastGPT的时候,可以发现针对索引和召回有非常丰富的方式,这里只是给大家抛砖引玉,有兴趣的可以多尝试交流。当然,不管是远程还是本地化部署,想要使用核心功能,还是得付费。。
示例代码
查看示例代码:github
由于上传了本地模型到 github
上,所以要先配置 git-lfs
指令,安装git-lfs
由于使用了 GPT
的会话接口,所以需要设置 GPT
的 API KEY 。创建 .env
在根目录下,设置 OPENAI_URL
为 openai
的地址、OPENAI_KEY
为自己的 API KEY。
代码执行步骤:
pnpm install
- 本地跑chroma向量数据库
pnpm run start:chroma
打开另一个终端:
pnpm run local
执行本地模型向量化pnpm run remote
执行远程模型向量化
随后可以看到提示,输入查询后,会打印提示词和会话结果。
结语
在写这个RAG的文章时遇到了很多困难。。。有时候困难不来自于技术,而来自于各种外部限制,例如:
- 在使用
Openai API
接口时,如何充值openai账户,是一个棘手的问题,最终我还是没充值成功(因为卡被拒了),找了一个同等价位的转发接口。 - 使用
chroma
时,拉取不到默认的模型,只能使用自定义的模型 - 使用
HuggingFace
的transformer
时,拉取不到模型,只能上网站手动下载 - 使用Node开发,资料较python来说少一些,只能看着python的资料然后让GPT转成Node代码
转载自:https://juejin.cn/post/7399982698846928937