使用 Langgraph 构建本地 RAG 知识库:从文档加载到检索

概述

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 加载 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)

最佳实践

  1. 文档拆分:chunk_size 设置为 500-1000,overlap 为 10-20%
  2. 向量化:选择与 LLM 同源的 embedding 模型
  3. 检索数量:k 设置为 3-5,避免上下文过长
  4. 数据质量:清洗文档,去除噪声内容
  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: 有一天,小明去面试,面试官问:"你有什么特长?"
小明说:"我会算命。"
面试官:"那你算算我今天会不会录用你?"
小明:"不会。"
面试官:"为什么?"
小明:"因为你还没看我的简历!"

扩展建议

  1. 添加更多节点:如天气查询、新闻摘要等
  2. 优化路由策略:使用意图识别模型提高准确率
  3. 缓存机制:对相似查询缓存结果,减少向量检索
  4. 多轮对话:结合历史记录进行上下文理解
  5. 流式输出:为长文本生成添加流式输出,提升用户体验

总结

通过 LangChain 构建 RAG 知识库的关键步骤:

  1. 使用合适的 Loader 加载文档
  2. 合理拆分文档,保留语义完整性
  3. 选择合适的 embedding 模型
  4. 存储到向量数据库(推荐 FAISS 本地存储)
  5. 检索相关文档,结合 LLM 生成答案
相关推荐
大流星7 小时前
LangChainJs之基础模型(一)
javascript·langchain
你好潘先生7 小时前
别再记命令了,用 yeero do 说句人话就能跑脚本,而且不烧 token
服务器·python·命令行
AIOps打工人7 小时前
我以为 LangChain 就是调用大模型,直到我写出第一条 Chain
langchain
Agent_大师7 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
荣码7 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
copyer_xyf7 小时前
FastAPI 如何连接 MySQL
后端·python
apocelipes21 小时前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
用户8356290780511 天前
使用 Python 在 PDF 中创建与管理书签
后端·python
MeixianAgent1 天前
Python 回测数据入口怎么验?历史 K 线入库前先做 5 个检查
后端·python
大模型真好玩1 天前
LangChain DeepAgents 速通指南(十)—— DeepAgents Code 智能体服务核心源码解读
人工智能·langchain·agent