Command:resume / goto / update 三合一原语
职责
Command 是 LangGraph 的「跨图控制原语」,定义在 libs/langgraph/langgraph/types.py(Command class:759)。一个 Command 实例可以同时承载三种语义——resume(从中断处恢复执行)、goto(指定下一步跳到哪个节点)、update(往当前图的状态通道写值)。节点函数返回 Command(...),或者调用方把它作为 graph.invoke(Command(...)) 的输入,Pregel 把它拆成 pending writes 写进通道。
它的位置在节点和 Pregel 主循环之间:节点产出 Command,Pregel 用 map_command(map_command:56)把它的字段拆成 (task_id, channel, value) 三元组写进 pending writes,下一轮 prepare_next_tasks 自然会看到这些 writes 并据此调度——goto 变成 Send 写进 TASKS 通道、update 直接写对应通道、resume 走 RESUME 通道交给 interrupt() 处理。
设计动机
- 统一表达「副作用」:节点除了往状态写值,可能还想「跳到非默认下一节点」「向父图传消息」「恢复中断」——以前这些是分散的 API,
Command把它们打包成单一返回类型,节点函数签名-> Command[Literal["node_a", "node_b"]]还能让编译期校验合法跳转目标。 Command.PARENT跨图层级:子图里的节点可以return Command(graph=Command.PARENT, update=...),把更新写到父图状态——map_command看到graph == "__parent__"(PARENT guard:58)raise InvalidUpdateError("There is no parent graph"),真正落到父图是在父图的_first里处理子图上报的 writes。goto接受Send:Command(goto=Send("node", arg))等价于扇出一个 PUSH 任务;goto="node"等价于goto=Send("node", START)(触发那个节点的 PULL)。所以Command把 conditional edges 的能力内嵌到节点返回值里,不需要单独写add_conditional_edges。resume单点入口:Command(resume=...)是interrupt()的唯一恢复路径——既支持单值Command(resume="answer")自动路由到唯一 pending interrupt,也支持 dictCommand(resume={interrupt_id: value, ...})多 interrupt 并发恢复。update配合 reducer:Command(update={"messages": [msg]})走的还是通道的 reducer——messages通道配operator.add,这条 update 就是 append 而非覆盖。Overwrite包装可以绕过 reducer(Overwrite:938),走 BinaryOperatorAggregate 通道的直写路径。
关键文件
Command class:759—@dataclass class Command(graph, update, resume, goto),泛型N表示 goto 节点名类型。Command fields:780—graph/update/resume/goto字段定义及 docstring。_update_as_tuples:791— 把update拆成[(channel, value), ...],支持 dict、list[tuple]、annotated dataclass 三种形式。map_command:56— 把Command拆成 pending writes 序列的核心函数。PARENT guard:58—cmd.graph == Command.PARENT时报「没有父图」错误。goto as sends:62—cmd.goto拆成Send列表,Send写进TASKS通道,str写进branch:to:<node>触发 START。resume write:72—cmd.resume写进RESUME通道,由interrupt()在重入时读取。update writes:74—cmd.update拆成(channel, value)写进对应通道,走 reducer。Command resume mapping:904— 父图收到Command(resume=...)后,把 resume 路由到对应 pending interrupt 的入口。Overwrite:938—Overwrite(value=...)包装,配BinaryOperatorAggregate通道时绕过 reducer 直写。
数据流
map_command 是 Command 解析的核心(map_command:56):
def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is no parent graph")
if cmd.goto:
if isinstance(cmd.goto, (tuple, list)):
sends = cmd.goto
else:
sends = [cmd.goto]
for send in sends:
if isinstance(send, Send):
yield (NULL_TASK_ID, TASKS, send)
elif isinstance(send, str):
yield (NULL_TASK_ID, f"branch:to:{send}", START)
else:
raise TypeError(
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
if cmd.resume is not None:
yield (NULL_TASK_ID, RESUME, cmd.resume)
if cmd.update:
for k, v in cmd._update_as_tuples():
yield (NULL_TASK_ID, k, v)task_id 全是 NULL_TASK_ID——这些 writes 不属于任何具体任务,而是图级别的「外发」:goto 的 Send 进 TASKS 通道(下一轮 prepare_next_tasks 看到后会作为 PUSH 任务调度),goto 的 str 走 branch:to:<node> 通道(条件边专用通道,写 START 表示「触发那个节点的入口」);resume 进 RESUME 通道,在节点重入时由 interrupt() 消费;update 的每个 (k, v) 进对应状态通道,由 reducer 合并。
Command 作为 graph.stream(...) 的输入时,PregelLoop._first(Command resume mapping:904)先把它转成 pending writes,再按 task_id 分组写到 checkpoint_pending_writes,然后 prepare_next_tasks 自然会把它们处理掉。Command 作为节点返回值时,attach_node在节点writers里挂了_get_updatesmapper(<SrcLink path="libs/langgraph/langgraph/graph/state.py" lines="1431" label="attach_node"/>),从Command._update_as_tuples()抽出 update 写进状态通道,goto 和 resume 通过_control_branchmapper 路由到branch:to:通道和RESUME`。
边界与失败
Command.PARENT在根图报错:map_command直接raise InvalidUpdateError("There is no parent graph")(PARENT guard:58)——Command.PARENT只在子图节点里有意义,根图调用直接报错。goto类型限制:goto只接受Send/str或它们的序列,其他类型raise TypeError(goto type:69)——不允许直接传整数、dict 之类。- resume 不指定 id 时强制单 pending:
Command(resume="single")在多个 pending interrupt 时raise RuntimeError(multi interrupt error:917),必须改Command(resume={id: value, ...})。 update走 reducer 不能直写:BinaryOperatorAggregate通道(如Annotated[list, operator.add])默认走 reducer,多个 update 合并;想绕过 reducer 用Overwrite(value=...)(Overwrite:938),但同一超步对同一通道多次Overwrite会InvalidUpdateError。Command与entrypoint.final互斥:@entrypoint函数返回entrypoint.final(value=..., save=...),内部走END/PREVIOUS通道;返回Command走控制通道——两者不能同时用,entrypoint 函数应该返回Command或final,不混。- 空
Command报错:Command()三个字段都是None/()时,map_command什么都不 yield,_first检测到 writes 为空会raise EmptyInputError("Received empty Command input")(empty command:928)。
小结
Command 是 resume / goto / update 的三合一原语,通过 map_command 拆成通道 writes,下一轮 prepare_next_tasks 自然处理。它把 conditional edges、interrupt resume、状态更新统一到一个返回值里。配套的 interrupt() 看 /interrupt/interrupt,Send 扇出语义看 /subgraph/send-command。对照官方资料:LangGraph 文档 · README。