先放结论:LangGraph 的核心不是"图",而是"状态机"。 图只是状态流转的可视化表达,真正的威力在于:状态可以被持久化、被人工修改、被条件路由动态调度。掌握这三点,你才能从"写流程"升级到"设计系统"。
一、状态管理:LangGraph 的"共享白板"
1.1 为什么需要状态管理
在 LangChain 的 Chain 模式里,数据是沿着链条单向流动的:输入 → 节点A → 节点B → 输出。每个节点只能拿到上一个节点的输出,无法"回头看"更早的状态。
LangGraph 的做法完全不同:它维护一块共享白板(State) ,所有节点都读写同一份状态。节点之间不再通过"传递数据"通信,而是通过"修改白板"通信。
css
┌─────────────────────────────────────────────┐
│ State(共享白板) │
│ { │
│ "user_query": "...", │
│ "intermediate_result": "...", │
│ "final_answer": "", │
│ "step_history": [...] │
│ } │
└──────────┬──────────────────────┬───────────┘
↓ ↓
┌─────────────┐ ┌─────────────┐
│ 节点 A │ │ 节点 B │
│ 读状态 → 写 │ │ 读状态 → 写 │
└─────────────┘ └─────────────┘
1.2 状态定义:TypedDict + Annotated
LangGraph 用 TypeScript 风格的 TypedDict 定义状态结构,配合 Annotated 实现合并策略而非覆盖策略。
python
from typing import Annotated, TypedDict, Literal
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
# 消息列表:使用 add_messages 合并器,新消息追加而非覆盖
messages: Annotated[list[BaseMessage], add_messages]
# 普通字段:后续节点的返回值会覆盖前值
category: str
confidence: float
needs_human_review: bool
关键点:
add_messages是 LangGraph 内置的合并器,保证消息列表不断追加- 普通字段(如
category)默认是覆盖语义 :节点返回{category: "billing"}会覆盖之前的值 - 自定义合并器:你可以实现自己的 reducer 函数,比如累加计数器、合并字典等
1.3 状态的生命周期
状态不是"用完即弃"的。配合 Checkpointer,状态可以被:
- 持久化:保存到数据库,服务重启不丢失
- 回滚:回到任意历史快照,重新执行
- 人工修改:在暂停点让外部系统修改状态值
ini
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
# 开发环境:内存存储
checkpointer = MemorySaver()
# 生产环境:SQLite 持久化
checkpointer = SqliteSaver.from_conn_string("checkpoints.db")
app = graph.compile(checkpointer=checkpointer)
二、条件路由:告别 if-else spaghetti
2.1 条件边的基本原理
条件边(Conditional Edge)是 LangGraph 实现动态路由的核心机制。它不是一个固定的"下一步",而是一个路由函数,根据当前状态决定跳转到哪个节点。
python
from langgraph.graph import StateGraph, START, END
def route_by_priority(state: TicketState) -> Literal["responder", "human_review"]:
"""根据优先级决定路由"""
if state["priority"] == "high":
return "human_review"
return "responder"
graph = StateGraph(TicketState)
graph.add_node("classifier", classify_ticket)
graph.add_node("responder", draft_reply)
graph.add_node("human_review", escalate_to_human)
graph.add_edge(START, "classifier")
# 条件边:从 classifier 出发,根据 route_by_priority 的返回值决定去向
graph.add_conditional_edges(
"classifier",
route_by_priority,
{
"responder": "responder",
"human_review": "human_review",
}
)
graph.add_edge("responder", END)
graph.add_edge("human_review", END)
三个参数:
- 源节点:从哪个节点产生分支
- 路由函数:接收 state,返回字符串
- 映射表:路由函数的返回值 → 目标节点名称
2.2 循环路由:实现"打回重做"
条件边最强大的能力是实现循环------这是 Chain 模式做不到的。
perl
def review_router(state: ReviewState) -> Literal["fix_code", END]:
if state["passed"]:
return END
# ️ 循环一定要有终止条件,否则死循环
if state["review_round"] >= 5:
return END
return "fix_code"
graph.add_edge("write_code", "review")
graph.add_conditional_edges("review", review_router)
graph.add_edge("fix_code", "review") # 修完回去再审 → 形成循环
防死循环原则:任何循环路由都必须有明确的退出条件(最大次数、超时、预算上限)。
2.3 多条件路由:复杂决策树
当决策维度变多时,路由函数可以组合多个判断条件:
perl
def escalation_decision(state: CustomerServiceState) -> Literal["human_handover", "ai_response"]:
# 维度1:情感因素
if state["sentiment"] in ["angry", "frustrated"]:
return "human_handover"
# 维度2:紧急程度
if state["urgency_level"] == "high":
return "human_handover"
# 维度3:置信度
if state["category_confidence"] < 0.6:
return "human_handover"
# 维度4:敏感关键词
sensitive_keywords = ["投诉", "退款", "赔偿", "法律", "起诉"]
if any(kw in state["user_question"] for kw in sensitive_keywords):
return "human_handover"
return "ai_response"
设计原则:路由逻辑越重要,越应该保持简单、透明、可测试。能用确定性代码完成的决策,不要交给 LLM。
三、人工介入(Human-in-the-Loop):让 Agent 停下来等人
3.1 为什么需要人工介入
自主规划的 Agent 再聪明,也不该在以下场景中独自做决定:
- 不可逆操作:删除数据、执行转账、发布内容
- 高风险决策:医疗诊断建议、法律条款审查
- 低置信度场景:AI 自己都拿不准,需要人类专家把关
- 合规要求:企业内部流程要求关键步骤必须有人审批
LangGraph 提供了两套人工介入机制:静态中断 和动态中断。
3.2 静态中断:interrupt_before / interrupt_after
在编译图时声明,在指定节点执行前或执行后自动暂停。
python
from langgraph.checkpoint.memory import MemorySaver
class ApprovalState(TypedDict):
plan: str
approved: bool
result: str
def generate_plan_node(state: ApprovalState) -> dict:
return {"plan": generate_plan()}
def execute_node(state: ApprovalState) -> dict:
return {"result": execute_plan(state["plan"])}
def approval_router(state: ApprovalState) -> str:
if state["approved"]:
return "execute"
return "generate_plan" # 被拒了,重新生成
graph = StateGraph(ApprovalState)
graph.add_node("generate_plan", generate_plan_node)
graph.add_node("execute", execute_node)
graph.add_edge(START, "generate_plan")
graph.add_conditional_edges("generate_plan", approval_router)
graph.add_edge("execute", END)
# ️ Human-in-the-loop 必须配合 checkpointer
# interrupt_before=["execute"] 表示在执行节点之前暂停
app = graph.compile(
checkpointer=MemorySaver(),
interrupt_before=["execute"]
)
# 第一轮:生成方案,到 execute 前暂停
config = {"configurable": {"thread_id": "thread-1"}}
result = app.invoke(
{"plan": "", "approved": False, "result": ""},
config=config
)
# 此时停在 execute 前,plan 已生成,等人拍板
# 模拟人工审批
user_approved = True
app.update_state(config, {"approved": user_approved})
# 继续执行 ------ 传 None 表示从断点继续
result = app.invoke(None, config)
print(result["result"])
关键流程:
- 首次
invoke遇到中断点,图暂停,返回当前状态 - 外部系统(前端/审批界面)展示待审批内容
- 人工决策后,用
update_state修改状态 - 再次
invoke(None, config)从断点恢复
3.3 动态中断:interrupt() 函数
在节点内部根据运行时条件动态触发暂停,更灵活。
python
from langgraph.types import interrupt, Command
def review_plan_node(state: ResearchState) -> dict:
# 准备给人看的 payload
payload = {
"type": "research_plan_review",
"question": state["question"],
"round": state.get("round", 0) + 1,
"proposed_queries": state["proposed_queries"],
}
# 调用 interrupt() 暂停图执行
decision = interrupt(payload)
# 只有恢复后才会运行到这里
# decision 就是外部传入的 Command(resume=...) 中的值
if decision["action"] == "approve":
return {"review_status": "approved"}
elif decision["action"] == "edit":
return {
"review_status": "edited",
"proposed_queries": decision["query"],
}
else:
return {"review_status": "rejected"}
恢复方式:
ini
from langgraph.types import Command
# 首次运行,遇到 interrupt() 暂停
thread_config = {"configurable": {"thread_id": "research-42"}}
paused = graph.invoke(
{"question": "LangGraph 的 interrupt 机制是什么?"},
config=thread_config,
)
assert "__interrupt__" in paused
# 恢复:用同一个 thread_id,传入 Command(resume=...)
result = graph.invoke(
Command(resume={"action": "approve"}),
config=thread_config,
)
两种中断的对比:
| 维度 | 静态中断(interrupt_before) | 动态中断(interrupt()) |
|---|---|---|
| 触发方式 | 编译时声明,固定节点前后暂停 | 节点内代码触发,可带条件 |
| 灵活性 | 低,只能暂停/继续 | 高,可以携带 payload 和多种决策 |
| 适用场景 | 简单审批门 | 复杂交互(改写、多选项) |
| 恢复方式 | update_state + invoke(None) |
Command(resume=...) |
3.4 副作用隔离:一个容易被踩的坑
interrupt() 前的代码在恢复时会重放。如果节点里有 API 调用、数据库写入等副作用操作,恢复时会重复执行。
python
# 错误做法:副作用在 interrupt() 之前
def bad_node(state):
db.write(data) # 恢复时会重复写入!
decision = interrupt({"data": data})
return {"status": decision}
# 正确做法:副作用在 interrupt() 之后,或放到独立节点
def good_node(state):
decision = interrupt({"data": state["data"]})
db.write(data) # 只有恢复后才执行一次
return {"status": decision}
四、完整案例:销售报告自动生成与审批系统
下面用一个完整的案例,把状态管理、条件路由、人工介入串起来。
场景:每月自动生成销售报告,如果发现有区域销售额下降超过 10%,生成报告并等待人工确认后才发布。
4.1 状态定义
python
from typing import TypedDict, List, Optional
class ReportState(TypedDict, total=False):
task_id: str
current_month: str
previous_month: str
current_data: dict[str, float]
previous_data: dict[str, float]
changes: list[dict]
risky_regions: list[str]
report: str
needs_confirmation: bool
approval_status: str # "pending" / "approved" / "rejected"
published: bool
error: Optional[str]
retry_count: int
total=False 表示字段可以在任务的不同阶段逐步出现。
4.2 节点实现
python
from langchain_openai import ChatOpenAI
from langgraph.types import interrupt, Command
llm = ChatOpenAI(model="gpt-4o-mini")
# 节点1:查询本月数据
def load_current_data(state: ReportState) -> dict:
data = query_sales(state["current_month"]) # 调用业务API
return {"current_data": data, "retry_count": 0}
# 节点2:查询上月数据
def load_previous_data(state: ReportState) -> dict:
data = query_sales(state["previous_month"])
return {"previous_data": data}
# 节点3:计算变化率,识别风险区域
def calculate_changes(state: ReportState) -> dict:
current = state["current_data"]
previous = state["previous_data"]
changes = []
risky_regions = []
for region in current:
prev_val = previous.get(region, 0)
curr_val = current[region]
if prev_val > 0:
change_rate = (curr_val - prev_val) / prev_val
changes.append({
"region": region,
"previous": prev_val,
"current": curr_val,
"change_rate": change_rate,
})
if change_rate < -0.10: # 下降超过10%
risky_regions.append(region)
return {"changes": changes, "risky_regions": risky_regions}
# 节点4:生成报告
def generate_report(state: ReportState) -> dict:
risky = state["risky_regions"]
if not risky:
return {"report": "本月无异常区域", "needs_confirmation": False}
prompt = f"""
请根据以下数据生成一份销售报告:
风险区域:{risky}
详细变化:{state['changes']}
要求:Markdown 格式,包含数据表格和改进建议。
"""
response = llm.invoke(prompt)
return {"report": response.content, "needs_confirmation": True}
# 节点5:人工审核(动态中断)
def human_review(state: ReportState) -> dict:
if not state.get("needs_confirmation"):
return {"approval_status": "approved"}
payload = {
"type": "report_approval",
"report": state["report"],
"risky_regions": state["risky_regions"],
}
decision = interrupt(payload)
# decision 是 Command(resume=...) 传入的值
return {"approval_status": decision["action"]} # "approved" or "rejected"
# 节点6:发布报告
def publish_report(state: ReportState) -> dict:
if state["approval_status"] == "approved":
publish_to_system(state["report"]) # 调用发布API
return {"published": True}
return {"published": False, "error": "报告被拒绝发布"}
4.3 条件路由
python
from langgraph.graph import StateGraph, START, END
def route_after_calculation(state: ReportState) -> str:
if state["risky_regions"]:
return "generate_report"
return "publish_report" # 无风险区域,直接发布简单报告
def route_after_review(state: ReportState) -> str:
if state["approval_status"] == "approved":
return "publish_report"
return END # 被拒绝,流程结束
# 构建图
workflow = StateGraph(ReportState)
workflow.add_node("load_current", load_current_data)
workflow.add_node("load_previous", load_previous_data)
workflow.add_node("calculate", calculate_changes)
workflow.add_node("generate_report", generate_report)
workflow.add_node("human_review", human_review)
workflow.add_node("publish_report", publish_report)
# 并行查询本月和上月数据
workflow.add_edge(START, "load_current")
workflow.add_edge(START, "load_previous")
workflow.add_edge("load_current", "calculate")
workflow.add_edge("load_previous", "calculate")
# 条件路由
workflow.add_conditional_edges("calculate", route_after_calculation)
workflow.add_edge("generate_report", "human_review")
workflow.add_conditional_edges("human_review", route_after_review)
workflow.add_edge("publish_report", END)
# 编译(必须带 checkpointer 才能支持中断恢复)
from langgraph.checkpoint.sqlite import SqliteSaver
app = workflow.compile(checkpointer=SqliteSaver.from_conn_string("reports.db"))
4.4 运行与恢复
ini
import uuid
# 首次运行
thread_id = f"report-{uuid.uuid4()}"
config = {"configurable": {"thread_id": thread_id}}
initial_state = {
"current_month": "2026-08",
"previous_month": "2026-07",
}
result = app.invoke(initial_state, config=config)
# 如果有风险区域,图会在 human_review 节点暂停
# 检查是否处于中断状态
state = app.get_state(config)
if state.next == ("human_review",):
print(" 报告已生成,等待人工审核")
print(f" 报告内容:{state.values['report']}")
# 模拟人工审核通过
result = app.invoke(
Command(resume={"action": "approved"}),
config=config,
)
print(f" 发布结果:{result['published']}")
4.5 案例架构图
sql
START
/ \
↓ ↓
load_current load_previous
\ /
↓ ↓
calculate
↓
┌──────┴──────┐
↓ ↓
有风险区域 无风险区域
↓ ↓
generate_report │
↓ │
human_review │
(interrupt) │
↓ │
┌──────┴──────┐ │
↓ ↓ ↓
批准 → publish_report ←─┘
↓
END
拒绝 → END
五、工程实践:三个绕不开的坑
5.1 状态序列化问题
Checkpointer 保存状态时需要序列化。确保 State 中的所有字段都是 JSON 可序列化的。
python
# 不可序列化
class BadState(TypedDict):
db_connection: object # 数据库连接对象无法序列化
# 正确做法
class GoodState(TypedDict):
data: dict # 只存数据,不存连接对象
5.2 循环无终止条件
条件路由形成循环时,必须设置最大重试次数或超时机制。
perl
def review_router(state):
if state["passed"]:
return END
if state["review_round"] >= 5: # 最多审5轮
return END
return "fix_code"
5.3 子图嵌套时的中断恢复
主图调用含 interrupt 的子图时,恢复遵循层级化快照原则:子图从中断点重执行,主图从调用子图的节点重执行。
python
# 子图含 interrupt
sub_app = sub_graph.compile(checkpointer=SqliteSaver(...))
# 主图调用子图
def call_subgraph(state: MainState) -> dict:
result = sub_app.invoke({"query": state["task"]}, config=sub_config)
return {"sub_result": result["results"]}
# 恢复时,子图和主图都需要正确的 config