这是一篇关于LangChain和RAG的快速入门文章,主要参考了由Harrison Chase和Andrew Ng讲授的Langchain chat with your data课程。你可以在rag101仓库中查看完整代码。本文翻译自我的英文博客,最新修订内容可随时参考:LangChain 与 RAG 最佳实践。
LangChain与RAG最佳实践
简介
LangChain
LangChain是用于构建大语言模型(LLM)应用的开源开发框架,其组件如下:
提示(Prompt)
- 提示模板(Prompt Templates):用于生成模型输入。
- 输出解析器(Output Parsers):处理生成结果的实现。
- 示例选择器(Example Selectors):选择合适的输入示例。
模型(Models)
- 大语言模型(LLMs)
- 聊天模型(Chat Models)
- 文本嵌入模型(Text Embedding Models)
索引(Indexes)
- 文档加载器(Document Loaders)
- 文本分割器(Text Splitters)
- 向量存储(Vector Stores)
- 检索器(Retrievers)
链(Chains)
- 可作为其他链的构建块。
- 提供超过20种特定应用的链。
代理(Agents)
- 支持5种代理帮助语言模型使用外部工具。
- 代理工具包(Agent Toolkits):提供超过10种实现,代理通过特定工具执行任务。
RAG流程
整个RAG流程基于向量存储加载(Vector Store Loading)和检索增强生成(Retrieval-Augmented Generation)。
向量存储加载
从不同来源加载数据,拆分并将其转换为向量嵌入。
检索增强生成
- 用户输入查询(Query)后,系统将从向量存储中检索最相关的文档片段(Relevant Splits)。
- 检索到的相关片段将组合成一个提示(Prompt),该提示会与上下文一起传递给大语言模型(LLM)。
- 最后,语言模型将根据检索到的片段生成答案并返回给用户。
加载器(Loaders)
可以使用加载器处理不同种类和格式的数据。有些是公开的,有些是专有的;有些是结构化的,有些是非结构化的。
一些有用的库:
- PDF:pypdf
- YouTube音频:yt_dlp、pydub
- 网页:beautifulsoup4
现在进行实践:
首先安装库:
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)
网页加载器(Web Base Loader)
同样先安装库:
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)
分割器(Splitters)
将文档分割成更小的块,同时保留有意义的关系。
为何分割?
- GPU限制:参数超过10亿的GPT模型,前向传播无法处理如此大的参数,因此分割是必要的。
- 计算更高效。
- 某些模型有固定的序列长度限制。
- 更好的泛化能力。
然而,分割点可能会丢失一些信息,因此分割时应考虑语义。
分割器类型
- 字符文本分割器(CharacterTextSplitter)
- Markdown标题文本分割器(MarkdownHeaderTextSplitter)
- Token文本分割器(TokenTextsplitter)
- SentenceTransformersTokenTextSplitter
- 递归字符文本分割器(RecursiveCharacterTextSplitter):递归尝试按不同字符分割,找到可行的分割方式。
- 语言分割器(Language):适用于C++、Python、Ruby、Markdown等语言
- NLTK文本分割器(NLTKTextSplitter):使用NLTK(自然语言工具包)分割句子
- Spacy文本分割器(SpacyTextSplitter):使用Spacy分割句子
更多信息请查看文档。
字符文本分割器与递归字符文本分割器示例
完整代码可在这里查看。
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分割
向量存储与嵌入(Vectorstores and Embeddings)
回顾RAG流程:
优势:
- 提高查询的准确性:查询相似块时,准确性更高。
- 提高查询效率:查询相似块时减少计算量。
- 提高查询的覆盖范围:块可以覆盖文档的每个点。
- 便于嵌入处理。
嵌入(Embeddings)
如果两个句子含义相似,那么它们在高维语义空间中会更接近。
向量存储(Vector Stores)
将每个块存储在向量存储中。当用户查询时,查询会被嵌入,然后找到最相似的向量,即这些块的索引,然后返回这些块。
实践
嵌入
完整代码可在这里查看。
首先安装库: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
文件。
检索(Retrieval)
这部分是RAG的核心部分。
上一部分我们已经使用了similarity_search
方法。除此之外,我们还有其他方法:
- 基本语义相似性(Basic semantic similarity)
- 最大边际相关(Maximum Marginal Relevance, MMR)
- 元数据(Metadata)
- LLM辅助检索(LLM Aided Retrieval)
相似性搜索(Similarity Search)
相似性搜索计算查询向量与数据库中所有文档向量的相似性,以找到最相关的文档。相似性度量方法包括余弦相似度和欧氏距离,它们可以有效度量两个向量在高维空间中的接近程度。
然而,仅依赖相似性搜索可能导致多样性不足,因为它只关注查询与内容的匹配,忽略了不同信息之间的差异。在某些应用中,尤其是需要覆盖多个不同方面的信息时,最大边际相关(MMR)的扩展方法可以更好地平衡相关性和多样性。
实践
实践部分见上一部分。
最大边际相关(MMR, Maximum Marginal Relevance)
仅检索最相关的文档可能会忽略信息的多样性。例如,如果只选择最相似的响应,结果可能非常相似甚至包含重复内容。MMR的核心思想是平衡相关性和多样性,即选择与查询最相关的信息,同时确保信息在内容上具有多样性。通过减少不同片段之间的信息重复,MMR可以提供更全面和多样化的结果集。
MMR的流程如下:
- 查询向量存储:首先使用嵌入模型将查询转换为向量。
- 选择
fetch_k
个最相似的响应:从向量存储中找到前k
个最相似的向量。 - 在这些响应中选择
k
个最具多样性的:通过计算每个响应之间的相似性,MMR会更倾向于选择彼此差异更大的结果,从而增加信息的覆盖范围。这个过程确保返回的结果不仅“最相似”,而且“互补”。
关键参数是lambda
,它是相关性和多样性的权重:
- 当lambda接近1时,MMR更像相似性搜索。
- 当lambda接近0时,MMR更像随机搜索。
实践
我们可以调整“向量存储”部分的代码以使用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])
元数据(Metadata)
当我们的查询有特定条件时,可以使用元数据来过滤结果。例如,页码、作者、时间戳等信息,这些信息可以在检索时作为过滤条件,从而提高查询的准确性。
实践
完整代码可在这里查看。
从另一个网站添加新文档,然后过滤特定网站的结果。
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
### LLM辅助检索(LLM Aided Retrieval)
该方法利用语言模型自动解析句子语义,提取过滤信息。
#### SelfQueryRetriever
LangChain提供的`SelfQueryRetriever`模块可通过语言模型分析问题语义,提取**向量搜索的搜索词**和**文档元数据的过滤条件**。
- 例如,对于问题“除了维基百科,还有哪些健康网站?”,SelfQueryRetriever可推断“维基百科”为过滤条件,即排除来源为维基百科的文档。
#### 实践
```python
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 = "第二讲的主题是什么?"
压缩(Compression)
向量检索返回的完整文档片段可能包含大量冗余信息,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)
问答(Question Answering)
流程
- 从向量存储中检索多个相关文档。
- 压缩相关片段以适应LLM上下文,生成系统提示(System Prompt)并整合用户问题。
- 将信息传递给LLM生成答案。
RetrievalQA链
优势
- 提高答案准确性:结合检索结果与LLM生成能力。
- 支持知识库实时更新:依赖向量存储数据,可动态更新。
- 减轻模型记忆负担:通过外部知识减少对模型内部参数的依赖。
其他方法
- Map-Reduce:将文档分块,每个块独立生成答案后合并(适合大量文档并行处理)。
- Refine:基于首个块生成初始答案,后续块逐步优化(适合高质量答案生成)。
- Map-Rerank:分块生成答案后按相关性排序,取最高分结果(适合精准匹配场景)。
实践
完整代码见这里。
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"])
对话检索链(Conversational Retrieval Chain)
流程
结合对话历史(Memory)与检索能力,实现上下文感知的多轮交互:
- 对话历史:记录用户对话上下文。
- 问题:用户提问传递给检索模块。
- 检索器:从向量存储获取相关内容。
- 提示整合:将问题、检索结果与对话历史组合为LLM输入。
- LLM生成答案:基于完整上下文生成响应。
记忆模块(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
按语法结构分割。
- 使用
- 向量存储选择
- 小规模数据:Chroma(本地部署便捷)。
- 大规模数据:FAISS(高效检索)或Pinecone(云端扩展)。
- 检索策略优化
- 复杂查询结合MMR与元数据过滤,平衡相关性与多样性。
- 使用
SelfQueryRetriever
自动解析用户意图,减少手动配置成本。
- 对话系统设计
- 采用
ConversationalRetrievalChain
维护对话历史,提升交互连贯性。 - 限制记忆长度(如
ConversationTokenBufferMemory
),避免上下文膨胀。
- 采用
资源链接
- 代码仓库:rag101(含完整示例与实验代码)。
- 课程参考:Langchain chat with your data(Andrew Ng主讲)。
- 官方文档:LangChain Python API。
通过合理组合LangChain组件与RAG流程,可高效构建基于自定义知识库的智能问答系统,显著提升LLM在垂直领域的应用效果。