概述
RAG(Retrieval-Augmented Generation)结合了检索和生成的优势,能够让大模型基于外部知识库进行回答。本文介绍如何使用 LangChain 从零构建一个本地的 RAG 知识库。
RAG 的工作流程
RAG 通常分为两个主要阶段:
1. Indexing(索引阶段)
- 文档加载:将各种格式的文档转换为 Document 对象
- 文档拆分:将长文档拆分成小的文本段(segments)
- 向量化:将文本段转换为向量 embeddings
- 存储:将向量保存到向量数据库
2. Retrieval(检索阶段)
- 根据用户查询,在向量数据库中检索最相关的文档段
- 将检索结果作为上下文,组合成提示词
- 让大模型基于这些信息生成回答
实现代码
环境准备
bash
pip install langchain langchain-community dashscope faiss-cpu
完整代码示例
python
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
import re
# ============================================
# Indexing 阶段:构建知识库
# ============================================
## 1. 加载文档
# 方式1:加载单个 PDF 文件
# documents = PyPDFLoader("/path/to/file.pdf").load()
# 方式2:加载文件夹下所有 PDF 文件(推荐)
documents = DirectoryLoader(
"/www/learning_langchain/",
glob="*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
).load()
print(f"已加载 {len(documents)} 个文档")
## 2. 文档拆分
segments = []
for i in range(len(documents)):
# 使用正则表达式按 "#" 符号拆分
texts = re.split("#", documents[i].page_content)
for text in texts:
if text.strip(): # 过滤空文本
segments.append(Document(page_content=text))
print(f"拆分后得到 {len(segments)} 个文本段")
## 3. 向量化
embeddings = DashScopeEmbeddings(
model="text-embedding-v1",
dashscope_api_key="your-dashscope-api-key"
)
## 4. 保存向量库
vector_store = FAISS.from_documents(segments, embeddings)
vector_store.save_local("./save_knowledgeDB/faiss_index")
print("向量库已保存")
# ============================================
# Retrieval 阶段:检索并生成答案
# ============================================
## 1. 加载向量库
vectorstore = FAISS.load_local(
folder_path="./save_knowledgeDB/faiss_index",
embeddings=embeddings,
allow_dangerous_deserialization=True
)
## 2. 创建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
## 3. 检索相关文档
query = "大米" # 用户查询
related_segments = retriever.invoke(query)
print(f"\n关于 '{query}' 的相关文档段:")
for i, doc in enumerate(related_segments):
print(f"\n【文档段 {i+1}】")
print(doc.page_content[:200] + "...") # 显示前200字符
关键点解析
1. 文档加载器
LangChain 提供了多种文档加载器:
| 加载器 | 支持格式 | 用途 |
|---|---|---|
| PyPDFLoader | 加载 PDF 文件 | |
| TextLoader | txt, md | 加载纯文本文件 |
| CSVLoader | CSV | 加载 CSV 表格 |
| DirectoryLoader | 多种 | 批量加载文件夹 |
| UnstructuredLoader | 多种 | 加载复杂格式 |
批量加载技巧:
python
# 加载文件夹下所有 PDF
loader = DirectoryLoader(
"./docs",
glob="*.pdf",
loader_cls=PyPDFLoader,
show_progress=True, # 显示进度条
use_multithreading=True # 多线程加速
)
documents = loader.load()
2. 文档拆分策略
方法1:使用正则表达式(自定义拆分)
python
import re
from langchain_core.documents import Document
# 按特定符号拆分
texts = re.split("#", document.page_content)
segments = [Document(page_content=text) for text in texts if text.strip()]
方法2:使用 RecursiveCharacterTextSplitter(推荐)
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每块最大字符数
chunk_overlap=200, # 块之间重叠字符数
separators=["\n\n", "\n", "。", ",", " ", ""] # 分隔符优先级
)
segments = splitter.split_documents(documents)
参数说明:
chunk_size:每个文本块的最大长度chunk_overlap:相邻块之间的重叠部分,保证语义连贯性separators:按优先级顺序尝试的分隔符
3. 向量化
使用 DashScope(阿里云)
python
from langchain_community.embeddings import DashScopeEmbeddings
embeddings = DashScopeEmbeddings(
model="text-embedding-v1",
dashscope_api_key="your-api-key"
)
其他选择
- OpenAI:
OpenAIEmbeddings - HuggingFace:
HuggingFaceEmbeddings - 本地模型:
OllamaEmbeddings
4. 向量存储
FAISS(本地,免费)
python
# 创建
vector_store = FAISS.from_documents(documents, embeddings)
vector_store.save_local("./faiss_index")
# 加载
vector_store = FAISS.load_local(
"./faiss_index",
embeddings,
allow_dangerous_deserialization=True
)
# 检索
results = vector_store.similarity_search(query, k=5)
其他向量数据库
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Redis + RediSearch | 高性能,分布式 | 生产环境,大规模 |
| Pinecone | 托管服务 | 快速部署,云原生 |
| Chroma | 轻量级 | 开发测试 |
| Qdrant | 高性能 | 企业级应用 |
5. 检索技巧
python
# 相似度检索
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5} # 返回最相关的 5 个结果
)
# 最大边际相关性(MMR)检索(增加多样性)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 5, "fetch_k": 10}
)
# 相似度阈值检索
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"score_threshold": 0.7, # 只返回相似度 > 0.7 的结果
"k": 5
}
)
查看已保存的 FAISS 向量库
FAISS 文件是二进制格式,无法直接查看。需要加载后检索:
python
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import DashScopeEmbeddings
import pickle
# 方法1:通过 FAISS 加载并检索
embeddings = DashScopeEmbeddings(
model="text-embedding-v1",
dashscope_api_key="your-api-key"
)
vector_store = FAISS.load_local(
"./save_knowledgeDB/faiss_index",
embeddings,
allow_dangerous_deserialization=True
)
# 查看文档总数
print(f"文档总数: {len(vector_store.docstore._dict)}")
# 检索示例
results = vector_store.similarity_search("查询关键词", k=3)
for i, doc in enumerate(results):
print(f"\n结果 {i+1}:")
print(doc.page_content[:200])
# 方法2:直接读取 pkl 文件
with open("./save_knowledgeDB/faiss_index/index.pkl", "rb") as f:
data = pickle.load(f)
print(f"包含 {len(data._dict)} 个文档")
完整的 RAG 应用示例
python
from langchain_community.chat_models import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 1. 检索
retriever = vectorstore.as_retriever()
docs = retriever.invoke(query)
# 2. 构建提示词
prompt = ChatPromptTemplate.from_template("""
基于以下文档回答用户的问题。如果文档中没有相关信息,请如实回答。
文档:
{context}
问题:{question}
回答:
""")
# 3. 初始化大模型
llm = ChatOpenAI(
model="qwen-plus",
api_key="your-dashscope-api-key",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 4. 生成回答
context = "\n\n".join([doc.page_content for doc in docs])
response = llm.invoke(prompt.format(context=context, question=query))
print(response.content)
最佳实践
- 文档拆分:chunk_size 设置为 500-1000,overlap 为 10-20%
- 向量化:选择与 LLM 同源的 embedding 模型
- 检索数量:k 设置为 3-5,避免上下文过长
- 数据质量:清洗文档,去除噪声内容
- 性能优化:批量处理大文件,使用多线程
进阶:结合 LangGraph 构建多智能体 RAG 系统
在复杂的应用场景中,我们可能需要构建一个多智能体系统,根据用户的不同需求调用不同的功能。下面展示如何使用 LangGraph 构建一个包含 RAG 检索功能的多智能体系统。
系统架构
用户输入 → Supervisor(分发器)→ [旅游节点 / 笑话节点 / 购物节点(RAG) / 其他节点]
↓
各节点独立处理
↓
返回结果给 Supervisor
↓
输出最终答案
完整实现代码
python
from typing import List, TypedDict, Annotated
from langchain_core.messages import AnyMessage
from operator import add
from langgraph.graph import StateGraph
from langchain_core.messages import HumanMessage
from langchain_community.chat_models import ChatTongyi
import os
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import DashScopeEmbeddings
from langgraph.constants import START, END
from langgraph.checkpoint.memory import InMemorySaver
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
# ============================================
# 初始化组件
# ============================================
# Embedding 模型
embeddings = DashScopeEmbeddings(
model="text-embedding-v1",
dashscope_api_key='your-dashscope-api-key'
)
# 加载 FAISS 向量库
vector_store = FAISS.load_local(
embeddings=embeddings,
folder_path="./save_knowledgeDB/faiss_index",
allow_dangerous_deserialization=True
)
# 大语言模型
os.environ["DASHSCOPE_API_KEY"] = "your-dashscope-api-key"
llm = ChatTongyi(
model_name="qwen-max",
temperature=0.7,
streaming=False
)
# ============================================
# MCP 工具客户端(用于旅游查询)
# ============================================
async def create_use_mcp_agent(user_str):
"""使用 MCP 工具获取地图信息"""
client = MultiServerMCPClient({
"amap-maps": {
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {"AMAP_MAPS_API_KEY": "your-amap-api-key"},
"transport": "stdio"
}
})
tools = await client.get_tools()
agent = create_react_agent(model=llm, tools=tools)
response = await agent.ainvoke({
"messages": [{"role": "user", "content": user_str}]
})
return response
# ============================================
# 定义状态
# ============================================
class State(TypedDict):
messages: Annotated[List[AnyMessage], add]
type: str # 用于路由的类型标识
# ============================================
# 节点定义
# ============================================
def supervisor_node(state: State):
"""分发器节点:分析用户意图,路由到对应节点"""
print(">>> supervisor_node")
sys_prompt = """你是一个专业的人工智能小助手,负责对用户说的话进行分类。
- 如果用户的问题和旅游相关,返回 travel
- 如果和笑话相关,返回 joker
- 如果和购物相关,返回 gouwu
- 如果和其他相关,返回 other
"""
prompt = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": state["messages"][0]}
]
res = llm.invoke(prompt).content
print(f"supervisor_node 输出: {res}")
if "type" in state:
return {"type": END}
elif res in ["travel", "joker", "gouwu", "other"]:
return {"type": res}
else:
return {"type": "other"}
def travel_node(state: State):
"""旅游节点:调用 MCP 工具查询地图信息"""
print(">>> travel_node")
import asyncio
response = asyncio.run(create_use_mcp_agent(state["messages"][0]))
return {
"messages": [response],
"type": "travel_node"
}
def joker_node(state: State):
"""笑话节点:生成笑话"""
print(">>> joker_node")
sys_prompt = "你是一个搞笑专家。能够根据用户需求,生成一个笑话。"
prompt = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": state["messages"][0]}
]
return {
"messages": [llm.invoke(prompt).content],
"type": "joker_node"
}
def extract_name_node(state: State):
"""商品名称提取节点:从用户需求中提取商品名称"""
print(">>> extract_name_node")
sys_prompt = "你是一个解析用户需求的专家,能够根据用户的需求,提取出用户需求中的商品名称。"
prompt = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": state["messages"][0]}
]
return {
"messages": [llm.invoke(prompt).content],
"type": "extract_name_node"
}
def gouwu_node(state: State):
"""购物节点:基于 RAG 检索相关商品并推荐"""
print(">>> gouwu_node")
# 1. 从向量库检索相关商品
retriever = vector_store.as_retriever()
related_segments = retriever.invoke(state["messages"][-1], k=5)
# 2. 构建推荐提示词
sys_prompt = f"""你是一个商场导购销售专家,能够根据目前店内搜到的和用户相关的商品集合,
自己发挥合适的推销理由给用户,需要推荐可以满足用户需求的商品。
现在搜出来的商品有:{related_segments}"""
prompt = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": state["messages"][0]}
]
# 3. 生成推荐回复
return {
"messages": [llm.invoke(prompt).content],
"type": "gouwu_node"
}
def other_node(state: State):
"""其他节点:处理无法识别的问题"""
print(">>> other_node")
return {
"messages": ["我暂时回答不了这个问题。"],
"type": "other"
}
# ============================================
# 条件路由函数
# ============================================
def routing_func(state: State):
"""根据类型路由到对应的节点"""
if state["type"] == "travel":
return "travel_node"
elif state["type"] == "joker":
return "joker_node"
elif state["type"] == "gouwu":
return "extract_name_node" # 先提取商品名,再进入购物节点
elif state['type'] == END:
return END
else:
return "other_node"
# ============================================
# 构建图
# ============================================
builder = StateGraph(State)
# 添加节点
builder.add_node("supervisor_node", supervisor_node)
builder.add_node("travel_node", travel_node)
builder.add_node("joker_node", joker_node)
builder.add_node("gouwu_node", gouwu_node)
builder.add_node("other_node", other_node)
builder.add_node("extract_name_node", extract_name_node)
# 添加边
builder.add_edge(START, "supervisor_node")
builder.add_conditional_edges(
"supervisor_node",
routing_func,
["travel_node", "joker_node", "other_node", "extract_name_node", END]
)
# 各节点处理完后返回到 supervisor_node
builder.add_edge("travel_node", "supervisor_node")
builder.add_edge("joker_node", "supervisor_node")
builder.add_edge("extract_name_node", "gouwu_node") # 提取商品名后进入购物节点
builder.add_edge("gouwu_node", "supervisor_node")
builder.add_edge("other_node", "supervisor_node")
# 编译图(使用内存检查点)
checkpointers = InMemorySaver()
graph = builder.compile(checkpointer=checkpointers)
# ============================================
# 运行示例
# ============================================
if __name__ == "__main__":
config = {"configurable": {"thread_id": "1"}}
# 示例1:购物相关
result = graph.invoke({"messages": ["有没有大米"]}, config)
print(result["messages"][-1])
# 示例2:笑话
result = graph.invoke({"messages": ["给我讲个笑话"]}, config)
print(result["messages"][-1])
# 示例3:旅游
result = graph.invoke({"messages": ["推荐一个旅游景点"]}, config)
print(result["messages"][-1])
关键技术点解析
1. 状态管理
python
class State(TypedDict):
messages: Annotated[List[AnyMessage], add] # 消息列表,会自动累加
type: str # 类型标识,用于路由
messages使用add装饰器,每次添加新消息时会追加而不是覆盖type用于 Supervisor 节点判断应该路由到哪个节点
2. 节点设计原则
- Supervisor 节点:作为中央调度器,分析意图并分发任务
- 功能节点:各自独立处理特定任务
- 单向流转:节点处理完后返回 Supervisor,形成闭环
3. RAG 在多智能体中的集成
python
def gouwu_node(state: State):
# 使用向量库检索
retriever = vector_store.as_retriever()
related_segments = retriever.invoke(state["messages"][-1], k=5)
# 将检索结果作为上下文
sys_prompt = f"""...搜出来的商品有:{related_segments}"""
# LLM 基于检索结果生成推荐
return {"messages": [llm.invoke(prompt).content], "type": "gouwu_node"}
4. 多节点协作流程
用户问"有没有大米"
↓
Supervisor 识别为购物类型 → type="gouwu"
↓
路由到 extract_name_node(提取"大米")
↓
自动跳转到 gouwu_node
↓
RAG 检索包含"大米"的商品
↓
生成推荐回复 → 返回 Supervisor
↓
输出最终答案
5. 检查点机制
python
from langgraph.checkpoint.memory import InMemorySaver
checkpointers = InMemorySaver()
graph = builder.compile(checkpointer=checkpointers)
检查点可以:
- 保存执行状态,支持中断后恢复
- 实现对话记忆功能
- 调试时追踪执行流程
运行结果示例
用户: 有没有大米
>>> supervisor_node
supervisor_node 输出: gouwu
>>> extract_name_node
>>> gouwu_node
AI推荐: 我们有以下大米产品:
1. 东北大米 - 优质东北长粒香米,口感软糯香甜
2. 泰国香米 - 进口原粮,自然清香
3. 五常大米 - 国家地理标志产品,品质保证
根据您的需求,我推荐您尝试东北大米,性价比很高!
---
用户: 给我讲个笑话
>>> supervisor_node
supervisor_node 输出: joker
>>> joker_node
AI: 有一天,小明去面试,面试官问:"你有什么特长?"
小明说:"我会算命。"
面试官:"那你算算我今天会不会录用你?"
小明:"不会。"
面试官:"为什么?"
小明:"因为你还没看我的简历!"
扩展建议
- 添加更多节点:如天气查询、新闻摘要等
- 优化路由策略:使用意图识别模型提高准确率
- 缓存机制:对相似查询缓存结果,减少向量检索
- 多轮对话:结合历史记录进行上下文理解
- 流式输出:为长文本生成添加流式输出,提升用户体验
总结
通过 LangChain 构建 RAG 知识库的关键步骤:
- 使用合适的 Loader 加载文档
- 合理拆分文档,保留语义完整性
- 选择合适的 embedding 模型
- 存储到向量数据库(推荐 FAISS 本地存储)
- 检索相关文档,结合 LLM 生成答案