跳转到内容

基于 StateGraph 的状态图 Agent 设计

基于 StateGraph 的状态图 Agent 设计

学习目标

本章要解决什么问题:

  • 如何使用 LangGraph 的 StateGraph 声明式构建 Agent 工作流
  • 如何设计状态 Schema(TypedDict vs Pydantic)
  • 如何定义节点、边、条件边实现动态路由
  • 编译后的图如何执行和交互

前置知识

本章涉及 Agent 状态图的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 LangGraph 的具体实现。


项目实践

StateGraph 的 Builder 模式

LangGraph 的 StateGraph 使用 Builder 模式逐步构建图:

from langgraph.graph import StateGraph, START, END
class State(TypedDict):
messages: Annotated[list, add_messages]
current_step: str
graph = StateGraph(state_schema=State)
graph.add_node("model", call_model)
graph.add_node("tools", execute_tools)
graph.add_edge(START, "model")
graph.add_conditional_edges("model", route)
graph.add_edge("tools", "model")
compiled = graph.compile()

关键流程:

  1. 定义状态 Schema(state_schema
  2. 可选定义输入/输出 Schema(input_schemaoutput_schema
  3. 可选定义上下文 Schema(context_schema
  4. 注册节点(add_node
  5. 定义边(add_edge)和条件边(add_conditional_edges
  6. 编译(compile()

状态 Schema 的三种角色

StateGraph 接受四个 Schema 参数,各承担不同角色:

Schema职责默认值
state_schema运行时完整状态必填
input_schema调用 invoke() 时的输入格式同 state_schema
output_schema图的最终输出格式同 state_schema
context_schema不可变的运行时上下文(如 user_id、db_conn)

为什么需要分离:输入可能只需要部分字段(如只需用户消息),但内部状态需要更多字段(如计数器、中间结果)。分离后,调用者不需要知道内部状态的全部结构。

条件边的动态路由

add_conditional_edges 接受一个路由函数,根据当前状态决定下一步:

def route(state):
if state["messages"] and state["messages"][-1].tool_calls:
return "tools"
return END
graph.add_conditional_edges("model", route)

路由函数的返回值必须是图中已定义的节点名或 END。如果返回不存在的节点名,编译时会报错。

Send 原语:动态多目标分发

除了固定的条件边,Send 允许节点在运行时动态决定向哪些目标节点分发任务:

from langgraph.types import Send
def fan_out(state):
queries = extract_queries(state["messages"])
return [Send("research_agent", {"query": q}) for q in queries]

每个 Send 创建目标节点的一个独立执行实例,有自己的状态副本。

编译与执行

compile()StateGraph(Builder 对象)转换为 CompiledStateGraph(可执行对象):

app = graph.compile()
# 同步调用
result = app.invoke({"messages": ["你好"]})
# 流式输出
for chunk in app.stream({"messages": ["你好"]}, stream_mode="updates"):
print(chunk)

编译后的图支持 invoke()stream()ainvoke()astream() 等方法。

问题与规避

状态 Schema 不匹配导致编译失败

问题:如果 input_schemaoutput_schema 中包含了 state_schema 中没有的 Channel,或者类型不一致,编译时会报错。

规避:确保 input/output schema 是 state schema 的子集或超集,Channel 类型必须一致。

条件边返回不存在的节点名

问题:路由函数返回了未在图中注册的节点名,运行时会抛出 ValueError

规避:使用类型安全的枚举或常量定义节点名,路由函数的返回值受类型检查约束。

无限循环

问题:如果条件边始终回到前一个节点而没有退出条件,图会无限循环。

规避

  • 设置 recursion_limit 参数
  • 在状态中使用 RemainingSteps managed value 追踪剩余步数
  • 确保条件边有明确的退出路径

设计取舍

优势

  • 声明式:图结构一目了然,可视化友好
  • 类型安全:Schema 驱动,IDE 可推断状态字段
  • 灵活:支持条件分支、并行、子图嵌套等复杂模式

代价

  • 学习曲线:需要理解 Builder 模式、Schema、Channel 等多个概念
  • 冗长:简单线性流程也需要定义 Schema、注册节点、定义边
  • 编译时 vs 运行时:编译时无法检测所有运行时错误(如条件边返回无效节点名)

替代方案

参考来源