LangGraph 入门第九课:用 Command 同时更新 State 与控制路由
配套示例:
command_routing_demo.py。本课使用本地评分数据,不依赖模型或外部服务。
一个节点算完结果后,常见做法是先返回 State 更新,再由条件边读取更新后的 State,决定下一步去哪里。如果这两个动作紧密相关,也可以直接从节点返回 Command,把状态更新和路由目标放在同一个结果里。
业务逻辑相同,可以用两种方式组织:返回字典配合条件边,或者返回 Command(update=..., goto=...)。
写法一:节点返回 State 更新,条件边负责选路
python
def review(state):
return {
"approved": state["score"] >= 80,
"status": "reviewed",
}
def route_after_review(state):
return "accept" if state["approved"] else "revise"
builder.add_node("review", review)
builder.add_conditional_edges("review", route_after_review)
review 是节点函数,负责产生本次状态更新;route_after_review 是普通路由函数,由图在节点完成后调用。处理逻辑和流程选择分开写,结构比较清楚,也便于复用同一个节点逻辑。
写法二:节点返回 Command
python
from typing import Literal
from langgraph.types import Command
def review(state) -> Command[Literal["accept", "revise"]]:
approved = state["score"] >= 80
destination = "accept" if approved else "revise"
return Command(
update={"approved": approved, "status": "reviewed"},
goto=destination,
)
这里 update 提交 State 更新,goto 指定后续执行目标。LangGraph 会在节点完成后安排目标节点;goto 不是在 Python 函数里直接调用另一个函数。官方 Command 文档
返回类型 Command[Literal["accept", "revise"]] 声明这个节点可能导航到哪些目标,便于图结构分析和可视化。它不会自动创建 accept、revise 节点,也不会替代评分判断;这些节点仍要注册到图里。官方 update 与 goto 说明
Command.update 仍然是 State 更新
本例还有一个 events 字段,用 operator.add 累积事件:
python
events: Annotated[list[str], add]
review 返回 {"events": ["review"]},通过分支再返回本次新增事件。Command.update 遵循字段原本配置的 reducer 规则,不会绕过 State 的合并逻辑。使用累加字段时,返回本次新增项即可,不要把完整历史列表再次提交。
关键实验:Command 不会取消普通静态边
假设节点返回:
python
return Command(
update={"approved": True},
goto="accept",
)
同时建图时还写了:
python
builder.add_edge("review", "static_notice")
那么 accept 和 static_notice 都会被安排执行。Command(goto=...) 增加动态跳转目标,不会覆盖节点上已有的普通边。LangGraph 官方也建议对一个节点选择一种路由方式,避免同时使用 Command 路由和普通边制造意外分支。官方 Command 路由说明
这也会带来并发 State 更新问题:本例中 accept 会更新普通字段 status,所以 static_notice 只更新配置了 add reducer 的 events。如果两个并行节点都写入没有 reducer 的 status,就会遇到并发写冲突。并行事件的列表顺序也不应当被用作业务顺序;需要稳定顺序时,应在数据里保留标识并显式排序。
Command 的两种使用位置不要混淆
Command 既可以从节点返回,也可以作为恢复中断时的调用输入,但用途不同:
| 用法 | 放在哪里 | 作用 |
|---|---|---|
Command(update=..., goto=...) |
节点函数的返回值 | 更新 State,并导航到后续节点 |
Command(resume=value) |
调用方再次调用 invoke 时 |
把值传给等待中的 interrupt() |
如果要在已完成的线程中提交下一轮用户输入,仍然传普通输入字典。不要把单独的 Command(update=...) 当作新一轮输入;它表达的是基于现有检查点继续执行的控制指令。官方 invoke 输入说明
Send 又是另一种工具:它可以给每个动态任务指定独立输入;第八课中用它按文本列表生成多项 worker 任务。本课的 Command 则主要表达"这个节点更新了什么,并决定走哪条控制路径"。
什么时候选哪一种?
| 场景 | 合适的写法 |
|---|---|
| 固定顺序执行 | 普通边 |
| 只根据 State 决定下一节点 | 条件边 |
| 一个节点产出更新,并基于该结果决定下一步 | Command(update=..., goto=...) |
| 运行时才知道要派发多少项工作,且每项输入不同 | Send |
没有必要为了使用新 API 而改写所有条件边。将路由留在独立函数,还是放在节点的返回值里,取决于哪种方式能让业务流程更容易读懂。
运行与观察
powershell
python -B -X utf8 command_routing_demo.py
| 场景 | 预期路径 |
|---|---|
score=90 |
review → accept → END |
score=50 |
review → revise → END |
score=90,且额外添加静态边 |
accept 与 static_notice 都会执行 |
示例还记录 events,用来观察静态边与动态目标同时存在时产生的两个后续任务。由于它们处于并行执行阶段,不应依赖两个事件在列表中的固定先后顺序。
本课小结
Command(update=..., goto=...)将 State 更新和节点路由放在同一个返回值中。Command.update仍遵守对应 State 字段的 reducer 规则。Command[Literal[...]]声明可能的导航目标,有助于图的分析和渲染;目标节点仍需自行注册。goto不会取消节点上已有的普通静态边;混用时可能形成并行分支。Command(resume=...)是调用方恢复interrupt()的用法,与节点返回的update/goto不同。
下一课会把本地规则分类替换为真实模型请求,并学习怎样校验模型输出,再让程序根据结构化结果选择 LangGraph 分支。