13620 字
68 分钟

LangGraph 源码深潜:Plan-and-Execute 与 Subgraph 运行机制解剖

核心问题: LangGraph 如何用 StateGraph + Node + Conditional Edge + Subgraph + partial state update 表达 Plan-and-Execute 范式,并支持执行进度、步骤校验、失败重规划和最终响应?

源码主线: StateGraph.add_node()StateGraph.add_conditional_edges()StateGraph.compile()CompiledStateGraph.invoke()Pregel super-stepplanner_node / executor_node / route_next_stepstate update / END

前置文章: 第 6 篇 StateGraph 源码解剖、第 7 篇 Conditional Edge 与 Router、第 8 篇 Reducer 与并行状态合并

依赖基线: langgraph==1.2.7

源码基线: langchain-ai/langgraph tag 1.2.7,release commit 5931a5f

阅读边界: 本文只解释 Plan-and-Execute 如何落到 LangGraph 的图构建、状态更新、条件路由、子图调用与运行时调度;不展开 LLM 如何生成高质量计划、不展开具体搜索工具实现、不展开 Checkpoint/HITL 的完整源码。


0. 本篇在源码学习主线中的位置#

前几篇已经建立了 LangGraph 的三个基础认知:

StateGraph
负责把状态、节点、边组织成图结构
Conditional Edge
负责在节点执行后根据当前 state 选择下一个节点
Reducer
负责在多节点并发写入同一个 state key 时定义合并规则

本篇进入更接近 Agent 范式的一层:

Plan-and-Execute
planner node 生成计划
executor node 或 executor subgraph 执行当前步骤
verifier node 检查步骤结果
conditional edge 决定继续、重规划或结束

本篇只解决:

  • 计划列表应该如何进入 state
  • current_step_index 如何控制执行进度。
  • 每一步执行结果如何写回 state
  • route_next_step 如何通过 conditional edge 驱动循环。
  • executor 应该直接作为节点,还是封装成子图。
  • replan 是覆盖旧计划、追加修复步骤,还是生成新计划版本。

本篇不展开:

  • 复杂 Planner Prompt 的设计。
  • LLM 计划质量评估指标。
  • 工具调用源码细节。
  • Checkpoint / Interrupt 的完整恢复机制。
  • Multi-Agent Supervisor 的完整实现。

1. 本篇问题、学习目标与能力边界#

1.1 核心问题#

LangGraph 如何把 Plan-and-Execute 从“提示词里的计划步骤”变成可执行、可路由、可重试、可重规划的状态图?

1.2 学习目标#

完成本篇后,读者必须能够:

  1. 设计承载计划的 State schema,明确 plan_stepscurrent_step_indexstep_resultsfinal_answer 的职责。
  2. 解释 planner_node 如何返回 partial state update,并由 LangGraph 合并进全局状态。
  3. 解释 executor_node 如何读取当前步骤、执行任务、写回步骤状态与执行结果。
  4. 解释 route_next_step 如何通过 add_conditional_edges() 控制继续执行、重规划或结束。
  5. 判断 executor 应该直接调用工具、作为普通节点,还是封装为独立 subgraph。
  6. 判断 replan 应该覆盖计划、追加修复步骤,还是保留计划版本历史。

1.3 能力边界#

能力本篇是否覆盖说明
Plan-and-Execute 状态建模重点讲 plan_stepscurrent_step_indexstep_results
Planner Node 源码机制重点讲节点注册、运行、partial update
Executor Node / Subgraph重点讲普通节点与子图两种落地方式
Step Verifier 与 Replan 路由重点讲条件边与状态驱动循环
复杂工具调用源码已在 Tool Calling 篇讲解
Checkpoint / HITL 恢复机制后续 Checkpoint 与 Interrupt 篇展开
多 Agent 协同计划后续 Multi-Agent / Supervisor 篇展开

2. 核心概念与最小心智模型#

2.1 一句话定义#

Plan-and-Execute 在 LangGraph 中不是一个内置魔法类,而是一种由 planner nodeexecutor node/subgraphstep verifierconditional edge 和共享 state 共同表达的图运行模式。

它负责:

  • 先生成结构化计划;
  • 再按步骤执行;
  • 每步后检查结果;
  • 根据 state 决定继续、重试、重规划或结束。

它不负责:

  • 自动保证计划质量;
  • 自动选择最合适工具;
  • 自动防止无限循环;
  • 自动解决所有业务失败。

这些仍然需要 schema、verifier、router、recursion limit、checkpoint、middleware 和业务规则配合。

2.2 最小心智模型#

user_request
planner_node
plan_steps + current_step_index
execute_step
step_results + updated plan_steps
check_step_result
route_next_step
├── execute_step
├── replan
└── final_response

2.3 核心术语#

术语源码对象语义不要误解为
Planner NodeStateGraph.add_node("planner_node", fn)生成计划并写入 state 的节点框架内置 Planner 类
Plan Stateplan_steps / current_step_index计划结构与执行游标普通临时变量
Executor Node普通节点或 compiled subgraph执行当前步骤一定要是 ToolNode
Step Verifier普通节点检查当前步骤是否成功必须是 LLM
Replanconditional edge 目标节点失败后修正计划只是在 prompt 里说重新规划
SubgraphCompiledStateGraph 作为节点把复杂执行器模块化Python 函数嵌套调用的同义词
Partial Update节点返回的 dict对 state 的局部更新完整状态覆盖

2.4 与相邻抽象的边界#

对象负责什么不负责什么与本篇对象的关系
StateGraph注册状态、节点、边并编译图不自动生成计划Plan-and-Execute 的承载结构
add_conditional_edges()根据 path function 选择后继节点不执行业务任务控制 continue / replan / final
Reducer合并并发或追加式状态更新不判断步骤是否成功用于保存步骤日志、消息、工具结果
ToolNode执行模型生成的 tool calls不管理计划游标可作为 executor subgraph 内部节点
Command同时返回 state update 与 goto不替代所有条件边可用于更动态的步骤执行跳转
Checkpointer保存执行快照不设计计划结构支持长任务恢复与人工确认

3. 完整执行链路#

3.1 高层链路#

用户输入
StateGraph(TravelState)
add_node(planner_node / execute_step / check_step_result / replan / final_response)
add_edge + add_conditional_edges
compile 生成 CompiledStateGraph
graph.invoke(initial_state)
Pregel 按 super-step 调度节点
节点返回 partial state update
根据 route_next_step 继续、重规划或 END

3.2 最小代码骨架#

观察目标:下面代码展示 Plan-and-Execute 的最小骨架。重点不是计划质量,而是观察 plan_stepscurrent_step_index 如何驱动图循环。

from typing_extensions import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class PlanStep(TypedDict):
step_id: int
task: str
status: str
class TravelState(TypedDict):
user_request: str
plan_steps: list[PlanStep]
current_step_index: int
step_results: list[str]
final_answer: str | None
def planner_node(state: TravelState):
return {
"plan_steps": [
{"step_id": 1, "task": "确认目的地和天数", "status": "pending"},
{"step_id": 2, "task": "查询景点和交通信息", "status": "pending"},
{"step_id": 3, "task": "生成每日行程", "status": "pending"},
],
"current_step_index": 0,
"step_results": [],
}
def execute_step(state: TravelState):
index = state["current_step_index"]
step = state["plan_steps"][index]
result = f"执行步骤 {step['step_id']}{step['task']}"
updated_steps = [dict(s) for s in state["plan_steps"]]
updated_steps[index]["status"] = "done"
return {
"plan_steps": updated_steps,
"step_results": state["step_results"] + [result],
"current_step_index": index + 1,
}
def route_next_step(state: TravelState) -> Literal[
"execute_step",
"final_response",
]:
if state["current_step_index"] >= len(state["plan_steps"]):
return "final_response"
return "execute_step"
def final_response(state: TravelState):
return {
"final_answer": "\n".join(state["step_results"])
}
builder = StateGraph(TravelState)
builder.add_node("planner_node", planner_node)
builder.add_node("execute_step", execute_step)
builder.add_node("final_response", final_response)
builder.add_edge(START, "planner_node")
builder.add_edge("planner_node", "execute_step")
builder.add_conditional_edges("execute_step", route_next_step)
builder.add_edge("final_response", END)
graph = builder.compile()
result = graph.invoke({
"user_request": "我想去东京玩 5 天",
"plan_steps": [],
"current_step_index": 0,
"step_results": [],
"final_answer": None,
})

代码解释:

  • planner_node 不执行任务,只生成计划列表和游标初始值。
  • execute_step 每次只处理 current_step_index 指向的一个步骤。
  • 执行结束后,current_step_index + 1 表示下一轮执行下一个步骤。
  • route_next_step 不修改状态,只读取状态并返回下一个节点名。
  • final_response 读取所有步骤结果并生成最终输出。

3.3 对象流转#

阶段输入类型核心函数输出类型状态变化
构建状态type[TravelState]StateGraph.__init__()StateGraph解析 state schema
注册节点CallableStateGraph.add_node()StateGraph保存 node spec
注册边str, strStateGraph.add_edge()StateGraph保存固定边
注册路由CallableStateGraph.add_conditional_edges()StateGraph保存 branch spec
编译图builderStateGraph.compile()CompiledStateGraph生成 Pregel 应用
执行 plannerTravelStateplanner_node()Partial[TravelState]写入计划
执行 stepTravelStateexecute_step()Partial[TravelState]更新步骤状态和游标
路由TravelStateroute_next_step()str决定后继节点
结束TravelStatefinal_response()Partial[TravelState]写入最终回答

3.4 时序链路#

Caller
│ graph.invoke(initial_state)
CompiledStateGraph
│ 初始化 state channels
planner_node
│ return {plan_steps, current_step_index}
state merge
│ edge: planner_node → execute_step
execute_step
│ return {updated plan_steps, step_results, current_step_index + 1}
route_next_step
├── execute_step
└── final_response → END

3.5 正常结束条件#

一次正常 Plan-and-Execute 执行完成的条件是:

current_step_index >= len(plan_steps)
route_next_step 返回 final_response
final_response 写入 final_answer
final_response → END

最终结果保存在:

result["final_answer"]

中;完整执行轨迹可通过 stream(..., stream_mode="updates")、checkpoint history 或 LangSmith trace 观察。


4. 源码地图、关键文件与阅读顺序#

4.1 核心目录#

langgraph/
├── graph/
│ ├── state.py # StateGraph / CompiledStateGraph
│ ├── graph.py # 基础 Graph / CompiledGraph
│ └── _branch.py # BranchSpec / conditional edge 路由
├── pregel/
│ ├── main.py # Pregel 可执行应用入口
│ ├── _loop.py # super-step 循环
│ ├── _algo.py # 任务准备、写入应用、channel 更新
│ ├── _read.py # ChannelRead / PregelNode 读取
│ └── _write.py # ChannelWrite / 写入表达
├── channels/
│ ├── last_value.py
│ ├── binop.py
│ └── base.py
└── types.py # Command / Send / Interrupt 等控制类型

4.2 关键文件#

优先级文件核心对象阅读目的
1langgraph/graph/state.pyStateGraphCompiledStateGraph理解节点、边、branch、compile 如何构建
2langgraph/graph/_branch.pyBranchSpec理解 route_next_step 如何被包装与执行
3langgraph/pregel/main.pyPregel理解 compiled graph 为什么是可执行应用
4langgraph/pregel/_loop.pyPregel loop理解 super-step 调度
5langgraph/pregel/_algo.py写入应用与任务准备理解 partial update 如何合并
6langgraph/channels/LastValueBinaryOperatorAggregate理解覆盖与追加语义
7langgraph/types.pyCommandSend理解动态 goto 与 map-reduce 扩展

4.3 推荐阅读顺序#

1. StateGraph.__init__
2. StateGraph.add_node
3. StateGraph.add_edge
4. StateGraph.add_conditional_edges
5. StateGraph.compile
6. CompiledStateGraph.attach_node / attach_branch
7. BranchSpec.run / _route / _finish
8. Pregel.invoke / stream
9. Pregel super-step loop
10. channel update / state merge

4.4 不建议的阅读顺序#

不建议直接从 pregel/_loop.py 开始阅读。Pregel 是运行时调度核心,如果还没有理解 StateGraph 如何把节点、边和 state channel 编译成 Pregel 节点,直接读调度循环会很难判断哪些 channel 来自 state key、哪些 channel 来自 branch、哪些写入来自节点返回值。

也不建议从 ToolNode 开始阅读 Plan-and-Execute。ToolNode 只解决工具调用,不解决计划游标、计划状态、步骤循环和重规划。


5. 对象模型、继承关系与协议边界#

5.1 核心对象关系#

StateGraph builder
↓ compile()
CompiledStateGraph
↓ inherits / wraps Pregel executable semantics
Pregel runtime
↓ schedules nodes by channels and super-steps
Node function
↓ returns partial state update
Channel / reducer
↓ merges update into state

5.2 对象职责#

对象生命周期输入输出核心职责
StateGraph构建期schema、node、edgebuilder保存图声明,不执行
CompiledStateGraph编译后graph specrunnable graph暴露 invoke/stream/ainvoke
Pregel运行时input state、configstate / stream按 super-step 调度节点
BranchSpec构建期 + 运行时path functionnext node(s)条件路由
Node function运行时full statepartial update执行业务逻辑
Channel构建期 + 运行时updatesmerged value管理 state key 合并
Subgraph编译后parent-transformed statepartial output模块化复杂 executor

5.3 协议边界#

StateGraph 协议
负责:描述 state schema、nodes、edges、branches
不负责:实际执行节点
Pregel 协议
负责:运行 super-step、调度节点、传播写入
不负责:理解业务计划语义
Node 协议
负责:读取 state,返回 partial update
不负责:直接修改全局 state
Branch 协议
负责:读取 state,返回后继节点名或 END
不负责:执行业务任务
Subgraph 协议
负责:把一组内部节点封装成一个外部节点
不负责:自动适配父子图 state schema

5.4 稳定接口与内部实现#

类型对象文章中的使用原则
公共 APIStateGraphadd_node()add_edge()add_conditional_edges()compile()可用于工程示例
公共 APICompiledStateGraph.invoke()stream()ainvoke()可用于工程运行
公共 APICommandSend可用于动态控制和 map-reduce
扩展接口subgraph as node可用于模块化执行器
内部实现BranchSpec、Pregel loop、channel write只用于理解源码,不建议业务代码依赖

6. 源码阅读策略与证据标准#

6.1 本篇阅读策略#

先看公开代码骨架
确认 Plan State 字段
沿 add_node / add_edge / add_conditional_edges 看构建期
沿 compile 看 builder 如何变成 CompiledStateGraph
沿 invoke / stream 看运行时 super-step
沿 branch 看 continue / replan / final 的路由
沿 channel / reducer 看 partial update 如何合并
回到工程判断:executor 用 node 还是 subgraph

6.2 证据等级#

标记含义写作要求
源码事实当前正式版源码可证明附 tag/commit 源码链接
官方契约官方文档或 API Reference 明确承诺附官方文档链接
简化伪代码对真实控制流的压缩表达明确标注“伪代码”
作者推断根据调用链得出的设计理解明确使用“从调用关系可以推断”
工程建议面向项目实践说明适用条件

6.3 本篇证据清单#

结论证据类型文件或文档定位
StateGraph 是 builder,需 compile 后执行官方契约 / 源码事实state.py、StateGraph reference类文档与 compile()
节点签名是 State → Partial官方契约 / 源码事实Graph API、state.pyStateGraph 类文档
图执行基于 super-step 与 message passing官方契约Graph APIgraph algorithm 描述
subgraph 可以作为节点或在节点内调用官方契约Subgraphs docssubgraph communication
conditional edge path function 选择后继节点官方契约 / 源码事实Graph API、_branch.pybranch route
Command 可以组合 state update 与 goto官方契约types.Command referenceCommand 签名

7. 构建期源码解剖#

本章回答:

用户写下 planner_nodeexecute_steproute_next_step 与子图后,LangGraph 如何把这些函数注册、归一化、校验并编译成可执行图?

7.1 构建期职责#

输入归一化动作构建结果
TravelState解析 state schema 和 channelsself.channels / self.schemas
planner_node推断或使用节点名,包装成 runnableStateNodeSpec
固定边校验起点终点,保存 edgeself.edges
route_next_step包装为 BranchSpecself.branches[source]
executor subgraph作为 runnable node 注册node spec 中 action 为 compiled graph
compile 参数checkpointer/cache/interrupt/debugCompiledStateGraph

7.2 构建期总链路#

StateGraph(TravelState)
_add_schema 解析 state fields
add_node 注册 planner / executor / verifier / replan / final
add_edge 注册固定执行顺序
add_conditional_edges 注册 route_next_step
compile 校验图结构并 attach node / edge / branch
CompiledStateGraph

7.3 StateGraph.__init__() 与 Plan State schema 源码解剖#

职责与所处阶段#

构建期。负责把 TravelState 解析成图的状态协议,确定哪些字段可以被节点读取和写入。

真实源码签名#

以下签名来自当前正式版源码的公开构造入口:

class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
def __init__(
self,
state_schema: type[StateT],
context_schema: type[ContextT] | None = None,
*,
input_schema: type[InputT] | None = None,
output_schema: type[OutputT] | None = None,
**kwargs,
) -> None:
...

调用方与被调用方#

builder = StateGraph(TravelState)
StateGraph.__init__
_add_schema(state_schema)
_get_channels(schema)

输入、输出与状态变化#

项目类型说明
输入type[TravelState]用户定义的 TypedDict state schema
输出StateGraphbuilder 对象
状态变化self.channelsself.schemas保存每个 state key 对应的 channel
副作用无外部副作用只修改 builder 内部结构

细粒度伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def __init__(state_schema, context_schema=None, input_schema=None, output_schema=None):
# 1. 初始化图结构容器
self.nodes = {}
self.edges = set()
self.branches = defaultdict(dict)
self.waiting_edges = set()
# 2. 初始化状态相关容器
self.schemas = {}
self.channels = {}
self.managed = {}
# 3. 保存 schema 边界
self.state_schema = state_schema
self.input_schema = input_schema or state_schema
self.output_schema = output_schema or state_schema
self.context_schema = context_schema
# 4. 解析主状态 schema
self._add_schema(self.state_schema)
# 5. 解析输入输出 schema,禁止 managed value 进入 I/O schema
self._add_schema(self.input_schema, allow_managed=False)
self._add_schema(self.output_schema, allow_managed=False)
# 6. 标记尚未编译
self.compiled = False

逐段解释#

第 1 段初始化的是图结构容器:nodes 保存节点,edges 保存固定边,branches 保存条件边。Plan-and-Execute 里的 planner_node → execute_step 是固定边,而 execute_step → route_next_step 是条件边。

第 2 段初始化的是状态模型容器:channels 是理解 LangGraph state 的关键。plan_stepscurrent_step_indexstep_results 并不是运行时随便塞进 dict 的字段,而是在 schema 解析阶段就被注册成可读写 channel。

第 4、5 段说明 state_schemainput_schemaoutput_schema 可以不同。Plan-and-Execute 常见做法是:输入只需要 user_request,内部状态包含 plan_stepsstep_results,输出只暴露 final_answer。如果不显式定义,默认三者都等于 state_schema

正常路径#

TravelState TypedDict
_get_channels
plan_steps / current_step_index / final_answer 等字段变成 channels
StateGraph builder 可注册节点和边

关键分支与异常路径#

条件行为结果
字段没有 reducer使用默认覆盖式 channel适合 current_step_indexfinal_answer
字段有 Annotated[..., reducer]使用 reducer channel适合 step_resultsmessages、日志
input/output schema 包含不允许的 managed value抛出 ValueError防止运行时管理字段暴露给外部 I/O

设计原因与工程影响#

Plan-and-Execute 中,plan_stepscurrent_step_index 是执行控制字段。它们必须是 state schema 的一部分,因为后续节点和条件边都依赖它们。不要把它们藏在函数局部变量里,否则每个节点之间无法共享执行进度。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.__init__
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py

7.4 add_node() 注册 Planner / Executor / Verifier 源码解剖#

职责与所处阶段#

构建期。负责把普通 Python 函数或 Runnable 转换为图节点,并保存节点名、执行函数、输入 schema、retry/cache/error/timeout 策略。

真实源码签名#

def add_node(
self,
node: str | StateNode[NodeInputT, ContextT],
action: StateNode[NodeInputT, ContextT] | None = None,
*,
defer: bool = False,
metadata: dict[str, Any] | None = None,
input_schema: type[NodeInputT] | None = None,
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
cache_policy: CachePolicy | None = None,
error_handler: StateNode[Any, ContextT] | None = None,
destinations: dict[str, str] | tuple[str, ...] | None = None,
timeout: float | timedelta | TimeoutPolicy | None = None,
**kwargs,
) -> Self:
...

调用方与被调用方#

builder.add_node("planner_node", planner_node)
StateGraph.add_node
coerce_to_runnable / StateNodeSpec
self.nodes[name] = spec

输入、输出与状态变化#

项目类型说明
输入node name + callable"planner_node"planner_node 函数
输出Self支持链式调用
状态变化self.nodes新增节点定义
副作用无外部副作用仅更新 builder

细粒度伪代码#

def add_node(node, action=None, *, input_schema=None, retry_policy=None, cache_policy=None, error_handler=None, destinations=None, timeout=None):
# 1. 如果 node 不是字符串,说明用户传的是函数或 Runnable
if not isinstance(node, str):
action = node
node_name = infer_name_from_callable_or_runnable(action)
else:
node_name = node
# 2. 校验节点名
if node_name is None:
raise ValueError("Node name must be provided")
if node_name in self.nodes:
raise ValueError("Node already exists")
if node_name in {START, END}:
raise ValueError("Reserved node name")
# 3. 推断输入 schema
node_input_schema = input_schema or self.state_schema
self._add_schema(node_input_schema, allow_managed=False)
# 4. 将 action 归一化为 runnable-like node
runnable = coerce_to_runnable(action, name=node_name)
# 5. 保存节点规格
self.nodes[node_name] = StateNodeSpec(
runnable=runnable,
metadata=metadata,
input_schema=node_input_schema,
retry_policy=retry_policy,
cache_policy=cache_policy,
error_handler=error_handler,
destinations=destinations,
timeout=timeout,
)
return self

逐段解释#

第 1 段解决命名问题。builder.add_node(planner_node) 会使用函数名作为节点名;builder.add_node("planner_node", planner_node) 则显式指定节点名。源码解剖时建议显式写节点名,便于后续 add_edge() 和 trace 对齐。

第 3 段说明每个节点可以有自己的输入 schema。Plan-and-Execute 的 planner_node 往往读取完整 state,但 executor subgraph 可能只接收一个 ExecutorState,这时需要通过 wrapper 转换。

第 4 段说明节点函数会被归一化为可运行对象。节点并不是运行时直接裸调函数,而是进入 LangGraph 的 runnable 调度体系。

正常路径#

planner_node function
add_node
StateNodeSpec
compile 时 attach 为 PregelNode

关键分支与异常路径#

条件行为结果
未显式传节点名从函数名推断适合 demo,不适合大型项目
节点名重复抛错防止边指向歧义
已 compile 后继续 add_node通常会警告已编译图不一定同步更新
传入 compiled subgraph作为 Runnable 节点保存可实现 executor subgraph

设计原因与工程影响#

Plan-and-Execute 的节点通常比普通 Workflow 更有状态依赖。显式节点名可以让 trace、conditional edge、error handler 更清晰。对于 executor,如果内部包含多个步骤、工具和校验,推荐把 executor 编译成 subgraph 再作为节点注册。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_node
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py

7.5 add_conditional_edges() 注册 route_next_step 源码解剖#

职责与所处阶段#

构建期。负责把 route_next_step 这样的路径函数注册为 branch,让 LangGraph 在节点执行后根据 state 决定下一步。

真实源码签名#

def add_conditional_edges(
self,
source: str,
path: Callable[..., Hashable | Sequence[Hashable]] | Runnable[Any, Hashable | Sequence[Hashable]],
path_map: dict[Hashable, str] | list[str] | None = None,
) -> Self:
...

调用方与被调用方#

builder.add_conditional_edges("execute_step", route_next_step)
StateGraph.add_conditional_edges
BranchSpec.from_path(path, path_map, infer_schema=True)
self.branches[source][branch_name] = branch_spec

输入、输出与状态变化#

项目类型说明
输入source node + route function"execute_step"route_next_step
输出Selfbuilder
状态变化self.branches保存条件边定义
副作用无外部副作用仅保存 branch spec

细粒度伪代码#

def add_conditional_edges(source, path, path_map=None):
# 1. 校验 source 是否可作为边起点
if source == END:
raise ValueError("END cannot have outgoing edges")
# 2. 将 path 函数转换为 branch spec
branch = BranchSpec.from_path(
path=path,
path_map=path_map,
infer_schema=True,
)
# 3. 生成 branch 名称,避免同一 source 下重复
branch_name = branch.name or "condition"
if branch_name in self.branches[source]:
raise ValueError("Branch already exists for source")
# 4. 保存到 builder
self.branches[source][branch_name] = branch
return self

逐段解释#

第 1 段说明 END 是终止哨兵,不能再作为 source 注册条件边。Plan-and-Execute 的循环必须挂在 execute_stepcheck_step_resultvalidate_plan 之后,而不是挂在 END 后。

第 2 段是核心:route_next_step 不会立即执行,而是被包装成 BranchSpec,等待运行时某个 source node 执行完成后再调用。

第 4 段说明条件边保存于 self.branches[source],不是普通 self.edges。这就是固定边和条件边的源码边界。

正常路径#

execute_step 执行完成
BranchSpec.run(route_next_step)
返回 execute_step / replan / final_response
Pregel 写入对应 branch channel

关键分支与异常路径#

条件行为结果
route_next_step 返回节点名进入对应节点继续执行或结束前节点
返回 END图分支终止当前执行路径结束
返回多个节点名触发多个后继可并行执行
配置 path_map将业务标签映射到节点名解耦业务状态和节点名
返回不存在节点编译或运行时失败路由定义错误

设计原因与工程影响#

Plan-and-Execute 的核心不是简单循环,而是“每步执行后根据状态选择下一步”。add_conditional_edges() 把这个控制权从节点内部 if/else 中抽离出来,使执行逻辑和路由逻辑分离。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edges
  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec.from_path
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/_branch.py

7.6 Executor Subgraph 构建源码解剖#

职责与所处阶段#

构建期。负责把复杂 executor 拆成独立子图,然后作为父图的一个节点使用。

真实源码签名#

子图本身仍通过 StateGraph.compile() 生成:

subgraph = subgraph_builder.compile()
parent_builder.add_node("execute_step", subgraph)

调用方与被调用方#

executor_builder = StateGraph(ExecutorState)
executor_builder.add_node(...)
executor_graph = executor_builder.compile()
parent_builder.add_node("execute_step", executor_graph)

输入、输出与状态变化#

项目类型说明
输入ExecutorState 或 parent TravelState子图输入 state
输出partial state update返回给父图合并
状态变化子图内部 state / 父图 state取决于是否共享 schema key
副作用可能有子图内部可能调用工具

细粒度伪代码#

def build_executor_subgraph():
# 1. 定义 executor 私有状态
class ExecutorState(TypedDict):
current_step: PlanStep
tool_results: list[str]
step_status: str
step_error: str | None
# 2. 构建子图
executor = StateGraph(ExecutorState)
executor.add_node("prepare_step", prepare_step)
executor.add_node("call_tools", call_tools)
executor.add_node("verify_step", verify_step)
# 3. 添加内部边
executor.add_edge(START, "prepare_step")
executor.add_edge("prepare_step", "call_tools")
executor.add_edge("call_tools", "verify_step")
executor.add_edge("verify_step", END)
# 4. 编译为可执行子图
return executor.compile()
def parent_execute_step_node(parent_state):
# 5. 将父图 state 映射为子图输入
sub_input = {
"current_step": parent_state["plan_steps"][parent_state["current_step_index"]],
"tool_results": [],
"step_status": "running",
"step_error": None,
}
# 6. 调用子图
sub_output = executor_graph.invoke(sub_input)
# 7. 将子图输出映射回父图 partial update
return convert_executor_output_to_parent_update(parent_state, sub_output)

逐段解释#

第 1–4 段说明子图本身也是 StateGraph → compile() 的结果。它可以拥有自己的私有状态,例如 tool_resultsstep_error,这些字段不一定暴露给父图。

第 5–7 段是父子图 state 边界。官方文档中给出了两种通信模式:父子图 state 不同则在节点函数里手动转换;父子图共享 key 则可直接把 compiled subgraph 作为节点。Plan-and-Execute 中,如果 executor 内部复杂,通常推荐使用不同 schema + wrapper 转换,避免父图 state 被执行细节污染。

正常路径#

parent TravelState
wrapper 提取 current_step
executor subgraph.invoke(ExecutorState)
subgraph 执行内部工具/校验
wrapper 转成 TravelState partial update

关键分支与异常路径#

条件行为结果
executor 只有一个简单工具调用可直接写普通 node不需要 subgraph
executor 内部有多个步骤使用 subgraph模块边界更清晰
父子图 state schema 完全相同可直接作为 node 注册子图读写父图 channels
父子图 state schema 不同wrapper 转换隔离执行器内部状态
子图抛异常由父节点异常路径处理可进入 replan 或失败

设计原因与工程影响#

Plan-and-Execute 的 executor 经常会膨胀:查工具、重试、校验、整理结果、记录 trace。把它作为子图可以防止父图变得过大,也能让 executor 独立测试。但子图边界越强,父子 state 映射成本也越高。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.compile
  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_node
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py

7.7 compile() 组装 Plan-and-Execute 图源码解剖#

职责与所处阶段#

构建期结束。负责把 builder 中保存的 schema、nodes、edges、branches 编译为可执行的 CompiledStateGraph

真实源码签名#

def compile(
self,
checkpointer: Checkpointer = None,
*,
cache: BaseCache | None = None,
store: BaseStore | None = None,
interrupt_before: All | list[str] | None = None,
interrupt_after: All | list[str] | None = None,
debug: bool = False,
name: str | None = None,
) -> CompiledStateGraph:
...

调用方与被调用方#

graph = builder.compile()
StateGraph.compile
validate graph structure
CompiledStateGraph(...)
attach_node / attach_edge / attach_branch
validate compiled graph

输入、输出与状态变化#

项目类型说明
输入builder 内部状态nodes、edges、branches、channels
输出CompiledStateGraph可执行图
状态变化self.compiled = Truebuilder 标记已编译
副作用无外部副作用只构造 compiled object

细粒度伪代码#

def compile(checkpointer=None, cache=None, store=None, interrupt_before=None, interrupt_after=None, debug=False, name=None):
# 1. 校验 builder 图结构
self.validate(
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
)
# 2. 准备输出和 stream channels
output_channels = infer_output_channels(self.output_schema)
stream_channels = infer_stream_channels(self.channels)
# 3. 构造 CompiledStateGraph
compiled = CompiledStateGraph(
builder=self,
channels=self.channels,
nodes={},
input_channels=START,
output_channels=output_channels,
stream_channels=stream_channels,
checkpointer=checkpointer,
cache=cache,
store=store,
interrupt_before_nodes=interrupt_before,
interrupt_after_nodes=interrupt_after,
debug=debug,
name=name,
)
# 4. 挂载所有普通节点
for node_name, node_spec in self.nodes.items():
compiled.attach_node(node_name, node_spec)
# 5. 挂载固定边
for start, end in self.edges:
compiled.attach_edge(start, end)
# 6. 挂载等待边和条件分支
for starts, end in self.waiting_edges:
compiled.attach_edge(starts, end)
for source, branches in self.branches.items():
for branch_name, branch_spec in branches.items():
compiled.attach_branch(source, branch_name, branch_spec)
# 7. 校验 compiled graph
compiled.validate()
self.compiled = True
return compiled

逐段解释#

第 1 段保证图结构合法,例如不能有孤立节点、不能从未知节点出发、interrupt 节点名必须存在。

第 3 段说明 CompiledStateGraph 不是简单复制 builder,而是把 state channels、节点、边、运行时参数一起组装成 Pregel 应用。

第 4–6 段是最重要的组装过程:Plan-and-Execute 的固定顺序边、条件路由、子图节点都在这一步进入 compiled graph。

正常路径#

builder.nodes / edges / branches
compile
compiled.nodes / channels / triggers / writes
invoke 时可调度

关键分支与异常路径#

条件行为结果
图没有入口校验失败无法执行
边指向未知节点校验失败防止运行时找不到节点
条件边目标未知校验失败或运行失败路由定义错误
compile 后再改 builder警告或不会影响已编译对象应重新 compile

设计原因与工程影响#

Plan-and-Execute 的执行图往往包含循环。compile() 必须把这些循环边、条件分支和 channel 写入规则提前编译好,运行时才能高效调度。业务代码应该把 compile 当作“图结构冻结点”。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.compile
  • libs/langgraph/langgraph/graph/state.py::CompiledStateGraph
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py

7.8 构建期产物#

产物保存的信息运行时用途
self.channelsstate key 与 reducer/channel合并 partial update
self.nodesplanner/executor/verifier/final 节点编译为 PregelNode
self.edges固定边顺序调度
self.branchesroute function条件路由
CompiledStateGraph可执行 Pregel 图invoke/stream/ainvoke
executor subgraph内部执行图作为父图节点运行

8. 运行时主链源码解剖#

本章回答:

构建完成后,一次 graph.invoke() 如何真正执行 Plan-and-Execute 的 planner、executor、router 和 final 节点?

8.1 运行时入口#

调用方式公开入口核心内部入口返回类型
同步调用graph.invoke(input)Pregel.stream() / loop最终 state
异步调用graph.ainvoke(input)async Pregel loop最终 state
流式调用graph.stream(input, stream_mode="updates")event stream节点更新流
批量调用graph.batch(inputs)Runnable batchlist[state]

8.2 运行时总链路#

graph.invoke(initial_state)
初始化 input channels
START 激活 planner_node
planner_node 返回 plan_steps / current_step_index
state channels 合并 update
固定边激活 execute_step
execute_step 读取当前步骤并写回结果
route_next_step 读取最新 state
进入 execute_step / replan / final_response
所有节点 inactive 且无消息在途
返回最终 state

8.3 输入归一化源码解剖#

职责与所处阶段#

运行时。负责把用户传入的 dict 初始化为图 state 的 channel 值。

真实源码签名#

公开入口继承 runnable 语义:

def invoke(self, input: InputT, config: RunnableConfig | None = None, **kwargs) -> OutputT:
...

调用方与被调用方#

graph.invoke(initial_state)
Pregel.invoke
Pregel.stream
loop.input / input channel writes

输入、输出与状态变化#

项目类型说明
输入dict初始 TravelState
输出channel writes写入图输入 channels
状态变化初始化 stateuser_request 等字段进入 channel
副作用可选 checkpoint取决于是否配置 checkpointer

细粒度伪代码#

def invoke(input_state, config=None):
# 1. 归一化 RunnableConfig
config = ensure_config(config)
# 2. 创建运行时 loop
loop = create_pregel_loop(
input=input_state,
config=config,
channels=compiled.channels,
nodes=compiled.nodes,
)
# 3. 执行 stream,但只收集最终输出
final_output = None
for event in loop.stream():
if event_is_output(event):
final_output = event_to_output(event)
# 4. 返回最终 state 或 output schema 投影
return final_output

逐段解释#

第 1 段说明 compiled graph 仍然遵守 Runnable 协议,所以 config 可以传入 recursion_limitthread_id、tags 等运行时配置。

第 2 段将 input_state 写入图的输入 channel。Plan-and-Execute 的 user_request、空 plan_steps、初始 current_step_index 会在这里成为可被 planner node 读取的 state。

第 3 段说明 invoke() 本质上可以理解为消费一次 stream,只是最终只返回结果,而不是每步事件。

正常路径#

initial_state dict
input channels
START message
planner_node active

关键分支与异常路径#

条件行为结果
输入缺少必需 key可能由 schema 或节点访问时报错输入不完整
传入额外 key取决于 schema 类型与 channel可能忽略或保留
配置 checkpointer + thread_id保存执行状态支持恢复
配置 recursion_limit限制循环步数防止无限执行

设计原因与工程影响#

Plan-and-Execute 是状态驱动范式,初始 state 必须包含足够信息让 planner 生成计划,也必须给后续字段提供默认值。建议工程上统一由 build_initial_state() 函数创建初始状态,避免调用方漏字段。

源码证据#

  • libs/langgraph/langgraph/pregel/main.py::Pregel.invoke
  • libs/langgraph/langgraph/pregel/main.py::Pregel.stream
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/main.py

8.4 Config、Context 与 State 传播源码解剖#

数据边界#

数据来源生命周期下游消费者
Configinvoke(..., config=...)单次运行或线程运行runtime、节点、子图
Contextcontext_schema + runtime context单次运行节点函数
StateTravelState channels整个图执行planner/executor/router

细粒度伪代码#

def run_node(node, current_state, config, runtime):
# 1. 从 channels 读取当前 state 快照
state_snapshot = read_state_from_channels(current_state)
# 2. 准备节点参数
if node_accepts_runtime(node):
args = (state_snapshot, runtime)
else:
args = (state_snapshot,)
# 3. 调用节点函数或 runnable
update = node.invoke_or_call(*args, config=config)
# 4. 将节点返回值转换成 channel writes
writes = map_update_to_channel_writes(update)
return writes

逐段解释#

第 1 段说明节点读到的是当前 super-step 开始时的一致状态快照。Plan-and-Execute 中,execute_step 应基于最新合并后的 current_step_index 执行。

第 3 段说明节点可以是普通函数,也可以是 Runnable 或 compiled subgraph。子图作为节点时,也通过统一运行接口被调用。

第 4 段说明节点返回的是 partial update,不是直接改 state。真正合并发生在 channel 写入应用阶段。

8.5 planner_node 运行源码解剖#

职责与所处阶段#

运行时主链。负责生成完整计划,并初始化执行游标。

真实源码签名#

用户节点函数签名:

def planner_node(state: TravelState) -> dict:
...

调用方与被调用方#

Pregel runtime
PregelNode for planner_node
planner_node(state)
ChannelWrite(plan_steps, current_step_index)

输入、输出与状态变化#

项目类型说明
输入TravelState初始状态,包含 user_request
输出dictplan_stepscurrent_step_index、可选 plan_version
状态变化覆盖计划字段写入新计划
副作用不建议有planner 应尽量纯函数化

细粒度伪代码#

def planner_node(state):
# 1. 读取用户请求
user_request = state["user_request"]
# 2. 调用 LLM / 规则 planner 生成结构化计划
plan_steps = build_plan_steps(user_request)
# 3. 校验计划基本结构
if not plan_steps:
return {
"plan_error": "empty_plan",
"plan_status": "invalid",
}
# 4. 标准化 step_id 与 status
normalized_steps = []
for i, step in enumerate(plan_steps):
normalized_steps.append({
"step_id": i + 1,
"task": step["task"],
"status": "pending",
})
# 5. 返回 partial state update
return {
"plan_steps": normalized_steps,
"current_step_index": 0,
"step_results": [],
"plan_status": "ready",
}

逐段解释#

第 2 段中的 build_plan_steps() 可以是 LLM structured output,也可以是规则模板。LangGraph 不关心计划如何生成,只关心节点返回什么 state update。

第 3 段是工程上必须补的计划校验。不要让空计划进入 executor,否则 current_step_index 会立即越界或直接进入 final。

第 5 段是源码机制重点:返回值不会立即“赋值给全局变量”,而是转换为 channel writes,由 Pregel 在 super-step 结束时统一合并。

正常路径#

user_request
planner_node
{plan_steps, current_step_index=0}
state merge
execute_step

关键分支与异常路径#

条件行为结果
计划为空写入 plan_status=invalid路由到 replan 或失败
计划字段缺失validation node 捕获不进入 executor
planner 抛异常Pregel 节点失败可由 retry/error handler 处理
planner 返回完整 state仍按 partial update 合并可能覆盖无关字段,工程上不推荐

设计原因与工程影响#

Planner 应该只负责计划生成和初始游标,不应该执行工具。否则 Plan-and-Execute 会退化成“一个节点做所有事”,失去可观测、可重试、可重规划的优势。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_node
  • libs/langgraph/langgraph/pregel/_write.py::ChannelWrite
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py

8.6 execute_step 运行源码解剖#

职责与所处阶段#

运行时主链。负责读取 current_step_index 指向的步骤,执行该步骤,并写回结果与新的游标。

真实源码签名#

def execute_step(state: TravelState) -> dict:
...

调用方与被调用方#

Pregel runtime
execute_step node
read state["plan_steps"][current_step_index]
call tool / subgraph / LLM
return partial update

输入、输出与状态变化#

项目类型说明
输入TravelState最新计划与游标
输出dict步骤状态、结果、游标更新
状态变化plan_stepsstep_resultscurrent_step_index推进执行进度
副作用视步骤而定可能调用工具或外部 API

细粒度伪代码#

def execute_step(state):
# 1. 读取执行游标
index = state["current_step_index"]
steps = state["plan_steps"]
# 2. 边界检查
if index < 0 or index >= len(steps):
return {
"step_error": "index_out_of_range",
"execution_status": "failed",
}
# 3. 取出当前步骤
current_step = steps[index]
# 4. 标记 running,避免重复执行时状态不清
updated_steps = copy_steps(steps)
updated_steps[index]["status"] = "running"
# 5. 执行当前步骤
try:
step_result = run_step_task(current_step, state)
updated_steps[index]["status"] = "done"
next_index = index + 1
execution_status = "ok"
step_error = None
except RecoverableStepError as exc:
step_result = None
updated_steps[index]["status"] = "failed"
next_index = index
execution_status = "recoverable_failed"
step_error = str(exc)
except Exception as exc:
raise
# 6. 返回 partial update
update = {
"plan_steps": updated_steps,
"current_step_index": next_index,
"execution_status": execution_status,
"step_error": step_error,
}
if step_result is not None:
update["step_results"] = state["step_results"] + [step_result]
return update

逐段解释#

第 1–2 段说明 current_step_index 是 Plan-and-Execute 的运行时游标。它不是为了展示给用户,而是为了让图知道下一次 executor 应执行哪个步骤。

第 5 段分出可恢复错误和不可恢复异常。可恢复错误应写入 state,让 route_next_step 决定是否 replan;不可恢复异常应继续抛出,由 LangGraph runtime、retry policy 或上层系统处理。

第 6 段说明步骤结果、步骤状态和游标必须一起更新。只写 step_results 不写 current_step_index 会导致重复执行同一步;只写 current_step_index 不写结果会丢失执行证据。

正常路径#

current_step_index = 0
execute step 1
step_results += result_1
current_step_index = 1

关键分支与异常路径#

条件行为结果
index 越界写入失败状态route 到 replan/final/error
步骤执行成功status=done,index+1继续下一步
可恢复失败status=failed,index 不变route 到 replan
不可恢复异常raise由 runtime/retry 处理

设计原因与工程影响#

current_step_index 是执行进度的单一事实来源。不要同时用多个字段表达执行位置,例如 current_step_idnext_stepremaining_steps 混用,否则路由会变得难以维护。

源码证据#

  • libs/langgraph/langgraph/pregel/_loop.py
  • libs/langgraph/langgraph/pregel/_algo.py
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/_loop.py

8.7 route_next_step 运行源码解剖#

职责与所处阶段#

运行时主链。负责在每次步骤执行后读取最新 state,并决定进入继续执行、重规划或最终响应。

真实源码签名#

用户定义:

def route_next_step(state: TravelState) -> Literal[
"execute_step",
"replan",
"final_response",
]:
...

框架注册:

builder.add_conditional_edges("check_step_result", route_next_step)

调用方与被调用方#

check_step_result / execute_step 完成
BranchSpec.run
route_next_step(state)
BranchSpec._finish
write branch:to:<target>

输入、输出与状态变化#

项目类型说明
输入TravelState最新合并后的 state
输出str / END / list后继节点
状态变化route function 不应修改 state
副作用不建议有应保持纯路由

细粒度伪代码#

def route_next_step(state):
# 1. 如果执行出现可恢复失败,进入 replan
if state.get("execution_status") == "recoverable_failed":
return "replan"
# 2. 如果计划本身无效,进入 replan
if state.get("plan_status") == "invalid":
return "replan"
# 3. 如果还有未执行步骤,继续 executor
if state["current_step_index"] < len(state["plan_steps"]):
return "execute_step"
# 4. 所有步骤完成,进入最终响应
return "final_response"

框架侧 branch 伪代码:

def branch_route(input_state):
# 1. 调用用户 path function
result = route_next_step(input_state)
# 2. 归一化为目标列表
if not isinstance(result, list):
destinations = [result]
else:
destinations = result
# 3. path_map 映射业务标签到节点名
destinations = [path_map.get(d, d) for d in destinations]
# 4. 校验目标
for dest in destinations:
if dest is None or dest == START:
raise ValueError("Invalid branch destination")
# 5. 写入 branch channel,激活后继节点
return write_to_branch_channels(destinations)

逐段解释#

第 1–2 段把失败优先级放在继续执行之前。否则步骤失败后 current_step_index 可能没变,图会无限重复执行同一步。

第 3 段是正常循环条件。只要游标还没到计划末尾,就返回 execute_step

第 4 段不要直接返回 END,而是先进入 final_response。这样可以把所有步骤结果整理成用户可读结果。如果你不需要 final node,也可以让 route 直接返回 END

正常路径#

execute_step done
route_next_step
├── current_step_index < len(plan_steps) → execute_step
└── current_step_index >= len(plan_steps) → final_response

关键分支与异常路径#

条件行为结果
execution_status=ok 且仍有步骤返回 execute_step继续循环
execution_status=recoverable_failed返回 replan重新规划
current_step_index >= len(plan_steps)返回 final_response准备结束
返回 END当前分支终止不再进入 final node
返回多个节点并行分支需 reducer 合并输出

设计原因与工程影响#

路由函数应该只根据 state 做决策,不应该写 state。这样可以让执行逻辑、校验逻辑、路由逻辑分离,便于测试与复盘。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec.run
  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._route
  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/_branch.py

8.8 final_response 运行源码解剖#

职责与所处阶段#

运行时主链末尾。负责将执行过程中的结构化结果转换成最终输出字段。

真实源码签名#

def final_response(state: TravelState) -> dict:
...

调用方与被调用方#

route_next_step returns "final_response"
Pregel activates final_response node
final_response(state)
return {"final_answer": ...}
edge final_response → END

输入、输出与状态变化#

项目类型说明
输入TravelState已完成的 plan 和 step_results
输出dictfinal_answer
状态变化final_answer写入最终答案
副作用不建议有应只生成结果

细粒度伪代码#

def final_response(state):
# 1. 读取计划和执行结果
steps = state["plan_steps"]
results = state["step_results"]
# 2. 检查是否所有步骤都完成
unfinished = [s for s in steps if s["status"] != "done"]
if unfinished:
return {
"final_answer": "计划未完整执行,无法生成最终行程。",
"final_status": "incomplete",
}
# 3. 聚合结果
answer = render_final_answer(
user_request=state["user_request"],
steps=steps,
results=results,
)
# 4. 写入最终字段
return {
"final_answer": answer,
"final_status": "done",
}

逐段解释#

第 2 段是防御式校验。即使路由逻辑判断所有步骤已执行,也建议 final node 再做一次状态完整性检查。

第 4 段只写最终输出字段,不应再修改计划和步骤结果。否则 final node 会同时承担“总结”和“修复计划”两个职责。

正常路径#

all plan_steps done
final_response
final_answer
END

关键分支与异常路径#

条件行为结果
所有步骤 done生成最终答案正常结束
有步骤 pending/failed写入 incomplete可回到 replan 或结束失败
results 为空写入失败说明暴露执行异常

设计原因与工程影响#

Final node 是输出边界。它应该只负责把 state 转换成用户或 API 需要的最终格式。复杂业务中建议使用结构化输出 schema,例如 FinalTravelPlan,避免从自由文本中再解析。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::CompiledStateGraph
  • libs/langgraph/langgraph/pregel/main.py::Pregel.invoke
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/main.py

8.9 运行时主链总结#

input TravelState
planner_node returns plan partial update
state merge
execute_step reads current_step_index
execute_step returns step result + index update
route_next_step reads latest state
loop / replan / final
final_response writes final_answer

9. 关键分支、异常与边界#

本章回答:

当计划无效、步骤失败、路由返回多目标、executor 是子图或图循环过长时,LangGraph 如何分流、恢复、终止或失败?

9.1 分支矩阵#

分支类型触发条件核心函数结果
继续执行current_step_index < len(plan_steps)route_next_step回到 execute_step
正常完成current_step_index >= len(plan_steps)route_next_step进入 final_response
重规划execution_status=recoverable_failedroute_next_step进入 replan
直接终止路由返回 ENDbranch _finish当前分支停止
并行执行路由返回多个节点branch _finish多节点下一 super-step 并行
框架保护超过 recursion_limitPregel loop抛出图递归限制错误

9.2 同步与异步分支#

维度同步路径异步路径
入口invoke()ainvoke()
节点函数同步 callableasync callable / runnable
子图subgraph.invoke()await subgraph.ainvoke()
工具调用同步工具async 工具
资源语义简单直观更适合 I/O 密集 executor
降级行为同步节点不能安全取消async 节点可配合 timeout

9.3 Batch、Stream、Parallel 或路由分支#

Plan-and-Execute 不是单个对象,而是一种图结构,因此:

Batch
由 CompiledStateGraph / Runnable batch 提供。
Stream
由 CompiledStateGraph.stream 提供,可观察 planner、executor、replan 等节点 update。
Parallel
由图边、conditional edge 多目标返回、Send API 或同一 super-step 多节点激活提供。
路由
由 add_conditional_edges / BranchSpec 提供。

如果 route_next_step 返回多个 executor 节点,例如同时执行“查景点”和“查天气”,后续聚合字段必须有 reducer,否则多节点同时写同一个 key 可能冲突。

9.4 异常分类#

异常类别抛出位置是否可恢复处理策略是否反馈上层
计划为空planner_node / validate_planroute 到 replan
计划字段错误validate_plan修复或重规划
index 越界execute_step写入错误状态并 replan
工具可恢复失败execute_step / subgraphretry 或 replan
子图异常executor subgraph视情况捕获转 state 或向上抛
无限循环Pregel runtimerecursion_limit 终止
非预期系统异常任意节点抛出并告警

9.5 异常路径源码解剖#

职责与所处阶段#

分支与异常阶段。负责把可恢复错误转成 state,让路由处理;把不可恢复错误交给 runtime。

细粒度伪代码#

def check_step_result(state):
# 1. 读取执行状态
status = state.get("execution_status")
error = state.get("step_error")
# 2. 成功路径
if status == "ok":
return {
"last_step_valid": True,
"decision": "continue",
}
# 3. 可恢复失败:让 route 进入 replan
if status == "recoverable_failed":
return {
"last_step_valid": False,
"decision": "replan",
"replan_reason": error,
}
# 4. 输入或状态异常:终止或人工接管
if status == "failed":
return {
"last_step_valid": False,
"decision": "abort",
"abort_reason": error,
}
# 5. 未知状态:不要沉默吞掉
raise ValueError(f"Unknown execution_status: {status}")

逐段解释#

第 3 段体现 Plan-and-Execute 的核心恢复机制:失败不是立即中止,而是写入 replan_reason,让 route_next_step 进入 replan

第 5 段不要把未知状态当成功处理。Agent 系统中最危险的 bug 是“失败被伪装成成功”。

9.6 Retry、Fallback 与恢复边界#

机制适用条件不适用条件幂等要求
Retry查询类工具失败、LLM 输出格式错误写操作、扣费、发邮件必须可重复执行
Fallback模型服务失败、检索服务不可用需要强一致业务结果fallback 结果要标记来源
Replan当前步骤不可完成但目标仍可达目标本身无效需保留失败原因
Repair计划格式错误、少字段事实缺失或任务不清不涉及外部副作用
Human Handoff高风险动作、连续失败低风险自动查询需要 checkpoint

9.7 停止条件与保护上限#

正常结束:route_next_step 返回 final_response,final_response → END
提前结束:route function 直接返回 END
人工中断:executor 或 verifier 调用 interrupt,等待 Command(resume=...)
框架保护:达到 recursion_limit
异常失败:节点抛出未处理异常

9.8 能力边界#

容易误判的能力实际提供者本篇对象的真实职责
自动生成高质量计划LLM / planner prompt / structured outputLangGraph 只承载计划状态
自动执行工具ToolNode / BaseTool / 自定义节点Executor node 决定如何调用
自动避免无限循环recursion_limit + 路由设计route 只返回后继节点
自动合并并发结果Reducer / channel节点只返回 update
自动恢复长任务CheckpointerPlan state 只是可保存数据

10. 扩展机制与框架协作#

本章回答:

Plan-and-Execute 在工程中如何与 subgraph、Command、Send、reducer、checkpoint、tool calling 和 streaming 协作?

10.1 扩展点总览#

扩展点扩展方式执行时机可修改内容约束
Planner schemaPydantic / TypedDictplanner 输出计划结构需可序列化
Executor subgraphcompiled graph as node执行步骤时内部工具与校验需处理父子 state 映射
Conditional edgeadd_conditional_edges节点后下一节点route 不应有副作用
Commandnode return节点执行后update + goto更动态但可视化弱
Sendbranch returnmap-reduce 分发并行任务需要 reducer 汇总
ReducerAnnotatedupdate 合并时状态合并规则需避免非幂等副作用
Checkpointercompile(checkpointer=...)super-step 边界保存状态需 thread_id
Streamgraph.stream运行时观察 updates不改变执行语义

10.2 Command 作为动态计划跳转源码解剖#

职责与所处阶段#

扩展阶段。允许节点同时返回 state update 和下一跳目标,减少额外 conditional edge。

真实源码签名#

Command(
*,
graph: str | None = None,
update: Any | None = None,
resume: dict[str, Any] | Any | None = None,
goto: Send | Sequence[Send | N] | N = (),
)

调用方与被调用方#

execute_step returns Command(update=..., goto="replan")
Pregel runtime
apply update
activate goto target

输入、输出与状态变化#

项目类型说明
输入state当前执行状态
输出Commandupdate + goto
状态变化Command.update合并进 state
副作用无额外控制流由 goto 决定

细粒度伪代码#

def execute_step_with_command(state):
index = state["current_step_index"]
step = state["plan_steps"][index]
try:
result = run_step_task(step, state)
update = {
"step_results": state["step_results"] + [result],
"current_step_index": index + 1,
"execution_status": "ok",
}
goto = "execute_step" if index + 1 < len(state["plan_steps"]) else "final_response"
return Command(update=update, goto=goto)
except RecoverableStepError as exc:
return Command(
update={
"execution_status": "recoverable_failed",
"step_error": str(exc),
},
goto="replan",
)

逐段解释#

Command 把“状态更新”和“下一跳”合并到同一个节点返回值中。它适合高度动态的流程,但会让控制流分散在节点内部,不如 add_conditional_edges() 清晰。

正常路径#

node returns Command(update=..., goto="next")
apply update
activate next

关键分支与异常路径#

条件行为结果
goto 是节点名激活该节点动态跳转
goto 是多个目标多节点并行需要 reducer
goto=END终止当前路径结束
update 与 reducer 冲突抛错或按 channel 处理取决于 state key

设计原因与工程影响#

对于 Plan-and-Execute,入门建议优先使用 add_conditional_edges(),因为控制流更可视化;当执行节点内部必须基于工具结果即时跳转时,再使用 Command

源码证据#

  • libs/langgraph/langgraph/types.py::Command
  • https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/types.py

10.3 Send 与步骤并行执行源码解剖#

职责与所处阶段#

扩展阶段。允许一个路由函数返回多个带独立输入的目标,用于 map-reduce 风格并行执行。

细粒度伪代码#

def route_parallel_research(state):
# 1. 根据计划中的 research 步骤拆分多个子任务
research_tasks = extract_research_tasks(state["plan_steps"])
# 2. 将每个子任务发送给同一个或不同节点
sends = []
for task in research_tasks:
sends.append(
Send(
"research_executor",
{
"user_request": state["user_request"],
"research_task": task,
},
)
)
return sends

逐段解释#

Send 不是简单返回节点名,而是返回“目标节点 + 该目标节点输入”。它适合并行检索、并行工具调用、多子计划执行等场景。

关键分支与异常路径#

条件行为结果
多个 Send 写同一个结果 keyreducer 合并适合 research_notes
没有 reducer并发写冲突抛出 concurrent update 错误
子任务很多并行提升吞吐成本和限流压力增加

设计原因与工程影响#

Plan-and-Execute 中,所有步骤不一定必须串行。信息收集类步骤可以并行,决策和写操作应串行。是否并行不是框架决定,而是业务语义决定。

10.4 与相邻框架组件的协作#

相邻组件输入协议输出协议协作边界
Structured Output用户请求 / 上下文plan_steps生成结构化计划,不执行
ToolNodeAIMessage.tool_callsToolMessage执行工具,不管理计划
Reducerstate updatesmerged state合并多步骤日志、检索结果
Checkpointerstate snapshotsresumable thread保存计划执行进度
Interruptpause payloadresume value高风险步骤人工确认
Streamingruntime eventsupdates / values观察计划执行轨迹

10.5 公共扩展接口与内部实现#

业务代码可以依赖:
StateGraph
add_node
add_edge
add_conditional_edges
compile
Command
Send
CompiledStateGraph.invoke / stream
业务代码避免依赖:
BranchSpec 内部字段
Pregel 私有 loop 状态
channel 内部写入格式
未文档化的 branch channel 名称

10.6 自定义扩展示例:旅行规划 executor subgraph#

class ExecutorState(TypedDict):
current_step: PlanStep
tool_results: list[str]
step_status: str
step_error: str | None
def prepare_step(state: ExecutorState):
return {"step_status": "prepared"}
def call_tools(state: ExecutorState):
return {"tool_results": [f"工具结果:{state['current_step']['task']}"]}
def verify_step(state: ExecutorState):
if not state["tool_results"]:
return {"step_status": "failed", "step_error": "empty_tool_results"}
return {"step_status": "done"}

说明:

  1. 子图接收 ExecutorState,不直接暴露父图全部 state。
  2. 子图输出由 wrapper 转换为父图 partial update。
  3. 子图异常应被捕获并转成父图 execution_status
  4. 子图内部可以有自己的 retry、cache、tool node 和 verifier。
  5. 如果需要恢复子图内部状态,应配合 checkpointer。

10.7 选择扩展还是重写流程#

条件选择普通节点选择子图选择 Command
每步只有一个简单函数
每步需要多个内部阶段视情况
需要独立测试 executor
路由逻辑清晰固定
跳转高度动态视情况
需要图可视化清晰不优先

11. 工程决策与适用场景#

11.1 适用场景#

场景是否推荐原因
旅行规划先规划、再检索、再合成,非常适合 Plan-and-Execute
报告生成可拆分研究、写作、审稿步骤
订单履约是,但需 Workflow 约束高风险动作需要 verifier 和 HITL
简单 FAQ过度复杂,普通 RAG 即可
开放聊天计划步骤不稳定,成本高
代码修复定位、修改、测试、验证天然多步

11.2 工程决策表#

决策点推荐选择前提风险
计划存哪里state["plan_steps"]多节点需要共享schema 太松会难校验
进度怎么存current_step_index串行计划并行计划需改为 task queue
步骤结果怎么存step_results + reducer 或覆盖列表需要复盘结果过大会污染上下文
replan 怎么做保留失败原因,生成新 plan version需要可追溯直接覆盖会丢历史
executor 形式简单用 node,复杂用 subgraph看步骤复杂度过早子图化增加成本
终止保护recursion_limit + route 条件存在循环设置过低会误杀长任务

11.3 性能、可靠性与安全边界#

性能:
串行步骤会增加延迟;可并行的信息收集步骤可用 Send 或多分支,但需要 reducer。
可靠性:
Planner 可能生成不可执行计划;必须有 validate_plan 和 step verifier。
安全:
写操作步骤必须经过权限校验、幂等控制和人工确认。
可观测性:
必须记录 plan version、current_step_index、step status、tool result、replan reason。

12. 常见误区与源码纠正#

12.1 误区:Plan-and-Execute 是 LangGraph 内置类#

错误原因:很多教程把范式和框架 API 混在一起,好像存在一个 PlanAndExecuteAgent

源码事实:

LangGraph 提供 StateGraph、Node、Edge、Branch、Reducer、Subgraph。
Plan-and-Execute 是你用这些基础抽象组合出来的图结构。

工程影响:

如果以为框架自动管理计划质量,就会忽略 validate_planstep verifierreplan

12.2 误区:计划列表可以放在局部变量里#

错误原因:初学者会把计划当成函数内部临时变量。

源码事实:

节点之间只通过 state 共享信息。
node 返回 partial update,LangGraph 合并到 state channels。

工程影响:

计划不进 state,executor、verifier、router 都无法稳定读取计划。

12.3 误区:current_step_index 只是展示字段#

错误原因:看起来只是一个整数。

源码事实:

route_next_step 和 execute_step 都依赖 current_step_index 控制循环。
它是执行游标,不是 UI 字段。

工程影响:

游标更新错误会导致重复执行、跳步或提前结束。

12.4 误区:每一步执行都应该直接调用工具#

错误原因:把 executor 等同于 tool call。

源码事实:

executor 可以是普通节点、ToolNode、Runnable、CompiledStateGraph 或 wrapper 函数。

工程影响:

复杂步骤直接堆在一个函数里会难以测试、难以恢复,也难以观测。

12.5 误区:replan 就是覆盖旧计划#

错误原因:只关注最终能不能完成。

源码事实:

state update 是按 key 合并或覆盖。
你可以覆盖 plan_steps,也可以追加 plan_versions。
这是 schema 与 reducer 的设计问题。

工程影响:

直接覆盖会丢失失败原因和历史计划,不利于评估与排查。

12.6 误区:conditional edge 可以顺便改状态#

错误原因:路由函数拿到了 state,容易顺手修改。

源码事实:

path function 的职责是返回后继节点,不是返回 partial update。
状态更新应由节点或 Command 完成。

工程影响:

在路由函数里做副作用会让执行不可预测,也不利于 checkpoint 恢复。

12.7 误区:循环一定会自动结束#

错误原因:只写了 route_next_step 的正常条件。

源码事实:

LangGraph 会按照条件边继续调度,直到 END 或无活跃节点。
如果路由一直返回 execute_step,就会依赖 recursion_limit 保护。

工程影响:

必须显式设计结束条件,并在 config 中设置合理的 recursion_limit


13. 最终心智模型与掌握检查#

13.1 构建期心智模型#

TravelState schema
StateGraph builder
add_node 注册 planner / executor / verifier / replan / final
add_edge 注册固定顺序
add_conditional_edges 注册步骤路由
compile 生成 CompiledStateGraph

13.2 运行时心智模型#

initial state
planner_node 写入 plan_steps
execute_step 读取 current_step_index
执行一个 step
写入 step result + index update
route_next_step 决定继续 / replan / final
END

13.3 分支与异常心智模型#

正常路径 → execute_step 循环直到 plan_steps 全部 done
分支路径 → recoverable failure 进入 replan
可恢复异常 → 写入 execution_status / step_error,由 route 处理
不可恢复异常 → 抛给 Pregel runtime / retry / 上层系统
保护上限 → recursion_limit 防止无限循环

13.4 一句话总结#

Plan-and-Execute 在 LangGraph 中不是一个独立 Agent 类,而是通过 StateGraph 构建计划状态、执行节点、校验节点、条件路由和可选子图来表达;运行时每个节点返回 partial state update,Pregel 调度器按边和 super-step 推进状态,条件边根据最新 state 决定继续执行、重规划或结束。

13.5 掌握检查#

  • 能说清 planner_nodeexecute_step 的职责边界。
  • 能说明 plan_steps 为什么必须写入 state。
  • 能说明 current_step_index 如何驱动循环。
  • 能画出 planner → executor → verifier → route → executor/final/replan 图结构。
  • 能解释节点返回 partial update 后如何进入 state。
  • 能说明什么时候 executor 应该使用 subgraph。
  • 能说明 replan 覆盖和追加的差异。
  • 能说明正常结束和 recursion_limit 保护性终止的区别。
  • 能区分 add_conditional_edgesCommand(goto=...) 的使用边界。
  • 能为旅行规划助手设计一套可回放的 plan state。

14. 参考资料与下一篇衔接#

14.1 官方概念文档#

  1. LangGraph Graph API Overview
    https://docs.langchain.com/oss/python/langgraph/graph-api

  2. LangGraph Use the Graph API
    https://docs.langchain.com/oss/python/langgraph/use-graph-api

  3. LangGraph Subgraphs
    https://docs.langchain.com/oss/python/langgraph/use-subgraphs

  4. LangGraph Streaming
    https://docs.langchain.com/oss/python/langgraph/streaming

  5. LangGraph PyPI
    https://pypi.org/project/langgraph/

14.2 官方 API Reference#

  1. StateGraph
    https://reference.langchain.com/python/langgraph/graph/state/StateGraph

  2. StateGraph.add_node
    https://reference.langchain.com/python/langgraph/graph/state/StateGraph/add_node

  3. StateGraph.add_conditional_edges
    https://reference.langchain.com/python/langgraph/graph/state/StateGraph/add_conditional_edges

  4. StateGraph.compile
    https://reference.langchain.com/python/langgraph/graph/state/StateGraph/compile

  5. Command
    https://reference.langchain.com/python/langgraph/types/Command

14.3 官方源码#

  1. langgraph/graph/state.py::StateGraph
    https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py

  2. langgraph/graph/_branch.py::BranchSpec
    https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/_branch.py

  3. langgraph/pregel/main.py::Pregel
    https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/main.py

  4. langgraph/pregel/_loop.py
    https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/_loop.py

  5. langgraph/types.py::Command
    https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/types.py

14.4 下一篇衔接#

下一篇进入:

第 10 篇:Checkpoint / Thread / Durable Execution 源码解剖

需要继续回答:

thread_id 如何定位一条执行线?
checkpoint 在哪个 super-step 边界保存?
StateSnapshot 如何记录 values、next、tasks 与 metadata?
get_state / get_state_history 如何读取执行历史?
Plan-and-Execute 长任务如何在失败或人工确认后恢复?
LangGraph 源码深潜:Plan-and-Execute 与 Subgraph 运行机制解剖
https://jupiter-ws.cn/posts/agent-frameworks/langgraph-plan-execute-subgraph-deep-dive/
作者
Jupiter
发布于
2026-03-13
许可协议
CC BY-NC-SA 4.0