14326 字
72 分钟

LangGraph 源码深潜:Conditional Edge 与 Router 路由机制解剖

核心问题: LangGraph 如何把“路由函数返回下一个节点”的业务分支,编译成 Pregel 运行时可调度的条件边?

源码主线: StateGraph.add_conditional_edges()BranchSpec.from_path()CompiledStateGraph.attach_branch()BranchSpec._route()BranchSpec._finish()ChannelWrite → 目标节点触发

前置文章: 第 6 篇《StateGraph 源码解剖》、第 3 篇《OutputParser 与结构化输出》、第 5 篇《create_agent 与 ReAct 实现机制》

依赖基线: langgraph==1.2.7

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

阅读边界: 本文只解剖 StateGraph.add_conditional_edges()BranchSpecCompiledStateGraph.attach_branch() 在构建期、运行时、分支异常和扩展协作中的源码机制;不展开 checkpoint、interrupt、subgraph、完整 Pregel 调度器和 reducer 源码。


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

上一篇已经解决 StateGraph 的整体骨架:

StateGraph 是 builder
add_node 注册节点
add_edge 注册固定边
compile 生成 CompiledStateGraph
CompiledStateGraph.invoke 进入 Pregel 运行时
节点返回 partial state update
channel / reducer 合并 state

本篇进入 StateGraph 中最重要的动态控制流机制:add_conditional_edges()。它解决的是 Workflow Agent、Router Agent、Reflection Loop、错误恢复路由中的共同问题:

一个节点执行完成后,下一步不是固定节点,
而是由当前 state、结构化输出、工具结果或错误状态动态决定。

本篇只解决:

  • add_conditional_edges() 如何在构建期注册路由函数。
  • BranchSpec.from_path() 如何处理 path_mapLiteral 类型提示和 input schema。
  • compile() 如何把 branch 挂到 CompiledStateGraph
  • 运行时如何调用 router,如何把返回值转成目标节点触发写入。
  • END、多目标返回、Send、无 path_map、无 Literal、异常返回的边界。

本篇不展开:

  • Command 的完整源码机制。
  • Send 的 map-reduce 深度实现。
  • Pregel 的完整 super-step 调度算法。
  • interrupt、checkpoint 与 durable execution。
StateGraph 固定边源码
Conditional Edge / Router 源码
Reducer、并行状态合并、Send API

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

1.1 核心问题#

StateGraph.add_conditional_edges() 如何把一个普通 Python 路由函数,转换成运行时可根据 state 选择下一个或多个节点的图分支?

1.2 学习目标#

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

  1. 说清 add_conditional_edges() 在构建期保存了什么,以及为什么它不直接执行 router。
  2. 解释 pathpath_mapLiteral 返回类型、END、多目标返回、Send 的源码语义。
  3. 画出 source node 执行完成后,branch 如何读取最新 state、调用 router、写入目标节点 channel 的运行时链路。
  4. 区分普通固定边 add_edge()、条件边 add_conditional_edges()CommandSend 的边界。
  5. 能把意图路由、状态路由、错误路由落到旅行规划助手的 StateGraph 中,并判断何时需要 path_map、何时需要 Literal

1.3 能力边界#

能力本篇是否覆盖说明
意图路由重点讲 state.intent -> node_name
状态路由讲节点 partial update 之后,router 读取最新 state。
错误路由讲错误字段驱动 fallback / retry / human 节点。
多目标并行讲返回 list[str]Send 的边界。
Command 深入源码只比较边界,后续在控制流增强篇展开。
完整 Pregel 调度器本文只解剖 branch 如何转成 Pregel 可消费的写入。
checkpoint / interrupt后续 durable execution / HITL 篇展开。

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

2.1 一句话定义#

Conditional Edge 是 StateGraph 的动态分支机制,负责在某个 source node 执行完成后调用 router,根据 router 返回值决定下一个或多个目标节点;它不负责生成业务状态、不负责执行目标节点本身,也不负责合并目标节点的 partial update。

2.2 最小心智模型#

source node 执行
写入 partial state update
branch reader 读取最新 state
path router 被 invoke
返回目标标识
映射为目标节点 channel 写入
下一轮 Pregel super-step 触发目标节点

2.3 核心术语#

术语源码对象语义不要误解为
条件边StateGraph.add_conditional_edges()构建期注册 source node 退出后的 branch直接运行 router 的函数
路由函数path / Runnable根据当前 state 返回目标标识节点函数本身
branch specBranchSpec保存 router、目标映射、input schema 的规格运行时调度器
目标映射path_map / BranchSpec.ends把逻辑 label 映射到 node name状态更新规则
ENDlanggraph.constants.END终止图执行的特殊目标普通节点名
多目标路由Sequence[Hashable]一次 branch 返回多个目标reducer 合并机制本身
Sendlanggraph.types.Send动态发送不同 state slice 到目标节点普通字符串节点名

2.4 与相邻抽象的边界#

对象负责什么不负责什么与本篇对象的关系
add_node()注册节点函数、输入 schema、retry/cache/timeout 等策略决定节点后续走向条件边的 source 与 target 必须是节点或 START/END
add_edge()注册固定拓扑根据 state 动态分支条件边是固定边的动态版本。
BranchSpec保存并运行 router执行目标节点业务逻辑是条件边的构建期规格和运行时 wrapper。
CompiledStateGraph挂接节点、固定边、条件边到 Pregel定义业务路由逻辑attach_branch()BranchSpec 编译进运行时。
Pregel按 channel / trigger 调度节点理解业务 intent条件边最终变成 channel writes,交给 Pregel 调度。
Command节点同时返回 state update 与 goto注册 source node 退出后的 branch与条件边都能改变控制流,但职责不同。

3. 完整执行链路#

3.1 高层链路#

用户输入 user_request
classify_intent 节点执行
返回 {"intent": ...}
state.intent 合并完成
route_by_intent(state)
返回 "budget_node" / "attraction_node" / "general_node"
CompiledStateGraph 把返回值写入目标节点触发 channel
Pregel 下一轮执行对应目标节点

3.2 最小代码#

观察目标:这段代码展示条件边最小用法。classify_intent 是普通节点,route_by_intent 是 router。router 不返回 state update,而是返回下一个节点名。

from typing_extensions import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class TravelState(TypedDict):
user_request: str
intent: str | None
result: str | None
def classify_intent(state: TravelState):
text = state["user_request"]
if "预算" in text:
intent = "budget_planning"
elif "景点" in text:
intent = "attraction_planning"
else:
intent = "general_planning"
return {"intent": intent}
def route_by_intent(state: TravelState) -> Literal[
"budget_node",
"attraction_node",
"general_node",
]:
if state["intent"] == "budget_planning":
return "budget_node"
if state["intent"] == "attraction_planning":
return "attraction_node"
return "general_node"
def budget_node(state: TravelState):
return {"result": "进入预算规划"}
def attraction_node(state: TravelState):
return {"result": "进入景点规划"}
def general_node(state: TravelState):
return {"result": "进入通用规划"}
builder = StateGraph(TravelState)
builder.add_node("classify_intent", classify_intent)
builder.add_node("budget_node", budget_node)
builder.add_node("attraction_node", attraction_node)
builder.add_node("general_node", general_node)
builder.add_edge(START, "classify_intent")
builder.add_conditional_edges("classify_intent", route_by_intent)
builder.add_edge("budget_node", END)
builder.add_edge("attraction_node", END)
builder.add_edge("general_node", END)
graph = builder.compile()
result = graph.invoke({
"user_request": "我想规划东京旅行预算",
"intent": None,
"result": None,
})

关键解释:

classify_intent 返回的是 state update。
route_by_intent 返回的是 control flow destination。

两个函数的返回语义完全不同:

函数返回值被谁消费作用
classify_intentdictstate channel writer更新 state.intent
route_by_intentstr / Literalbranch writer决定下一个节点

3.3 对象流转#

阶段输入类型核心函数输出类型状态变化
构建 builderTravelState schemaStateGraph.__init__()StateGraph初始化 nodes / edges / branches / channels
注册节点Callableadd_node()Self写入 self.nodes
注册条件边source + path + path_mapadd_conditional_edges()Self写入 self.branches[source][name]
编译 branchBranchSpecCompiledStateGraph.attach_branch()runtime writers给 source 节点挂 branch writer
运行 sourceTravelStateclassify_intent()dict合并 intent
运行 router最新 stateBranchSpec._route()destination list写入目标 channel
执行目标节点最新 statebudget_node()dict合并 result

3.4 时序链路#

Caller
│ graph.invoke(input_state)
CompiledStateGraph / Pregel
│ START channel 触发 classify_intent
classify_intent node
│ return {"intent": "budget_planning"}
State channels
│ state.intent 更新完成
BranchSpec._route
│ route_by_intent(state) -> "budget_node"
ChannelWrite
│ 写入 branch:to:budget_node
budget_node
│ return {"result": "进入预算规划"}
END

3.5 正常结束条件#

一次条件路由正常结束需要满足:

  1. source node 正常执行并完成 state update。
  2. router 函数返回合法目标。
  3. 目标不是 START,也不是非法节点名。
  4. 若返回 END,图在该路径终止。
  5. 若返回一个或多个目标节点,Pregel 在后续 super-step 触发对应节点。
  6. 所有活跃路径都到达 END 或没有新的 channel 写入后,本次图调用完成。

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

4.1 核心目录#

langgraph/
├── graph/
│ ├── state.py
│ ├── _branch.py
│ └── graph.py
├── pregel/
│ ├── __init__.py
│ ├── _read.py
│ └── _write.py
└── types.py

4.2 关键文件#

优先级文件核心对象阅读目的
1langgraph/graph/state.pyStateGraph.add_conditional_edges()看条件边如何注册到 builder。
2langgraph/graph/_branch.pyBranchSpecpath_mapLiteral、router 执行与结果写入。
3langgraph/graph/state.pyCompiledStateGraph.attach_branch()看 branch 如何被编译成 channel writes。
4langgraph/pregel/_write.pyChannelWrite看目标节点触发如何变成 channel 写入。
5langgraph/types.pySendCommand看多目标动态分发与条件边边界。

4.3 推荐阅读顺序#

1. StateGraph.add_conditional_edges
2. BranchSpec.from_path
3. _get_branch_path_input_schema
4. StateGraph.compile
5. CompiledStateGraph.attach_branch
6. BranchSpec.run
7. BranchSpec._route / _aroute
8. BranchSpec._finish
9. ChannelWrite / ChannelWriteEntry
10. Pregel super-step 调度

4.4 不建议的阅读顺序#

不建议先读完整 Pregel 调度器。Pregel 负责所有图执行的通用 runtime,包含 checkpoint、stream、retry、interrupt、cache、debug 等大量横切机制。如果还没理解 add_conditional_edges() 如何变成 BranchSpec,直接读 Pregel 会看不清“条件边”这条具体机制链。


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

5.1 核心对象关系#

StateGraph builder
├── nodes: dict[str, StateNodeSpec]
├── edges: set[(start, end)]
├── waiting_edges: set[((starts...), end)]
└── branches: defaultdict[str, dict[str, BranchSpec]]
↓ compile()
CompiledStateGraph
├── Pregel nodes
├── channels
├── branch writers
└── runtime triggers

BranchSpec 本身不是节点,它是 branch 的规格对象:

BranchSpec
├── path: Runnable
├── ends: dict[Hashable, str] | None
└── input_schema: type | None

5.2 对象职责#

对象生命周期输入输出核心职责
StateGraph构建期schema、node、edge、branchbuilder保存图定义。
BranchSpec构建期 + 运行时router、path_mapbranch runnable保存并执行路由逻辑。
CompiledStateGraph编译后运行期builder artifactsPregel application将 branch 连接到 channel / trigger。
ChannelWrite运行时destination entrieschannel writes触发目标节点。
Pregel运行时channel writesnode scheduling按 super-step 调度节点。

5.3 协议边界#

Node 协议
负责:读取 state,返回 partial state update。
不负责:决定条件分支目标,除非使用 Command。
Branch 协议
负责:读取 state,返回下一个或多个 destination。
不负责:更新 state 字段。
Channel 协议
负责:保存 state 字段或触发信号。
不负责:理解业务 intent。
Pregel 协议
负责:根据 channel 更新触发下一轮节点。
不负责:解释 router 返回值语义。

5.4 稳定接口与内部实现#

类型对象文章中的使用原则
公共 APIStateGraph.add_conditional_edges()工程代码可以稳定使用。
公共常量STARTEND工程代码可以直接使用。
扩展接口SendCommand可用于高级控制流,但需要理解边界。
内部实现BranchSpec_route()_finish()用于源码理解,业务代码不直接依赖。
内部 channelbranch:to:{node}只解释运行机制,不在业务代码中硬编码。

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

6.1 本篇阅读策略#

先读公开入口 add_conditional_edges
看它如何保存 BranchSpec
读 BranchSpec.from_path 的 path_map / Literal 推断
回到 compile / attach_branch 看 branch 如何进入 Pregel
读 BranchSpec._route / _finish 看运行时如何选择目标
最后分析 END、多目标、Send、异常与可视化边界

6.2 证据等级#

标记含义写作要求
源码事实由 tag 1.2.7 源码直接证明给出源码路径或 permalink。
官方契约官方文档或 API Reference 明确承诺给出官方文档链接。
简化伪代码对真实控制流的压缩表达明确标注“以下为保留关键控制流的简化伪代码,不是源码逐字复制”。
作者推断根据调用链得出的设计理解使用“从调用关系可以推断”。
工程建议面向项目实践的建议说明适用条件。

6.3 本篇证据清单#

结论证据类型文件或文档定位
add_conditional_edges() 保存 branch 到 self.branches[source][name]源码事实graph/state.pyStateGraph.add_conditional_edges()
router 返回 END 会停止图执行官方契约 + 源码事实API Reference / state.pyadd_conditional_edges docstring
path_map 可以是 dict 或 list,省略时可从 Literal 推断源码事实graph/_branch.pyBranchSpec.from_path()
_route() 会读取 state 后 invoke path源码事实graph/_branch.pyBranchSpec._route()
_finish() 会把 router 返回值规范为列表并写入 destinations源码事实graph/_branch.pyBranchSpec._finish()
path_map 和无 Literal 时可视化会假设可能到任意节点官方契约API Reference / docstringadd_conditional_edges warning

7. 构建期源码解剖#

本章回答:

用户调用 builder.add_conditional_edges(source, path, path_map) 时,LangGraph 在 builder 内保存了什么?

7.1 构建期职责#

输入归一化动作构建结果
source保存为 branch 所属节点self.branches[source]
pathcoerce_to_runnable()BranchSpec.path
path_mapdict/list/Literal 推断为 ends 映射BranchSpec.ends
router 输入类型提示尝试推断 input schemaBranchSpec.input_schema

7.2 构建期总链路#

add_conditional_edges(source, path, path_map)
如果 graph 已 compile,发出 warning
coerce_to_runnable(path, trace=True)
确定 branch name
校验同一个 source 下 branch name 不重复
BranchSpec.from_path(path, path_map, infer_schema=True)
保存到 self.branches[source][name]
如果推断出 input_schema,则加入 graph schemas

7.3 StateGraph.add_conditional_edges() 源码解剖#

职责与所处阶段#

add_conditional_edges() 是条件边的公开构建入口。它只注册 branch,不执行 router,也不立即检查 router 返回值是否合法。

真实源码签名#

以下签名来自 langgraph==1.2.7

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

调用方与被调用方#

用户代码
StateGraph.add_conditional_edges
coerce_to_runnable
BranchSpec.from_path
self._add_schema(optional)

输入、输出与状态变化#

项目类型说明
输入source: str条件边挂在哪个节点退出之后。
输入path路由函数、异步路由函数或 Runnable。
输入path_map可选 label 到 node name 的映射。
输出Selfbuilder 本身,支持链式调用。
状态变化self.branches[source][name]保存 BranchSpec
副作用warninggraph 已 compile 后继续添加 branch 只影响 builder,不影响已编译图。

细粒度伪代码#

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

def add_conditional_edges(self, source, path, path_map=None):
# 1. 编译后修改 builder 只给 warning
if self.compiled:
logger.warning("compiled graph will not be changed")
# 2. 把普通函数 / async 函数 / Runnable 统一成 Runnable
path_runnable = coerce_to_runnable(
path,
name=None,
trace=True,
)
# 3. 给这个 branch 找一个名字
branch_name = path_runnable.name or "condition"
# 4. 同一个 source 下,不允许同名 branch 重复注册
if branch_name in self.branches[source]:
raise ValueError("Branch already exists for source")
# 5. 构造 BranchSpec:保存 path、ends、input_schema
branch_spec = BranchSpec.from_path(
path=path_runnable,
path_map=path_map,
infer_schema=True,
)
# 6. 保存到 builder 的 branch registry
self.branches[source][branch_name] = branch_spec
# 7. 如果 branch path 有独立 input schema,则加入 schemas
if branch_spec.input_schema is not None:
self._add_schema(branch_spec.input_schema)
return self

逐段解释#

第 1 段说明 StateGraph 是 builder。compile() 后产生的是另一个 CompiledStateGraph 对象,后续修改 builder 不会自动影响已编译图,所以源码只发 warning。

第 2 段是关键归一化:router 可以是普通函数、异步函数或 Runnable,但运行时 branch 需要统一用 Runnable 协议执行,所以构建期先调用 coerce_to_runnable()

第 3–4 段说明一个 source node 可以拥有多个 branch,但 branch 必须有唯一名字。默认名字通常来自 Runnable name,如果拿不到则用 condition

第 5 段把 path_mapLiteral 推断、input schema 推断交给 BranchSpec.from_path(),避免 add_conditional_edges() 变成复杂解析函数。

第 6–7 段说明 builder 保存的是 branch spec,而不是目标边集合。真正变成运行时 channel 写入,要等 compile()attach_branch()

正常路径#

用户注册 route_by_intent
route_by_intent 被转成 Runnable
BranchSpec 保存 path 和 ends
self.branches["classify_intent"]["route_by_intent"] = BranchSpec(...)

关键分支与异常路径#

条件行为结果
graph 已 compile记录 warning已编译图不受影响。
同名 branch 重复ValueError防止 source 下 branch 冲突。
path 有 input schema调用 _add_schema()branch 可读取独立 schema。
path 无 input schema不新增 schema默认读取 source / state schema。

设计原因与工程影响#

构建期只保存 branch spec,不执行 router,是因为 router 依赖运行时 state。add_conditional_edges() 发生时还没有用户输入、没有 source node 的 partial update,也没有最新 state。因此任何“根据 state 选择分支”的动作都必须延迟到运行时。

工程影响:

不要在 add_conditional_edges() 里期待 router 立即运行。
不要把 router 写成依赖外部可变全局状态的函数。
应该让 router 只依赖当前 state / context。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edges
  • https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/state.py

7.4 BranchSpec.from_path() 源码解剖#

职责与所处阶段#

BranchSpec.from_path() 负责把 path_map 和返回类型提示转换为 ends 映射,并可选推断 router 的 input schema。

真实源码签名#

@classmethod
def from_path(
cls,
path: Runnable[Any, Hashable | list[Hashable]],
path_map: dict[Hashable, str] | list[str] | None,
infer_schema: bool = False,
) -> BranchSpec:
...

调用方与被调用方#

StateGraph.add_conditional_edges
BranchSpec.from_path
├── path_map dict/list 归一化
├── Literal return type 推断
└── _get_branch_path_input_schema

输入、输出与状态变化#

项目类型说明
输入path已经被转成 Runnable 的 router。
输入path_map用户显式提供的映射。
输入infer_schema是否推断 router 输入 schema。
输出BranchSpec保存 path / ends / input_schema
状态变化返回不可变规格对象,不修改 builder。

细粒度伪代码#

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

@classmethod
def from_path(cls, path, path_map, infer_schema=False):
path_map_ = None
# 1. 用户传 dict:逻辑 label -> 实际节点名
if isinstance(path_map, dict):
path_map_ = path_map.copy()
# 2. 用户传 list:每个名字映射到自己
elif isinstance(path_map, list):
path_map_ = {name: name for name in path_map}
# 3. 用户未传 path_map:尝试从 Literal 返回类型推断
else:
func = extract_callable_from_runnable(path)
if func is not None:
return_type = get_type_hints(func).get("return")
if get_origin(return_type) is Literal:
path_map_ = {
name: name
for name in get_args(return_type)
}
# 4. 可选推断 router 输入 schema
if infer_schema:
input_schema = _get_branch_path_input_schema(path)
else:
input_schema = None
# 5. 构造 BranchSpec
return BranchSpec(
path=path,
ends=path_map_,
input_schema=input_schema,
)

逐段解释#

第 1 段是最明确的映射方式:

path_map={
"budget": "budget_node",
"attraction": "attraction_node",
"general": "general_node",
}

此时 router 可以返回业务 label,不必返回真实节点名。

第 2 段是简化形式:

path_map=["budget_node", "attraction_node"]

等价于:

{"budget_node": "budget_node", "attraction_node": "attraction_node"}

第 3 段是 Literal 推断:如果 router 写了:

def route(state) -> Literal["budget_node", "general_node"]:
...

且没有显式传 path_map,源码会尝试从 return type 中取出候选目标。

第 4 段用于 schema 推断。branch 本质也是一个 Runnable,它可以有自己的 input schema。这对复杂图、子图、可视化和 schema 校验有帮助。

正常路径#

path_map 显式 dict
复制成 ends
BranchSpec.ends 保存 label → node 映射

或者:

path_map None + router return Literal
从 Literal 提取候选目标
BranchSpec.ends 保存目标集合

关键分支与异常路径#

条件行为结果
path_map 是 dictcopy支持 label 到 node name 映射。
path_map 是 list{name: name}支持节点名白名单。
path_map is None 且 return 是 Literal从 Literal 推断可视化和校验更准确。
path_map is None 且无 Literalends=None运行仍可行,但可视化会假设可到任意节点。
类型提示解析失败捕获异常并跳过不阻断构建。

设计原因与工程影响#

path_mapLiteral 都不是为了让 router 能运行,它们主要解决两个工程问题:

1. 把 router 的逻辑返回值映射到真实节点名。
2. 让框架在编译、校验、可视化时知道可能的目标集合。

工程建议:

生产级 Router 尽量使用 Literal 或 path_map。
如果 router 返回业务枚举 label,则必须使用 path_map。
如果 router 直接返回节点名,也建议加 Literal,便于可视化和维护。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec.from_path
  • https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/_branch.py

7.5 _get_branch_path_input_schema() 源码解剖#

职责与所处阶段#

该函数尝试从 router callable 的第一个参数类型提示中推断 input schema。它服务于 branch 的输入读取与可视化,不改变 router 返回逻辑。

真实源码签名#

def _get_branch_path_input_schema(
path: Callable[..., Hashable | Sequence[Hashable]]
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
| Runnable[Any, Hashable | Sequence[Hashable]],
) -> type[Any] | None:
...

调用方与被调用方#

BranchSpec.from_path(infer_schema=True)
_get_branch_path_input_schema
get_type_hints / inspect.signature

输入、输出与状态变化#

项目类型说明
输入pathRunnable 或 callable。
输出`type[Any]None`
状态变化只返回推断结果。
副作用类型解析失败时静默跳过。

细粒度伪代码#

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

def _get_branch_path_input_schema(path):
input_schema = None
try:
callable_ = None
# 1. 如果 path 是 RunnableCallable / RunnableLambda,
# 尝试找到内部同步函数或异步函数
if isinstance(path, (RunnableCallable, RunnableLambda)):
callable_ = find_inner_func_or_afunc(path)
# 2. 如果 path 本身就是 callable
elif callable(path):
callable_ = path
# 3. 读取类型提示
if callable_ is not None:
hints = get_type_hints(callable_)
first_param = first_parameter_name(signature(callable_))
input_hint = hints.get(first_param)
# 4. 只有 input_hint 是可展开类型时才返回
if isinstance(input_hint, type) and get_type_hints(input_hint):
input_schema = input_hint
except (TypeError, StopIteration):
pass
return input_schema

逐段解释#

第 1 段说明 router 最终已被包装为 Runnable,因此源码需要从 Runnable 里反向找原始函数。普通教程只看到 route_by_intent(state),源码看到的是 RunnableCallable(func=route_by_intent)

第 3–4 段只读取第一个参数的类型提示。对 router 来说,第一个参数通常就是 state:

def route_by_intent(state: TravelState) -> Literal[...]:
...

如果 TravelState 是有字段注解的 TypedDict,它就可能被识别为 branch input schema。

正常路径#

route_by_intent(state: TravelState)
get_type_hints(route_by_intent)
first parameter = state
input_hint = TravelState
BranchSpec.input_schema = TravelState

关键分支与异常路径#

条件行为结果
path 是 RunnableLambda读取内部 func / afunc支持包装后的 router。
path 是普通 callable直接读取类型提示支持普通函数。
无类型提示返回 None默认使用 builder / source schema。
解析失败捕获并返回 None不影响 graph 构建。

设计原因与工程影响#

router 的 input schema 不是运行必需条件。即使没有 schema,router 也能读完整 state。源码把 schema 推断设计为“尽力而为”,避免类型提示错误导致构建失败。

工程影响:

给 router 补全 state 类型提示,主要提升可读性、校验和可视化质量。
不要依赖类型提示承担业务安全校验。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::_get_branch_path_input_schema

7.6 compile() 中 branch 挂接源码解剖#

职责与所处阶段#

compile() 把 builder 中保存的 branch specs 转换为 CompiledStateGraph 的运行时结构。条件边真正进入执行图,发生在 compiled.attach_branch(start, name, branch)

真实源码签名#

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,
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
...

调用方与被调用方#

用户 graph = builder.compile()
StateGraph.compile
self.validate
CompiledStateGraph(...)
compiled.attach_node(...)
compiled.attach_edge(...)
compiled.attach_branch(...)
compiled.validate()

输入、输出与状态变化#

项目类型说明
输入builder 内部 self.branches构建期保存的所有条件边。
输出CompiledStateGraph可执行 Pregel graph。
状态变化compiled graph nodes/channels/writersbranch 从 builder registry 变成 runtime writer。
副作用self.compiled=True后续修改 builder 会 warning。

细粒度伪代码#

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

def compile(self, ...):
# 1. 校验 graph 结构
self.validate(interrupt=...)
# 2. 构造 CompiledStateGraph / Pregel app
compiled = CompiledStateGraph(
builder=self,
channels={
**self.channels,
**self.managed,
START: EphemeralValue(self.input_schema),
},
input_channels=START,
output_channels=resolve_output_channels(...),
stream_channels=resolve_stream_channels(...),
...
)
# 3. 挂 START 和普通节点
compiled.attach_node(START, None)
for key, node in self.nodes.items():
compiled.attach_node(key, node)
# 4. 挂固定边
for start, end in self.edges:
compiled.attach_edge(start, end)
# 5. 挂条件边
for start, branches in self.branches.items():
for name, branch in branches.items():
compiled.attach_branch(start, name, branch)
return compiled.validate()

逐段解释#

第 1 段会验证图结构。对条件边来说,如果 BranchSpec.ends 已知,目标节点可以在编译期被校验。

第 5 段是本篇重点:条件边并不是存在 self.edges 中,而是单独存在 self.branches,编译时通过 attach_branch() 挂到 compiled graph。

正常路径#

self.branches["classify_intent"]["route_by_intent"]
compile()
compiled.attach_branch("classify_intent", "route_by_intent", BranchSpec)
source node 出口获得 branch writer

关键分支与异常路径#

条件行为结果
branch ends 中有未知节点validate() 抛错编译失败。
branch ends 为 None可视化认为可能到任意节点运行时仍可执行 router 返回节点名。
没有 START 入口validate() 抛错图无法运行。

设计原因与工程影响#

条件边被单独放在 self.branches,而不是混进 self.edges,是因为它不是固定拓扑:

add_edge:
构建期已经知道 start → end
add_conditional_edges:
构建期只知道 source → branch,
运行时才知道 branch → destination

工程影响:

如果想查图的静态边,只看 edges 不够,还要看 branches。
如果画图不准确,通常是 path_map / Literal 信息不足。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.compile

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

本章回答:

条件边注册和编译完成后,一次 graph.invoke() 中 router 是如何被调用,并如何触发目标节点的?

8.1 运行时入口#

调用方式公开入口核心内部入口返回类型
同步调用graph.invoke(input)Pregel.invoke() / branch _route()final state
异步调用graph.ainvoke(input)Pregel.ainvoke() / branch _aroute()final state
流式调用graph.stream(input)Pregel stream loopupdates / values / messages 等 chunk
批量调用graph.batch(inputs)Runnable batch 机制list[final state]

8.2 运行时总链路#

graph.invoke(input_state)
START 写入初始 state
source node 被触发
source node 返回 partial update
state channel 合并完成
branch reader 读取最新 state
BranchSpec._route 调用 path.invoke
BranchSpec._finish 规范化目标
ChannelWrite 写入目标 channel
Pregel 下一轮触发目标节点

8.3 CompiledStateGraph.attach_branch() 源码解剖#

职责与所处阶段#

attach_branch() 是条件边从 builder spec 进入运行时的关键函数。它把 BranchSpec 转成 source node 的 writer,使 source node 执行完成后可以触发 branch router。

真实源码签名#

def attach_branch(
self,
start: str,
name: str,
branch: BranchSpec,
*,
with_reader: bool = True,
) -> None:
...

调用方与被调用方#

StateGraph.compile
CompiledStateGraph.attach_branch
├── get_writes
├── ChannelRead.do_read
├── branch.run
└── source node writer append

输入、输出与状态变化#

项目类型说明
输入startsource node 名称。
输入namebranch 名称。
输入branchBranchSpec
输出None修改 compiled graph 内部结构。
状态变化source node writers / branch channels让 source node 退出后执行 branch。
副作用新增 channel 写入路径为目标节点触发写入创建 channel entry。

细粒度伪代码#

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

def attach_branch(self, start, name, branch, with_reader=True):
# 1. 定义 destinations -> channel writes 的转换函数
def get_writes(packets, static=False):
writes = []
for packet in packets:
if isinstance(packet, Send):
writes.append(packet)
else:
if static or packet != END:
channel = (
packet
if packet == END
else f"branch:to:{packet}"
)
writes.append(ChannelWriteEntry(channel, None))
return writes
# 2. 为 branch 准备 reader:运行时读取最新 state
if with_reader:
schema = branch.input_schema or source_node_input_schema(start)
channels = channels_for_schema(schema)
mapper = mapper_for_schema(schema)
reader = partial(
ChannelRead.do_read,
select=channels,
fresh=True,
mapper=mapper,
)
else:
reader = None
# 3. 把 branch.run(...) 包成可写 channel 的 Runnable
branch_runnable = branch.run(
writer=get_writes,
reader=reader,
)
# 4. 将 branch_runnable 挂到 source node 的 writers
self.nodes[start].writers.append(
ChannelWriteTupleEntry(
mapper=branch_runnable,
)
)

逐段解释#

第 1 段定义 get_writes()。router 返回的是目标标识,但 Pregel 运行时需要的是 channel write。get_writes() 负责把目标标识变成:

branch:to:{target_node}

第 2 段准备 reader。条件边必须读取 source node 执行后的最新 state,所以 reader 使用 fresh=True。这是“router 能看到 classify_intent 刚写入的 intent”的关键。

第 3 段调用 branch.run(writer, reader),生成一个 RunnableCallable。这个 RunnableCallable 内部会执行 _route()_aroute()

第 4 段把 branch writer 挂到 source node 的 writers。source node 完成后,不仅写 state update,还会执行 branch writer。

正常路径#

source node 完成
source node writers 被执行
branch.run 包装的 writer 被调用
_route 读取 fresh state 并运行 router
write 写入 branch:to:target

关键分支与异常路径#

条件行为结果
with_reader=True读取最新 staterouter 能看到 source update。
branch.input_schema 存在按 branch schema 读 channels支持 router 独立输入 schema。
branch.ends 存在可预先建立目标映射可视化更准确。
目标为 END不写普通目标 channel当前路径终止。
packet 是 Send直接保留 Send支持动态 state slice 分发。

设计原因与工程影响#

条件边不是在 source node 函数内部被调用,而是挂在 source node 的 writer 阶段。这样设计带来两个结果:

1. source node 仍只负责返回 partial update。
2. router 永远在 source update 合并之后读取 state。

工程影响:

router 可以安全读取 source 节点刚写入的字段。
但是 router 不应该返回 state update,因为它的职责是返回 destination。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::CompiledStateGraph.attach_branch

8.4 BranchSpec.run() 源码解剖#

职责与所处阶段#

BranchSpec.run() 把 branch 的 _route() / _aroute() 包装成一个可注册 writer 的 RunnableCallable,并携带静态 ends 信息。

真实源码签名#

def run(
self,
writer: _Writer,
reader: Callable[[RunnableConfig], Any] | None = None,
) -> RunnableCallable:
...

调用方与被调用方#

CompiledStateGraph.attach_branch
BranchSpec.run
ChannelWrite.register_writer
RunnableCallable(func=_route, afunc=_aroute)

输入、输出与状态变化#

项目类型说明
输入writerdestinations 到 channel writes 的转换器。
输入reader运行时读取最新 state 的函数。
输出RunnableCallable可挂到 source writer 的 branch runnable。
状态变化返回包装对象。

细粒度伪代码#

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

def run(self, writer, reader=None):
# 1. 创建 RunnableCallable,运行时调用 _route/_aroute
branch_callable = RunnableCallable(
func=self._route,
afunc=self._aroute,
writer=writer,
reader=reader,
name=None,
trace=False,
)
# 2. 如果 ends 已知,注册静态 writer 信息
if self.ends:
static_writes = writer(
[end for end in self.ends.values()],
static=True,
)
labels = [str(label) for label in self.ends.keys()]
writer_metadata = zip_longest(static_writes, labels)
else:
writer_metadata = None
# 3. 返回注册 writer 后的 RunnableCallable
return ChannelWrite.register_writer(
branch_callable,
writer_metadata,
)

逐段解释#

第 1 段生成真正的运行时 branch runnable。同步图调用走 _route(),异步图调用走 _aroute()

第 2 段只在 ends 已知时发生。ends 来自 path_mapLiteral 推断。它的作用主要是让运行时和可视化系统知道这个 branch 可能写向哪些目标。

第 3 段通过 ChannelWrite.register_writer() 把 branch callable 和 channel write 元信息绑定起来。

正常路径#

BranchSpec(path=route_by_intent, ends={...})
run(writer, reader)
RunnableCallable(_route/_aroute)
source node writer 阶段被执行

关键分支与异常路径#

条件行为结果
self.ends 存在注册静态 writes可视化和静态分析更准确。
self.ends 不存在writer metadata 为 None运行时仍可根据返回值动态写入。
同步执行调用 _route()普通 graph.invoke() 路径。
异步执行调用 _aroute()graph.ainvoke() 路径。

设计原因与工程影响#

run() 把“branch 逻辑”封装成 RunnableCallable,这让条件边也可以融入 Runnable / Pregel 的统一运行协议。业务层看起来只是一个 router 函数,运行时看到的是一个带 writer / reader 的 Runnable。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec.run

8.5 BranchSpec._route()_aroute() 源码解剖#

职责与所处阶段#

_route() 是同步条件路由主链,_aroute() 是异步条件路由主链。它们负责读取最新 state,调用 router,并把 router 返回值交给 _finish()

真实源码签名#

def _route(
self,
input: Any,
config: RunnableConfig,
*,
reader: Callable[[RunnableConfig], Any] | None,
writer: _Writer,
) -> Runnable:
...
async def _aroute(
self,
input: Any,
config: RunnableConfig,
*,
reader: Callable[[RunnableConfig], Any] | None,
writer: _Writer,
) -> Runnable:
...

调用方与被调用方#

source node writer
RunnableCallable
BranchSpec._route / _aroute
reader(config)
self.path.invoke / ainvoke
self._finish

输入、输出与状态变化#

项目类型说明
输入inputsource node 的输出或运行时传入值。
输入configRunnableConfig。
输入reader可读取 fresh state 的函数。
输入writerdestination 写入转换器。
输出`RunnableAny`
状态变化无直接 state update通过 writer 写目标 channel。

细粒度伪代码#

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

def _route(self, input, config, *, reader, writer):
# 1. 如果有 reader,优先读取最新 state
if reader is not None:
value = reader(config)
# 2. dict state 场景下,透传 input 的额外 key
if (
isinstance(value, dict)
and isinstance(input, dict)
and self.input_schema is None
):
value = {**input, **value}
else:
# 3. 没有 reader 时,直接用 input 调用 router
value = input
# 4. 调用 router path
result = self.path.invoke(value, config)
# 5. 处理 router 返回值并写入目标 channel
return self._finish(
writer=writer,
input=input,
result=result,
config=config,
)

异步版本只在第 4 步不同:

async def _aroute(self, input, config, *, reader, writer):
if reader is not None:
value = reader(config)
if isinstance(value, dict) and isinstance(input, dict) and self.input_schema is None:
value = {**input, **value}
else:
value = input
result = await self.path.ainvoke(value, config)
return self._finish(writer, input, result, config)

逐段解释#

第 1 段是条件边能读到最新 state 的关键。router 不是用旧输入判断,而是通过 reader(config) 读取经过 source node 更新后的 state。

第 2 段处理 dict passthrough。source node 可能返回一些额外信息,或者 input 中有额外字段。若 branch 没有独立 input schema,源码会尝试把 input 和 reader value 合并,让 router 有机会看到更多上下文。

第 4 段说明 router 也被当作 Runnable 执行。这就是为什么普通函数、async 函数、Runnable 都能作为 path

第 5 段把 router 返回值交给 _finish(),而不是 _route() 自己决定目标写入。这样运行和结果规范化职责被拆开。

正常路径#

reader 读取最新 TravelState
route_by_intent.invoke(state)
返回 "budget_node"
_finish 规范化并写入目标 channel

关键分支与异常路径#

条件行为结果
有 reader读取 fresh staterouter 看见最新 state。
无 reader使用 input适用于直接 branch 输入。
input/value 都是 dict 且无 input_schema合并 {**input, **value}透传额外字段。
router 是 async_aroute()ainvoke()支持异步路由。
router 抛异常异常向上冒泡图执行失败,除非上层 retry/error handler 处理。

设计原因与工程影响#

_route() 不是节点函数,不返回 state update。它的唯一职责是把 state 映射成 destination。

工程影响:

router 应该保持纯函数风格:state -> destination。
不要在 router 中调用外部副作用操作。
复杂判断可以在上一个节点写入结构化字段,再由 router 读取字段。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._route
  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._aroute

8.6 BranchSpec._finish() 源码解剖#

职责与所处阶段#

_finish() 是条件边运行时结果处理的核心。它负责把 router 返回值规范成 destinations,并通过 writer 写入目标节点 channel。

真实源码签名#

def _finish(
self,
writer: _Writer,
input: Any,
result: Any,
config: RunnableConfig,
) -> Runnable | Any:
...

调用方与被调用方#

BranchSpec._route / _aroute
BranchSpec._finish
├── normalize result to list
├── map labels through self.ends
├── validate destinations
└── ChannelWrite.do_write / ChannelWrite(entries)

输入、输出与状态变化#

项目类型说明
输入resultrouter 返回值。
输入writerdestinations → writes 转换器。
输出inputChannelWrite取决于是否需要 passthrough。
状态变化目标 channel 写入触发下一轮目标节点。
副作用ChannelWrite.do_write写 Pregel channel。

细粒度伪代码#

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

def _finish(self, writer, input, result, config):
# 1. router 可以返回单个目标,也可以返回 list/tuple
if not isinstance(result, (list, tuple)):
result = [result]
# 2. 如果 ends 存在,先把逻辑 label 映射为真实 destination
if self.ends:
destinations = []
for r in result:
if isinstance(r, Send):
destinations.append(r)
else:
destinations.append(self.ends[r])
else:
destinations = result
# 3. 目标合法性校验:不能是 None,不能是 START
for dest in destinations:
if dest is None or dest == START:
raise ValueError(
"Branch did not return a valid destination"
)
# 4. Send 不能发送到 END
for dest in destinations:
if isinstance(dest, Send) and dest.node == END:
raise InvalidUpdateError(
"Cannot send a packet to the END node"
)
# 5. 把 destinations 转换为 channel writes
entries = writer(destinations, static=False)
# 6. 如果没有可写 entry,返回原 input
if not entries:
return input
# 7. 如果 entries 需要 passthrough,返回 ChannelWrite 对象
if any(entry.value is PASSTHROUGH for entry in entries):
return ChannelWrite(entries)
# 8. 否则直接执行 channel write
ChannelWrite.do_write(config, entries)
return input

逐段解释#

第 1 段解释为什么 router 可以返回单个节点名,也可以返回多个节点名。源码会把单个返回值包装为列表,后续统一处理。

第 2 段是 path_map 的运行时作用。如果 self.ends 存在,router 返回的不是最终节点名,而是 key,源码会执行:

self.ends[r]

例如:

path_map={"budget": "budget_node"}
route() -> "budget"
最终 destination = "budget_node"

第 3 段说明 router 不允许返回 STARTSTART 是入口 channel,不是运行中可跳回的普通节点。

第 4 段说明 Send 不能发送到 ENDSend 表示“给某个节点发送 packet/state slice”,而 END 不是可执行节点,不能接收 packet。

第 5–8 段说明 _finish() 的最终结果不是直接调用目标节点,而是写 channel。目标节点由 Pregel 在后续 super-step 中触发。

正常路径#

route_by_intent -> "budget_node"
result = ["budget_node"]
destinations = ["budget_node"]
writer(destinations)
ChannelWriteEntry("branch:to:budget_node", None)
Pregel 触发 budget_node

关键分支与异常路径#

条件行为结果
result 是单值包装为 list统一处理。
result 是 list/tuple原样多目标处理可能并行触发多个目标。
self.ends 存在按 key 映射到 node支持业务 label。
destination 是 NoneSTARTValueError非法分支。
Send(node=END)InvalidUpdateErrorEND 不能接收 packet。
writer 返回空返回 input例如只返回 END 的路径。
需要 passthrough返回 ChannelWrite交给上层处理。
普通写入ChannelWrite.do_write触发目标 channel。

设计原因与工程影响#

_finish() 把 router 的业务返回值和 Pregel 的 channel 写入隔离开。业务只关心:

下一步去哪个节点?

运行时关心:

应该写哪个 channel?
是否需要触发多个节点?
是否终止?

工程影响:

router 不要直接调用目标节点。
router 返回值必须是可映射到节点的 hashable 值、节点名、END 或 Send。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish

8.7 运行时主链总结#

TravelState
classify_intent 写入 intent
Branch reader 读取 fresh state
route_by_intent.invoke(state)
_finish 映射目标
ChannelWrite 写 branch:to:{node}
Pregel 调度目标节点
目标节点返回 partial update
state 合并并最终输出

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

本章回答:

当输入、执行模式或运行结果偏离正常主链时,框架如何分流、恢复、终止或失败?

9.1 分支矩阵#

分支类型触发条件核心函数结果
直接节点名router 返回 "node_a"_finish()写入 branch:to:node_a
业务 labelrouter 返回 "budget"path_map 存在_finish()self.ends["budget"] -> "budget_node"
ENDrouter 返回 END_finish() / writer当前路径终止。
多目标router 返回 list[str]_finish()多个目标 channel 写入。
动态 Sendrouter 返回 Send(...)_finish()发送 packet 到目标节点。
非法 STARTrouter 返回 START_finish()ValueError
未知 path_map keyrouter 返回 key 不在 self.ends_finish()映射时抛出异常。

9.2 同步与异步分支#

维度同步路径异步路径
入口graph.invoke()graph.ainvoke()
branch 函数_route()_aroute()
router 调用self.path.invoke(value, config)await self.path.ainvoke(value, config)
资源语义阻塞同步调用可 await 异步 router
失败行为异常同步抛出异常在 await 时抛出

9.3 END 终止路径源码解剖#

职责与所处阶段#

END 是条件边的特殊 destination。router 返回 END 表示当前路径不再触发任何普通节点。

真实源码签名#

END 处理发生在 BranchSpec._finish()CompiledStateGraph.attach_branch().get_writes() 中。

调用方与被调用方#

router -> END
_finish
writer([END], static=False)
get_writes 过滤 END
无普通 channel write
当前路径结束

输入、输出与状态变化#

项目类型说明
输入END特殊终止常量。
输出通常无普通目标 write不触发后续节点。
状态变化只终止控制流。

细粒度伪代码#

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

def route_or_end(state):
if state["result"] is not None:
return END
return "next_node"
result = route_or_end(state)
if not isinstance(result, (list, tuple)):
result = [result]
writes = []
for dest in result:
if dest == END:
# 运行时 END 不写普通 target channel
continue
writes.append(ChannelWriteEntry(f"branch:to:{dest}", None))
if not writes:
return input

逐段解释#

END 不代表一个会被执行的节点,它是“没有后续目标”的控制标记。因此运行时不会把 END 当作普通目标节点触发。

正常路径#

route -> END
不触发任何普通目标节点
当前活跃路径结束

关键分支与异常路径#

条件行为结果
返回 END不写普通目标 channel正常终止。
返回 Send(END, ...)InvalidUpdateErrorEND 不能接收 packet。
path_map 中映射到 END允许label 可映射到终止。

设计原因与工程影响#

END 作为控制标记,而不是普通节点,能让图终止逻辑保持统一:没有新的目标节点被触发,运行时自然收敛。

源码证据#

  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edges docstring
  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish

9.4 path_map 映射路径源码解剖#

职责与所处阶段#

path_map 让 router 返回业务 label,而不是节点名。它使业务路由语义和图节点名称解耦。

真实源码签名#

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

调用方与被调用方#

add_conditional_edges(..., path_map={...})
BranchSpec.from_path
BranchSpec.ends
BranchSpec._finish
self.ends[result_key]

输入、输出与状态变化#

项目类型说明
输入router label"budget"
输入path_map{ "budget": "budget_node" }
输出target node"budget_node"
状态变化只改变控制流目标。

细粒度伪代码#

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

def route_by_intent(state) -> Literal["budget", "attraction", "general"]:
if state["intent"] == "budget_planning":
return "budget"
if state["intent"] == "attraction_planning":
return "attraction"
return "general"
builder.add_conditional_edges(
"classify_intent",
route_by_intent,
path_map={
"budget": "budget_node",
"attraction": "attraction_node",
"general": "general_node",
},
)
# _finish 中
logical_result = "budget"
actual_target = self.ends[logical_result] # "budget_node"
writer([actual_target], False)

逐段解释#

router 返回值 "budget" 是业务枚举,节点名 "budget_node" 是图拓扑标识。path_map_finish() 中完成二者映射。

正常路径#

intent = budget_planning
route -> "budget"
self.ends["budget"] -> "budget_node"
触发 budget_node

关键分支与异常路径#

条件行为结果
label 存在于 path_map映射为 node正常路由。
label 不存在映射失败运行时异常。
path_map 是 listlabel 必须等于节点名简化白名单。
path_map 目标是 END允许可用业务 label 表示结束。

设计原因与工程影响#

path_map 的最大价值是稳定业务语义。你可以改节点名,但不改 router 返回枚举。

工程建议:

如果 router 返回的是 intent 枚举,使用 path_map。
如果 router 直接返回节点名,使用 Literal。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec.from_path
  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish

9.5 Literal 类型提示与可视化边界源码解剖#

职责与所处阶段#

Literal 类型提示让 LangGraph 在没有 path_map 时推断可能目标集合,尤其服务于图校验、渲染和维护。

真实源码签名#

Literal 推断发生在 BranchSpec.from_path() 中。

调用方与被调用方#

router return annotation
get_type_hints(func).get("return")
get_origin(return_type) is Literal
get_args(return_type)
path_map_ = {name: name for name in args}

输入、输出与状态变化#

项目类型说明
输入return typeLiteral["a", "b", END]
输出ends{ "a": "a", "b": "b", END: END }
状态变化BranchSpec.ends保存候选目标。

细粒度伪代码#

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

def route(state) -> Literal["budget_node", "general_node", "__end__"]:
...
if path_map is None:
func = extract_func(path)
return_type = get_type_hints(func).get("return")
if get_origin(return_type) is Literal:
choices = get_args(return_type)
path_map_ = {choice: choice for choice in choices}

逐段解释#

当没有 path_map 时,源码仍尝试从 router 的 return annotation 中获得候选目标。这不是 Python 运行时必须的信息,而是框架静态分析信息。

官方 warning 的含义是:如果既没有 path_map,也没有 Literal,图可视化无法知道 router 可能去哪里,只能保守认为它可能跳到任意节点。

正常路径#

route return Literal["budget_node", "general_node"]
BranchSpec.ends = {
"budget_node": "budget_node",
"general_node": "general_node",
}

关键分支与异常路径#

条件行为结果
Literal推断 ends可视化更准确。
Literal 但有 path_map使用 path_map同样准确。
二者都无ends=None可视化假设任意节点。
Literal 中目标不存在compile validate 可能失败提前发现错误。

设计原因与工程影响#

类型提示在这里不是为了 Python 类型安全,而是为了图结构可解释性。

工程影响:

Router 文章、博客、团队代码都应保留 Literal。
这能让 Mermaid 图、review 和调试更直观。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec.from_path
  • libs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edges warning

9.6 多目标返回与并行触发#

router 可以返回多个目标:

def route_parallel(state) -> list[str]:
targets = []
if state["need_budget"]:
targets.append("budget_node")
if state["need_weather"]:
targets.append("weather_node")
return targets or [END]

运行时关键路径:

result = ["budget_node", "weather_node"]
_finish 不再包装,因为已经是 list
writer 为两个 destination 生成两个 ChannelWriteEntry
Pregel 后续触发两个节点

边界:

多个目标节点如果写不同 state key,比较安全。
多个目标节点如果写同一个 state key,必须设计 reducer。

9.7 异常分类#

异常类别抛出位置是否可恢复处理策略是否反馈上层
source 不存在validate()修正图构建
target 不存在validate() 或运行时 _finish()补节点或 path_map
router 返回 START_finish()修正 router
Send(END)_finish()改为返回 END
path_map key 不存在_finish()视业务而定增加 default / fallback
router 内部异常path.invoke()视业务而定retry / fallback / error node
并行写冲突downstream state merge视 reducer 而定加 reducer 或拆字段

9.8 Retry、Fallback 与恢复边界#

机制适用条件不适用条件幂等要求
Retryrouter 内部调用 LLM 或远程分类服务偶发失败router 是纯代码判断且逻辑错误router 不应有副作用。
Fallbackintent 不确定、分类结果不在枚举目标节点缺失fallback 节点应安全。
Repair上游结构化输出字段非法router 本身返回非法节点名repair 应发生在 router 之前。
Human handoff多次路由失败或高风险分支普通低风险分支需要 checkpoint / interrupt 支持。

9.9 停止条件与保护上限#

正常结束:router 返回 END,或所有活跃目标最终到达 END。
提前结束:middleware / Command / error handler 将流程导向 END。
人工中断:后续 HITL 篇通过 interrupt_before / interrupt_after 处理。
框架保护:循环图依赖 recursion_limit 防止无限执行。
异常失败:router 返回非法目标、path_map key 不存在或 router 抛出未处理异常。

9.10 能力边界#

容易误判的能力实际提供者本篇对象的真实职责
状态合并channel / reducer只决定下一个目标,不合并 state。
节点执行Pregel node runtime只写目标 channel,不直接调用目标函数。
动态 state slice 分发Send普通字符串路由只触发节点,不携带独立 packet。
节点内 goto + updateCommand条件边是 source 退出后的外部 branch。
循环保护Pregel runtime recursion_limit条件边可以形成循环,但不负责限制循环次数。

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

本章回答:

框架允许在哪里插入自定义行为,它如何与相邻模块协作,哪些内部实现不应该被业务代码依赖?

10.1 扩展点总览#

扩展点扩展方式执行时机可修改内容约束
path_mapdict/list构建期 + 运行时映射label 到 nodekey 必须覆盖 router 返回。
Literal类型提示构建期推断可视化目标集合不替代运行时校验。
async routerasync callable运行时异步决策异常仍向上抛。
multi-target返回 list/tuple运行时多节点触发state 合并需 reducer。
Send返回 Send运行时动态 state slice不可发送到 END
Command节点返回 Command节点运行时state update + goto不是 add_conditional_edges 机制。

10.2 条件边与 Send 协作源码解剖#

职责与所处阶段#

Send 让 router 不只是返回“去哪个节点”,还可以给目标节点发送不同的输入 packet,常用于 map-reduce。

真实源码签名#

Sendlanggraph.types 中的控制流对象,条件边对它的处理发生在 BranchSpec._finish()attach_branch().get_writes()

调用方与被调用方#

router returns [Send("worker", item_state), ...]
BranchSpec._finish
writer(destinations)
Send entries 被 Pregel runtime 处理

输入、输出与状态变化#

项目类型说明
输入Send(node, arg)动态目标和输入。
输出Pregel send packet触发目标节点并携带 state slice。
状态变化目标节点后续写 state通常配合 reducer 合并。

细粒度伪代码#

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

from langgraph.types import Send
def route_search_tasks(state):
sends = []
for query in state["search_queries"]:
sends.append(
Send(
"search_worker",
{"query": query},
)
)
return sends
# _finish 中
if not isinstance(result, (list, tuple)):
result = [result]
if self.ends:
destinations = [
r if isinstance(r, Send) else self.ends[r]
for r in result
]
else:
destinations = result
for dest in destinations:
if isinstance(dest, Send) and dest.node == END:
raise InvalidUpdateError(
"Cannot send a packet to the END node"
)
entries = writer(destinations, False)
ChannelWrite.do_write(config, entries)

逐段解释#

普通条件边返回 "node_a" 表示触发节点读取全局 state;Send("node_a", local_state) 表示用独立输入 packet 触发节点。

Send 不能发到 END,因为 END 不是可运行节点,无法接收 packet。

正常路径#

route_search_tasks -> [Send("search_worker", q1), Send("search_worker", q2)]
Pregel 调度多个 search_worker task
worker 结果通过 reducer 合并

关键分支与异常路径#

条件行为结果
返回 Send("worker", state)动态触发 worker支持 map。
返回多个 Send多 task 并行需要 reducer。
Send(END, ...)InvalidUpdateError非法。
普通字符串和 Send 混合都交给 writer高级用法,需谨慎。

设计原因与工程影响#

Send 是条件边的高级扩展。它让 branch 从“选择路径”升级为“动态创建任务”。

工程影响:

普通 Router 用字符串。
需要 map-reduce 才用 Send。
使用 Send 时,提前设计 reducer 和结果聚合字段。

源码证据#

  • libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish

10.3 条件边与 Command 的边界#

职责与所处阶段#

Command 是节点返回值层面的控制流对象,允许一个 node 同时返回 state update 和 goto。它和 conditional edge 都能改变控制流,但处于不同层。

调用链#

Conditional Edge:
source node returns partial update
branch router reads updated state
router returns destination
Command:
node itself returns Command(update=..., goto=...)
CompiledStateGraph.attach_node extracts update and control branch
直接写 state update 和 goto

细粒度伪代码#

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

# 条件边风格
def classify_intent(state):
return {"intent": "budget"}
def route_by_intent(state):
return "budget_node"
builder.add_node("classify_intent", classify_intent)
builder.add_conditional_edges("classify_intent", route_by_intent)
# Command 风格
from langgraph.types import Command
def classify_and_route(state) -> Command[Literal["budget_node", "general_node"]]:
if "预算" in state["user_request"]:
return Command(
update={"intent": "budget"},
goto="budget_node",
)
return Command(
update={"intent": "general"},
goto="general_node",
)

逐段解释#

条件边把“写状态”和“选路径”拆成两个函数,更适合 Workflow Agent 的可解释流程。

Command 把“写状态”和“选路径”合并到一个节点返回值里,更适合强控制、紧耦合、节点内部已确定下一步的场景。

设计原因与工程影响#

需要清晰可视化流程:优先 conditional edge。
节点天然同时产生 update 和 goto:可以用 Command。
复杂业务工作流:conditional edge 更利于调试与评估。

10.4 条件边与结构化输出协作#

结构化输出常常发生在 router 之前:

LLM intent classifier
Pydantic IntentResult
写入 state.intent_result
route_by_intent_result(state)
目标节点

推荐模式:

class IntentResult(BaseModel):
intent: Literal[
"budget_planning",
"attraction_planning",
"general_planning",
"unsupported",
]
confidence: float
need_clarification: bool
def route_by_intent_result(state) -> Literal[
"budget_node",
"attraction_node",
"general_node",
"ask_user_node",
"unsupported_node",
]:
result = state["intent_result"]
if result.need_clarification:
return "ask_user_node"
if result.intent == "budget_planning":
return "budget_node"
if result.intent == "attraction_planning":
return "attraction_node"
if result.intent == "unsupported":
return "unsupported_node"
return "general_node"

工程原则:

LLM 负责生成结构化判断;
router 负责用代码做确定性分支;
不要让 router 再调用 LLM 做复杂推理。

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

相邻组件输入协议输出协议协作边界
OutputParser / with_structured_output文本 / messagesPydantic / dict生成 router 所需字段。
add_nodestatepartial update先写入路由字段。
add_conditional_edgesstatedestination决定下一步。
Reducer多节点 update合并 state处理并行分支写冲突。
Senddynamic packetworker input支持 map-reduce。
Commandnode returnupdate + goto替代部分条件边场景。
ENDdestinationterminate终止路径。

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

业务代码可以依赖:
StateGraph.add_conditional_edges()
START / END
path_map
Literal return annotation
Send
Command
业务代码避免依赖:
BranchSpec._route()
BranchSpec._finish()
branch:to:{node} channel 名称
self.branches 内部结构
ChannelWriteEntry 内部细节

10.7 自定义扩展示例#

示例只展示扩展契约,不重新实现框架:

from typing_extensions import Literal
class TravelState(TypedDict):
intent: str | None
error_count: int
need_human: bool
def route_with_error_guard(state: TravelState) -> Literal[
"normal_node",
"retry_node",
"human_node",
"__end__",
]:
if state["need_human"]:
return "human_node"
if state["error_count"] >= 3:
return "__end__"
if state["error_count"] > 0:
return "retry_node"
return "normal_node"

说明:

  1. 扩展点接收当前 state。
  2. 扩展点允许修改控制流目标,不允许直接修改 state。
  3. 扩展点必须返回合法 destination、END 或 destination 列表。
  4. 异常会向上抛出,除非外层 runtime 策略处理。
  5. 如果返回多个目标,需要考虑 reducer。

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

条件选择条件边选择更底层机制
只根据 state 选择下一节点
需要 label 到 node name 映射
需要一对多并行分支视情况使用 Send
需要节点同时 update + gotoCommand
需要动态创建多个 worker 输入Send
需要人审中断条件边可导向人审节点还需 interrupt/checkpoint

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

11.1 适用场景#

场景是否推荐原因
意图路由先结构化识别 intent,再用条件边进入不同节点。
状态机分支根据 state 字段确定下一步,稳定可测。
错误恢复路由根据 error_count / last_error_type 进入 retry/fallback/human。
Reflection loop根据 evaluator 结果回到 revise 或 END。
Tool-use ReAct loop视情况标准 ReAct 可用 create_agent / ToolNode,自定义流程可用条件边。
大规模 map-reduce仅普通条件边不够需要配合 Send 和 reducer。
高风险动作审批是,但需 HITL条件边导向 approval node,再配合 interrupt/checkpoint。

11.2 工程决策表#

决策点推荐选择前提风险
router 返回节点名还是业务 label简单图返回节点名;复杂业务返回 label + path_maplabel 与节点名需要解耦path_map key 漏配会运行失败。
是否写 Literal推荐写目标集合稳定忘记包含 END 会影响可视化/类型提示。
是否在 router 调 LLM不推荐除非必须实时推理难测、慢、失败路径复杂。
多目标返回谨慎使用有 reducer 或不同字段多节点写冲突。
错误路由推荐显式错误字段上游节点写 last_error异常不应被静默吞掉。
循环分支必须设置 recursion_limit 并设计退出条件有反思/修正循环无限循环。

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

性能:router 最好是纯 Python 判断,避免在条件边里调用 LLM 或远程接口。
可靠性:router 返回值必须被 Literal 或 path_map 收敛,避免任意字符串目标。
安全:高风险分支必须导向审批节点,不能直接进入写操作节点。
可观测性:记录 source node、router 名称、router 输入关键字段、router 返回值、最终目标节点。

11.4 旅行规划助手推荐路由设计#

intent_node
route_by_intent
├── ask_user_node
├── budget_planner_node
├── attraction_planner_node
├── route_planner_node
├── weather_node
└── unsupported_node

推荐 state:

class TravelState(TypedDict):
user_request: str
intent_result: IntentResult | None
plan: TravelPlan | None
last_error: str | None
retry_count: int
result: str | None

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

12.1 误区:add_conditional_edges() 会立即执行 router#

错误原因:API 名称里有 edges,容易让人误以为注册时已经得到目标边。

源码事实:

add_conditional_edges 只把 path 转成 Runnable,构造 BranchSpec,保存到 self.branches。
真正执行 router 发生在运行时 BranchSpec._route。

工程影响:

如果 router 依赖运行时 state,就不能在构建期验证所有业务逻辑。需要通过测试样本覆盖 router 分支。

12.2 误区:router 返回的是 state update#

错误原因:LangGraph 节点函数返回 dict 更新 state,router 函数也接收 state,容易混淆。

源码事实:

node 返回 partial update,进入 state updater。
router 返回 destination,进入 BranchSpec._finish 的 destination writer。

工程影响:

不要写:

def route(state):
return {"intent": "budget"}

应该写:

def route(state):
return "budget_node"

12.3 误区:没有 path_map 也完全没问题#

错误原因:简单 demo 中 router 直接返回节点名,确实可以运行。

源码事实:

没有 path_map 且没有 Literal 时,BranchSpec.ends=None。
运行时仍能使用返回值作为目标,但可视化会保守认为可能到任意节点。

工程影响:

大型图里不写 path_map / Literal 会导致图可视化混乱,review 困难。

12.4 误区:返回多个节点就是自动安全并行#

错误原因:条件边支持 Sequence[Hashable],看起来可以随便返回多个节点。

源码事实:

_finish 会为多个 destinations 生成多个 channel writes。
并行目标节点后续 state update 如何合并,不由 conditional edge 负责。

工程影响:

多个目标节点写同一个字段时,必须设计 reducer;否则会出现状态覆盖或冲突。

12.5 误区:END 是一个普通节点#

错误原因:代码里 add_edge("node", END) 看起来像连到某个节点。

源码事实:

END 是终止标记,不是可执行节点。
get_writes 在运行时不会把 END 当作普通 branch target channel。

工程影响:

不要尝试定义名为 END 的节点,也不要 Send(END, ...)

12.6 误区:router 里适合写复杂 LLM 推理#

错误原因:Router Agent 概念容易让人把 router 本身写成 LLM 调用。

源码事实:

router 是 branch path Runnable,理论上可以是 LLM chain,
但它处在控制流关键路径上,异常和延迟会直接影响图调度。

工程影响:

更推荐在前置节点完成 LLM 结构化分类,把结果写入 state,然后 router 用纯代码判断。


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

13.1 构建期心智模型#

source + path + path_map
coerce_to_runnable(path)
BranchSpec.from_path
self.branches[source][name]
compile()
attach_branch()
source node writer 获得 branch runnable

13.2 运行时心智模型#

source node partial update
state channel 合并
branch reader 读取 fresh state
path.invoke(state)
返回 destination / destinations / END / Send
_finish 映射并校验
ChannelWrite 写目标 channel
Pregel 调度目标节点

13.3 分支与异常心智模型#

正常路径 → 返回合法目标节点,触发后续节点。
END 路径 → 不触发普通目标,当前路径终止。
多目标路径 → 写多个目标 channel,后续节点并行,state 合并交给 reducer。
path_map 路径 → router 返回 label,_finish 映射为实际节点。
可恢复异常 → 上游写 error state,router 导向 retry/fallback。
不可恢复异常 → router 抛错或返回非法目标,图执行失败。
保护上限 → 如果条件边形成循环,依赖 recursion_limit 防止无限执行。

13.4 一句话总结#

Conditional Edge 通过构建期的 BranchSpec 保存 router 与目标映射,编译期通过 attach_branch() 把 branch 挂到 source node 的 writer,运行时在 source node 更新 state 后读取 fresh state 调用 router,再把返回的 destination 写入目标 channel,从而用 Pregel 调度实现 Workflow Agent 的动态分支。

13.5 掌握检查#

  • 能说清 add_conditional_edges() 只注册 branch,不运行 router。
  • 能画出 source node -> state update -> branch reader -> path.invoke -> _finish -> ChannelWrite 链路。
  • 能解释 path_map 的构建期和运行时作用。
  • 能解释 Literal 为什么影响可视化和校验。
  • 能解释 router 返回 END 时为什么不会执行普通节点。
  • 能解释 router 返回多个节点时为什么需要 reducer。
  • 能区分 add_edgeadd_conditional_edgesCommandSend
  • 能指出 BranchSpec._route()_finish() 的职责边界。
  • 能为旅行规划助手写一个纯代码 router,并说明每个目标节点的业务含义。
  • 能判断什么时候该用 path_map,什么时候直接返回节点名。

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

14.1 官方概念文档#

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

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

  3. LangGraph overview
    https://docs.langchain.com/oss/python/langgraph/overview

  4. LangGraph Pregel runtime
    https://docs.langchain.com/oss/python/langgraph/pregel

14.2 官方 API Reference#

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

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

  3. Graphs API Reference
    https://reference.langchain.com/python/langgraph/graphs

  4. Send
    https://reference.langchain.com/python/langgraph/types/Send

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

14.3 官方源码#

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

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

  3. libs/langgraph/langgraph/graph/_branch.py::BranchSpec._route
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/_branch.py

  4. libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/_branch.py

  5. libs/langgraph/langgraph/graph/state.py::CompiledStateGraph.attach_branch
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/state.py

14.4 下一篇衔接#

下一篇进入:

第 8 篇:Reducer 与并行状态合并源码解剖

需要继续回答:

多个节点同时写同一个 state key 时,LangGraph 如何合并?
Annotated reducer 如何被 StateGraph 解析成 channel?
BinaryOperatorAggregate、LastValue、EphemeralValue 的职责分别是什么?
并行分支和 Send API 为什么必须理解 reducer?
LangGraph 源码深潜:Conditional Edge 与 Router 路由机制解剖
https://jupiter-ws.cn/posts/agent-frameworks/langgraph-conditional-edge-router-deep-dive/
作者
Jupiter
发布于
2026-03-11
许可协议
CC BY-NC-SA 4.0