python实战(七)——基于LangChain的RAG实践
一、任务目标
基于之前的RAG实战,相信大家对RAG的实现已经有了一定的了解了。这篇文章将使用LangChain作为辅助,实现一个高效、便于维护的RAG程序。
二、什么是LangChain
LangChain是一个用于构建大模型应用程序的开源框架,它内置了多个模块化组件。通过这些组件,我们能够快速且便捷地搭建一个强大的大模型应用程序。首先,我们来看一下如何通过LangChain来调用大语言模型:
from langchain_community.chat_models.openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
import os
API_SECRET_KEY = "your api key"
BASE_URL = "your api base"
os.environ["OPENAI_API_KEY"] = API_SECRET_KEY
os.environ["OPENAI_API_BASE"] = BASE_URL
# 这里的temperature参数控制答案生成的随机性,0表示按照概率最大的结果生成,也就是最稳定
# 如果温度设置为1则表示生成极富随机性的结果
# 由于我们没有指定调用哪个openai模型,默认会是gpt-3.5-turbo
chat = ChatOpenAI(temperature=0.0)
# 定义template字符串,这里不需要使用f字符串来赋值
template = """请将下面的中文文本翻译为{language}。\
文本:'''{text}'''
"""
# 将template字符串转换为langchain模板,这时候会自动识别prompt模板需要的参数,即{}中的内容
prompt_template = ChatPromptTemplate.from_template(template)
customer_language = '英文'
customer_text = '你好,我来自中国。'
# 传入相应字符串生成符合大模型输入要求的prompt
customer_messages = prompt_template.format_messages(language=customer_language, text=customer_text)
print(customer_messages[0])
# 调用大语言模型
customer_response = chat.invoke(customer_messages, temperature=0.0)
print(customer_response.content)
相比起我们直接使用openai库调用大模型的方式,LangChain库调用大语言模型要更加结构化和模块化,尤其是prompt模板的构建部分。这样做的好处是,我们可以很方便地进行程序的复用。另外,LangChain内置了多个常用场景的Prompt模板,可以拿来即用,省去了我们重新设计和构造prompt的时间。
三、RAG流程构建
1、文档加载
LangChain支持多种文档格式的加载,比如doc、pdf、markdown甚至html等。这里,我们加载几个pdf文档(文档中的内容是调用大模型生成的关于各个西游记主角的人物特征):
from langchain_community.document_loaders.pdf import PyPDFLoader
# 这里会逐页加载文档
loaders = [PyPDFLoader('孙悟空.pdf'), PyPDFLoader('猪八戒.pdf'), PyPDFLoader('沙和尚.pdf'), PyPDFLoader('唐僧.pdf')]
docs = []
for loader in loaders:
pages = loader.load()
# 打印pages的元素个数
print(len(pages))
# 打印第一个元素,也就是第一页的一部分文字看看
print(pages[0].page_content[:10])
# 打印第一个元素的元数据
print(pages[0].metadata)
docs.extend(pages)
2、文档分割
加载后的文档可能很大,我们需要将文档分割成一个一个的小块进行存储。在进行检索的时候,也能够直接返回相关的文档块,而不需要整个文档都传给大模型。LangChain提供字符级别的文档分隔、token级别的文档分隔等多个工具。这里我们使用字符分割:
from langchain.text_splitter import RecursiveCharacterTextSplitter
# chunk_size用于设置每个文本块的大小,chunk_overlap用于设置每个文本块直接的重叠部分大小
text_splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=10)
blocks = text_splitter.split_documents(docs)
3、文本向量化存储
为了便于后续使用,我们把文本转化成向量并存储到本地。向量化的意义在于可以很方便地比较文本相似度,从而检索出与query相关的信息返回给大模型。
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# 定义向量化工具
embedding_model = OpenAIEmbeddings(model='text-embedding-3-small')
directory = './save_folder'
# Chroma是一个轻量级的向量存储库,langchain提供了数十个向量存储库,这里我们使用高效的Chroma
vectordb = Chroma.from_documents(
documents=blocks,
embedding=embedding_model,
persist_directory=directory
)
# 永久化存储
vectordb.persist()
# 打印文本向量数
print('文本向量数:', vectordb._collection.count())
# 可以进行相似性检索,k指定返回结果数量
query = '请介绍一下唐僧的性格特点'
search_results = vectordb.similarity_search(query, k=3)
print('相似文本示例:', search_results[0])
这里,我们使用了similarity_search方法检索相关的文本向量,这是纯相似度比较的方式,返回结果将高度相关,但也会减少多样性。如果希望在查询的相关性和多样性之间保持平衡,也可以使用max_marginal_relevance_search方法进行向量检索。
4、问答
这里,我们构造一个检索式问答链,并让程序返回它所用到的检索结果:
from langchain_community.chat_models import ChatOpenAI
from langchain.chains.retrieval_qa.base import RetrievalQA
from langchain.prompts import PromptTemplate
llm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0.0)
template = '请根据上下文回答以下问题。上下文:{context},问题:{question}'
qa_prompt = PromptTemplate.from_template(template)
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={'prompt':qa_prompt}
)
question = '请介绍一下唐僧的性格特点'
response = qa_chain({'query':question})
# 打印模型响应
print(response['result'])
# 打印第一个相关的检索结果
print(response['source_documents'][0])
上面的代码中,{context}是检索结果,{question}是指我们的提问,这两个变量不可缺少,变量命名和相对位置也应当与上面一致,至于放置的绝对位置则无特别要求。
四、完整代码
代码运行过程中如果存在报错是因为一些依赖库没有安装,pip install就可以了。如果是无报错退出且状态码是00005结尾的,那么应该是Chroma读取内存数据的时候报错了,persist存储到本地之后,使用Chroma(persist_directory='你的存储地址', embedding_function='初始化之后的embedding模型')即可正常运行。
from langchain_community.document_loaders.pdf import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.chat_models import ChatOpenAI
from langchain.chains.retrieval_qa.base import RetrievalQA
from langchain.prompts import PromptTemplate
import os
API_SECRET_KEY = "your api key"
BASE_URL = "your api base"
os.environ["OPENAI_API_KEY"] = API_SECRET_KEY
os.environ["OPENAI_API_BASE"] = BASE_URL
# 这里会逐页加载文档
loaders = [PyPDFLoader('孙悟空.pdf'), PyPDFLoader('猪八戒.pdf'), PyPDFLoader('沙和尚.pdf'), PyPDFLoader('唐僧.pdf')]
docs = []
for loader in loaders:
pages = loader.load()
docs.extend(pages)
# chunk_size用于设置每个文本块的大小,chunk_overlap用于设置每个文本块直接的重叠部分大小
text_splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=10)
blocks = text_splitter.split_documents(docs)
# 定义向量化工具
embedding_model = OpenAIEmbeddings(model='text-embedding-3-small')
directory = 'save_folder'
# Chroma是一个轻量级的向量存储库,langchain提供了数十个向量存储库,这里我们使用高效的Chroma
vectordb = Chroma.from_documents(
documents=blocks,
embedding=embedding_model,
persist_directory=directory
)
# 永久化存储
vectordb.persist()
# 指定模型
llm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0.0)
# 构建模板
template = '请根据上下文回答以下问题。上下文:{context},问题:{question}'
qa_prompt = PromptTemplate.from_template(template)
# 构建检索式问答链
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={'prompt':qa_prompt}
)
quest = '请介绍一下唐僧的性格特点'
response = qa_chain({'query':quest})
# 打印模型响应
print('模型响应:', response['result'])
# 打印第一个相关的检索结果
print('第一个相关的检索结果:', response['source_documents'][0])
五、总结
本文实现了基于LangChain的RAG问答助手程序,但是其中仍然有许多值得打磨的细节,例如目前的问答助手只能支持一问一答的互动形式,如果需要基于历史聊天记录进行问答,则需要使用LangChain中其他的模块化工具进行聊天记录的存储和调用,这些内容将在后续的博文中讨论。