LangGraph多Agent图编排与状态机工作流深度实战:从节点定义到持久化检查点的复杂推理网络构建全解析
LangGraph多Agent图编排与状态机工作流深度实战:从节点定义到持久化检查点的复杂推理网络构建全解析
引言
LangChain作为最早的LLM应用框架,其核心抽象Chain(链式调用)在简单场景中高效,但面对多Agent协作、条件分支、循环推理、人机协作(Human-in-the-loop)等复杂需求时捉襟见肘。LangGraph作为LangChain团队2024年推出的图编排框架,以有向图(StateGraph)为核心抽象,节点是处理单元,边定义控制流,共享State在节点间传递与演化,天然支持循环、条件路由、并行分支、持久化检查点与中断恢复。这一范式让构建"研究助手"(多轮搜索-分析-综合)、“代码生成Agent”(规划-编码-测试-修复循环)、“多专家协作系统”(分工+汇总+质疑)等复杂AI应用成为可能。本文将从LangGraph的核心抽象讲起,覆盖StateGraph定义、条件路由、子图与并行、检查点持久化、Human-in-the-loop中断、多Agent架构模式(Supervisor与Swarm)、工具集成、流式输出与可观测性,并提供完整可运行的Python代码。
一、LangGraph核心抽象:StateGraph
1.1 状态、节点与边
LangGraph的三个基石:State(所有节点共享的可变状态,TypedDict或Pydantic定义)、Node(接收state返回更新,纯函数)、Edge(定义节点间控制流,包括固定边与条件边)。以下定义一个基础研究Agent的状态与图:
# langgraph_research/state.py - 共享状态定义
from __future__ import annotations
from typing import Annotated, TypedDict, Literal
from langgraph.graph.message import add_messages
from pydantic import BaseModel, Field
class SearchResult(BaseModel):
title: str
url: str
snippet: str
relevance: float = 0.0
class ResearchState(TypedDict):
# 消息历史(add_messages追加而非覆盖)
messages: Annotated[list, add_messages]
# 检索到的信息
search_results: list[SearchResult]
# 子问题分解
sub_questions: list[str]
# 当前处理的子问题索引
current_sub_index: int
# 各子问题的部分答案
partial_answers: list[str]
# 综合答案
final_answer: str
# 控制流标志
iteration: int
max_iterations: int
# 检索质量自评估
needs_more_research: bool
# 引用
citations: list[dict]
1.2 构建第一个图
# langgraph_research/graph.py - 研究Agent图
from langgraph.graph import StateGraph, END, START
from langgraph.checkpoint.memory import MemorySaver
from .state import ResearchState
# ===== 节点定义 =====
async def plan_node(state: ResearchState) -> dict:
"""将用户问题分解为子问题"""
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
llm = ChatOpenAI(model="gpt-4o", temperature=0)
user_question = state["messages"][-1].content
response = await llm.ainvoke([
SystemMessage(content="""将用户的研究问题分解为2-4个子问题。
每个子问题应能独立检索回答。输出JSON:{"sub_questions": [...]}"""),
HumanMessage(content=user_question),
])
import json
plan = json.loads(response.content)
return {
"sub_questions": plan["sub_questions"],
"current_sub_index": 0,
"partial_answers": [""] * len(plan["sub_questions"]),
"max_iterations": len(plan["sub_questions"]),
"iteration": 0,
}
async def search_node(state: ResearchState) -> dict:
"""对当前子问题执行搜索"""
idx = state["current_sub_index"]
sub_q = state["sub_questions"][idx]
# 调用搜索工具(示例用模拟)
results = await web_search(sub_q, top_k=5)
return {"search_results": results}
async def analyze_node(state: ResearchState) -> dict:
"""分析检索结果,生成部分答案"""
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
idx = state["current_sub_index"]
sub_q = state["sub_questions"][idx]
context = "\n".join(
f"[{i}] {r.title}: {r.snippet}" for i, r in enumerate(state["search_results"])
)
response = await llm.ainvoke([
{"role": "system", "content": "基于检索结果回答子问题。标注来源[编号]。"},
{"role": "user", "content": f"子问题:{sub_q}\n\n检索结果:\n{context}"},
])
partial = state["partial_answers"]
partial[idx] = response.content
return {"partial_answers": partial}
async def reflect_node(state: ResearchState) -> dict:
"""反思:当前部分答案是否充分"""
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
idx = state["current_sub_index"]
sub_q = state["sub_questions"][idx]
partial = state["partial_answers"][idx]
response = await llm.ainvoke([
{"role": "system", "content": "评估部分答案是否充分回答了子问题。输出JSON:{\"sufficient\": true/false, \"gap\": \"缺失信息描述\"}"},
{"role": "user", "content": f"子问题:{sub_q}\n答案:{partial[:500]}"},
])
import json
eval = json.loads(response.content)
return {"needs_more_research": not eval["sufficient"]}
async def synthesize_node(state: ResearchState) -> dict:
"""综合所有部分答案为最终回答"""
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
parts = "\n\n---\n\n".join(
f"子问题{i+1}: {state['sub_questions'][i]}\n答案: {state['partial_answers'][i]}"
for i in range(len(state["sub_questions"]))
)
user_question = state["messages"][-1].content
response = await llm.ainvoke([
{"role": "system", "content": "综合各子问题的回答,给出对原始问题的完整回答。保持引用编号。"},
{"role": "user", "content": f"原始问题:{user_question}\n\n子问题与答案:\n{parts}"},
])
return {"final_answer": response.content,
"messages": [{"role": "assistant", "content": response.content}]}
# ===== 条件路由 =====
def should_continue_research(state: ResearchState) -> str:
"""反思后决定:补充检索 or 下一个子问题 or 综合"""
if state["needs_more_research"] and state["iteration"] < 2:
return "search_again"
if state["current_sub_index"] < len(state["sub_questions"]) - 1:
return "next_sub"
return "synthesize"
def route_after_plan(state: ResearchState) -> str:
if not state["sub_questions"]:
return "synthesize"
return "search"
# ===== 构图 =====
def build_research_graph():
graph = StateGraph(ResearchState)
graph.add_node("plan", plan_node)
graph.add_node("search", search_node)
graph.add_node("analyze", analyze_node)
graph.add_node("reflect", reflect_node)
graph.add_node("synthesize", synthesize_node)
graph.add_edge(START, "plan")
graph.add_conditional_edges("plan", route_after_plan, {
"search": "search",
"synthesize": "synthesize",
})
graph.add_edge("search", "analyze")
graph.add_edge("analyze", "reflect")
graph.add_conditional_edges("reflect", should_continue_research, {
"search_again": "search",
"next_sub": "advance_index",
"synthesize": "synthesize",
})
# 子问题推进节点
def advance_index(state: ResearchState) -> dict:
return {"current_sub_index": state["current_sub_index"] + 1,
"iteration": 0}
graph.add_node("advance_index", advance_index)
graph.add_edge("advance_index", "search")
graph.add_edge("synthesize", END)
checkpointer = MemorySaver()
return graph.compile(checkpointer=checkpointer)
async def web_search(query: str, top_k: int = 5) -> list:
"""模拟搜索工具"""
return [
type("S", (), {
"title": f"Result {i} for {query}",
"url": f"https://example.com/{i}",
"snippet": f"Snippet about {query}...",
"relevance": 0.8 - i * 0.1,
})() for i in range(top_k)
]
二、条件路由与并行分支
2.1 条件边的Map模式
LangGraph的条件边用dict映射返回值到节点名,清晰且可测试。复杂路由逻辑(如基于LLM的意图分类)封装为纯函数:
# langgraph_code_agent/router.py - 代码Agent的意图路由
def route_intent(state: CodeAgentState) -> str:
"""根据用户意图路由到不同处理节点"""
last_message = state["messages"][-1].content.lower()
if any(kw in last_message for kw in ["实现", "创建", "写一个"]):
return "implement"
if any(kw in last_message for kw in ["修复", "bug", "错误"]):
return "diagnose"
if any(kw in last_message for kw in ["重构", "优化", "简化"]):
return "refactor"
if any(kw in last_message for kw in ["解释", "分析", "为什么"]):
return "explain"
return "clarify"
graph.add_conditional_edges("intent_classifier", route_intent, {
"implement": "plan_implementation",
"diagnose": "diagnose_bug",
"refactor": "plan_refactor",
"explain": "explain_code",
"clarify": "ask_clarification",
})
2.2 并行扇出与汇聚
LangGraph通过Send API实现动态并行:一个节点扇出多个子任务并行执行,结果汇聚后继续。以下实现多文件分析并行扇出:
# langgraph_parallel/fanout.py - 并行文件分析
from langgraph.graph import StateGraph, START, END
from langgraph.constants import Send
from typing import Annotated
import operator
class AnalysisState(TypedDict):
files: list[str]
results: Annotated[list, operator.add] # 并行结果追加合并
summary: str
async def dispatch_analyses(state: AnalysisState) -> list[Send]:
"""为每个文件创建并行分析任务"""
return [
Send("analyze_file", {"file": f, "results": []})
for f in state["files"]
]
async def analyze_file(state: dict) -> dict:
"""单个文件分析(并行执行)"""
file = state["file"]
# 实际调用LLM分析
result = f"分析结果: {file} 包含3个函数,复杂度中等"
return {"results": [result]}
async def summarize(state: AnalysisState) -> dict:
"""汇聚并行结果生成摘要"""
all_results = "\n".join(state["results"])
return {"summary": f"共分析{len(state['results'])}个文件:\n{all_results}"}
graph = StateGraph(AnalysisState)
graph.add_node("analyze_file", analyze_file)
graph.add_node("summarize", summarize)
graph.add_conditional_edges(START, dispatch_analyses, ["analyze_file"])
graph.add_edge("analyze_file", "summarize")
graph.add_edge("summarize", END)
compiled = graph.compile()
三、检查点持久化与中断恢复
3.1 持久化后端
MemorySaver仅存内存,生产环境需要数据库后端。LangGraph支持PostgreSQL、Redis等:
# langgraph_persistence/checkpoint.py - PostgreSQL检查点
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool
pool = ConnectionPool(
conninfo="postgresql://user:pass@localhost:5432/langgraph",
max_size=20,
)
checkpointer = PostgresSaver(conn=pool)
checkpointer.setup() # 创建表结构
graph = build_research_graph().compile(checkpointer=checkpointer)
3.2 Human-in-the-loop中断
LangGraph的interrupt_before/interrupt_after让图在指定节点暂停,等待人工介入后继续。典型场景:代码修改前需要人工确认、危险操作需要审批:
# langgraph_hitl/human_approval.py - 人机协作中断
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
class DeployState(TypedDict):
change_description: str
proposed_code: str
test_results: dict
approved: bool
deployed: bool
async def propose_change(state: DeployState) -> dict:
return {"proposed_code": "diff --git a/...",
"change_description": "Add rate limiting"}
async def run_tests(state: DeployState) -> dict:
return {"test_results": {"passed": 42, "failed": 0}}
async def deploy(state: DeployState) -> dict:
return {"deployed": True}
graph = StateGraph(DeployState)
graph.add_node("propose", propose_change)
graph.add_node("test", run_tests)
graph.add_node("deploy", deploy)
graph.add_edge(START, "propose")
graph.add_edge("propose", "test")
# 关键:在deploy前暂停,等待人工审批
graph.add_edge("test", "deploy")
graph.add_edge("deploy", END)
# interrupt_before让图在进入deploy前暂停
compiled = graph.compile(
checkpointer=MemorySaver(),
interrupt_before=["deploy"],
)
# 使用示例:第一次运行到deploy前暂停
config = {"configurable": {"thread_id": "deploy-001"}}
result = await compiled.ainvoke(
{"change_description": "Add rate limiting"}, config,
)
# 此时图暂停在deploy节点前
# 人工审核proposed_code后,更新state并恢复
state = await compiled.aget_state(config)
if human_approves(state.values["proposed_code"]):
# 恢复执行
result = await compiled.ainvoke(None, config) # 传None继续
3.3 动态更新状态
人工介入时可更新State再恢复,实现"修改计划后继续":
# 人工修改代码后恢复
await compiled.aupdate_state(
config,
{"proposed_code": revised_code, "approved": True},
)
result = await compiled.ainvoke(None, config)
四、多Agent架构模式
4.1 Supervisor模式
Supervisor作为中心调度器,决定调用哪个专家Agent、何时汇总、何时结束。适合任务边界清晰、需要中央控制的场景:
# langgraph_multiagent/supervisor.py - Supervisor编排
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
import json
class TeamState(TypedDict):
messages: Annotated[list, add_messages]
team_members: list[str]
next_speaker: str
task_complete: bool
final_report: str
EXPERTS = {
"researcher": "负责信息检索与事实核查",
"analyst": "负责数据分析与统计建模",
"writer": "负责撰写报告与文案",
"reviewer": "负责质量审核与逻辑检查",
}
async def supervisor(state: TeamState) -> dict:
"""Supervisor:决定下一个发言者与是否完成"""
llm = ChatOpenAI(model="gpt-4o", temperature=0)
conversation = "\n".join(
f"{m['role']}: {m['content'][:200]}" for m in state["messages"][-6:]
)
response = await llm.ainvoke([
{"role": "system", "content": f"""你是团队主管,协调以下专家完成任务:
{json.dumps(EXPERTS, ensure_ascii=False, indent=2)}
决定下一步:调用某专家 or 完成任务(输出"FINISH")。
输出JSON:{{"next": "expert_name 或 FINISH", "instruction": "给该专家的指令"}}"""},
{"role": "user", "content": f"对话历史:\n{conversation}\n\n原始任务:{state['messages'][0]['content']}"},
])
decision = json.loads(response.content)
if decision["next"] == "FINISH":
return {"task_complete": True, "next_speaker": "FINISH"}
# 将supervisor指令注入为消息
return {
"next_speaker": decision["next"],
"messages": [{"role": "supervisor", "content": decision["instruction"]}],
}
async def expert_node_factory(expert_name: str):
"""创建专家节点:每个专家有不同系统提示"""
llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
system_prompt = EXPERT_SYSTEMS[expert_name]
async def expert_node(state: TeamState) -> dict:
last_msg = state["messages"][-1]
response = await llm.ainvoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"指令:{last_msg['content']}\n\n对话上下文:{[m['content'][:100] for m in state['messages'][-3:]]}"},
])
return {"messages": [{"role": expert_name, "content": response.content}]}
return expert_node
def route_from_supervisor(state: TeamState) -> str:
if state["task_complete"]:
return "finalize"
return state["next_speaker"]
# 构图
graph = StateGraph(TeamState)
graph.add_node("supervisor", supervisor)
for name in EXPERTS:
graph.add_node(name, await expert_node_factory(name))
graph.add_node("finalize", finalize_node)
graph.add_edge(START, "supervisor")
graph.add_conditional_edges("supervisor", route_from_supervisor, {
"finalize": "finalize",
**{name: name for name in EXPERTS},
})
for name in EXPERTS:
graph.add_edge(name, "supervisor") # 专家完成后回supervisor
graph.add_edge("finalize", END)
async def finalize_node(state: TeamState) -> dict:
llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
response = await llm.ainvoke([
{"role": "system", "content": "综合团队讨论,生成最终报告。"},
{"role": "user", "content": "\n".join(m["content"] for m in state["messages"])},
])
return {"final_report": response.content}
4.2 Swarm模式
Swarm模式中Agent间直接传递控制权(handoff),无中心调度。适合对话式场景,Agent根据消息内容自主决定交接:
# langgraph_multiagent/swarm.py - Swarm handoff
from langgraph.graph import StateGraph, START, END
class SwarmState(TypedDict):
messages: Annotated[list, add_messages]
current_agent: str
context: dict
HANDOFF_TOOLS = {
"triage": {
"name": "transfer_to_billing",
"description": "转交账单问题专家",
"target": "billing",
},
"billing": {
"name": "transfer_to_tech",
"description": "转交技术支持专家",
"target": "tech",
},
}
async def triage_agent(state: SwarmState) -> dict:
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Agent可用handoff工具决定交接
response = await llm.ainvoke([
{"role": "system", "content": "你是分流Agent。判断问题类型并转交。"},
{"role": "user", "content": state["messages"][-1]["content"]},
])
# 简化:基于内容判断是否需要交接
if "账单" in response.content or "付款" in response.content:
return {"current_agent": "billing",
"messages": [{"role": "triage", "content": "转交账单专家"}]}
return {"messages": [{"role": "triage", "content": response.content}]}
async def billing_agent(state: SwarmState) -> dict:
# 处理账单问题,必要时转交tech
...
def route_swarm(state: SwarmState) -> str:
return state["current_agent"]
五、工具集成与ReAct循环
5.1 工具节点与错误恢复
LangGraph的工具调用通过ToolNode封装,自动处理工具调用与结果回传。错误恢复是生产关键:
# langgraph_tools/react_agent.py - ReAct Agent with error recovery
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
@tool
def execute_sql(query: str) -> str:
"""执行只读SQL查询"""
# 实际执行
return f"Query result for: {query}"
@tool
def search_docs(query: str) -> str:
"""搜索内部文档"""
return f"Docs for: {query}"
@tool
def run_tests(file_path: str) -> str:
"""运行测试文件"""
return f"All tests passed in {file_path}"
tools = [execute_sql, search_docs, run_tests]
tool_node = ToolNode(tools)
llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
async def agent_node(state: dict) -> dict:
response = await llm.ainvoke(state["messages"])
return {"messages": [response]}
def should_use_tools(state: dict) -> str:
last_msg = state["messages"][-1]
if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
return "tools"
return END
async def tool_error_handler(state: dict) -> dict:
"""工具执行失败时的恢复节点"""
last_msg = state["messages"][-1]
if hasattr(last_msg, "status") and last_msg.status == "error":
# 向模型报告错误让它自行修正
error_msg = f"工具执行失败:{last_msg.content}。请修正查询后重试。"
return {"messages": [{"role": "tool", "content": error_msg,
"tool_call_id": last_msg.tool_call_id}]}
return {}
graph = StateGraph({"messages": Annotated[list, add_messages]})
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.add_node("error_handler", tool_error_handler)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_use_tools, {
"tools": "tools", END: END,
})
# 工具结果回到error_handler检查,再回agent
graph.add_edge("tools", "error_handler")
graph.add_edge("error_handler", "agent")
compiled = graph.compile()
六、流式输出与可观测性
6.1 流式Token输出
LangGraph支持节点级流式,让用户实时看到Agent的推理过程:
# langgraph_streaming/stream.py - 流式输出
async def stream_research(graph, question: str):
config = {"configurable": {"thread_id": "research-001"}}
# astream提供三种流模式
async for mode, chunk in graph.astream(
{"messages": [{"role": "user", "content": question}]},
config, stream_mode="updates",
):
# updates模式:每个节点的状态更新
for node_name, update in chunk.items():
print(f"[{node_name}] 更新: {list(update.keys())}")
if "messages" in update:
for msg in update["messages"]:
print(f" {msg.get('role', 'system')}: {msg['content'][:100]}...")
# values模式:获取每步完整状态快照
async for chunk in graph.astream(
{"messages": [{"role": "user", "content": question}]},
config, stream_mode="values",
):
print(f"当前子问题索引: {chunk.get('current_sub_index', 0)}")
# messages模式:逐token流式LLM输出
async for msg, metadata in graph.astream(
{"messages": [{"role": "user", "content": question}]},
config, stream_mode="messages",
):
if msg.content:
print(msg.content, end="", flush=True)
6.2 LangSmith可观测性
LangGraph与LangSmith深度集成,自动追踪每个节点的输入输出、LLM调用、工具执行:
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "ls__..."
os.environ["LANGSMITH_PROJECT"] = "research-agent-prod"
# 设置后所有graph.invoke/astream自动上报到LangSmith
# 在LangSmith UI可看到:
# - 节点执行时间线
# - 每个节点的输入输出state diff
# - LLM调用的prompt/completion/token数
# - 工具调用的参数与返回
# - 条件路由的决策路径
七、生产部署与运维
7.1 长时运行与并发管理
生产LangGraph服务需要管理长时运行的图执行(多轮Agent可能运行数分钟),关键设计:异步非阻塞(FastAPI + async),检查点让任意中断可恢复,线程/会话隔离(thread_id),超时与熔断。以下是一个生产级API服务:
# server/main.py - LangGraph生产API
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
from langgraph.checkpoint.postgres import PostgresSaver
from contextlib import asynccontextmanager
import asyncio
app = FastAPI()
graph = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global graph
pool = ConnectionPool(...)
checkpointer = PostgresSaver(conn=pool)
checkpointer.setup()
graph = build_research_graph().compile(checkpointer=checkpointer)
yield
pool.close()
app = FastAPI(lifespan=lifespan)
class QueryRequest(BaseModel):
thread_id: str
question: str
stream: bool = False
@app.post("/research")
async def research(req: QueryRequest):
config = {"configurable": {"thread_id": req.thread_id}}
if req.stream:
return StreamingResponse(
stream_graph(req, config),
media_type="text/event-stream",
)
result = await asyncio.wait_for(
graph.ainvoke(
{"messages": [{"role": "user", "content": req.question}]},
config,
),
timeout=300, # 5分钟超时
)
return {"answer": result.get("final_answer", ""),
"citations": result.get("citations", [])}
async def stream_graph(req, config):
async for mode, chunk in graph.astream(
{"messages": [{"role": "user", "content": req.question}]},
config, stream_mode="updates",
):
yield f"data: {json.dumps({'node': list(chunk.keys())})}\n\n"
yield f"data: [DONE]\n\n"
@app.get("/research/{thread_id}/state")
async def get_state(thread_id: str):
"""获取会话当前状态(支持前端恢复中断的会话)"""
config = {"configurable": {"thread_id": thread_id}}
state = await graph.aget_state(config)
return {
"values": state.values,
"next": state.next, # 下一个要执行的节点
"interrupted": state.next is not None and len(state.tasks) > 0,
}
总结
LangGraph代表了LLM应用编排范式的成熟:从线性的Chain走向图状的StateGraph,节点是处理单元、边是控制流、共享State在节点间演化,条件路由实现意图分发,Send API实现并行扇出汇聚,检查点持久化让任意中断可恢复,Human-in-the-loop让图在关键节点暂停等待人工审批,Supervisor与Swarm两种多Agent模式分别覆盖中心调度与对等交接场景,工具节点与ReAct循环连接外部能力,流式输出与LangSmith可观测性让Agent推理过程透明可调试。工程实践的核心原则:节点保持纯函数(便于测试与重放),State用TypedDict明确定义(类型安全+减少隐式依赖),条件路由函数可独立测试,长时图必须配检查点(崩溃恢复是生产底线),Human-in-the-loop中断点要前置设计(而非事后补丁),多Agent模式按任务结构选择(Supervisor适合流程化任务,Swarm适合对话式交接)。当Agent应用从单轮问答走向多步推理、从单体走向多Agent协作、从一次性走向可中断可恢复的长期任务,LangGraph的图编排抽象正是支撑这一跃迁的工程基础设施。
- 点赞
- 收藏
- 关注作者
评论(0)