像chatgpt这样的大语言模型(LLM)可以回答很多类型的问题,但是,如果只依赖LLM,它只知道训练过的内容,不知道你的私有数据:如公司内部没有联网的企业文档,或者在LLM训练完成后新产生的数据。(即使是最新的GPT-4 Turbo,训练的数据集也只更新到2023年4月)所以,如果我们开发一个聊天机器人,可以与自己的文档对话,让LLM基于文档的信息回答我们的问题,是一件很有意义的事情。
本次我们会基于RAG的原理,通过LangChain来实现与pdf文档对话。
本次用到的文档放在这里的docs目录:https://github.com/fireshort/langchain-chat-with-your-data 我们这次会以吴恩达教授CS229(斯坦福的机器学习课程)的pdf为例子。
什么是RAG?
RAG是Retrieval-augmented generation(检索增强生成)的简称,它结合了检索和生成的能力,为文本序列生成任务引入额外的外部知识(通常是私有的或者是实时的数据),就是用外部信息来增强LLM的知识。RAG 将传统的语言生成模型与大规模的外部知识库相结合,使模型在生成响应或文本时可以动态地从这些知识库中检索相关信息。这种结合方法旨在增强模型的生成能力,使其能够产生更为丰富、准确和有根据的内容,特别适合需要具体细节或外部事实支持的场合。
RAG一般分为下面几步:
检索:对于给定的输入(问题),模型首先使用检索系统从大型文档集合中查找相关的文档或段落。这个检索系统通常基于密集向量搜索。
上下文编码:找到相关的文档或段落后,模型将它们与原始输入(问题)一起放到Prompt里。
生成:使用编码的上下文信息,模型生成输出(答案)。这通常通过大模型完成。
RAG原理
使用LangChain实现
RAG看起来还是比较抽象,我们接下来会用LangChain实现,可以细分为下面5步:
- Document Loading:文档加载器把 Documents 加载为以 LangChain 能够读取的形式。
- Splitting:文本分割器把 Documents 切分为指定大小的、语义上有意义的块,一般称为“文档块”或者“文档片”。
- Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
- Retrieval:应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
- Output:把问题和相似的文档块传递给语言模型(LLM),使用包含问题、检索到的文档块的提示生成答案。
注意,最新版的openai库与当前的LangChain不兼容,要安装0.28.1版的openai库。
!pip install openai==0.28.1
要先用.env文件来初始化环境变量。
关于如何用.env文件初始化环境变量和LangChain的入门教程,推荐阅读专栏《基于LangChain的LLM应用开发》:https://juejin.cn/column/7290751135904038953
from langchain.document_loaders import PyPDFLoader
from langchain.memory import ConversationBufferMemory
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chat_models import AzureChatOpenAI
from langchain.chains import ConversationalRetrievalChain
# 用.env文件初始化环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
文档加载
为了创建一个与pdf文档对话的应用,首先要将pdf文档加载为LangChain可以使用的格式。LangChain提供了文档加载器来完成这件事。LangChain有超过80种不同类型的文档加载器。
文档加载器把各种不同来源的数据格式转换成标准化的格式:Document类,包括page_content(文档内容)和关联的metadata(元数据,如果是pdf的话会包括来源和页码{‘source’: ‘docs/cs229_lectures/MachineLearning-Lecture01.pdf’, ‘page’: 0});如果是其他的文档类型,如Notion则没有页码)
需要先安装pypdf库:! pip install pypdf
# 加载文档
pdffiles = [
"docs/cs229_lectures/MachineLearning-Lecture01.pdf",
"docs/cs229_lectures/MachineLearning-Lecture01.pdf", # 故意重复以模拟杂乱数据
"docs/cs229_lectures/MachineLearning-Lecture02.pdf",
"docs/cs229_lectures/MachineLearning-Lecture03.pdf"
]
docs = []
for file_path in pdffiles:
loader=PyPDFLoader(file_path)
docs.extend(loader.load())
print(f"The number of docs:{len(docs)}")
# print(docs[0])
这里故意重复加载第一章的pdf,目的是为了演示如何处理重复数据。在实际的工程中,即使经过数据清洗,很多时候也难以避免重复数据。
文档分割
文档已经加载了,但是这些文档仍然相当大,我们需要将加载的文本分割成更小的块,以便进行嵌入和向量存储。这一步很重要,因为我们对文档检索,只需要检索最相关的内容,没必要加载整个巨大的文档,一般只需要得到与主题相关的段落或句子就够了。
这一步看似简单