LangGraph 重构个人知识库问答系统(稳定 + 可扩展版)

用 LangGraph 把之前的 RAG 系统重构为模块化、可扩展、带持久化、带错误处理 的生产级架构。核心设计思想是:节点解耦、状态清晰、流程灵活、易于扩展。

一、系统架构设计(可扩展核心)

1. 核心流程(图结构)

复制代码
用户提问 → 检索文档 → 生成回答 → 反思检查(可选)→ 结束
         ↓(失败)      ↓(失败)
      重试/兜底      备用模型

2. 模块化节点设计(方便扩展)

节点 职责 可扩展方向
retrieve 从向量库检索文档 多路召回、重排序、元数据过滤
generate 基于检索结果生成回答 多模型切换、Prompt 模板切换
reflect 反思检查回答质量(可选) 多维度校验、人工介入
fallback 兜底节点(失败时调用) 返回固定回复、搜索网络

3. 状态设计(清晰 + 可扩展)

复制代码
class RAGState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]  # 对话历史
    question: str                    # 用户原始问题
    context: str                     # 检索到的文档上下文
    answer: str                      # 生成的回答
    loop_count: int                  # 防死循环计数
    error: str | None                # 错误信息
    user_id: str                     # 用户ID(绑定长期记忆)

二、完整代码实现(直接复制运行)

1. 安装依赖

复制代码
pip install -U langgraph langchain langchain-community chromadb python-dotenv pydantic-settings pymupdf

2. 完整代码

复制代码
import os
from typing import TypedDict, Sequence, Annotated, Literal

from langchain_community.document_loaders import DirectoryLoader, PyMuPDFLoader, PyPDFLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.redis import RedisSaver
from langgraph.constants import START, END
from langgraph.graph import add_messages, StateGraph
from pydantic_settings import BaseSettings, SettingsConfigDict

from work.laanggraph_tool_异常处理 import backup_llm, checkpointer

os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
# 配置管理 (Pydantic Setting 避免硬编码)
class Settings(BaseSettings):
    ZHIPU_API_KEY: str
    ZHIPU_BASE_URL: str
    LLM_MODEL: str
    LLM_BACKUP_MODEL: str
    EMBEDDING_MODEL: str
    CHROMA_DB_DIR: str
    DOCS_DIR: str
    CHUNK_SIZE: int
    CHUNK_OVERLAP: int
    RETRIEVE_TOP_K: int = 3
    LLM_TIMEOUT: int = 30
    MAX_LOOP_COUNT: int = 3


    # ✅ Pydantic 2.x 官方标准配置写法(替代旧版 class Config)
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore"
    )

settings = Settings()

# 状态定义 清洗+可扩展
class RAGState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
    question: str
    context: str
    answer: str
    loop_count: int
    error: str | None
    user_id: str

# 向量库初始化(模块化,方便切换)
def init_vector_store():
    """初始化或者加载向量库"""
    # 初始化嵌入模型
    # embeddings = HuggingFaceEmbeddings(
    #     model_name="https://hf-mirror.com",
    #     model_kwargs={"device": "cpu"},
    #     encode_kwargs={"normoalize_embeddings": True},
    # )
    embeddings = HuggingFaceEmbeddings(
        model_name="D:\\models\\bge-large-zh", # 本地路径
        model_kwargs={'device': 'cpu'},
        encode_kwargs={'normalize_embeddings': True}
    )

    # 如果向量库已存在 直接加载
    if os.path.exists(settings.CHROMA_DB_DIR) and len(os.listdir(settings.CHROMA_DB_DIR)) > 0:
        print("✅️ 加载已有向量")
        return Chroma(
            persist_directory=settings.CHROMA_DB_DIR,
            embedding_function=embeddings
        )
    # else:
    #     print("🔨 构建新的向量库")
    #     vector_store = Chroma.from_documents(...)  # ← 这个分支没执行
    # 否则构建新的向量库
    print("🔨 构建新的向量库")
    os.makedirs(settings.DOCS_DIR, exist_ok=True)

    # 加载文档
    loader = DirectoryLoader(
        settings.DOCS_DIR,
        glob="*.pdf",
        loader_cls=PyMuPDFLoader,
        show_progress=True,
    )

    documents = loader.load()
    print(f"文档数量: {len(documents)}")
    # 分割文档
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=settings.CHUNK_SIZE,
        chunk_overlap=settings.CHUNK_OVERLAP,
        separators=["\n\n", "\n", "。", "!", "?", " ", ""]
    )

    split_chunks = text_splitter.split_documents(documents)
    # 去除文档中的空
    chunks = [chunk for chunk in split_chunks if chunk.page_content.strip()]
    # 构建向量库
    vector_store = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings, # ✅ 改成 embedding
        persist_directory=settings.CHROMA_DB_DIR
    )

    print("✅️ 向量库构建完成")
    return vector_store

# 全局向量库实例
vector_store = init_vector_store()

# LLM 初始化 (主模型 + 备用模型)
# 主
llm = ChatOpenAI(
    api_key=settings.ZHIPU_API_KEY,
    base_url=settings.ZHIPU_BASE_URL,
    model_name=settings.LLM_MODEL,
    temperature=0,
    timeout=settings.LLM_TIMEOUT,
    max_retries=0
)

# 备用模型
backup_llm = ChatOpenAI(
    api_key=settings.ZHIPU_API_KEY,
    base_url=settings.ZHIPU_BASE_URL,
    model_name=settings.LLM_BACKUP_MODEL,
    temperature=0
)

# RAG Prompt 模板
RAG_PROMPT = ChatPromptTemplate.from_template("""
你是专业的文档问答助手,必须严格遵守以下规则:
1. 仅使用下方【文档上下文】中的内容回答用户问题,绝对禁止编造。
2. 如果文档上下文中没有相关内容,直接回复:「抱歉,文档中没有相关内容。」
3. 回答简洁、准确、条理清晰。

【文档上下文】
{context}

【用户问题】
{question}
"""
)

# 节点定义 解耦 + 错误解析 + 可扩展
def retrieve_node(state: RAGState):
    """检索节点: 从向量库检索相关文档"""
    print("\n-- 🔍执行节点:文档检索")
    question = state["question"]
    error = None
    context = ""
    try:
        # 检索top------k文档
        docs = vector_store.similarity_search(question, k = settings.RETRIEVE_TOP_K)
        context = "\n\n".join([f"第{i+1}段:{doc.page_content}" for i, doc in enumerate(docs)])
        print(f'✅️ 检索到 {len(docs)} 段文档')
    except Exception as e:
        print(f'❌️ 检索失败:{str(e)}')
        error = str(e)
        context = "文档检索失败,请稍后再试。"

    return {
        "context": context,
        "loop_count": state["loop_count"] + 1,
        "error": error
    }

def generate_node(state: RAGState):
    """生成节点:基于结果生成回答"""
    print("\n-- 🤖执行节点,生成回答 --")
    question = state["question"]
    context = state["context"]
    error = None
    answer = ""
    try:
        # 构造Prompt 并调用LLM
        prompt = RAG_PROMPT.format(context=context, question=question)
        response = llm.invoke(prompt)
        answer = response.content
        print("✅️ 回答生产完成")
    except Exception as e:
        print(f"❌️ 回答生成失败:{str(e)}")
        error = str(e)
        answer = "抱歉,回答生成失败,请稍后再试"

    return {
        "answer": answer,
        "messages": [AIMessage(content=answer)],
        "loop_count": state["loop_count"] + 1,
        "error": error
    }

def backup_generate_node(state: RAGState):
    """备用生成节点:主节点失败时调用"""
    print("\n-- 🔄 切换到备用生成节点 --")
    question = state["question"]
    context = state["context"]
    error = None
    answer = ""

    try:
        prompt = RAG_PROMPT.format(context=context, question=question)
        response = backup_llm.invoke(prompt)
        answer = response.content
    except Exception as e:
        print(f"❌️ 备用生成也失败:{str(e)}")
        error = str(e)
        answer = "抱歉,服务暂时不可用,请稍后再试"

    return {
        "answer": answer,
        "messages": [AIMessage(content=answer)],
        "loop_count": state["loop_count"] + 1,
        "error": error
    }

# 条件路由 灵活决策
def should_continue(state: RAGState) -> Literal["retrieve", "generate", "backup_generate", "end"]:
    """条件判断:决定下一步走哪个节点"""
    # 优先处理错误
    if state.get("error"):
        if state["loop_count"] < 2:
            if "检索" in state["error"]:
                return "retrieve" # 检索失败 重新检索
            else:
                return "backup_generate" # 生成失败 走备用
        else:
            return "end"    # 失败次数过多,结束

    # 正常流程
    if not state.get("context"):
        return "retrieve" # 还没检索 先走检索
    elif not state.get("answer"):
        return "generate" # 还没生成 先生成
    else:
        print("→ 结束:流程完成")
        return "end"

# 构建图 (模块化 + 可扩展)
def build_rag_graph():
    builder = StateGraph(RAGState)

    # 添加节点
    builder.add_node("retrieve", retrieve_node)
    builder.add_node("generate", generate_node)
    builder.add_node("backup_generate", backup_generate_node)

    #边: 开始 -> 条件判断
    builder.add_edge(START, "retrieve")
    builder.add_conditional_edges(
        "retrieve",
        should_continue,
        {
            "retrieve": "retrieve",
            "generate": "generate",
            "backup_generate": "backup_generate",
            "end": END
        }
    )

    builder.add_conditional_edges(
        "generate",
        should_continue,
        {
            "backup_generate": "backup_generate",
            "end": END
        }
    )

    builder.add_conditional_edges(
        "backup_generate",
        should_continue,
        {
            "end": END
        }
    )

    # 编译图(带持久化)
    checkpointer = MemorySaver()
    return builder.compile(checkpointer=checkpointer)

# 全局 RAG Agent 实例
rag_agent = build_rag_graph()

# 测试运行 (会话回复 + 错误处理)
if __name__ == '__main__':
    print("===== 📚 LangGraph 重构版 RAG 知识库问答系统 =====")
    # 配置: 同一个thread_id 可以恢复对话
    config = {
        "configurable": {
            "thread_id": "rag_session_001",
            "user_id": "user_001"
        }
    }

    # 第一轮对话
    print("\n-- 第一轮对话 --")
    initial_state = {
        "messages": [],
        "question": "最大的区别?",
        "context": "",
        "answer": "",
        "loop_count": 0,
        "error": None,
        "user_id": "user_001"
    }
    result1 = rag_agent.invoke(initial_state, config=config)
    print(f"第一轮答案:{result1['answer']}")

    # 第二轮对话(恢复对话)
    print("\n--- 第二轮对话(恢复会话)---")
    result2 = rag_agent.invoke(
        {
            "messages": [HumanMessage(content="Dubbo的核心功能?")],
            "question": "Dubbo的核心功能?",
            "loop_count": 0
        },
        config=config
    )

    print(f"第二轮答案:{result2['answer']}")

运行结果

复制代码
===== 📚 LangGraph 重构版 RAG 知识库问答系统 =====

-- 第一轮对话 --

-- 🔍执行节点:文档检索
✅️ 检索到 3 段文档

-- 🤖执行节点,生成回答 --
✅️ 回答生产完成
→ 结束:流程完成
第一轮答案:根据文档上下文,Dubbo 和 Spring Cloud 最大的区别如下:

1.  **底层通信**:Dubbo 底层使用 Netty (NIO框架),基于 TCP 协议传输,配合 Hessian 序列化完成 RPC 通信;Spring Cloud 基于 Http 协议和 Rest 接口调用远程过程。
2.  **性能**:Http 请求会有更大的报文,占用的带宽也会更多。
3.  **灵活性**:REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖。

--- 第二轮对话(恢复会话)---

-- 🔍执行节点:文档检索
✅️ 检索到 3 段文档
→ 结束:流程完成
第二轮答案:根据文档上下文,Dubbo 和 Spring Cloud 最大的区别如下:

1.  **底层通信**:Dubbo 底层使用 Netty (NIO框架),基于 TCP 协议传输,配合 Hessian 序列化完成 RPC 通信;Spring Cloud 基于 Http 协议和 Rest 接口调用远程过程。
2.  **性能**:Http 请求会有更大的报文,占用的带宽也会更多。
3.  **灵活性**:REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖。

三、系统优势(稳定 + 可扩展)

1. 稳定性

  • 错误处理 :每个节点都有 try-except,失败时返回友好提示
  • 备用方案:主 LLM 失败时自动切换到更轻量的备用模型
  • 防死循环loop_count 限制最大循环次数
  • 超时控制:LLM 调用设置超时,防止卡死
  • 会话持久化 :用 MemorySaver 实现会话恢复,重启不丢

2. 可扩展性

  • 节点解耦:检索、生成、反思等节点独立,方便修改和替换
  • 模块化设计:向量库、LLM、Prompt 都封装成独立模块,方便切换
  • 条件边灵活:可以轻松添加新节点和新的条件分支
  • 状态清晰:State 定义明确,方便添加新字段(如检索分数、重排序结果)

四、扩展建议(让系统更强大)

1. 加多路召回和重排序

复制代码
# 在 retrieve_node 中添加
def retrieve_node(state: RAGState):
    # ... 原有检索逻辑 ...
    # 多路召回:同时用语义检索和关键词检索
    # 重排序:用 BGE-Rerank 对召回结果二次精排
    ...

2. 加反思检查节点

复制代码
def reflect_node(state: RAGState):
    """反思节点:检查回答质量"""
    # 调用 LLM 检查回答是否完整、准确
    # 如果不合格,回到生成节点重新生成
    ...

3. 加工具调用(搜索网络)

复制代码
@tool
def web_search(query: str) -> str:
    """搜索网络获取最新信息"""
    # 调用搜索引擎 API
    ...

# 在图中添加工具节点
builder.add_node("tool_executor", ToolNode([web_search]))

4. 切换到 Redis 持久化

复制代码
from langgraph.checkpoint.redis import RedisSaver

# 替换 MemorySaver
checkpointer = RedisSaver.from_url("redis://localhost:6379/0")

五、使用说明

  1. 把你的 PDF 文档放入 docs 目录
  2. 运行代码,第一次会自动构建向量库
  3. 后续运行会直接加载已有向量库
  4. 用同一个 thread_id 可以恢复之前的会话
相关推荐
qq_392690661 小时前
Go语言如何做图片缩放_Go语言图片缩放裁剪教程【推荐】
jvm·数据库·python
IT北辰1 小时前
一键整理试题库!用Python自动化处理Excel选择题
python·自动化·excel
m0_736439301 小时前
Golang怎么连接MySQL数据库_Golang MySQL连接教程【总结】
jvm·数据库·python
CLX05051 小时前
c++怎么以独占模式打开文件_fsopen与_SH_DENYRW【详解】
jvm·数据库·python
老纪1 小时前
如何处理SQL复杂业务关联删除_通过触发器实现级联清理
jvm·数据库·python
运气好好的1 小时前
golang如何理解Go 1.23迭代器协议_golang 1.23迭代器协议详解
jvm·数据库·python
2401_824697661 小时前
Go语言如何用systemd_Go语言systemd服务管理教程【总结】
jvm·数据库·python
2301_775639891 小时前
mysql修改字段长度是否影响数据_隐式转换与字符集限制分析
jvm·数据库·python
Dshuishui1 小时前
我用 Claude Code 做了一个学术论文搜索工具
开发语言·人工智能·python·pip·uv