用 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")
五、使用说明
- 把你的 PDF 文档放入
docs目录 - 运行代码,第一次会自动构建向量库
- 后续运行会直接加载已有向量库
- 用同一个
thread_id可以恢复之前的会话