使用 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 生成答案
相关推荐
月下雨(Moonlit Rain)19 小时前
宇宙飞船游戏项目
python·游戏·pygame
清水白石00819 小时前
测试金字塔实战:单元测试、集成测试与E2E测试的边界与平衡
python·单元测试·log4j·集成测试
布局呆星19 小时前
Python 入门:FastAPI + SQLite3 + Requests 基础教学
python·sqlite·fastapi
先做个垃圾出来………19 小时前
Flask框架特点对比
后端·python·flask
Mr -老鬼19 小时前
RustSalvo框架上传文件接口(带参数)400错误解决方案
java·前端·python
海天一色y19 小时前
使用 Python + Tkinter 打造“猫狗大战“回合制策略游戏
开发语言·python·游戏
好奇心害死薛猫19 小时前
全网首发_api方式flashvsr批量视频高清增强修复教程
python·ai·音视频
郝学胜-神的一滴19 小时前
计算思维:数字时代的超级能力
开发语言·数据结构·c++·人工智能·python·算法
尘缘浮梦20 小时前
websockets处理流式接口
开发语言·python
蜜獾云20 小时前
Java集合遍历方式详解(for、foreach、iterator、并行流等)
java·windows·python