第 3 章:LangGraph 图编排实战¶
LangGraph 是 LangChain 团队推出的基于图的状态机编排框架,适合构建复杂、可控的 Agent 工作流。本章将带你深入理解和使用 LangGraph。
3.1 为什么选择 LangGraph?¶
与 LangChain 的关系¶
LangChain LangGraph
┌─────────┐ ┌─────────────┐
│ Chains │ → │ State Graph │
│ 线性流程 │ │ 有环有向图 │
└─────────┘ └─────────────┘
LangGraph 是 LangChain 的"生产级"编排层
核心优势¶
| 特性 | 说明 |
|---|---|
| 状态图(State Graph) | 显式定义状态,类型安全 |
| 循环支持 | Agent 可以循环决策,直到完成任务 |
| 条件路由 | 根据状态动态选择下一步 |
| 持久化 | 内置 Checkpointer,支持断点续传与时间旅行 |
| Human-in-the-Loop | 原生支持人工审核节点(interrupt) |
3.2 环境准备¶
pip install "langgraph>=0.2" langchain-openai langchain-core
# 持久化检查点(可选)
pip install langgraph-checkpoint-sqlite
3.3 第一个 LangGraph Agent¶
3.3.1 定义状态类型¶
# graph_state.py
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
"""Agent 的状态定义"""
messages: Annotated[list[BaseMessage], operator.add] # 累加式更新
next_node: str # 下一个要执行的节点
should_ask_human: bool # 是否需要人工确认
3.3.2 定义节点函数¶
# nodes.py
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from graph_state import AgentState
# 初始化 LLM
llm = ChatOpenAI(model="gpt-4o-mini")
# 定义工具(OpenAI 格式,bind_tools 支持 dict 列表)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "执行数学计算",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string"}
},
"required": ["expression"]
}
}
}
]
llm_with_tools = llm.bind_tools(tools)
def chatbot(state: AgentState) -> dict:
"""聊天机器人节点:调用 LLM,可能返回 tool_calls"""
response = llm_with_tools.invoke(state["messages"])
# ⚠️ 不要在这里用 Command(goto=...) 跳转!
# 一旦在节点里用 Command 指定 goto,条件边(add_conditional_edges)
# 就不会再被触发,路由逻辑会绕过条件判断。
# 正确做法:只返回状态更新,让"条件边"统一负责路由。
return {"messages": [response]}
def should_continue(state: AgentState) -> str:
"""条件路由函数(决定从 chatbot 去哪里)"""
last_message = state["messages"][-1]
if last_message.tool_calls: # 需要调用工具
return "tool_executor"
if state.get("should_ask_human"): # 需要人工审核
return "human_review"
return END # 正常结束
def tool_executor(state: AgentState) -> dict:
"""工具执行节点"""
messages = state["messages"]
last_message = messages[-1]
# 执行工具调用
tool_messages = []
for tool_call in last_message.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
# 根据工具名称执行(生产环境请用真实工具/安全求值)
if tool_name == "get_weather":
result = f"{tool_args['city']}今日天气:晴,25°C"
elif tool_name == "calculate":
# ⚠️ 不要用 eval()!这里用 ast 安全求值(见第 2 章)
result = _safe_eval_math(tool_args["expression"])
else:
result = f"未知工具: {tool_name}"
tool_messages.append(ToolMessage(
content=str(result),
tool_call_id=tool_call["id"] # 必须回传 tool_call_id
))
return {"messages": tool_messages}
def human_review(state: AgentState) -> dict:
"""人工审核节点"""
print("\n📋 需要人工审核的内容:")
print(state["messages"][-1].content)
approved = input("\n是否批准?(yes/no): ").lower()
# ⚠️ 不要直接修改 state!LangGraph 的状态是不可变的,
# 节点必须返回"要更新的字段"dict,由框架合并到状态。
if approved == "yes":
return {"should_ask_human": False}
else:
return {"messages": [HumanMessage(
content="请重新生成,上面的内容不符合要求"
)]}
3.3.3 构建图¶
# build_graph.py
from langgraph.graph import StateGraph, END
from graph_state import AgentState
from nodes import chatbot, tool_executor, human_review, should_continue
# 创建图
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("chatbot", chatbot)
workflow.add_node("tool_executor", tool_executor)
workflow.add_node("human_review", human_review)
# 设置入口点
workflow.set_entry_point("chatbot")
# 工具执行完回到聊天节点(循环)
workflow.add_edge("tool_executor", "chatbot")
# 条件边:chatbot 之后的走向由 should_continue 决定
workflow.add_conditional_edges(
"chatbot",
should_continue,
{
"tool_executor": "tool_executor",
"human_review": "human_review",
END: END,
}
)
# 编译图(这一步只编译一次)
# checkpointer=None 是默认值(内存检查点),无需显式传
graph = workflow.compile()
# 持久化检查点(可选,见 3.5.2)
# from langgraph.checkpoint.sqlite import SqliteSaver
# with SqliteSaver.from_conn_string("checkpoints.db") as checkpointer:
# graph = workflow.compile(checkpointer=checkpointer)
3.3.4 运行 Agent¶
# main.py
from build_graph import graph
from langchain_core.messages import HumanMessage
# 通过 config 传入 thread_id 标识会话
thread_config = {"configurable": {"thread_id": "session-1"}}
# 示例 1:简单问答
result = graph.invoke(
{"messages": [HumanMessage(content="北京今天天气怎么样?")]},
config=thread_config
)
print("回复:", result["messages"][-1].content)
# 示例 2:多轮对话保持上下文(同一 thread_id)
result = graph.invoke(
{"messages": [HumanMessage(content="帮我计算 123 + 456 * 789")]},
config=thread_config
)
print("回复:", result["messages"][-1].content)
# 示例 3:Agent 记得之前的对话
result = graph.invoke(
{"messages": [HumanMessage(content="刚才的计算结果是多少?")]},
config=thread_config
)
print("回复:", result["messages"][-1].content)
⚠️ 多轮上下文的关键:同一个
thread_id。LangGraph 用 thread_id 区分会话,没有配置 thread_id 时每次 invoke 都是全新会话,不会"记得"上一轮。
3.4 实战:多步骤研究报告 Agent¶
# research_agent.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_openai import ChatOpenAI
import json
class ResearchState(TypedDict):
topic: str
outline: str
sections: list[str]
draft: str
needs_rewrite: bool # 必须声明,否则节点返回会报错
review_feedback: str # 必须声明,否则节点返回会报错
messages: Annotated[list[BaseMessage], operator.add]
class ResearchAgent:
"""研究报告生成 Agent"""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o")
self.graph = self._build_graph()
def _build_graph(self):
workflow = StateGraph(ResearchState)
workflow.add_node("generate_outline", self.generate_outline)
workflow.add_node("write_sections", self.write_sections)
workflow.add_node("draft_report", self.draft_report)
workflow.add_node("review", self.review_report)
workflow.set_entry_point("generate_outline")
workflow.add_edge("generate_outline", "write_sections")
workflow.add_edge("write_sections", "draft_report")
# 条件边:review 决定是重写还是结束
workflow.add_conditional_edges(
"review",
lambda state: "rewrite" if state.get("needs_rewrite") else "done",
{
"rewrite": "draft_report",
"done": END,
}
)
return workflow.compile()
def generate_outline(self, state: ResearchState) -> dict:
"""生成大纲"""
prompt = f"""请为以下主题生成研究报告大纲:{state['topic']}
要求:
1. 包含 5-8 个主要章节
2. 每个章节有 2-3 个子要点
3. 输出 JSON 格式"""
response = self.llm.invoke([HumanMessage(content=prompt)])
return {"outline": response.content}
def write_sections(self, state: ResearchState) -> dict:
"""撰写各章节内容"""
prompt = f"""根据以下大纲撰写报告内容:
{state['outline']}
请生成完整的报告草稿。"""
response = self.llm.invoke([HumanMessage(content=prompt)])
return {"sections": [response.content]}
def draft_report(self, state: ResearchState) -> dict:
"""生成报告草稿"""
sections_content = "\n\n".join(state["sections"])
prompt = f"""请根据以下内容整理成正式研究报告:
{sections_content}
要求:
1. 添加引言和结论
2. 使用专业语言
3. 格式规范"""
response = self.llm.invoke([HumanMessage(content=prompt)])
return {"draft": response.content}
def review_report(self, state: ResearchState) -> dict:
"""审核报告"""
prompt = f"""请审核以下研究报告,指出需要改进的地方:
{state['draft']}
如果报告质量达标,输出 JSON: {{"needs_rewrite": false}}
如果需要重大修改,输出 JSON: {{"needs_rewrite": true, "feedback": "具体反馈"}}"""
response = self.llm.invoke([HumanMessage(content=prompt)])
# 建议让模型输出严格 JSON(如用 ChatOpenAI 的 response_format)
review = json.loads(response.content)
return {
"needs_rewrite": review.get("needs_rewrite", False),
"review_feedback": review.get("feedback", ""),
}
def run(self, topic: str) -> str:
"""运行研究 Agent"""
initial_state = {
"topic": topic,
"outline": "",
"sections": [],
"draft": "",
"needs_rewrite": False,
"review_feedback": "",
"messages": [],
}
result = self.graph.invoke(initial_state)
return result["draft"]
提示:生产环境建议用
llm.with_structured_output()代替手写json.loads,让模型直接输出 Pydantic 对象,避免 JSON 解析失败。
3.5 LangGraph 高级特性¶
3.5.1 子图(Subgraph)¶
from langgraph.graph import StateGraph, END
# 创建子图
sub_workflow = StateGraph(SubState)
sub_workflow.add_node("step1", func1)
sub_workflow.add_node("step2", func2)
sub_workflow.add_edge("step1", "step2")
sub_workflow.set_entry_point("step1")
sub_workflow.add_edge("step2", END)
sub_graph = sub_workflow.compile()
# 在主图中嵌入子图(子图作为主图的一个节点)
main_workflow = StateGraph(MainState)
main_workflow.add_node("sub_graph_node", sub_graph)
3.5.2 检查点与时间旅行(Checkpointer)¶
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_core.messages import HumanMessage
# 使用 SQLite 持久化
with SqliteSaver.from_conn_string("checkpoints.db") as checkpointer:
graph = workflow.compile(checkpointer=checkpointer)
# 正常执行,自动记录检查点
result = graph.invoke(
{"messages": [HumanMessage(content="帮我写一个计划")]},
config={"configurable": {"thread_id": "session-1"}},
)
# 时间旅行:从指定检查点恢复(⚠️ 参数是 checkpoint_id,不是 checkpointed_at)
# checkpoint_id 可从 graph.get_state(config) 获取
snapshot = graph.get_state(
{"configurable": {"thread_id": "session-1"}}
)
resume_config = {
"configurable": {
"thread_id": "session-1",
"checkpoint_id": snapshot.config["configurable"]["checkpoint_id"],
}
}
# 从该检查点重新运行
result = graph.invoke(None, config=resume_config)
⚠️ LangGraph 没有
checkpointed_at参数。恢复历史状态用的是configurable.checkpoint_id(配合get_state获取),这被称为"时间旅行"(time-travel)。
3.5.3 Human-in-the-Loop(interrupt)¶
from langgraph.types import interrupt
def human_approval_node(state: AgentState) -> dict:
"""需要人工审批的节点"""
# interrupt 会暂停图执行,把值抛给调用方等待人工输入
approval = interrupt({
"message": "请审批以下内容",
"content": state["proposal"]
})
# 调用方用 Command(resume=...) 恢复执行后,interrupt 返回人工输入
if approval.get("approved"):
return {"approved": True}
else:
return {"approved": False, "feedback": approval.get("feedback")}
3.6 最佳实践¶
状态设计原则¶
# ✅ 好的状态设计
class GoodState(TypedDict):
messages: list[BaseMessage] # 必要的对话历史
decision: str # 明确的决策点
confidence: float # 量化的置信度
# ❌ 不好的状态设计
class BadState(TypedDict):
all_the_data: dict # 太模糊
everything: Any # 类型不安全
节点设计原则¶
# ✅ 好的节点设计:只返回需要更新的字段(状态不可变)
def clean_node(state: AgentState) -> dict:
return {"messages": [new_message]}
# ❌ 不好的节点设计:直接修改 state、返回无关字段
def messy_node(state: AgentState) -> dict:
state["messages"].append(...) # 直接改 state——反模式!
return {
"messages": [...],
"unrelated_thing": "whatever", # 污染状态
"temp_var": 123 # 临时变量不该留在状态里
}
3.7 本章小结¶
✅ 理解了 LangGraph 的图编排模型
✅ 掌握了状态定义和节点设计(节点返回更新 dict,不直接改 state)
✅ 学会了用条件边做路由(节点内避免用 Command(goto) 绕过条件边)
✅ 了解了子图、检查点(thread_id / checkpoint_id)和 Human-in-the-Loop 等高级特性