文章目录
-
- [第 7 章:Command 与动态流程控制](#第 7 章:Command 与动态流程控制)
-
- [7.1 本章目标](#7.1 本章目标)
- [7.2 核心概念](#7.2 核心概念)
-
- [Command 是什么?](#Command 是什么?)
- [Command vs 条件边](#Command vs 条件边)
- [Command 跳转示意图](#Command 跳转示意图)
- [7.3 实战](#7.3 实战)
-
- [实战 1:动态路由跳转](#实战 1:动态路由跳转)
- [实战 2:Command(update=) 同时更新状态和导航](#实战 2:Command(update=) 同时更新状态和导航)
- [实战 3:子图用 Command 控制父图](#实战 3:子图用 Command 控制父图)
- [7.4 API 速查](#7.4 API 速查)
- [7.5 错误与避坑指南](#7.5 错误与避坑指南)
-
- [坑 1:混淆 Command 和普通 dict](#坑 1:混淆 Command 和普通 dict)
- [坑 2:Command(goto=) 目标节点不存在](#坑 2:Command(goto=) 目标节点不存在)
- [坑 3:在不需要动态路由时滥用 Command](#坑 3:在不需要动态路由时滥用 Command)
- [坑 4:子图 Command 忘记 graph=Command.PARENT](#坑 4:子图 Command 忘记 graph=Command.PARENT)
- [7.6 最佳实践总结](#7.6 最佳实践总结)
第 7 章:Command 与动态流程控制

7.1 本章目标
学完本章你将能够:
- 理解 Command 在运行时动态控制流程的作用
- 掌握
Command(goto=)跳转节点和Command(update=)更新状态 - 学会 Send API 实现 map-reduce 并行模式
- 理解 Command 与条件边的区别和使用场景
7.2 核心概念
Command 是什么?
Command 是一个特殊的数据类,节点可以返回它来同时更新状态和导航流程。它与普通 dict 返回值的关键区别:
python
# 普通 dict 返回值:只更新 State,流程按照边(Edge)走
def normal_node(state: State) -> dict:
return {"counter": state["counter"] + 1} # 流程由 add_edge 决定
# Command 返回值:更新 State + 动态决定流程
def command_node(state: State) -> Command[Literal["node_a", "node_b"]]:
return Command(
update={"counter": state["counter"] + 1}, # 更新状态
goto="node_a", # 动态决定下一步去哪个节点
)
比喻:普通 dict 就像"完成工作后走固定传送带",Command 就像"完成工作后自己选择坐哪条传送带"。
Command vs 条件边
| 特性 | 条件边 add_conditional_edges |
Command goto |
|---|---|---|
| 决策时机 | 编译时定义路由规则 | 运行时动态决策 |
| 决策逻辑位置 | 独立的路由函数 | 节点内部 |
| 适用场景 | 固定的分支逻辑(如意图分类) | 动态跳转(如审批后跳转) |
| 类型安全 | 通过 Literal 标注 | 通过 Command 的泛型参数 |
Command 跳转示意图
#mermaid-svg-N22Thkr9JWw1DN9M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N22Thkr9JWw1DN9M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N22Thkr9JWw1DN9M .error-icon{fill:#552222;}#mermaid-svg-N22Thkr9JWw1DN9M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N22Thkr9JWw1DN9M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N22Thkr9JWw1DN9M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N22Thkr9JWw1DN9M .marker.cross{stroke:#333333;}#mermaid-svg-N22Thkr9JWw1DN9M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N22Thkr9JWw1DN9M p{margin:0;}#mermaid-svg-N22Thkr9JWw1DN9M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N22Thkr9JWw1DN9M .cluster-label text{fill:#333;}#mermaid-svg-N22Thkr9JWw1DN9M .cluster-label span{color:#333;}#mermaid-svg-N22Thkr9JWw1DN9M .cluster-label span p{background-color:transparent;}#mermaid-svg-N22Thkr9JWw1DN9M .label text,#mermaid-svg-N22Thkr9JWw1DN9M span{fill:#333;color:#333;}#mermaid-svg-N22Thkr9JWw1DN9M .node rect,#mermaid-svg-N22Thkr9JWw1DN9M .node circle,#mermaid-svg-N22Thkr9JWw1DN9M .node ellipse,#mermaid-svg-N22Thkr9JWw1DN9M .node polygon,#mermaid-svg-N22Thkr9JWw1DN9M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N22Thkr9JWw1DN9M .rough-node .label text,#mermaid-svg-N22Thkr9JWw1DN9M .node .label text,#mermaid-svg-N22Thkr9JWw1DN9M .image-shape .label,#mermaid-svg-N22Thkr9JWw1DN9M .icon-shape .label{text-anchor:middle;}#mermaid-svg-N22Thkr9JWw1DN9M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N22Thkr9JWw1DN9M .rough-node .label,#mermaid-svg-N22Thkr9JWw1DN9M .node .label,#mermaid-svg-N22Thkr9JWw1DN9M .image-shape .label,#mermaid-svg-N22Thkr9JWw1DN9M .icon-shape .label{text-align:center;}#mermaid-svg-N22Thkr9JWw1DN9M .node.clickable{cursor:pointer;}#mermaid-svg-N22Thkr9JWw1DN9M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N22Thkr9JWw1DN9M .arrowheadPath{fill:#333333;}#mermaid-svg-N22Thkr9JWw1DN9M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N22Thkr9JWw1DN9M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N22Thkr9JWw1DN9M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N22Thkr9JWw1DN9M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N22Thkr9JWw1DN9M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N22Thkr9JWw1DN9M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N22Thkr9JWw1DN9M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N22Thkr9JWw1DN9M .cluster text{fill:#333;}#mermaid-svg-N22Thkr9JWw1DN9M .cluster span{color:#333;}#mermaid-svg-N22Thkr9JWw1DN9M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N22Thkr9JWw1DN9M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N22Thkr9JWw1DN9M rect.text{fill:none;stroke-width:0;}#mermaid-svg-N22Thkr9JWw1DN9M .icon-shape,#mermaid-svg-N22Thkr9JWw1DN9M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N22Thkr9JWw1DN9M .icon-shape p,#mermaid-svg-N22Thkr9JWw1DN9M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N22Thkr9JWw1DN9M .icon-shape .label rect,#mermaid-svg-N22Thkr9JWw1DN9M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N22Thkr9JWw1DN9M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N22Thkr9JWw1DN9M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N22Thkr9JWw1DN9M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 普通 dict 返回
普通 dict 返回
Command(goto='C')
Command(goto='B')
Command(goto='END')
START
节点 A
节点 B
固定流程
节点 C
END
7.3 实战
实战 1:动态路由跳转
python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
class State(TypedDict):
step: str
data: str
def entry_node(state: State) -> Command[Literal["process_a", "process_b", "END"]]:
"""
入口节点:根据当前状态动态决定路由。
注意 Command 的泛型参数:Command[Literal[...]] 提供类型安全,
确保 goto 的目标是已注册的节点。
"""
if state["step"] == "start":
print(" → 路由到 process_a")
return Command(goto="process_a", update={"data": "从入口进入"})
elif state["step"] == "skip":
print(" → 直接结束")
return Command(goto="END")
else:
print(" → 路由到 process_b")
return Command(goto="process_b", update={"data": "从入口进入B"})
def process_a(state: State) -> Command[Literal["END"]]:
print(" [process_a] 处理中...")
return Command(goto="END", update={"data": f"{state['data']} → 经过A处理"})
def process_b(state: State) -> Command[Literal["END"]]:
print(" [process_b] 处理中...")
return Command(goto="END", update={"data": f"{state['data']} → 经过B处理"})
builder = StateGraph(State)
builder.add_node("entry", entry_node)
builder.add_node("process_a", process_a)
builder.add_node("process_b", process_b)
# 入口节点使用 Command 控制路由,不需要条件边
builder.add_edge(START, "entry")
# 注意:process_a 和 process_b 都通过 Command(goto=END) 结束
# 不需要显式添加 process_a → END 的边
graph = builder.compile()
print("=== 测试 1: step=start ===")
result = graph.invoke({"step": "start", "data": ""})
print(f"结果: {result['data']}")
print("\n=== 测试 2: step=other ===")
result = graph.invoke({"step": "other", "data": ""})
print(f"结果: {result['data']}")
print("\n=== 测试 3: step=skip ===")
result = graph.invoke({"step": "skip", "data": ""})
print(f"结果: {result['data']}")
实战 2:Command(update=) 同时更新状态和导航
python
from typing import TypedDict, Annotated, Literal
import operator
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
class State(TypedDict):
messages: Annotated[list[str], operator.add]
def initial_processor(state: State) -> Command[Literal["finalizer"]]:
"""
同时更新状态(添加消息)和导航到 finalizer 节点。
"""
return Command(
update={"messages": ["处理完成"]},
goto="finalizer",
)
def finalizer(state: State) -> Command[Literal["END"]]:
return Command(
update={"messages": ["✅ 最终确认"]},
goto="END",
)
builder = StateGraph(State)
builder.add_node("process", initial_processor)
builder.add_node("finalizer", finalizer)
builder.add_edge(START, "process")
graph = builder.compile()
result = graph.invoke({"messages": []})
print(f"消息: {result['messages']}")
实战 3:子图用 Command 控制父图
python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
# ============================================
# 子图
# ============================================
class SubState(TypedDict):
value: int
def sub_processor(state: SubState) -> Command:
"""
子图节点返回 Command(graph=Command.PARENT),控制父图流程。
"""
if state["value"] > 10:
# 值太大,让父图走 reject 路径
return Command(
graph=Command.PARENT,
goto="parent_reject",
update={"value": -1},
)
return {"value": state["value"] * 2}
sub_builder = StateGraph(SubState)
sub_builder.add_node("process", sub_processor)
sub_builder.add_edge(START, "process")
sub_builder.add_edge("process", END)
sub_graph = sub_builder.compile()
# ============================================
# 父图
# ============================================
class ParentState(TypedDict):
value: int
def parent_accept(state: ParentState) -> dict:
return {"value": state["value"] * 10}
def parent_reject(state: ParentState) -> dict:
return {"value": 0}
parent_builder = StateGraph(ParentState)
parent_builder.add_node("sub", sub_graph) # 子图作为节点
parent_builder.add_node("parent_accept", parent_accept)
parent_builder.add_node("parent_reject", parent_reject)
parent_builder.add_edge(START, "sub")
parent_builder.add_edge("parent_accept", END)
parent_builder.add_edge("parent_reject", END)
parent_graph = parent_builder.compile()
print("=== 测试: value=5(正常) ===")
result = parent_graph.invoke({"value": 5})
print(f"结果: {result['value']}") # 5*2=10, 然后 accept: 10*10=100
print("\n=== 测试: value=15(触发子图拒绝) ===")
result = parent_graph.invoke({"value": 15})
print(f"结果: {result['value']}") # 子图返回 reject,父图执行 reject: 0
7.4 API 速查
| API | 完整签名 | 入参说明 | 返回值 | 说明 |
|---|---|---|---|---|
Command(goto=node) |
Command(goto: str) |
goto: 目标节点名 |
Command 对象 |
跳转到指定节点 |
Command(update=dict) |
Command(update: dict) |
update: 状态更新字典 |
Command 对象 |
更新状态后按边继续 |
Command(goto=, update=) |
Command(goto: str, update: dict) |
goto + update |
Command 对象 |
更新状态并跳转 |
Command(resume=value) |
Command(resume: Any) |
resume: 恢复值 |
Command 对象 |
恢复中断 |
Command(graph=Command.PARENT) |
Command(graph: str) |
graph: 目标图 |
Command 对象 |
子图控制父图 |
Command.PARENT |
常量 | 无 | "__parent__" |
导航到父图 |
7.5 错误与避坑指南
坑 1:混淆 Command 和普通 dict
python
# ❌ 错误写法:期望跳转但实际不会
def bad_node(state: State) -> dict:
return {"goto": "other_node"} # 这只是更新 State 中的 "goto" 字段!
# ✅ 正确写法
def good_node(state: State) -> Command[Literal["other_node"]]:
return Command(goto="other_node") # 真的跳转到 other_node
坑 2:Command(goto=) 目标节点不存在
python
# ❌ 错误写法
def bad_node(state: State) -> Command[Literal["nonexistent"]]:
return Command(goto="nonexistent") # 目标节点未注册 → 运行时报错
# ✅ 正确写法
def good_node(state: State) -> Command[Literal["node_a", "node_b"]]:
if condition:
return Command(goto="node_a") # 确保 node_a 已注册
return Command(goto="node_b")
坑 3:在不需要动态路由时滥用 Command
python
# ❌ 不推荐:简单条件路由用 Command
def bad_node(state: State) -> Command[Literal["a", "b"]]:
if state["intent"] == "a":
return Command(goto="a")
return Command(goto="b")
# ✅ 推荐:固定路由用 add_conditional_edges
def route(state: State) -> Literal["a", "b"]:
return state["intent"]
builder.add_conditional_edges("classifier", route)
坑 4:子图 Command 忘记 graph=Command.PARENT
python
# ❌ 错误写法:子图跳转被当作子图内部跳转
def sub_node(state: SubState) -> Command:
return Command(goto="parent_node") # 在子图中找不到 parent_node!
# ✅ 正确写法:明确指定跳转到父图
def sub_node(state: SubState) -> Command:
return Command(graph=Command.PARENT, goto="parent_node")
7.6 最佳实践总结
- 编译时确定的路由用条件边,运行时决策用 Command(goto=):各司其职
- Command 返回类型用
Literal泛型标注:获得类型安全和 IDE 提示 - 并行处理用 Send + Reducer 实现 map-reduce:不要用 Command 做并行
- 子图控制父图时用
Command.PARENT:明确导航目标 - Command 适合"带数据跳转"的场景:如审批后带着审批结果跳转到对应处理节点