LangGraph 实战第五课:Send 并行派活,流水线升级并发工厂
生命不息,折腾不止。上一课你的 Agent 已经能「一个老板带一队专家」了,但专家还是一个一个上、活还是一个一个干。今天教你把同一批互不相干的活拆开同时干,再把结果收回汇总,全程不用改任何一个专家的内部代码。
一、第四课留下的尾巴:专家其实还在「排队」
第四课用 Supervisor 派活,看着是一队专家协同,实际执行时每个时间步只让一个专家上场:老板派活 → 甲专家干完交回 → 老板再派乙。可现实中一大批活彼此根本没依赖——翻译十篇文档、给五百条用户评论分类、并行搜索几个信源——这么串行干就是纯浪费时间。
LangGraph 给「同一类活、批量干」的场景备了两个家伙:Send 负责把一份活扇出成 N 份同时派出去,reducer 负责把 N 份结果收回来合并。合起来就是经典的 map-reduce 模式:map 拆开并行干,reduce 收回来汇总。
二、Send:一条返回值,扇出 N 个分身
Send 的用法一句话:在一个「条件边」函数里,返回一串 Send(节点名, 状态参数),LangGraph 就会在同一时间步把这些节点全部并行跑一遍。 每个 Send 只塞那个分支自己需要的那一点点状态,不用塞整份大状态。
先看最小例子(官方文档原样,能直接跑):
1 | from langgraph.graph import StateGraph, START, END |
三个关键点拆开讲:
Send(节点名, 参数字典):第一个参数node是要并行的那个节点(generate_joke),第二个参数arg是只给这个分支用的参数。这是精髓——每个并行分支只拿自己那份,不拿全量状态,上下文不冗余;- 挂在
add_conditional_edges上扇出:把返回Send列表的函数写在条件边里,调度器会把这些Send全收集起来,在同一时间步并行执行,跑完才进入下一步。注意这里的第二参数["generate_joke"]是声明这些 Send 可能落到的目标节点; Annotated[list, operator.add]这个 reducer 绝对不能省:并行分支同时往jokes里写结果,没有 reducer 就是「后写的覆盖先写的」,最后只会剩最后跑完那个分支的一条结果。加了operator.add,LangGraph 才会把每个分支的返回追加合并进同一个列表。
导入路径提醒:新版用
from langgraph.types import Send,老一点的环境里from langgraph.constants import Send也常见,装最新版照前面这个写即可。
三、实战:长文档 map-reduce,拆分并行到汇总
拿个真实场景练手:你有一批长文档(或一篇几万字长文切成 N 段),要逐段总结再合成一篇总报告。串行跑一篇篇等;用 Send 拆开,一次全上。
1 | class State(TypedDict): |
跑的时候:
1 | result = graph.invoke({"docs": ["段落A", "段落B", "段落C", "段落D"]}) |
A、B、C、D 四段会同时开跑,全部完成后 aggregate 才执行——LangGraph 自动帮你做了「汇合点」(fan-in):所有并行分支到齐才继续往下走,一行 join 逻辑都不用你自己写。
这里 llm 就是前几课你已经接好的模型客户端(ChatOpenAI 之类,指向任意 OpenAI 兼容接口即可)。想省点接入成本,用中转站 ai.aklibk.com 这种国内直连、免绑卡、人民币按量付费的也行,接口格式跟官方一致,代码一行不用改。
看完这段你可能想问:上一课的 Supervisor 里,那几个专家能不能也并行?能,但专家之间往往有依赖(先调研再计算),不是纯独立活;文档总结这种「互不相干、同类活批量」才是 Send 的主场。判断标准就一条:这批任务之间有没有依赖,没依赖就上 Send。
四、子图:把每个专家打包成可复用模块
并行能跑了,代码也越写越长。等专家逻辑多起来,每个专家内部还自带好几个节点(比如「调研专家」内部 = 搜索 + 筛选 + 写小结三步),全摊在一个大图里,节点名会撞、状态互相踩,维护起来想死。
这时候上子图(Subgraph):把一套完整逻辑单独建一个 StateGraph 编译好,然后当成一个节点塞进父图。官方给了两种接法,看状态 key 是否一致:
- 父子共享 key → 直接把编译好的子图传给
add_node,零胶水; - 父子 key 不同 → 外面包一层函数,先把父状态转成子图输入,跑完再转回来。
先说最省事的第一种,官方例子(能直接跑):
1 | from langgraph.graph import START, StateGraph |
看到了没,父图一个 add_node("node_2", subgraph) 就把整套子逻辑接进来了,子图内部那俩节点对父图完全透明。这正是「可复用模块」的意义:同一个「调研专家子图」,A 项目当节点用,B 项目也能复用,还各自带独立状态、独立检查点。
想 debug 子图内部的状态,
stream时要加subgraphs=True,否则默认只看父图层。
五、避坑清单
- 忘加 reducer:
items: list[str]没写Annotated[..., operator.add],并行结果互相覆盖,最后「怎么只剩一条」——这是 Send 最高频的翻车点; - 把
Send存进 state:Send必须从节点/条件边的返回值发出去,它不是普通数据,别塞进 state 存着; - Send 参数塞全量状态:每个分支只带自己需要的那点参数,别把整份大状态复制 N 份,上下文和开销一起爆炸;
- 有依赖的任务硬上 Send:任务之间有先后依赖(乙的输入是甲的输出),拆并行就是乱跑。无依赖才并行;
- 在节点里手动 for 循环调子图:想要多次子图运行,别在一个节点里命令式循环
invoke子图——checkpoint 会撞MULTIPLE_SUBGRAPHS命名冲突,官方论坛的明确建议是直接改用 Send 扇出; - 无脑开 N 个分支:fan-out 成本随分支数线性涨,几十个还好,上千个就该在代码层限制并发宽度(cap),别指望框架替你兜底。
六、小结
到这一课,你的 LangGraph 技能树是这样:
- 第一课:会画图(State / Node / Edge);
- 第二课:会调工具、断点续跑、人审、长期记忆;
- 第三课:能打包成 API 对外服务;
- 第四课:会带队(Supervisor 派活);
- 第五课:会并行——Send 扇出 + map-reduce 汇总 + 子图模块化。
把「专家子图 + Supervisor 调度 + Send 并行 + 长期记忆 + API 打包」拼起来,已经是一套能上生产的多智能体骨架了。Send 解决的是「无依赖的活别傻等」,子图解决的是「逻辑一改到处改」,这俩是让 Agent 系统从「能跑」迈向「能规模化跑」的关键一步。
生命不息,折腾不止。第五课先到这。下一篇是 LangGraph 实战的收官课:把第四课挖下的坑填上——用
Command精确接管控制权,一个 Agent 干到一半,怎么带着现场状态、带着一句「任务描述」把控制权连锅端转交给下一个 Agent(goto+graph=Command.PARENT),把 Supervisor 那套工具式 handoff 升级成能精确传神的徒手 handoff。学完这篇,多智能体的三件事——派活(Supervisor)、并行(Send)、交接(Command)——你就全齐了。