LangGraph 进阶:Command + Send 动态控制流、并行 Map-Reduce 实战与踩坑

初学 LangGraph 时,最先接触的是静态条件分支 add_conditional_edges。这种方式在编译期定义图的分支规则,ReAct Agent 循环就用它,图结构清晰、Mermaid 可视化友好。

但遇到更复杂的需求:

  • 节点内部执行完业务逻辑后,动态决定下一步去哪里,不想单独抽离路由函数;
  • 文档分片、多 Query 检索,需要批量派发任务,并行执行同一个节点(Map-Reduce);

这时就需要 CommandSend。 第一次上手会踩:并行更新 state 报 InvalidUpdateError,分不清 Command(goto=[nodeA, nodeB])[Send()] 的并行差异。

一、Command:节点内直接控制跳转,无需预定义条件边

1. Command

Command 是 LangGraph 提供的特殊返回对象,在 Node 函数 return 时返回。它可以一次性完成两件事:

  1. update:更新状态 State(等价于直接返回字典更新 state)
  2. 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] 显式定义合并策略:

  1. operator.add:列表追加(Map-Reduce 最常用);
  2. 自定义 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

  1. 节点业务逻辑和路由判断耦合在一起,不想单独抽离路由函数;
  2. 动态跳转逻辑复杂,依赖 LLM 输出结果;
  3. 运行时按需终止图:Command(goto=END)

✅ 什么时候用 Send

  1. Map-Reduce:文档切片并行摘要、多 query 并行检索;
  2. 批量任务分发,每个任务需要独立 state,避免任务之间互相污染。

❌ 不推荐场景

  1. 简单 if/else 分支、ReAct 循环:优先add_conditional_edges,图可视化清晰,便于调试;
  2. 复杂长期维护项目大量 Command:动态跳转 Mermaid 看不到,排查困难。

五、避坑清单(生产环境必看)

  1. Send 只是指令对象,构造 Send不会立刻执行节点,由 Pregel 调度器统一在 superstep 调度执行;
  2. Send 的 arg 是合并到副本 state,不是完全替换 state;
  3. 并行场景只要多任务写同一个 state key,必须加Annotated + reducer;
  4. Command.goto 语法允许混合节点名 + Send,但工程上禁止,可读性极差;
  5. Send 一次只能指向单个节点,多节点并发请写多个 Send 实例;
  6. Send 批量任务记得做并发限流,防止一次性发起上百个 LLM 请求,触发接口限流;
  7. Command 动态跳转不会出现在 mermaid 图,调试建议加stream逐 step 打印 state 排查问题。

六、总结

  1. add_conditional_edges:编译期静态分支,适合常规 Agent 循环,可视化友好;
  2. Command:Node 内部运行时动态路由,支持跳转、状态更新、批量并行指令;
  3. Send:Map 并行任务分发,创建隔离的 State 副本,是分片处理的核心;
  4. 并行写 state 记住Annotated + reducer,解决InvalidUpdateError
相关推荐
米小虾1 小时前
AI 观察:CEO 们集体喊"慢一点",钱却在加速进场
人工智能
微三云生态系统架构师-彭丹2 小时前
远方好物S2B2C系统架构:一级分销与保证金托管的合规技术实现
人工智能·算法
冬奇Lab2 小时前
DeepSeek Harness 系列(06):System Prompt 组装——动态提示词的工程实现
人工智能·deepseek
罗西的思考3 小时前
机器人模型(WM / WAM / VLA)综合分析与对比:从「看」到「想」再到「做」
人工智能·算法·机器学习
火山引擎开发者社区4 小时前
基于 AgentKit 的端到端需求交付平台:从个人提效到组织提效的 AI 落地实践
人工智能
三声三视4 小时前
75 条文章索引被一条 add 清成 1 条,退出码还是 0:tri-article 的 index.py 我读了 205 行
人工智能·ai·skill·tri-skills·tri-article
蓝速科技5 小时前
医院导诊 AI 数字人一体机场景适配与落地指南丨蓝速科技
运维·数据库·人工智能·科技·自然语言处理·技术分享
QYR-分析5 小时前
重轨受电弓行业深度报告:市场格局、技术迭代与发展前景
大数据·数据库·人工智能
火山引擎开发者社区5 小时前
# 开发者集结!共探 AI Agent 创新应用新可能
人工智能