LangGraph 高级用法:状态管理、条件路由、人工介入的完整案例

先放结论: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)

三个参数:

  1. 源节点:从哪个节点产生分支
  2. 路由函数:接收 state,返回字符串
  3. 映射表:路由函数的返回值 → 目标节点名称

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"])

关键流程:

  1. 首次 invoke 遇到中断点,图暂停,返回当前状态
  2. 外部系统(前端/审批界面)展示待审批内容
  3. 人工决策后,用 update_state 修改状态
  4. 再次 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

相关推荐
柒和远方1 小时前
DocResearch 项目面试:把每个模块讲明白,而不是背术语
python·llm·agent
拍手笑沙鸥1 小时前
系列4之「超体」工具调用与 Function Calling:给智能体装上"双手"
aigc·agent
Luhui_Dev1 小时前
数学问题在 Agent 实践中的难点
人工智能·数学·agent
柒和远方1 小时前
DocResearch 项目理解:从一条命令到一份有出处的报告
python·llm·agent
Maiko Star1 小时前
* LangChain Agent智能体详解(上):创建调用、工具绑定与系统提示词
python·langchain·agent
七夜zippoe2 小时前
Agent 自主决策机制:什么时候该检索、什么时候该直接回答
人工智能·ai·agent·检索·自主决策
沉默王二2 小时前
再见了 WebUI,DeepSeek 桌面版真不错。
openai·agent·deepseek
效率工作实验室2 小时前
用 AI Agent 做游戏数据分析,哪些场景效果最好?
ai·agent·智能体·游戏数据·游戏数据分析
小林ixn2 小时前
从 MySQL 的 LIKE 到 ES 倒排索引:一次把全文检索和混合检索讲透
sql·elasticsearch·全文检索·agent·关键词