RAG原理概述
RAG(Retrieval-Augmented Generation) 是一种结合了信息检索和生成式人工智能技术的模型架构,旨在让模型生成更有根据和更准确的回答。通俗来讲,它让模型不只是凭借自己的“记忆”(预训练数据)生成答案,还能即时去“查资料”,再结合查到的信息生成答案。它解决了模型只依赖自己有限的知识,回答不准确或过时的问题。
RAG的工作原理可以分为两部分:
-
检索(Retrieval):
- 当你向模型提出一个问题时,RAG 先会去一个外部知识库(比如一堆文档、维基百科、数据库等)“查找”相关内容。这就像你向搜索引擎询问某个问题,它会先给你返回一系列相关的网页或文章。
- 为了查找效率高,它把这些文档事先处理成计算机能够理解的“向量”(数字表示),方便进行快速匹配。
-
生成(Generation):
- 模型在查到的信息基础上进行“生成”,即使用一个类似于 GPT 这样的生成模型,结合检索到的内容,生成一个有逻辑、准确的回答。
- 比如你问一个问题,它查到相关的几段信息后,模型会把这些信息和自己的知识结合,生成一个自然语言的答案。
为什么 RAG 这么特别?
RAG 的关键好处是增强了生成模型的准确性。单纯的生成模型有时会“瞎猜”,尤其在面对最新的知识或较为专业的问题时。而 RAG 可以实时“查资料”,大大提高回答的准确性和时效性。
简单类比
- 单纯的生成模型:像一个很聪明的学生,知识广泛,但记忆有限,没办法查书,可能会给出一些不太准确的回答。
- RAG 模型:像一个知道自己不知道一切的学生,碰到不会的问题,可以去图书馆查资料再回答,答案自然更可靠。
应用场景
- 客服系统:能够根据公司文档或常见问题库生成精准的回答。
- 搜索引擎升级:提供比传统搜索引擎更智能的回答,既有检索又有生成。
- 医疗、法律等专业领域:实时查阅最新的相关文档,生成符合当前标准的建议。
总的来说,RAG 是一种让模型不仅“会想”,还“会查”的技术,能提升模型回答问题的准确性和实时性。
实战项目简介
我搭建的是一个Prompt技术的AI学习助手(基于自己搭建的和Prompt技术有关的文章与书籍)。
学习写Prompt需要一边写一边实践,否则就会“脑子说我会了,手说我废了”。平时看到好的Prompt,也会把它积累下来,也许哪天就能用上;在实践的时候,随着不断地修改,Prompt也会更新迭代;网上有很多学习Prompt技术好的资料(比如吴恩达出的Prompt Cookbook, 这个项目知识库里就有这本书),但是没有那么多时间去一一啃过,而且这类重实践的技术肯定是随学随用最好了,一边提问,大模型可以基于它本身的生成能力,以及知识库文档的内容,回答我的问题,同时根据我的需求帮我生成好的Prompt。
目前仅做了针对GPT、GLM生成文本类的Prompt,还没有加入Midjourney、stable diffusion的Prompt.
Embedding 模型:智谱清言配套的Embeding 模型
向量型数据库:Chroma
LLM : 调用ChatGLM-4的API
工具链:Langchain
前端:streamlit
知识库数据:
《面向开发者的LLM教程第一章:Engineering for Developers》(md文件)
《Prompt cookbook》(PDF文件)
安装依赖:
项目实战
一、知识库搭建
一般知识库的数据构建、清洗处理、转换成向量、存储到向量数据库都是提前离线做好的。经过一段时间再更新知识库内容时,还要把上面这些步骤再做一遍,一个上线的项目,这个过程可能是半个月或者一个月进行一次。
知识库的搭建包括以下几个部分:
-
数据预处理:收集相关的文本数据。数据源可以是结构化的数据库、非结构化的文本(如网页、PDF、Word 文档)、API 返回的内容、维基百科条目、社交媒体内容等。原始数据通常不适合直接使用,因此需要对数据进行清洗和预处理,如去掉无关的内容(广告、噪音)、标准化格式(统一编码、去除重复等)
-
数据分割:对于非结构化的长文档,需要将其分割成更小的片段(例如段落、句子)。片段的大小要合理,既保证能被检索到,又能让生成模型获取足够的信息来生成相关的回答。通常通过分段规则,如按段落或固定长度的字符数进行分割。
-
文本嵌入(向量化):预训练模型选择:选择适合的预训练模型来生成文本嵌入。常用的模型有 BERT、Sentence-BERT、OpenAI 的文本嵌入模型等。模型的选择会影响到后续的检索效果。
嵌入计算:将每一个片段转化为向量表示。这一步是将自然语言转化为固定维度的向量,以便后续通过向量相似度进行检索。
向量归一化:对生成的向量进行归一化处理,确保向量在检索时能够正确计算相似度(如使用余弦相似度或欧几里得距离)。 -
构建索引+向量入库:将所有文本片段的向量保存到向量数据库中,以便进行快速检索。常用的向量数据库有 Pinecone、FAISS、Milvus 等。这些数据库支持高效的相似度搜索,能够快速返回最相关的文档。
传统索引(可选):有时会将文本进行关键词索引(倒排索引),使用搜索引擎如 Elasticsearch、Whoosh 等。关键词搜索和向量搜索可以结合使用,以提高检索的准确性。
在搭建RAG应用的时候,一般都是用嵌入模型来构建词向量,此时有两个选择,一是选择在线大模型配套的Embedding模型 API,很多公司都有提供接口,不过有些是要付费的;再者也可以选择使用本地的Embedding模型,比如FlagEmbedding、BGE等等,目前已经有很多对中文语料进行Embedding表现得不错的开源模型,hugging face上有很多开源的中文Embedding模型可供选择。
开源中文Embedding模型排行榜
本项目中我使用的是智谱清言的Embedding模型 API
主流的向量数据库有:Chroma、Weaviate、Qdrant等等,这里我使用的是Chroma,它是一个轻量级向量数据库,拥有丰富的功能和简单的 API,具有简单、易用、轻量的优点,但功能相对简单且不支持GPU加速。
1.1 数据预处理
首先在项目目录下面新建一个文件夹/data_base/knowledge_db,把用到的资料放进knowledge_db里。
1.1.1 数据加载读取
首先来读取数据,由于我的知识库里文本类型是PDF和Markdown文件,我们可以使用 LangChain 的 PyMuPDFLoader 来读取知识库的 PDF 文件,用UnstructuredMarkdownLoader来读取Markdown文件。
首先看PDF文件的加载:
from langchain.document_loaders.pdf import PyMuPDFLoader
# 创建PyMuPDFLoader实例,输入为要加载的 pdf 文档路径
loader = PyMuPDFLoader("/root/data_base/knowledge_db/Prompt_cookbook.pdf")
# 加载PDF文件
pdf_pages = loader.load()
文档加载后储存在 pdf_pages 变量中,pdf_pages是一个list,PDF有多少页,list的长度就有多少。
pdf_pages列表里的每一个元素就是一页PDF的文档,这个元素的变量类型是langchain_core.documents.base.Document,文档变量类型包含两个属性:page_content 包含该文档的内容, meta_data 为文档相关的元数据。
一般我们都是从page_content里面取到文本的数据。
Markdown文件的加载也是一样步骤的,这次用 UnstructuredMarkdownLoader模块:
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader("/root/knowledge_db/1. 简介 Introduction.md")
md_pages = loader.load()
1.1.2 数据清洗
我们期望知识库的数据尽量是有序的、优质的、精简的,因此我们要删除低质量的、甚至影响理解的文本数据。这部分主要就是用python里的文本处理操作。小伙伴们可以按照自己项目使用的文档特点进行处理,文本处理不会的就去问chatgpt,不用死记硬背正则化那些的。
可以看到文本里还是有一些多余的字符,比如‘\n\n’,这样的地方全部换成单个’\n’。
md_page.page_content = md_page.page_content.replace('\n\n', '\n')
处理之后的结果比较干净了:
1.1.3 整合PDF和Markdown文件处理
由于知识库里的文档很多,而且格式不统一,我们可以根据文件后缀是.md还是.pdf来分类批量读取到内容,然后放进一个空列表里:
# 获取知识库knowledge_db文件夹下所有文件路径,储存在file_paths里,
file_paths = []
folder_path = '/root/knowledge_db'
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
file_paths.append(file_path)
from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
# 遍历文件路径并把实例化的loader存放在loaders里
loaders = []
for file_path in file_paths:
file_type = file_path.split('.')[-1]
if file_type == 'pdf':
loaders.append(PyMuPDFLoader(file_path))
elif file_type == 'md':
loaders.append(UnstructuredMarkdownLoader(file_path))
# 下载文件并存储到text列表里
texts = []
for loader in loaders: texts.extend(loader.load())
texts列表里的元素都是langchain_core.documents.base.Document对象,每一个Document对象里都有page_content和meta_data。
1.2 文档分块(Chunks)
由于单个文档的长度往往会超过模型支持的上下文,导致检索得到的知识太长超出模型的处理能力,因此,在构建向量知识库的过程中,我们往往需要对文档进行分割,将单个文档按长度或者按固定的规则分割成若干个 chunk,然后将每个 chunk 转化为词向量,存储到向量数据库中。
在检索时,我们会以 chunk 作为检索的元单位,也就是每一次检索到 k 个 chunk 作为模型可以参考来回答用户问题的知识,这个 k 是我们可以自由设定的。
Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。
图示如下:
Langchain中还有其他很多的文档分割方法,这里我使用的是RecursiveCharacterTextSplitter(): 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
在RAG应用中文档分块是非常重要的一个环节,分割得不合适会非常影响答案的质量。如何选择分割方式,往往具有很强的业务相关性——针对不同的业务、不同的源数据,往往需要设定个性化的文档分割方式。RecursiveCharacterTextSplitter()这个方法是比较通用的,可以先基于这个跑一个baseline,再去优化。
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 使用递归字符文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
split_docs=text_splitter.split_text(pdf_page.page_content)
split_docs里存储的就是切割之后的文本块了,split_docs也是一个list,和前文一样,list中的每一个元素都是一个Document对象,每个Document对象里page_content内容就是被切割成好的chunk内容。
1.3 文本嵌入(向量化)
文本嵌入就是将知识库
这里用的是智谱AI原生的API,langchain内部目前暂时没有直接可用的embeddings模型,所以我们得手动把Embeddings模型封装到langchain的工具链里面。
首先看智谱的Embeddings模型如何调用。在智谱的官网上有详细的教程(另外:它的一系列接口文档都可以好好看一下),本项目我用的是模型是Embedding-2
看响应示例里返回的结果,文本经过Embedding后得到的高维向量,在“data”的embedding里面,这就是我们存入向量数据库的内容。
我们通过response.data[0].embedding拿到向量内容。
有时Embedding可能失败,那么可以加一个if判断Embedding是否成功,如果没有成功,那么返回一个0向量,逻辑实现如下。
client=ZhipuAI(api_key='your API_KEY')
query='请介绍一下Prompt工程是什么?'
response = client.embeddings.create(model='embedding-2', input=query)
if hasattr(response, 'data') and response.data:
return response.data[0].embedding
return [0] * 1024 # 如果获取嵌入失败,返回零向量
不管对知识库文本内容还是用户输入的问题内容,其实都需要进行Embedding处理,所以这里我写了一个类,里面定义了两个函数,embed_documents():用于处理知识库里的文本内容,逻辑如下——准备一个空列表, 将分割好的知识库文本列表传入,遍历每一条数据拿它们embedding处理后的向量,添加到空列表里,这个函数最终返回的就是chunks转换成向量后的列表。
**embed_query()**用于对用户输入的问题内容进行向量化处理。
(在这里有一个优化的空间:如果你的知识库规模很大,在这里可以设计成异步处理,使用线程池,先挖个坑,以后有空填上)
from zhipuai import ZhipuAI
class EmbeddingGenerator:
def __init__(self, model_name):
self.model_name = model_name
self.client = ZhipuAI(api_key='你的API_KEY')
def embed_documents(self, texts):
embeddings = []
for text in texts:
response = self.client.embeddings.create(model=self.model_name, input=text)
if hasattr(response, 'data') and response.data:
embeddings.append(response.data[0].embedding)
else:
# 如果获取嵌入失败,返回一个零向量
embeddings.append([0] * 1024) # 假设嵌入向量维度为 1024
return embeddings
def embed_query(self, query):
# 使用相同的处理逻辑,只是这次只为单个查询处理
response = self.client.embeddings.create(model=self.model_name, input=query)
if hasattr(response, 'data') and response.data:
return response.data[0].embedding
return [0] * 1024 # 如果获取嵌入失败,返回零向量
以上就是RAG的前置数据准备了,一般知识库文本嵌入的工作是离线处理的。
1.4 向量入库
Langchain 集成了超过 30 个不同的向量存储库,这里我选择用langchain里的 Chroma。
首先实例化一个我们在上一步里写好的EmbeddingGenerator,并定义使用的embedding模型为"embedding-2".
embedding_generator = EmbeddingGenerator(model_name="embedding-2")
接着定义持久化路径,这就是向量数据库的路径地址,而且后续我们的操作里要让它持续保存到磁盘上。
persist_directory = '../../data_base/vector_db/chroma'
实例化一个Chroma数据库对象,documents参数定义我们要传入的文本列表、embedding参数这里填入我们实例化好的Embedding生成器embedding_generator ,persist_directory这个参数填入刚才定义的持久化路径,这允许我们将persist_directory目录持久地保存到磁盘上,再加上vectordb.persist(),这样保证在项目运行过程中,我们创建的vectordb随时都可以用。
from langchain.vectorstores.chroma import Chroma
vectordb = Chroma.from_documents(
documents=split_docs,
embedding=embedding_generator,
persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上
)
vectordb.persist()
查看向量数据库里的向量数目:
可以测试一下加载的向量数据库,使用一个问题 query 进行向量检索。如下代码会在向量数据库中根据相似性进行检索,返回前 k 个最相似的文档。(这里记得要安装一下 OpenAI 开源的快速分词工具 tiktoken 包:pip install tiktoken)
二、构建RAG
接下来构建LLM,并且把它接入工具链中
2.1 创建LLM
这里我用的是langchain来调用智谱AI的API