初学 LangGraph 时,最先接触的是静态条件分支 add_conditional_edges。这种方式在编译期定义图的分支规则,ReAct Agent 循环就用它,图结构清晰、Mermaid 可视化友好。
但遇到更复杂的需求:
- 节点内部执行完业务逻辑后,动态决定下一步去哪里,不想单独抽离路由函数;
- 文档分片、多 Query 检索,需要批量派发任务,并行执行同一个节点(Map-Reduce);
这时就需要 Command 和 Send。 第一次上手会踩:并行更新 state 报 InvalidUpdateError,分不清 Command(goto=[nodeA, nodeB]) 和 [Send()] 的并行差异。
一、Command:节点内直接控制跳转,无需预定义条件边
1. Command
Command 是 LangGraph 提供的特殊返回对象,在 Node 函数 return 时返回。它可以一次性完成两件事:
update:更新状态 State(等价于直接返回字典更新 state)goto:运行时指定下一跳,可以是单个节点、节点数组、Send 对象数组
核心区别:
add_conditional_edges:node 执行完成 → 执行独立路由函数判断跳转(编译期就注册分支规则)Command:在当前节点内部,业务逻辑跑完同时决定跳转目标,不需要提前注册条件边。
2. Command.goto 的 3 种合法写法
ini
# 1. 单个节点,串行跳转
Command(goto="even_node")
# 2. 多个节点名字数组:并行执行【不同节点】,共享同一份State
Command(goto=["search_node", "calc_node"])
# 3. Send对象数组:并行执行【同一个节点多份独立State副本】,Map场景
Command(goto=[Send("summarize_chunk", {"chunk": "片段1"}), Send("summarize_chunk", {"chunk": "片段2"})])
重要提醒:
Command的动态跳转不会体现在 Mermaid 图中,可视化调试是短板,简单分支优先选择add_conditional_edges。
最简示例:Command 基础路由
python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
class State(TypedDict):
num: int
def check_node(state: State) -> Command:
if state["num"] % 2 == 0:
return Command(goto="even_node", update={"tip": "偶数"})
else:
return Command(goto="odd_node", update={"tip": "奇数"})
def even_node(state):
print(f"{state['num']} 是偶数")
def odd_node(state):
print(f"{state['num']} 是奇数")
builder = StateGraph(State)
builder.add_node("check_node", check_node)
builder.add_node("even_node", even_node)
builder.add_node("odd_node", odd_node)
builder.add_edge(START, "check_node")
builder.add_edge("even_node", END)
builder.add_edge("odd_node", END)
graph = builder.compile()
graph.invoke({"num": 8})
二、Send:Map-Reduce 并行派发独立任务
1. Send
Send(node, arg) 返回Send 调度指令对象 ,不是直接调用节点! 它只是一个数据结构,告诉 Pregel 调度器:
创建一份全新的 State 副本 ,把
arg字典合并进副本,在这个副本上执行目标节点。
Send 类简化定义:
python
@dataclass
class Send:
node: str # 目标节点名称
arg: dict | None # 合并进子state的字典
2. 两种并行,容易混淆
表格
| 写法 | 并行类型 | State 特点 | 使用场景 |
|---|---|---|---|
Command(goto=["A", "B"]) |
多个不同节点并行 | 共享同一份 state | 多个独立工具同时执行,互不依赖 |
Command(goto=[Send("A", s1), Send("A", s2)]) |
同一个节点多次并行 | 每个 Send 拥有独立 state 副本 | 文档分片摘要、多 query 并行检索(Map-Reduce) |
重点:
- 节点名字数组并行:共用 state,多个节点同时写同一个 key,会冲突;
- Send 数组并行:每个任务 state 隔离,执行完成后统一合并回主 state。
3. Send 完整可运行示例(文档分片摘要)
重点:并行写入 state 必须用
Annotated + reducer,否则直接抛出InvalidUpdateError
python
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, Send
import operator
class State(TypedDict):
docs: list[str]
# operator.add 作为reducer,并行多个返回值自动列表追加
summaries: Annotated[list[str], operator.add]
def split_docs(state: State) -> Command:
sends = []
for doc in state["docs"]:
sends.append(Send("summarize_chunk", {"chunk": doc}))
return Command(goto=sends)
def summarize_chunk(state):
chunk = state["chunk"]
return {"summaries": [f"文档摘要:{chunk[:15]}..."]}
def merge_summary(state):
print("汇总所有分片摘要:", state["summaries"])
builder = StateGraph(State)
builder.add_node("split_docs", split_docs)
builder.add_node("summarize_chunk", summarize_chunk)
builder.add_node("merge_summary", merge_summary)
builder.add_edge(START, "split_docs")
builder.add_edge("summarize_chunk", "merge_summary")
builder.add_edge("merge_summary", END)
graph = builder.compile()
res = graph.invoke({
"docs": ["大模型Agent基础原理", "LangGraph状态图介绍", "Send并行任务讲解"]
})
print(res)
三、高频报错:InvalidUpdateError 根源与解决
vbnet
InvalidUpdateError: At key 'summaries': Can receive only one value per step. Use an Annotated key to handle multiple values.
原因
LangGraph State 默认字段的合并策略是覆盖。 在同一个 superstep(Pregel 的一步),多个并行节点同时更新同一个 state key,引擎不知道怎么合并,直接报错。
解决方案
使用 Annotated[类型, reducer] 显式定义合并策略:
operator.add:列表追加(Map-Reduce 最常用);- 自定义 reducer 函数,实现去重、合并、自定义逻辑。
自定义 reducer 示例:
python
def list_merge_reducer(old: list, new: list) -> list:
# 简单去重合并
return list(set(old + new))
class State(TypedDict):
docs: list[str]
summaries: Annotated[list[str], list_merge_reducer]
注意:并行节点返回值必须是列表,
operator.add做列表拼接;直接返回字符串会报错! ✅return {"summaries": ["摘要文本"]}❌return {"summaries": "摘要文本"}
四、Command & Send 使用场景选型
✅ 什么时候用 Command
- 节点业务逻辑和路由判断耦合在一起,不想单独抽离路由函数;
- 动态跳转逻辑复杂,依赖 LLM 输出结果;
- 运行时按需终止图:
Command(goto=END)
✅ 什么时候用 Send
- Map-Reduce:文档切片并行摘要、多 query 并行检索;
- 批量任务分发,每个任务需要独立 state,避免任务之间互相污染。
❌ 不推荐场景
- 简单 if/else 分支、ReAct 循环:优先
add_conditional_edges,图可视化清晰,便于调试; - 复杂长期维护项目大量 Command:动态跳转 Mermaid 看不到,排查困难。
五、避坑清单(生产环境必看)
- Send 只是指令对象,构造 Send不会立刻执行节点,由 Pregel 调度器统一在 superstep 调度执行;
- Send 的 arg 是合并到副本 state,不是完全替换 state;
- 并行场景只要多任务写同一个 state key,必须加
Annotated+ reducer; - Command.goto 语法允许混合节点名 + Send,但工程上禁止,可读性极差;
- Send 一次只能指向单个节点,多节点并发请写多个 Send 实例;
- Send 批量任务记得做并发限流,防止一次性发起上百个 LLM 请求,触发接口限流;
- Command 动态跳转不会出现在 mermaid 图,调试建议加
stream逐 step 打印 state 排查问题。
六、总结
add_conditional_edges:编译期静态分支,适合常规 Agent 循环,可视化友好;Command:Node 内部运行时动态路由,支持跳转、状态更新、批量并行指令;Send:Map 并行任务分发,创建隔离的 State 副本,是分片处理的核心;- 并行写 state 记住
Annotated + reducer,解决InvalidUpdateError。