用langchain+streamlit应用RAG实现个人知识库助手搭建

RAG原理概述

RAG(Retrieval-Augmented Generation) 是一种结合了信息检索和生成式人工智能技术的模型架构,旨在让模型生成更有根据和更准确的回答。通俗来讲,它让模型不只是凭借自己的"记忆"(预训练数据)生成答案,还能即时去"查资料",再结合查到的信息生成答案。它解决了模型只依赖自己有限的知识,回答不准确或过时的问题。

RAG的工作原理可以分为两部分:

  1. 检索(Retrieval)

    • 当你向模型提出一个问题时,RAG 先会去一个外部知识库(比如一堆文档、维基百科、数据库等)"查找"相关内容。这就像你向搜索引擎询问某个问题,它会先给你返回一系列相关的网页或文章。
    • 为了查找效率高,它把这些文档事先处理成计算机能够理解的"向量"(数字表示),方便进行快速匹配。
  2. 生成(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文件)

安装依赖:

项目实战

一、知识库搭建

一般知识库的数据构建、清洗处理、转换成向量、存储到向量数据库都是提前离线做好的。经过一段时间再更新知识库内容时,还要把上面这些步骤再做一遍,一个上线的项目,这个过程可能是半个月或者一个月进行一次。

知识库的搭建包括以下几个部分:

  1. 数据预处理:收集相关的文本数据。数据源可以是结构化的数据库、非结构化的文本(如网页、PDF、Word 文档)、API 返回的内容、维基百科条目、社交媒体内容等。原始数据通常不适合直接使用,因此需要对数据进行清洗和预处理,如去掉无关的内容(广告、噪音)、标准化格式(统一编码、去除重复等)

  2. 数据分割:对于非结构化的长文档,需要将其分割成更小的片段(例如段落、句子)。片段的大小要合理,既保证能被检索到,又能让生成模型获取足够的信息来生成相关的回答。通常通过分段规则,如按段落或固定长度的字符数进行分割。

  3. 文本嵌入(向量化):预训练模型选择:选择适合的预训练模型来生成文本嵌入。常用的模型有 BERT、Sentence-BERT、OpenAI 的文本嵌入模型等。模型的选择会影响到后续的检索效果。

    嵌入计算:将每一个片段转化为向量表示。这一步是将自然语言转化为固定维度的向量,以便后续通过向量相似度进行检索。

    向量归一化:对生成的向量进行归一化处理,确保向量在检索时能够正确计算相似度(如使用余弦相似度或欧几里得距离)。

  4. 构建索引+向量入库:将所有文本片段的向量保存到向量数据库中,以便进行快速检索。常用的向量数据库有 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文件的加载:

python 复制代码
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模块:

python 复制代码
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'。

python 复制代码
md_page.page_content = md_page.page_content.replace('\n\n', '\n')

处理之后的结果比较干净了:

1.1.3 整合PDF和Markdown文件处理

由于知识库里的文档很多,而且格式不统一,我们可以根据文件后缀是.md还是.pdf来分类批量读取到内容,然后放进一个空列表里:

python 复制代码
# 获取知识库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)
python 复制代码
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,再去优化。

python 复制代码
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向量,逻辑实现如下。

python 复制代码
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()**用于对用户输入的问题内容进行向量化处理。

(在这里有一个优化的空间:如果你的知识库规模很大,在这里可以设计成异步处理,使用线程池,先挖个坑,以后有空填上)

python 复制代码
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".

python 复制代码
embedding_generator = EmbeddingGenerator(model_name="embedding-2")

接着定义持久化路径,这就是向量数据库的路径地址,而且后续我们的操作里要让它持续保存到磁盘上。

python 复制代码
persist_directory = '../../data_base/vector_db/chroma'

实例化一个Chroma数据库对象,documents参数定义我们要传入的文本列表、embedding参数这里填入我们实例化好的Embedding生成器embedding_generator ,persist_directory这个参数填入刚才定义的持久化路径,这允许我们将persist_directory目录持久地保存到磁盘上,再加上vectordb.persist(),这样保证在项目运行过程中,我们创建的vectordb随时都可以用。

python 复制代码
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

三、部署streamlit

四、评估与优化

相关推荐
技术无疆1 小时前
【Python】Arrow使用指南:轻松管理日期与时间
开发语言·人工智能·python·深度学习·机器学习·数据挖掘·python3.11
三桥君1 小时前
Prompt:在AI时代,提问比答案更有价值
人工智能·ai·大模型·prompt·产品经理·三桥君
一轮秋月1 小时前
【工具】arxiv_latex_cleaner 去除latex注释
人工智能
通信仿真实验室1 小时前
(13)MATLAB莱斯(Rician)衰落信道仿真3
开发语言·人工智能·算法·matlab
xiandong204 小时前
240925-GAN生成对抗网络
图像处理·人工智能·深度学习·神经网络·生成对抗网络·php·gan
Q8343158194 小时前
华为 海思22AP10(SS524)H.265 编解码处理器用户指南
arm开发·人工智能·嵌入式硬件·音视频·硬件工程·h.265·视频编解码
Einstein·Jun4 小时前
深度学习----------------------------编码器、解码器架构
人工智能·深度学习
weixin_543662865 小时前
一个简单的摄像头应用程序3
人工智能·opencv·计算机视觉
墨染辉5 小时前
微调方法概述
人工智能·python·机器学习
浪子L5 小时前
YOLOv8改进 - 注意力篇 - 引入SEAttention注意力机制
人工智能·深度学习·计算机视觉