第 6 章 Edge、条件路由与 Command
6.1 Edge 的作用
Edge(边)定义节点之间的控制流,决定一个超步结束后下一个超步要调度哪些节点。所有图都必须存在从 START 出发、能最终到达 END 的路径,否则图无法启动或无法终止。
6.2 普通边与起终点
普通边通过 add_edge(source, target) 注册,表示 source 节点执行完后固定流转到 target:
builder.add_edge(START, "plan")
builder.add_edge("plan", "execute")
builder.add_edge("execute", END)- START(入口):常量
from langgraph.graph import START,表示图执行的起点。图必须至少有一条从 START 出发的边,set_entry_point("node")等价于add_edge(START, "node")。 - END(终点):常量
from langgraph.graph import END,到达 END 时图执行结束,对外返回当前完整 State(经 output_schema 裁剪)。 - 入边汇聚(Fan-in):多个节点都 add_edge 到同一个 target,target 会等所有前驱在同一超步完成后才执行(join 语义,详见 6.6)。
- 普通边是单目标的:
add_edge(a, b)只表示 a→b;a→b 与 a→c 并行必须写两条 add_edge(即静态 Fan-out,见 6.6)。
6.3 条件边
条件边通过 add_conditional_edges(source, path, path_map=None) 注册,根据当前 State 在 source 节点执行完之后动态决定下一跳:
def route_after_judge(state: JokeState) -> str:
if state.get("verdict") == "FUNNY" or state["attempts"] >= 3:
return "end"
return "rewrite"
builder.add_conditional_edges(
"judge_joke",
route_after_judge,
path_map={"rewrite": "generate_joke", "end": END},
)path 函数签名:(state) -> str | Sequence[str] | Send | list[Send]。
- 返回字符串:表示下一个节点名;返回
END(常量)或映射到 END 的 key 表示结束; - path_map:将 path 返回值映射到实际节点名/END。使用 path_map 后 path 内可返回语义 key(如
"rewrite"),不与节点名耦合,便于重命名节点; - 返回字符串列表:表示静态 Fan-out,列表中所有目标节点在下一个超步并发执行;
- 返回 Send 对象:动态 Fan-out,可在运行时根据数据决定派发多少份任务到同一节点(常用于 Map-Reduce,第 11 章详述)。
路由决策所依据的 state 是 source 节点执行完、Update 阶段写入后的最新状态。这意味着本超步 source 节点的写入会影响本超步末尾的路由选择——这是 BSP 模型中少见的"写入立即影响同轮决策"的特例,原因是路由发生在 Update 之后、下一轮 Plan 之前。
6.4 Command 原语
LangGraph 1.x 引入的 Command 原语让节点在返回值里同时完成"更新 State"与"指定下一跳"两件事,把条件路由从"边"侧搬到了"节点"侧,适合表达"节点执行完直接决定去哪"的紧凑语义。
from langgraph.types import Command
def judge_node(state: JokeState) -> Command:
verdict = call_judge(state["messages"][-1].content)
if verdict == "FUNNY" or state["attempts"] >= 3:
return Command(
update={"verdict": verdict},
goto=END,
)
return Command(
update={"verdict": verdict},
goto="generate_joke",
)构造签名:Command(update={...}, goto=..., graph=...)。
- update:要提交到 State 的增量字典,语义与节点返回 dict 完全一致;
- goto:下一跳,取值为:
- 节点名字符串(单跳);
- 节点名列表(静态并行 Fan-out);
Send(node_name, arg)或list[Send](动态并行 Fan-out,每个 Send 独立携带入参);Command.PARENT(见子图)或其他子图名,仅在跨图跳转时使用;
- graph:跳转目标图,子图向父图回传时使用
Command(graph=Command.PARENT, update=...); - Command(resume=value):专门用于从
interrupt()恢复执行,将人类输入作为 interrupt 的返回值注入被中断节点。
当节点返回 Command 时,该节点上注册的条件边依然会被评估,但推荐统一风格:要么用条件边做路由,要么用 Command 做路由,避免两者并存导致阅读困难。
6.5 路由设计模式
LLM-as-router(Supervisor 模式):由大模型根据当前对话与任务列表直接输出下一个要执行的 Agent 名。该模式灵活、易扩展,适合 Agent 数量较多或选择策略本身需要智能判断的场景,但存在路由不稳定(模型偶尔输出非法名字)的风险,需配合严格的输出结构化与兜底。
规则路由:基于确定性 if-else / 查表,适合判定条件可枚举、稳定性要求高的业务分支(如根据 status 字段决定流程去向)。优先使用规则路由,规则表达不了的再交给 LLM。
语义路由:基于嵌入相似度将输入映射到不同处理链路(如问答→RAG、闲聊→chat 模型),属于高级用法,一般在入口节点前置一个 embedding 检索步骤实现。
6.6 Fan-out 与 Fan-in
- 静态 Fan-out:从同一 source 出发有多条
add_edge指向不同 target,或条件边的 path 返回字符串列表。所有目标节点在下一个超步并发执行,读取的是同一版本的 state。 - 动态 Fan-out(Map):path 函数或节点 Command.goto 返回
list[Send],每个 Send 指定目标节点与差异化入参。适用于"对 N 份数据并发做同一处理"的 Map-Reduce 场景。 - Fan-in(Join):多个前驱节点都通过
add_edge连到同一个 target;运行时在 Update 阶段识别到 join 语义后,会等待所有前驱节点在同一超步"到齐"后才调度 target,target 执行时读取到的 state 是所有前驱写入合并后的版本。
6.7 死循环防范与时序
- recursion_limit:图执行的最大 Superstep 数,默认 25。超过时运行时抛出
GraphRecursionError。调用方可通过invoke(input, config={"recursion_limit": 50})覆盖。该参数是图级别的安全阀,不是节点重试次数。 - 终止条件必须显式:任何带循环的图都必须存在让条件路由最终走到 END 的路径(如达到最大轮次、模型判定完成、外部信号触发等),否则必然触发 GraphRecursionError。
边与状态更新时序:节点返回值(或 Command.update)在本超步的 Update 阶段提交到 Channel 并生成新版本 state;随后才执行条件边的 path 函数/Command.goto 评估,决定下一轮超步集合;下一轮节点执行时读取的是提交后的 state。该时序保证"节点写完立刻影响路由",但同轮节点之间依然不可见彼此写入(见第 3 章)。
本章小结
Edge 定义控制流:add_edge 描述固定跳转,add_conditional_edges 基于最新 State 动态路由,START/END 界定图的生命周期。Command 原语把"更新 State"与"指定下一跳"合并到节点返回值中,支持单跳、静态并行、动态并行(Send)、跨父图跳转与 interrupt 恢复。Fan-out/Fan-in 由多条边或返回 list/Send 实现,Fan-in 具备 join 语义。recursion_limit 是防死循环的硬性阀门,所有循环必须设计显式终止条件。
常见错误
- 条件边返回未注册的节点名。现象:执行到条件边时抛
KeyError或ValueError: Node ... not found。原因:path 返回的字符串既不在 path_map 中,也不是图中节点名或 END。修复:所有可能返回值都在 path_map 中显式映射,并为异常分支提供兜底(如路由到错误处理节点或 END)。 - 缺少到 END 的出口。现象:图执行到某节点后反复循环直到抛 GraphRecursionError。原因:循环条件永远不满足,或某个分支忘记连到 END。修复:为每个循环设计基于状态字段的终止条件(如 attempts >= N、答案已生成),并在条件路由中覆盖该出口。
- 误以为普通边支持多目标。现象:尝试
add_edge(a, [b, c])报类型错误,或以为 add_edge(a, b) 后 a 还能自动走到 c。原因:普通边 API 单目标。修复:多目标请写多条 add_edge(静态并行)或改用条件边/Command 返回 list。 - Command 中同时误用 goto 与 return 字典。现象:既返回
{"verdict": v}又在同一节点用Command(goto=...),后者被忽略或报错。原因:Command 已经通过 update 字段承载状态更新,外层再包 dict 会让语义冲突。修复:使用 Command 时把所有更新放入其 update 参数。 - recursion_limit 设置过大。现象:死循环消耗大量 token 与时间才报错。原因:为避免误报把 limit 设到数百。修复:按业务最坏路径估算超步数(一般 5–20 足够),留少量余量即可。
生产建议
- 业务主流程优先使用"普通边+少量条件边"的静态风格,让图结构一目了然;Command 保留给需要节点内闭环决策的场景(如 Supervisor、恢复 resume);
- 所有条件路由的 path 函数统一加兜底分支(未识别情况路由到错误处理节点或 END),避免因模型异常输出导致 KeyError;
- 循环条件以"显式状态字段"为准(如
is_done、retries_left),不要依赖隐式的消息长度或字符串匹配; - 使用 Send 做动态 Fan-out 时为每个 Send 任务设计幂等键,并在下游节点中对失败任务做单独收集与降级;
- 将 recursion_limit 写入图构建时的默认 config,不依赖全局默认值,避免跨版本升级时默认值变化引发行为差异。
