本节降通过 LangChain 构建了一个基于文档的问答系统,使用了本地模型和 FAISS向量存储来实现检索增强生成(RAG)。使用RetrievalQA 链和管道式(LECL)两种实现RAG(检索增强生成)的方法在本质上完成了相同的任务:从检索的文档片段中抽取信息并使用语言模型生成答案。但它们在结构、灵活性和适用场景上有所不同。
原理:使用了 LangChain 提供的封装好的 RetrievalQA 模块。
流程: 1. 使用 retriever 获取相关文档片段。 2. 将检索结果直接传递到 LLM,并通过预定义的提示模板处理上下文和问题。
优点: - 简单易用:LangChain 封装了检索和生成的逻辑,用户只需配置 retriever和提示模板即可。 - 快速搭建:适合初学者或标准的RAG场景,减少了低级别的配置工作。 - 自动化处理:RetrievalQA 可以处理更多内部逻辑,比如自动合并检索结果等。
缺点: - 灵活性较低:链的内部处理逻辑较固定,难以定制每一步操作。 - 调试难度较大:由于处理流程被封装,深入调试或自定义中间步骤较复杂。
RetrievalQA 链实现的黑神话悟空问答系统。
from langchain_community.vectorstores import FAISS # 导入FAISS向量存储库
from langchain_huggingface import HuggingFaceEmbeddings # 导入Hugging Face嵌入模型
from langchain_community.document_loaders import TextLoader # 导入文本加载器
from langchain.text_splitter import RecursiveCharacterTextSplitter # 导入递归字符文本分割器
from langchain_openai import ChatOpenAI # 导入ChatOpenAI模型
# 使用 OpenAI API 的 ChatOpenAI 模型
chat_model = ChatOpenAI(
openai_api_key="sk-qzzjmeaubyigjbumdbadlxmutffaffywummytnsflacorkxn",
base_url="https://api.siliconflow.cn/v1",
model="Qwen/Qwen2.5-7B-Instruct"
)
# 加载文本文件 "黑悟空.txt",编码格式为 'utf-8'
loader = TextLoader("黑悟空.txt", encoding='utf-8')
docs = loader.load() # 将文件内容加载到变量 docs 中
# 把文本分割成 200 字一组的切片,每组之间有 20 字重叠
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
chunks = text_splitter.split_documents(docs) # 将文档分割成多个小块
# 初始化嵌入模型,使用预训练的语言模型 'bge-large-zh-v1___5'
embedding = HuggingFaceEmbeddings(model_name='models/AI-ModelScope/bge-large-zh-v1___5')
# 构建 FAISS 向量存储和对应的 retriever
vs = FAISS.from_documents(chunks, embedding) # 将文本块转换为向量并存储在FAISS中
retriever = vs.as_retriever() # 创建一个检索器用于从向量存储中获取相关信息
from langchain.chains import RetrievalQA # 导入RetrievalQA链
from langchain.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
# 创建一个系统消息,用于定义机器人的角色
system_message = SystemMessagePromptTemplate.from_template(
"根据以下已知信息回答用户问题。\n 已知信息{context}"
)
# 创建一个人类消息,用于接收用户的输入
human_message = HumanMessagePromptTemplate.from_template(
"用户问题:{question}"
)
# 将这些模板结合成一个完整的聊天提示
chat_prompt = ChatPromptTemplate.from_messages([
system_message,
human_message,
])
# 定义链的类型参数,包括使用的提示模板
chain_type_kwargs = {"prompt": chat_prompt}
# 创建一个问答链,将语言模型、检索器和提示模板结合起来
# chat_model:生成回答的语言模型, stuff:所有检索到的文档内容合并成一个大文本块,然后传递给语言模型。
# retriever: 之前创建的一个 FAISS 检索器实例。它的作用是从 FAISS 向量存储中找到与用户问题最相关的文档或文本块。这些相关的文档会被传递给语言模型以生成回答。
# chain_type_kwargs 是一个字典,包含了用于配置问答链的一些关键参数。
qa = RetrievalQA.from_chain_type(llm=chat_model, chain_type="stuff", retriever=retriever, chain_type_kwargs=chain_type_kwargs)
# 用户的问题
user_question = "黑熊精自称为?"
# 使用检索器获取与问题相关的文档
related_docs = retriever.invoke(user_question)
# 使用问答链来回答问题 "黑熊精自称为?" 并打印结果
print(qa.invoke(user_question))
运行效果:
{'query': '黑熊精自称为?', 'result': '黑熊精自称为黑风大王。'}
原理:通过逐步创建一个自定义的链条(pipeline),将每一步操作(如检索、格式化、提示构建、生成和解析)显式定义。
流程: 1. 使用 retriever 检索文档片段。 2. 将检索到的文档格式化为字符串。 3. 将格式化的文档和用户问题通过 prompt 模板发送到 LLM。 4. 解析 LLM 的输出。
优点:
缺点: - 代码较复杂:需要手动构建每个步骤,增加了开发的复杂性。 - 可能增加维护成本:由于每一步都需要明确定义,代码可读性和可维护性可能受到影响。
管道式(LECL)实现的黑神话悟空问答系统。
from langchain_community.vectorstores import FAISS # 导入FAISS向量存储库
from langchain_huggingface import HuggingFaceEmbeddings # 导入Hugging Face嵌入模型
from langchain_community.document_loaders import TextLoader # 导入文本加载器
from langchain.text_splitter import RecursiveCharacterTextSplitter # 导入递归字符文本分割器
from langchain_openai import ChatOpenAI # 导入ChatOpenAI模型
# 使用 OpenAI API 的 ChatOpenAI 模型
chat_model = ChatOpenAI(
openai_api_key="sk-qzzjmeaubyigjbumdbadlxmutffaffywummytnsflacorkxn",
base_url="https://api.siliconflow.cn/v1",
model="Qwen/Qwen2.5-7B-Instruct"
)
# 加载文本文件 "黑悟空.txt",编码格式为 'utf-8'
loader = TextLoader("黑悟空.txt", encoding='utf-8')
docs = loader.load() # 将文件内容加载到变量 docs 中
# 把文本分割成 200 字一组的切片,每组之间有 20 字重叠
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
chunks = text_splitter.split_documents(docs) # 将文档分割成多个小块
# 初始化嵌入模型,使用预训练的语言模型 'bge-large-zh-v1___5'
embedding = HuggingFaceEmbeddings(model_name='models/AI-ModelScope/bge-large-zh-v1___5')
# 构建 FAISS 向量存储和对应的 retriever
vs = FAISS.from_documents(chunks, embedding) # 将文本块转换为向量并存储在FAISS中
retriever = vs.as_retriever() # 创建一个检索器用于从向量存储中获取相关信息
from langchain.chains import RetrievalQA # 导入RetrievalQA链
from langchain.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
# 创建一个系统消息,用于定义机器人的角色
system_message = SystemMessagePromptTemplate.from_template(
"根据以下已知信息回答用户问题。\n 已知信息{context}"
)
# 创建一个人类消息,用于接收用户的输入
human_message = HumanMessagePromptTemplate.from_template(
"用户问题:{question}"
)
# 将这些模板结合成一个完整的聊天提示
chat_prompt = ChatPromptTemplate.from_messages([
system_message,
human_message,
])
"""使用LECL实现"""
# 格式化文档,将多个文档连接成一个
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# 创建字符串输出解析器,用于解析LLM的输出
from langchain_core.output_parsers import StrOutputParser
output_parser = StrOutputParser()
from langchain_core.runnables import RunnablePassthrough
# 创建 rag_chain
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()} # 构建上下文字典
| chat_prompt
| chat_model
| output_parser
)
print(rag_chain.invoke("黑熊精自称为?"))
运行效果:
黑熊精自称为黑风大王。
两者在输出结果方面通常没有太大差别,关键区别在于实现的灵活性和复杂度。