构建AI智能体:十八、解密LangChain中的RAG架构:让AI模型突破局限学会“翻书”答题
一、相得益彰
在人工智能领域,我们常常遇到两个核心挑战:如何让模型获取最新知识,以及如何让模型基于特定信息生成准确答案。RAG(Retrieval-Augmented Generation:检索增强生成) 提供了一种解决这些挑战的范式,而 LangChain 则提供了实现这一范式的完整工具箱。二者的结合,就像RAG给了建筑师既有了设计蓝图,而LangChain又有了全套现代化工具,让构建智能应用变得前所未有的高效和可靠。
本文将深入探讨如何利用 LangChain 框架实现 RAG 架构,通过具体代码示例展示如何构建一个能够理解特定领域知识的智能问答系统。
二、什么是LangChain
LangChain 是一个用于构建大模型应用的开发框架。它本身不是 RAG,但它提供了实现 RAG 所需的所有组件。
LangChain 将构建LLM应用时常见的功能都模块化了,比如连接各种数据源、文本处理、调用模型、管理对话历史等。开发者可以像搭积木一样,将这些模块组合起来,快速构建应用。
结合《构建AI智能体:十七、大模型的幻觉难题:RAG 解决AI才华横溢却胡言乱语的弊病》中介绍的“开卷”的比喻,要实现“开卷考试”(RAG)这个流程,你需要很多工具:找书(文档加载)、翻书和找重点(文本分割与检索)、把找到的重点段落和问题写在草稿纸上(提示模板)、最后让学生答题(调用模型)。
LangChain 就是提供了所有这些工具的超强工具箱:它给你提供了各种“找书”的方法(Document Loaders)、“翻书”的技巧(Text Splitters 和 Vectorstores)、“写草稿纸”的模板(Prompt Templates)以及“组织考试流程”的说明书(Chains)。
三、为什么需要LangChain
想象一下我们使用过程中的一个痛点,大语言模型是一个超级聪明但生活不能自理的大脑。它知识渊博,但它不知道你的文件、公司的数据库;它一次只能回答一个问题,记不住之前的对话;它不会用工具,比如上网查资料或者按计算器;你想让它帮你分析公司财报、当个24小时客服、或者帮你订机票,直接告诉它去做是行不通的。你需要为它打理好一切。
当我们一个帮助手册或职工手册的PDF文档,一般我们需要什么信息都需要自己去逐页逐行的定位到精准的段落才能找到答案,而现在有了LangChain,它能够自动的按照自己的方式和逻辑快速精准的分析和答复问题:
-
检索:去你之前上传的员工手册PDF里搜索“年假”关键词。
-
组装:把找到的相关段落和你的问题组合成一个新的、更详细的提示。
-
提问:把这个组装好的提示发给AI“大脑”。
-
回复:把AI的回答清晰地返回给你。
四、LangChain是怎么去做的
LangChain 可以理解为是一个用于开发由语言模型驱动的应用程序的框架。其核心价值在于提供了模块化和可组合性。可以将 LangChain 想象成一副“乐高积木”,它提供了各种标准化组件(如模型交互、数据检索、记忆管理、工作流编排),让你可以通过组合这些组件,像搭积木一样快速、灵活地构建起复杂的LLM应用,而无需从零开始处理各种底层细节。就像大模型的私人助理。它会把各种工具组合起来,搭建一个完整的流水线,让大模型能真正干活;
-
数据连接:喂资料,把你的PPT、PDF、网站文章等各种文件,拆解、整理、打包好,等大模型需要时立刻递上去。
-
记忆管理:记笔记,把和用户的聊天记录记下来,下次聊天时提醒大模型:“这位用户上次说他叫小明,喜欢咖啡。”
-
工具调用:递工具,当大模型说“我需要算个数”或者“我需要查一下今天的新闻”时,助理立刻把计算器或者搜索引擎递过去。
-
工作流编排:安排工作,把一个复杂任务拆成几步,比如先让“A模型”总结文章,再让“B模型”把总结翻译成法语。
LangChain 是一个工具箱和脚手架,它能把强大的AI大脑(LLM)和你自己的数据、工具、业务流程连接起来,拼装成一个真正能用的、智能的应用程序。
五、LangChain 的核心模块
要深入了解LangChain,首先需要了解它的核心模块:
-
Models(模型):这是与各种LLM交互的抽象。LangChain 支持多种模型提供商(OpenAI, Hugging Face等),可以用统一的接口调用它们。
ChatModels:用于聊天模式,输入输出是消息列表。
LLMs:用于补全模式,输入输出是字符串。
-
Prompts(提示):管理、优化和模板化提示词的组件。PromptTemplate 允许你创建带有变量的模板,动态注入内容。如"请将以下文本翻译成{language}:{text}"
-
Indexes(索引):让模型能够与外部数据连接和交互,这是克服模型知识截止问题的关键。主要包括:
Document Loaders:从各种源(PDF, 网页, Notion)加载文档。
Text Splitters:将长文档分割成模型上下文窗口能处理的小块。
VectorStores:将文本块转换为向量(Embeddings)并存储,以便快速进行相似性搜索。
Retrievers:从 VectorStores 中检索与问题相关的文档片段。
-
Chains(链):将多个组件“链”在一起,形成一个序列化的工作流。这是 LangChain 的核心。一个链可以包含一个提示模板、一个模型和一个输出解析器。
LLMChain 是最基本的链。 RetrievalQA 是一个高级链,它集成了检索器和问答链。
-
Agents(代理):链是预先定义好的固定流程,而代理则让模型自己决定使用哪些工具、以什么顺序使用,来完成用户指令。代理相当于模型的“大脑”,工具则是它的“手脚”。
工具:可以是搜索引擎、计算器、数据库查询等任何函数。
-
Memory(记忆):用于在链或代理的多次调用之间持久化状态,如对话历史。这对于聊天应用至关重要。
六、LangChain中的Chain Type
通俗的讲,LangChain如果是一个功能强大的自动化汽车制造工厂,chain_type 就像是工厂里的特定车辆组装蓝图或流水线,是特定 Chain 的配置参数,参考以下代码中的load_qa_chain方法中配置的chain_type;
from langchain.prompts import PromptTemplate
custom_prompt = PromptTemplate(template="""请严格根据以下上下文来回答问题。如果上下文没有提供足够信息,请直接说"根据已知信息无法回答该问题",不要编造答案。
上下文:
{context}
问题:{question}
答案:""",input_variables=["context", "question"]
)
chain = load_qa_chain(llm, chain_type="stuff", prompt=custom_prompt)
在 load_qa_chain 中, chain_type 参数决定了 RAG 流程中“增强”这一步的具体工作方式。即:如何将检索到的多个相关文档块(context)与用户的问题(question)组合起来,发送给大语言模型(LLM)以生成最终答案。不同的 chain_type 在效果、成本和速度上有着显著的权衡。
四种主要的 Chain Type 详解:
假设我们针对问题 “LangChain 有哪些核心组件?” 检索到了 4 个相关的文档块(chunks)。
1. stuff(堆叠)
-
工作方式:这是最简单的方法。它将所有检索到的文档块简单地“堆叠”在一起,组合成一个巨大的提示(prompt),然后一次性发送给 LLM。Prompt 结构: 请根据以下信息回答问题:{context_chunk_1} ... {context_chunk_4} 问题:{question}
-
优点:
单次调用:只调用 LLM 一次,速度快。
最大上下文:LLM 可以同时看到所有信息,理论上能做出最综合、最连贯的回答。
-
缺点:
上下文长度限制:如果检索到的文档块很多或很长,很容易超过 LLM 的上下文窗口(context window)限制,导致报错或信息被截断。
-
适用场景:当检索到的文档总篇幅较短,并且确信不会超出所用 LLM 的上下文窗口时。这是最常用且默认的选项。
2. map_reduce(映射归纳)
-
工作方式:该方法分为两个阶段:
1. Map(映射):将每个文档块分别与问题组合,发送给 LLM,要求 LLM 针对该块本身提取答案。这会进行 N 次(文档块的数量)LLM 调用。Prompt 结构(每次): 根据以下片段回答问题:{context_chunk_i} 问题:{question}
2. Reduce(归纳):将所有来自 Map 阶段的答案(可能还有原始问题)再次组合,发送给 LLM,要求它将这些分散的答案归纳成一个最终、连贯的答案。这是第 N+1 次 LLM 调用。
-
优点:
突破上下文限制:可以处理任意数量的文档,因为每个文档都是独立处理的。
-
缺点:
高成本/高延迟:需要进行多次 LLM 调用(N+1 次),成本更高,速度更慢。
可能丢失全局视角:在 Map 阶段,LLM 无法看到不同文档块之间的关联,可能导致归纳阶段融合困难。
-
适用场景:当需要处理大量文档,并且 stuff 方法不适用时。
3. refine(迭代细化)
-
工作方式:这是一个迭代、序列化的过程。
首先,将第一个文档块和问题发送给 LLM,得到一个初始答案。
然后,将下一个文档块、当前已有的答案和问题再次发送给 LLM,要求它根据新信息修正或完善(refine) 当前的答案。
重复步骤 2,直到所有文档块都处理完毕。最终的答案就是最后一次迭代的结果。
-
优点:
答案质量高:答案在迭代中不断细化,可以产生非常精确和细致的回答。
能处理长文档:同样不受上下文窗口限制。
-
缺点:
速度最慢:串行处理意味着调用次数多(N 次),且后续调用必须等待前一次完成,延迟很高。
答案依赖性:文档块的处理顺序可能会影响最终答案。早期的错误可能被后续迭代放大或修正。
-
适用场景:当答案需要高度精确,并且愿意以速度和成本为代价来换取质量时。
4. map_rerank(映射重排序)
-
工作方式:
Map(映射):与 map_reduce 的 Map 阶段类似,为每个文档块单独调用 LLM。但不同的是,它要求 LLM 不仅给出答案,还要为该答案基于当前块的可信度输出一个分数(score)。
Rerank(重排序):不再进行 Reduce 步骤。而是直接选择分数最高的那个答案作为最终输出。
-
优点:
单文档答案:对于事实型、答案可能存在于单个文档块中的问题(例如,“某某人的生日是哪天?”)非常有效。
-
缺点:
不适用于综合型问题:如果答案需要从多个文档中综合信息,此方法会失败,因为它只选一个。
-
适用场景:主要用于问答(Answer Question) 而不是总结(Summarization),并且你确信答案完整地存在于某个单一的文档块中。
Chain Type的差异:
特性 | stuff | map_reduce | refine | map_rerank |
LLM调用次数 | 1 | N + 1 | N | N |
速度 | 最快 | 中等 | 最慢 | 慢 |
Token 消耗 | 低 | 高 | 高 | 高 |
处理长文档能力 | 差 | 好 | 好 | 好 |
答案质量 | 高 (上下文完整) | 中等 (可能丢失关联) | 最高 (迭代细化) | 取决于最佳块 |
适用场景 | 默认选择,文档短 | 文档多,需平衡 | 文档多,要求高精度 | 事实型问题,答案在单块中 |
为了更直观地理解和选择,可以参考下面的决策流程:
选择建议:
-
优先尝试 stuff:在大多数情况下,这是最佳选择。只需确保你的块大小(chunk_size)和检索数量( k)的乘积不会超出 LLM 的上下文窗口(记得为问题和答案留出空间)。
-
如果文档太多太长 → 选择 map_reduce 或 refine。
-
看重答案质量且不计较延迟 → 选 refine。
-
想要一种平衡的方案 → 选 map_reduce。
-
-
如果是事实型、答案明确的问题 → 可以尝试 map_rerank。
七、LangChain与RAG的关系
通俗的讲,RAG(检索增强生成)是一种技术理念或架构模式,是一个具体的“技术方案”或“模式”,就像“做一份番茄炒蛋的菜谱”;而LangChain 是一个实现这种模式(以及其他模式)的强大工具包和框架,就像“一整套厨房用具和智能灶台”。
你可以这样理解:
-
RAG 是“菜谱”:它告诉你做一道菜(构建一个能回答问题的AI)需要哪些步骤(检索、增强、生成)。
-
LangChain 是“厨房和全套厨具”:它为你提供了实现这个菜谱所需要的所有工具(锅、碗、瓢、盆、灶具),让你能更高效、更标准地做出这道菜。
八、LangChain与RAG的协调工作原理
当我们用 LangChain 构建一个文档问答应用时,实际上就是在实现一个标准的 RAG 流程。下图直观地展示了LangChain是如何实现RAG全流程的,它通过其丰富的组件和链,将RAG从一个理论架构变成了一个可以快速落地实现的具体方案。
综合可知,在 LangChain 中实现 RAG,本质上是将检索器(Retriever)和生成器(Generator)通过链(Chain)组合起来的工作流程。这个流程通常包含四个关键阶段:
-
文档加载与处理:从各种来源(PDF、网页、数据库等)加载文档
-
文档索引与存储:将文档分割并转换为向量表示,存入向量数据库
-
相关检索:根据用户查询找到最相关的文档片段
-
增强生成:将检索到的上下文与用户查询组合,生成最终答案
LangChain 为每个阶段都提供了丰富的组件和简化接口,让开发者可以专注于业务逻辑而不是底层实现细节。
九、综合示例:DeepSeek+ Faiss构建本地知识库检索
接下来我们实现了一个基于 RAG 架构的 PDF 文档问答系统,使用LangChain、DashScope中的deepseek-v3模型和 FAISS。读取本地的文档进行检索;
from PyPDF2 import PdfReader
from langchain.chains.question_answering import load_qa_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
from typing import List, Tuple
import os
import pickle
DASHSCOPE_API_KEY = os.getenv('DASHSCOPE_API_KEY')
if not DASHSCOPE_API_KEY:raise ValueError("请设置环境变量 DASHSCOPE_API_KEY")
def extract_text_with_page_numbers(pdf) -> Tuple[str, List[Tuple[str, int]]]:"""从PDF中提取文本并记录每个字符对应的页码参数:pdf: PDF文件对象返回:text: 提取的文本内容char_page_mapping: 每个字符对应的页码列表"""text = ""char_page_mapping = []for page_number, page in enumerate(pdf.pages, start=1):extracted_text = page.extract_text()if extracted_text:text += extracted_text# 为当前页面的每个字符记录页码char_page_mapping.extend([page_number] * len(extracted_text))else:print(f"No text found on page {page_number}.")return text, char_page_mapping
def process_text_with_splitter(text: str, char_page_mapping: List[int], save_path: str = None) -> FAISS:"""处理文本并创建向量存储参数:text: 提取的文本内容char_page_mapping: 每个字符对应的页码列表save_path: 可选,保存向量数据库的路径返回:knowledgeBase: 基于FAISS的向量存储对象"""# 创建文本分割器,用于将长文本分割成小块text_splitter = RecursiveCharacterTextSplitter(separators=["\n\n", "\n", ".", " ", ""],chunk_size=1000,chunk_overlap=200,length_function=len,)# 分割文本chunks = text_splitter.split_text(text)print(f"文本被分割成 {len(chunks)} 个块。")# 创建嵌入模型embeddings = DashScopeEmbeddings(model="text-embedding-v1",dashscope_api_key=DASHSCOPE_API_KEY,)# 从文本块创建知识库knowledgeBase = FAISS.from_texts(chunks, embeddings)print("已从文本块创建知识库。")# 为每个文本块找到对应的页码信息page_info = {}current_pos = 0for chunk in chunks:chunk_start = current_poschunk_end = current_pos + len(chunk)# 找到这个文本块中字符对应的页码chunk_pages = char_page_mapping[chunk_start:chunk_end]# 取页码的众数(出现最多的页码)作为该块的页码if chunk_pages:# 统计每个页码出现的次数page_counts = {}for page in chunk_pages:page_counts[page] = page_counts.get(page, 0) + 1# 找到出现次数最多的页码most_common_page = max(page_counts, key=page_counts.get)page_info[chunk] = most_common_pageelse:page_info[chunk] = 1 # 默认页码current_pos = chunk_endknowledgeBase.page_info = page_infoprint(f'页码映射完成,共 {len(page_info)} 个文本块')# 如果提供了保存路径,则保存向量数据库和页码信息if save_path:# 确保目录存在os.makedirs(save_path, exist_ok=True)# 保存FAISS向量数据库knowledgeBase.save_local(save_path)print(f"向量数据库已保存到: {save_path}")# 保存页码信息到同一目录with open(os.path.join(save_path, "page_info.pkl"), "wb") as f:pickle.dump(page_info, f)print(f"页码信息已保存到: {os.path.join(save_path, 'page_info.pkl')}")return knowledgeBase
def load_knowledge_base(load_path: str, embeddings = None) -> FAISS:"""从磁盘加载向量数据库和页码信息参数:load_path: 向量数据库的保存路径embeddings: 可选,嵌入模型。如果为None,将创建一个新的DashScopeEmbeddings实例返回:knowledgeBase: 加载的FAISS向量数据库对象"""# 如果没有提供嵌入模型,则创建一个新的if embeddings is None:embeddings = DashScopeEmbeddings(model="text-embedding-v1",dashscope_api_key=DASHSCOPE_API_KEY,)# 加载FAISS向量数据库,添加allow_dangerous_deserialization=True参数以允许反序列化knowledgeBase = FAISS.load_local(load_path, embeddings, allow_dangerous_deserialization=True)print(f"向量数据库已从 {load_path} 加载。")# 加载页码信息page_info_path = os.path.join(load_path, "page_info.pkl")if os.path.exists(page_info_path):with open(page_info_path, "rb") as f:page_info = pickle.load(f)knowledgeBase.page_info = page_infoprint("页码信息已加载。")else:print("警告: 未找到页码信息文件。")return knowledgeBase
# 读取PDF文件
pdf_reader = PdfReader('./电脑的日常维护.pdf')
# 提取文本和页码信息
text, char_page_mapping = extract_text_with_page_numbers(pdf_reader)
#print('page_numbers=',page_numbers)
print(f"提取的文本长度: {len(text)} 个字符。")
# 处理文本并创建知识库,同时保存到磁盘
save_dir = "./vector_db"
knowledgeBase = process_text_with_splitter(text, char_page_mapping, save_path=save_dir)
# 示例:如何加载已保存的向量数据库
# 注释掉以下代码以避免在当前运行中重复加载
"""
# 创建嵌入模型
embeddings = DashScopeEmbeddings(model="text-embedding-v1",dashscope_api_key=DASHSCOPE_API_KEY,
)
# 从磁盘加载向量数据库
loaded_knowledgeBase = load_knowledge_base("./vector_db", embeddings)
# 使用加载的知识库进行查询
docs = loaded_knowledgeBase.similarity_search("客户经理每年评聘申报时间是怎样的?")
# 直接使用FAISS.load_local方法加载(替代方法)
# loaded_knowledgeBase = FAISS.load_local("./vector_db", embeddings, allow_dangerous_deserialization=True)
# 注意:使用这种方法加载时,需要手动加载页码信息
"""
from langchain_community.llms import Tongyi
llm = Tongyi(model_name="deepseek-v3", dashscope_api_key=DASHSCOPE_API_KEY) # qwen-turbo
# 设置查询问题
query = "如何进行系统性能优化"
if query:# 执行相似度搜索,找到与查询相关的文档docs = knowledgeBase.similarity_search(query,k=10)# 加载问答链chain = load_qa_chain(llm, chain_type="stuff")# 准备输入数据input_data = {"input_documents": docs, "question": query}# 执行问答链response = chain.invoke(input=input_data)print(response["output_text"])print("来源:")# 记录唯一的页码unique_pages = set()# 显示每个文档块的来源页码for doc in docs:#print('doc=',doc)text_content = getattr(doc, "page_content", "")source_page = knowledgeBase.page_info.get(text_content.strip(), "未知")if source_page not in unique_pages:unique_pages.add(source_page)print(f"文本块页码: {source_page}")
输出结果:
提取的文本长度: 7494 个字符。
文本被分割成 10 个块。
已从文本块创建知识库。
页码映射完成,共 10 个文本块
向量数据库已保存到: ./vector_db
页码信息已保存到: ./vector_db\page_info.pkl
系统性能优化可以通过多种方式进行,以下是具体的方法和步骤:
### 1. **磁盘缓存优化**- 调整系统对磁盘数据的缓存策略,以提高读写效率。
### 2. **桌面菜单优化**- 减少桌面图标和菜单项的加载时间,提高桌面响应速度。
### 3. **文件系统优化**- 定期进行磁盘碎片整理,确保文件存储连续,减少磁盘寻道时间。
### 4. **网络系统优化**- 调整网络协议和设置,优化网络连接速度和稳定性。
### 5. **开机速度优化**- **设置开机信息停留时间**:通过拖动滑块调整开机信息显示的时间。- **采用快速启动方式**:选中“采用Windows XP快速启动方式”复选框。- **关闭不必要的开机自启动程序**:在“请选择开机不自动运行的项目”列表中取消选中不需要的程序。
### 6. **系统安全优化**- 关闭不必要的后台服务和防火墙规则,减少系统资源占用。
### 7. **系统个性优化**- 根据用户需求调整系统界面和功能设置。
### 8. **后台服务优化**- 停用或调整不常用的后台服务,以释放系统资源。
### 9. **使用系统工具**- 利用**Windows优化大师**等工具,对系统进行全面优化,包括垃圾文件清理、冗余DLL分析等。
### 操作步骤示例:
1. **单击“系统性能优化”选项**,选择“开机速度优化”。
2. **调整启动信息停留时间**:拖动滑块设置开机信息停留时间。
3. **设置启动方式**:勾选“采用Windows XP快速启动方式”和“异常时启动磁盘错误检查等待时间”。
4. **选择不开机自启动的项目**:在列表框中取消选中不需要开机自启动的程序。
5. **单击“优化”按钮**完成设置。
通过这些方法,可以显著提高系统的运行效率和响应速度。
来源:
文本块页码: 13
文本块页码: 10
文本块页码: 9
文本块页码: 7
文本块页码: 1
文本块页码: 18
文本块页码: 4
文本块页码: 16
文本块页码: 2
示例的文档是一份《电脑的日常维护.pdf》手册,我们提问的是“如何进行系统性能优化?”,以下截取文档的部分图片:
思路分析:
-
知识库构建(Indexing Pipeline):
-
从 PDF 提取文本和精确的页码信息。
-
对文本进行分块(chunking)。
-
使用 DashScope 的嵌入模型将文本块转换为向量。
-
将向量存入 FAISS 向量数据库。
-
将向量库和关键的页码映射信息保存到本地。
-
-
问答推理(Retrieval & Generation Pipeline):
-
从本地加载预先构建好的知识库(FAISS 索引和页码信息)。
-
接收用户查询。
-
在向量库中检索与查询最相关的文本块。
-
将相关文本块和查询组合,发送给 LLM(Tongyi 的 Deepseek-v3 模型)生成答案。
-
输出答案并标注答案来源的页码。
-
十、总结
LangChain 与 RAG 的结合为构建高效、准确的智能问答系统提供了强大基础。通过 LangChain 提供的模块化组件,我们可以轻松实现RAG架构的各个阶段,从文档处理到最终答案生成。
这种组合的优势在于:
-
知识实时性:可以随时更新知识库,而不需要重新训练模型
-
答案可追溯:可以显示答案来源,提高可信度和可解释性
-
定制化能力强:可以针对特定领域或企业需求定制知识库
-
开发效率高:LangChain抽象了复杂流程,大幅减少开发时间
无论是想构建企业知识库、智能客服系统还是专业问答助手,LangChain与RAG的组合都能提供强大的技术基础和灵活的定制能力,协助将AI技术转化为实际业务价值。