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/langgraphtag1.2.7,release commit5931a5f阅读边界: 本文只解剖
StateGraph.add_conditional_edges()、BranchSpec与CompiledStateGraph.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_map、Literal类型提示和 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 API1. 本篇问题、学习目标与能力边界
1.1 核心问题
StateGraph.add_conditional_edges()如何把一个普通 Python 路由函数,转换成运行时可根据 state 选择下一个或多个节点的图分支?
1.2 学习目标
完成本篇后,读者必须能够:
- 说清
add_conditional_edges()在构建期保存了什么,以及为什么它不直接执行 router。 - 解释
path、path_map、Literal返回类型、END、多目标返回、Send的源码语义。 - 画出
source node执行完成后,branch 如何读取最新 state、调用 router、写入目标节点 channel 的运行时链路。 - 区分普通固定边
add_edge()、条件边add_conditional_edges()、Command、Send的边界。 - 能把意图路由、状态路由、错误路由落到旅行规划助手的
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 spec | BranchSpec | 保存 router、目标映射、input schema 的规格 | 运行时调度器 |
| 目标映射 | path_map / BranchSpec.ends | 把逻辑 label 映射到 node name | 状态更新规则 |
END | langgraph.constants.END | 终止图执行的特殊目标 | 普通节点名 |
| 多目标路由 | Sequence[Hashable] | 一次 branch 返回多个目标 | reducer 合并机制本身 |
Send | langgraph.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, Literalfrom 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_intent | dict | state channel writer | 更新 state.intent |
route_by_intent | str / Literal | branch writer | 决定下一个节点 |
3.3 对象流转
| 阶段 | 输入类型 | 核心函数 | 输出类型 | 状态变化 |
|---|---|---|---|---|
| 构建 builder | TravelState schema | StateGraph.__init__() | StateGraph | 初始化 nodes / edges / branches / channels |
| 注册节点 | Callable | add_node() | Self | 写入 self.nodes |
| 注册条件边 | source + path + path_map | add_conditional_edges() | Self | 写入 self.branches[source][name] |
| 编译 branch | BranchSpec | CompiledStateGraph.attach_branch() | runtime writers | 给 source 节点挂 branch writer |
| 运行 source | TravelState | classify_intent() | dict | 合并 intent |
| 运行 router | 最新 state | BranchSpec._route() | destination list | 写入目标 channel |
| 执行目标节点 | 最新 state | budget_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": "进入预算规划"} ▼END3.5 正常结束条件
一次条件路由正常结束需要满足:
- source node 正常执行并完成 state update。
- router 函数返回合法目标。
- 目标不是
START,也不是非法节点名。 - 若返回
END,图在该路径终止。 - 若返回一个或多个目标节点,Pregel 在后续 super-step 触发对应节点。
- 所有活跃路径都到达
END或没有新的 channel 写入后,本次图调用完成。
4. 源码地图、关键文件与阅读顺序
4.1 核心目录
langgraph/├── graph/│ ├── state.py│ ├── _branch.py│ └── graph.py├── pregel/│ ├── __init__.py│ ├── _read.py│ └── _write.py└── types.py4.2 关键文件
| 优先级 | 文件 | 核心对象 | 阅读目的 |
|---|---|---|---|
| 1 | langgraph/graph/state.py | StateGraph.add_conditional_edges() | 看条件边如何注册到 builder。 |
| 2 | langgraph/graph/_branch.py | BranchSpec | 看 path_map、Literal、router 执行与结果写入。 |
| 3 | langgraph/graph/state.py | CompiledStateGraph.attach_branch() | 看 branch 如何被编译成 channel writes。 |
| 4 | langgraph/pregel/_write.py | ChannelWrite | 看目标节点触发如何变成 channel 写入。 |
| 5 | langgraph/types.py | Send、Command | 看多目标动态分发与条件边边界。 |
4.3 推荐阅读顺序
1. StateGraph.add_conditional_edges2. BranchSpec.from_path3. _get_branch_path_input_schema4. StateGraph.compile5. CompiledStateGraph.attach_branch6. BranchSpec.run7. BranchSpec._route / _aroute8. BranchSpec._finish9. ChannelWrite / ChannelWriteEntry10. 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 triggersBranchSpec 本身不是节点,它是 branch 的规格对象:
BranchSpec ├── path: Runnable ├── ends: dict[Hashable, str] | None └── input_schema: type | None5.2 对象职责
| 对象 | 生命周期 | 输入 | 输出 | 核心职责 |
|---|---|---|---|---|
StateGraph | 构建期 | schema、node、edge、branch | builder | 保存图定义。 |
BranchSpec | 构建期 + 运行时 | router、path_map | branch runnable | 保存并执行路由逻辑。 |
CompiledStateGraph | 编译后运行期 | builder artifacts | Pregel application | 将 branch 连接到 channel / trigger。 |
ChannelWrite | 运行时 | destination entries | channel writes | 触发目标节点。 |
Pregel | 运行时 | channel writes | node scheduling | 按 super-step 调度节点。 |
5.3 协议边界
Node 协议 负责:读取 state,返回 partial state update。 不负责:决定条件分支目标,除非使用 Command。
Branch 协议 负责:读取 state,返回下一个或多个 destination。 不负责:更新 state 字段。
Channel 协议 负责:保存 state 字段或触发信号。 不负责:理解业务 intent。
Pregel 协议 负责:根据 channel 更新触发下一轮节点。 不负责:解释 router 返回值语义。5.4 稳定接口与内部实现
| 类型 | 对象 | 文章中的使用原则 |
|---|---|---|
| 公共 API | StateGraph.add_conditional_edges() | 工程代码可以稳定使用。 |
| 公共常量 | START、END | 工程代码可以直接使用。 |
| 扩展接口 | Send、Command | 可用于高级控制流,但需要理解边界。 |
| 内部实现 | BranchSpec、_route()、_finish() | 用于源码理解,业务代码不直接依赖。 |
| 内部 channel | branch: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.py | StateGraph.add_conditional_edges() |
router 返回 END 会停止图执行 | 官方契约 + 源码事实 | API Reference / state.py | add_conditional_edges docstring |
path_map 可以是 dict 或 list,省略时可从 Literal 推断 | 源码事实 | graph/_branch.py | BranchSpec.from_path() |
_route() 会读取 state 后 invoke path | 源码事实 | graph/_branch.py | BranchSpec._route() |
_finish() 会把 router 返回值规范为列表并写入 destinations | 源码事实 | graph/_branch.py | BranchSpec._finish() |
无 path_map 和无 Literal 时可视化会假设可能到任意节点 | 官方契约 | API Reference / docstring | add_conditional_edges warning |
7. 构建期源码解剖
本章回答:
用户调用
builder.add_conditional_edges(source, path, path_map)时,LangGraph 在 builder 内保存了什么?
7.1 构建期职责
| 输入 | 归一化动作 | 构建结果 |
|---|---|---|
source | 保存为 branch 所属节点 | self.branches[source] |
path | coerce_to_runnable() | BranchSpec.path |
path_map | dict/list/Literal 推断为 ends 映射 | BranchSpec.ends |
| router 输入类型提示 | 尝试推断 input schema | BranchSpec.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 schemas7.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 的映射。 |
| 输出 | Self | builder 本身,支持链式调用。 |
| 状态变化 | self.branches[source][name] | 保存 BranchSpec。 |
| 副作用 | warning | graph 已 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_map、Literal 推断、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_edgeshttps://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。
真实源码签名
@classmethoddef 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。 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
@classmethoddef 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 是 dict | copy | 支持 label 到 node name 映射。 |
path_map 是 list | {name: name} | 支持节点名白名单。 |
path_map is None 且 return 是 Literal | 从 Literal 推断 | 可视化和校验更准确。 |
path_map is None 且无 Literal | ends=None | 运行仍可行,但可视化会假设可到任意节点。 |
| 类型提示解析失败 | 捕获异常并跳过 | 不阻断构建。 |
设计原因与工程影响
path_map 和 Literal 都不是为了让 router 能运行,它们主要解决两个工程问题:
1. 把 router 的逻辑返回值映射到真实节点名。2. 让框架在编译、校验、可视化时知道可能的目标集合。工程建议:
生产级 Router 尽量使用 Literal 或 path_map。如果 router 返回业务枚举 label,则必须使用 path_map。如果 router 直接返回节点名,也建议加 Literal,便于可视化和维护。源码证据
libs/langgraph/langgraph/graph/_branch.py::BranchSpec.from_pathhttps://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输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | path | Runnable 或 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/writers | branch 从 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 loop | updates / 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输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | start | source node 名称。 |
| 输入 | name | branch 名称。 |
| 输入 | branch | BranchSpec。 |
| 输出 | 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 | 读取最新 state | router 能看到 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)输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | writer | destinations 到 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_map 或 Literal 推断。它的作用主要是让运行时和可视化系统知道这个 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输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | input | source node 的输出或运行时传入值。 |
| 输入 | config | RunnableConfig。 |
| 输入 | reader | 可读取 fresh state 的函数。 |
| 输入 | writer | destination 写入转换器。 |
| 输出 | `Runnable | Any` |
| 状态变化 | 无直接 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 state | router 看见最新 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._routelibs/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)输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | result | router 返回值。 |
| 输入 | writer | destinations → writes 转换器。 |
| 输出 | input 或 ChannelWrite | 取决于是否需要 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 不允许返回 START。START 是入口 channel,不是运行中可跳回的普通节点。
第 4 段说明 Send 不能发送到 END。Send 表示“给某个节点发送 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 是 None 或 START | 抛 ValueError | 非法分支。 |
Send(node=END) | 抛 InvalidUpdateError | END 不能接收 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。 |
| 业务 label | router 返回 "budget" 且 path_map 存在 | _finish() | self.ends["budget"] -> "budget_node"。 |
END | router 返回 END | _finish() / writer | 当前路径终止。 |
| 多目标 | router 返回 list[str] | _finish() | 多个目标 channel 写入。 |
| 动态 Send | router 返回 Send(...) | _finish() | 发送 packet 到目标节点。 |
| 非法 START | router 返回 START | _finish() | 抛 ValueError。 |
| 未知 path_map key | router 返回 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, ...) | 抛 InvalidUpdateError | END 不能接收 packet。 |
path_map 中映射到 END | 允许 | label 可映射到终止。 |
设计原因与工程影响
END 作为控制标记,而不是普通节点,能让图终止逻辑保持统一:没有新的目标节点被触发,运行时自然收敛。
源码证据
libs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edgesdocstringlibs/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 是 list | label 必须等于节点名 | 简化白名单。 |
path_map 目标是 END | 允许 | 可用业务 label 表示结束。 |
设计原因与工程影响
path_map 的最大价值是稳定业务语义。你可以改节点名,但不改 router 返回枚举。
工程建议:
如果 router 返回的是 intent 枚举,使用 path_map。如果 router 直接返回节点名,使用 Literal。源码证据
libs/langgraph/langgraph/graph/_branch.py::BranchSpec.from_pathlibs/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 type | Literal["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_pathlibs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edgeswarning
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 与恢复边界
| 机制 | 适用条件 | 不适用条件 | 幂等要求 |
|---|---|---|---|
| Retry | router 内部调用 LLM 或远程分类服务偶发失败 | router 是纯代码判断且逻辑错误 | router 不应有副作用。 |
| Fallback | intent 不确定、分类结果不在枚举 | 目标节点缺失 | 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 + update | Command | 条件边是 source 退出后的外部 branch。 |
| 循环保护 | Pregel runtime recursion_limit | 条件边可以形成循环,但不负责限制循环次数。 |
10. 扩展机制与框架协作
本章回答:
框架允许在哪里插入自定义行为,它如何与相邻模块协作,哪些内部实现不应该被业务代码依赖?
10.1 扩展点总览
| 扩展点 | 扩展方式 | 执行时机 | 可修改内容 | 约束 |
|---|---|---|---|---|
path_map | dict/list | 构建期 + 运行时映射 | label 到 node | key 必须覆盖 router 返回。 |
Literal | 类型提示 | 构建期推断 | 可视化目标集合 | 不替代运行时校验。 |
| async router | async 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。
真实源码签名
Send 是 langgraph.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 | 文本 / messages | Pydantic / dict | 生成 router 所需字段。 |
add_node | state | partial update | 先写入路由字段。 |
add_conditional_edges | state | destination | 决定下一步。 |
Reducer | 多节点 update | 合并 state | 处理并行分支写冲突。 |
Send | dynamic packet | worker input | 支持 map-reduce。 |
Command | node return | update + goto | 替代部分条件边场景。 |
END | destination | terminate | 终止路径。 |
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"说明:
- 扩展点接收当前 state。
- 扩展点允许修改控制流目标,不允许直接修改 state。
- 扩展点必须返回合法 destination、
END或 destination 列表。 - 异常会向上抛出,除非外层 runtime 策略处理。
- 如果返回多个目标,需要考虑 reducer。
10.8 选择扩展还是重写流程
| 条件 | 选择条件边 | 选择更底层机制 |
|---|---|---|
| 只根据 state 选择下一节点 | 是 | 否 |
| 需要 label 到 node name 映射 | 是 | 否 |
| 需要一对多并行分支 | 是 | 视情况使用 Send |
| 需要节点同时 update + goto | 否 | 用 Command |
| 需要动态创建多个 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_map | label 与节点名需要解耦 | 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 | None12. 常见误区与源码纠正
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 runnable13.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_edge、add_conditional_edges、Command、Send。 - 能指出
BranchSpec._route()和_finish()的职责边界。 - 能为旅行规划助手写一个纯代码 router,并说明每个目标节点的业务含义。
- 能判断什么时候该用
path_map,什么时候直接返回节点名。
14. 参考资料与下一篇衔接
14.1 官方概念文档
-
LangGraph Graph API overview
https://docs.langchain.com/oss/python/langgraph/graph-api -
Use the graph API
https://docs.langchain.com/oss/python/langgraph/use-graph-api -
LangGraph overview
https://docs.langchain.com/oss/python/langgraph/overview -
LangGraph Pregel runtime
https://docs.langchain.com/oss/python/langgraph/pregel
14.2 官方 API Reference
-
StateGraph.add_conditional_edges()
https://reference.langchain.com/python/langgraph/graph/state/StateGraph/add_conditional_edges -
StateGraph
https://reference.langchain.com/python/langgraph/graph/state/StateGraph -
Graphs API Reference
https://reference.langchain.com/python/langgraph/graphs -
Send
https://reference.langchain.com/python/langgraph/types/Send -
Command
https://reference.langchain.com/python/langgraph/types/Command
14.3 官方源码
-
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 -
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 -
libs/langgraph/langgraph/graph/_branch.py::BranchSpec._route
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/_branch.py -
libs/langgraph/langgraph/graph/_branch.py::BranchSpec._finish
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/_branch.py -
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?