这是一篇关于LangChain和RAG的快速入门文章,主要参考了由Harrison Chase和Andrew Ng讲授的Langchain chat with your data课程。你可以在rag101仓库中查看完整代码。本文翻译自我的英文博客,最新修订内容可随时参考:LangChain 与 RAG 最佳实践。
LangChain是用于构建大语言模型(LLM)应用的开源开发框架,其组件如下:
整个RAG流程基于向量存储加载(Vector Store Loading)和检索增强生成(Retrieval-Augmented Generation)。
从不同来源加载数据,拆分并将其转换为向量嵌入。
可以使用加载器处理不同种类和格式的数据。有些是公开的,有些是专有的;有些是结构化的,有些是非结构化的。
一些有用的库:
现在进行实践:
首先安装库:
pip install langchain-community
pip install pypdf
可以在以下代码中查看演示:
from langchain.document_loaders import PyPDFLoader
# 实际上,langchain调用pypdf库来加载pdf文件
loader = PyPDFLoader("ProbRandProc_Notes2004_JWBerkeley.pdf")
pages = loader.load()
print(type(pages))
# <class 'list'>
print(len(pages))
# 打印总页数
# 以第一页为例
page = pages[0]
print(type(page))
# <class 'langchain_core.documents.base.Document'>
# 页面内容包括:
# 1. page_content:页面文本内容
# 2. metadata:页面元数据
print(page.page_content[0:500])
print(page.metadata)
同样先安装库:
pip install beautifulsoup4
WebBaseLoader基于beautifulsoup4库。
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://zh.d2l.ai/")
pages = loader.load()
print(pages[0].page_content[:500])
# 也可以使用json进行后处理
# import json
# convert_to_json = json.loads(pages[0].page_content)
将文档分割成更小的块,同时保留有意义的关系。
然而,分割点可能会丢失一些信息,因此分割时应考虑语义。
更多信息请查看文档。
完整代码可在这里查看。
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter
example_text = """撰写文档时,作者会使用文档结构对内容进行分组。这可以向读者传达哪些想法是相关的。例如,紧密相关的想法在句子中。相似的想法在段落中。段落构成文档。\n\n 段落通常用一个或两个回车符分隔。回车符就是你在这个字符串中看到的“反斜杠n”。句子以句号结尾,但同时也有空格。单词之间用空格分隔。"""
c_splitter = CharacterTextSplitter(
chunk_size=450, # 块大小
chunk_overlap=0, # 块重叠部分,可与前一个块共享
separator=' '
)
r_splitter = RecursiveCharacterTextSplitter(
chunk_size=450,
chunk_overlap=0,
separators=["\n\n", "\n", " ", ""] # 分隔符优先级
)
print(c_splitter.split_text(example_text))
# 按450个字符分割
print(r_splitter.split_text(example_text))
# 先按\n\n分割
回顾RAG流程:
优势:
如果两个句子含义相似,那么它们在高维语义空间中会更接近。
将每个块存储在向量存储中。当用户查询时,查询会被嵌入,然后找到最相似的向量,即这些块的索引,然后返回这些块。
完整代码可在这里查看。
首先安装库:
chromadb
是一个轻量级向量数据库。
pip install chromadb
我们需要一个好的嵌入模型,你可以选择你喜欢的。参考文档。
这里我使用ZhipuAIEmbeddings
,因此需要安装库:
pip install zhipuai
测试代码如下:
from langchain_community.embeddings import ZhipuAIEmbeddings
embed = ZhipuAIEmbeddings(
model="embedding-3",
api_key="输入你自己的api key"
)
input_texts = ["这是一个测试查询1。", "这是一个测试查询2。"]
print(embed.embed_documents(input_texts))
完整代码可在这里查看。
pip install langchain-chroma
然后我们可以使用Chroma
来存储嵌入。
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import ZhipuAIEmbeddings
# 加载网页
loader = WebBaseLoader("https://en.d2l.ai/")
docs = loader.load()
# 分割文本为块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1500,
chunk_overlap=150
)
splits = text_splitter.split_documents(docs)
# print(len(splits))
# 设置嵌入模型
embeddings = ZhipuAIEmbeddings(
model="embedding-3",
api_key="你的api key"
)
# 设置持久化目录
persist_directory = r'.'
# 创建向量数据库
vectordb = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory=persist_directory
)
# print(vectordb._collection.count())
# 查询向量数据库
question = "Recurrent"
docs = vectordb.similarity_search(question, k=3)
# print(len(docs))
print(docs[0].page_content)
然后你可以在特定目录中找到chroma.sqlite3
文件。
这部分是RAG的核心部分。
上一部分我们已经使用了similarity_search
方法。除此之外,我们还有其他方法:
相似性搜索计算查询向量与数据库中所有文档向量的相似性,以找到最相关的文档。相似性度量方法包括余弦相似度和欧氏距离,它们可以有效度量两个向量在高维空间中的接近程度。
然而,仅依赖相似性搜索可能导致多样性不足,因为它只关注查询与内容的匹配,忽略了不同信息之间的差异。在某些应用中,尤其是需要覆盖多个不同方面的信息时,最大边际相关(MMR)的扩展方法可以更好地平衡相关性和多样性。
实践部分见上一部分。
仅检索最相关的文档可能会忽略信息的多样性。例如,如果只选择最相似的响应,结果可能非常相似甚至包含重复内容。MMR的核心思想是平衡相关性和多样性,即选择与查询最相关的信息,同时确保信息在内容上具有多样性。通过减少不同片段之间的信息重复,MMR可以提供更全面和多样化的结果集。
MMR的流程如下:
fetch_k
个最相似的响应:从向量存储中找到前k
个最相似的向量。k
个最具多样性的:通过计算每个响应之间的相似性,MMR会更倾向于选择彼此差异更大的结果,从而增加信息的覆盖范围。这个过程确保返回的结果不仅“最相似”,而且“互补”。关键参数是lambda
,它是相关性和多样性的权重:
我们可以调整“向量存储”部分的代码以使用MMR方法。完整代码在retrieval/mmr.py
文件中。
# 使用MMR查询向量数据库
question = "How the neural network works?"
# 获取8个最相似的文档,然后选择2个最相关且最具多样性的文档
docs_mmr = vectordb.max_marginal_relevance_search(question, fetch_k=8, k=2)
print(docs_mmr[0].page_content[:100])
print(docs_mmr[1].page_content[:100])
当我们的查询有特定条件时,可以使用元数据来过滤结果。例如,页码、作者、时间戳等信息,这些信息可以在检索时作为过滤条件,从而提高查询的准确性。
完整代码可在这里查看。
从另一个网站添加新文档,然后过滤特定网站的结果。
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import ZhipuAIEmbeddings
# 加载网页
loader = WebBaseLoader("https://en.d2l.ai/")
docs = loader.load()
# 分割文本为块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1500,
chunk_overlap=150
)
splits = text_splitter.split_documents(docs)
# print(len(splits))
# 设置嵌入模型
embeddings = ZhipuAIEmbeddings(
model="embedding-3",
api_key="你的_api_key"
)
# 设置持久化目录
persist_directory = r'.'
# 创建向量数据库
vectordb = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory=persist_directory
)
# print(vectordb._collection.count())
# 从另一个网站添加新文档
new_loader = WebBaseLoader("https://www.deeplearning.ai/")
new_docs = new_loader.load()
# 分割新文档文本为块
new_splits = text_splitter.split_documents(new_docs)
# 添加到现有向量数据库
vectordb.add_documents(new_splits)
# 获取所有文档
all_docs = vectordb.similarity_search("What is the difference between a neural network and a deep learning model?", k=20)
# 打印文档的元数据
for i, doc in enumerate(all_docs):
print(f"Document {i+1} metadata: {doc.metadata}")
# Document 1 metadata: {'language': 'en', 'source': 'https://en.d2l.ai/', 'title': 'Dive into Deep Learning — Dive into Deep Learning 1.0.3 documentation'}
# Document 2 metadata: {'language': 'en', 'source': 'https://en.d2l.ai/', 'title': 'Dive into Deep Learning — D
该方法利用语言模型自动解析句子语义,提取过滤信息。
LangChain提供的SelfQueryRetriever
模块可通过语言模型分析问题语义,提取向量搜索的搜索词和文档元数据的过滤条件。
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
llm = OpenAI(temperature=0)
metadata_field_info = [
AttributeInfo(
name="source",
description="文档片段的来源,应为`docs/loaders.pdf`、`docs/text_splitters.pdf`或`docs/vectorstores.pdf`中的一个",
type="string",
),
AttributeInfo(
name="page",
description="文档所在的页码",
type="integer",
),
]
document_content_description = "检索增强生成相关的课程讲义"
retriever = SelfQueryRetriever.from_llm(
llm,
vectordb,
document_content_description,
metadata_field_info,
verbose=True
)
question = "第二讲的主题是什么?"
向量检索返回的完整文档片段可能包含大量冗余信息,LangChain的“压缩”机制通过以下步骤优化:
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
def pretty_print_docs(docs):
print(f"\n{'-' * 100}\n".join([f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]))
llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectordb.as_retriever()
)
question = "第二讲的主题是什么?"
compressed_docs = compression_retriever.get_relevant_documents(question)
pretty_print_docs(compressed_docs)
完整代码见这里。
from langchain_community.chat_models import ChatZhipuAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
loader = WebBaseLoader("https://en.d2l.ai/")
docs = loader.load()
splits = RecursiveCharacterTextSplitter(chunk_size=1500).split_documents(docs)
vectordb = Chroma.from_documents(splits, ZhipuAIEmbeddings(api_key="your_key"))
chat = ChatZhipuAI(model="glm-4-flash", api_key="your_key")
template = """基于以下上下文回答问题:\n{context}\n问题:{question}\n答案:"""
qa_chain = RetrievalQA.from_chain_type(
chat,
retriever=vectordb.as_retriever(),
chain_type_kwargs={"prompt": PromptTemplate.from_template(template)}
)
result = qa_chain({"query": "这本书的主题是什么?"})
print(result["result"])
结合对话历史(Memory)与检索能力,实现上下文感知的多轮交互:
ConversationBufferMemory
存储对话历史,支持多轮交互:
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
完整代码见这里。
qa = ConversationalRetrievalChain.from_llm(chat, vectordb.as_retriever(), memory=memory)
# 第一轮提问
result = qa.invoke({"question": "推荐一本深度学习书籍"})
print(result['answer'])
# 第二轮追问
result = qa.invoke({"question": "这本书的作者是谁?"}) # 自动关联历史对话
print(result['answer'])
RecursiveCharacterTextSplitter
保留语义,避免硬性切割。 LanguageTextSplitter
按语法结构分割。 SelfQueryRetriever
自动解析用户意图,减少手动配置成本。 ConversationalRetrievalChain
维护对话历史,提升交互连贯性。 ConversationTokenBufferMemory
),避免上下文膨胀。 通过合理组合LangChain组件与RAG流程,可高效构建基于自定义知识库的智能问答系统,显著提升LLM在垂直领域的应用效果。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。