主题
层级团队与并行协作
要点
- 上一篇我们已经把 handoff 和 swarm 接起来了
- 层级团队要先区分上层分工和下层执行
- 并行协作要把状态合并规则写清楚,否则结果可能互相覆盖
- LangGraph 的并行执行依赖同一 super-step 中多个节点一起推进,结果再按 reducer 合并
- 任务继续变复杂时,上层角色也可以继续拆成多层
- 这种模式适合资料调研、内容策划、事实核对这类可以拆成多个工作面的任务
内容
1. 角色再多一点,问题就不只是交接了
上一篇我们已经把 handoff 和 swarm 接起来了。
到了那一步,系统里的角色已经不止一个,总控也不一定永远站在最上面。
但角色一旦继续变多,又会出现另一类问题。
比如你要做一个内容策划助理,用户丢过来一句话:
「帮我整理一篇 LangGraph 入门文章的内容方案,给我资料要点、文章结构和事实风险。」
如果只靠一个 Agent 去做,它要自己搜资料、看资料、归纳结论、设计结构、列风险、整理输出。流程可以跑,但单个角色需要同时处理太多工作面。
拆成多个角色以后,可以让一个角色查资料,一个角色设计文章结构,一个角色梳理事实风险。
可新的问题又来了:
这些角色由谁分工,哪些任务可以同时做,做完以后由谁汇总。
这时候光靠 handoff 已经不太够了,因为这不再是「A 把对话交给 B」这么简单,而是「上层角色怎么指挥一组下层角色,并且允许其中一部分并行工作」。
这一篇处理的就是这三件事:分工、并行和汇总。
结合官方 Graph API 的执行模型看,并行不是一句注释,也不是让一个节点里 Promise.all 一下就结束。图层面的并行,通常来自同一个节点连出多条边,或者条件路由返回多个目标。LangGraph 会在后续 super-step 里推进这些节点,再把它们的状态更新合并回同一份 State。
这也带来一个工程约束:只要多个并行节点可能写同一个字段,就要提前设计 reducer。否则三路资料、结构、风险结果写回 results 时,默认覆盖语义可能只留下最后一份。
内容策划场景里可以按这条线理解:
leadAgent只负责拆任务,不自己下场写全部内容。researchAgent、outlineAgent、riskAgent在同一轮 super-step 中并行执行。results用追加 reducer 接住多路结果。summarize等结果合并后再生成最终方案。
2. 先把这两件事分开看
这一篇的题目里其实有两层意思。
一层是「层级团队」。这说的是角色之间有上下级关系。最上面有一个负责人,中间可能还有一层子负责人,下面才是具体执行角色。
另一层是「并行协作」。这说的是任务不是只能一条线往下走。有些工作天然可以同时做,等它们都回来以后,再统一汇总。
放到一起看,就会很像一个小团队:
上层角色负责拆任务,下层角色各自处理自己那一块,能同时做的部分就并行推进,最后再把结果汇回上层收口。
和上一篇相比,handoff 更像接力;层级团队与并行协作更关注多人同时处理任务以后,结果怎样回到同一个状态里。
3. 先看最小的层级团队
先别一上来把图写复杂,先看最小结构。
这里用四个角色来讲:
leadAgent负责拆任务和收口researchAgent负责查资料和案例outlineAgent负责整理文章结构riskAgent负责梳理事实风险
如果用户要的是一份完整内容方案,leadAgent 不会自己把所有事情都做完,而是先让下面几个角色各做一块,最后再把结果整理起来。
typescript
// hierarchical-state.ts
import { StateSchema, ReducedValue } from '@langchain/langgraph'
import { z } from 'zod'
const appendResults = (
current: Array<{ role: string; content: string }>,
update: Array<{ role: string; content: string }>,
) => {
return [...current, ...update]
}
const State = new StateSchema({
topic: z.string().default(''),
plan: z.string().default(''),
results: new ReducedValue(
z
.array(
z.object({
role: z.string(),
content: z.string(),
}),
)
.default([]),
{ reducer: appendResults },
),
finalAnswer: z.string().default(''),
})这一篇里需要重点看 results 字段。
因为一旦开始多人协作,最后总得有个地方把不同角色的结果接回来。
4. 让上层角色先拆任务
先把最上面这一层写出来。这里故意不让 leadAgent 自己查资料,而是只做两件事:拆任务,收结果。
typescript
// lead-agent.ts
import type { GraphNode } from '@langchain/langgraph'
const leadAgent: GraphNode<typeof State> = (state) => {
return {
plan: `围绕「${state.topic}」拆成三部分:先收集资料和案例,再设计文章结构,最后单独梳理事实风险。`,
}
}这一层越克制越好。
如果负责人一边拆任务,一边自己也下场把细节全做了,后面所谓的层级团队就很容易又退回成「一个 Agent 忙所有事」。
5. 并行不是一句话,要真的让节点同时跑
下面开始接几个执行角色。
这一步很容易只停留在「并行」这个概念上。落到图里,并行意味着要把一条状态流分成多条边,让多个节点都在下一步里工作。
typescript
// parallel-workers.ts
import {
StateGraph,
StateSchema,
ReducedValue,
START,
END,
} from '@langchain/langgraph'
import type { GraphNode } from '@langchain/langgraph'
import { z } from 'zod'
const appendResults = (
current: Array<{ role: string; content: string }>,
update: Array<{ role: string; content: string }>,
) => {
return [...current, ...update]
}
const State = new StateSchema({
topic: z.string().default(''),
plan: z.string().default(''),
results: new ReducedValue(
z
.array(
z.object({
role: z.string(),
content: z.string(),
}),
)
.default([]),
{ reducer: appendResults },
),
finalAnswer: z.string().default(''),
})
const leadAgent: GraphNode<typeof State> = (state) => {
return {
plan: `围绕「${state.topic}」拆成三部分:资料要点、文章结构和事实风险并行推进。`,
}
}
const researchAgent: GraphNode<typeof State> = (state) => {
return {
results: [
{
role: 'research',
content: `资料员反馈:${state.topic} 需要覆盖 StateGraph、状态字段、节点和边这几个基础概念。`,
},
],
}
}
const outlineAgent: GraphNode<typeof State> = (state) => {
return {
results: [
{
role: 'outline',
content: `结构建议:${state.topic} 可以按「为什么需要图」「StateGraph 怎么组成」「最小示例怎么跑」三段展开。`,
},
],
}
}
const riskAgent: GraphNode<typeof State> = (state) => {
return {
results: [
{
role: 'risk',
content: `风险分析:${state.topic} 容易把 StateGraph 讲成普通链式调用,需要提醒读者关注状态合并和条件路由。`,
},
],
}
}
const summarize: GraphNode<typeof State> = (state) => {
// 这里拿到的 state.results,已经是前面几个并行节点都写回后的合并结果
return {
finalAnswer: state.results.map((item) => item.content).join('\n'),
}
}
const graph = new StateGraph(State)
.addNode('leadAgent', leadAgent)
.addNode('researchAgent', researchAgent)
.addNode('outlineAgent', outlineAgent)
.addNode('riskAgent', riskAgent)
.addNode('summarize', summarize)
.addEdge(START, 'leadAgent')
// 从同一个节点分出多条边,下一步里几个执行角色会一起工作
.addEdge('leadAgent', 'researchAgent')
.addEdge('leadAgent', 'outlineAgent')
.addEdge('leadAgent', 'riskAgent')
.addEdge('researchAgent', 'summarize')
.addEdge('outlineAgent', 'summarize')
.addEdge('riskAgent', 'summarize')
.addEdge('summarize', END)
.compile()这里需要关注的不是 researchAgent、outlineAgent 和 riskAgent 本身,而是:
typescript
// parallel-edges.ts
.addEdge('leadAgent', 'researchAgent')
.addEdge('leadAgent', 'outlineAgent')
.addEdge('leadAgent', 'riskAgent')这些边一起出现,图在后续 super-step 里就会把几个执行节点都推进起来。也正因为这样,results 不能用普通字段,而要用 ReducedValue 把多边结果接到一起。
这里还有一个容易忽略的点:summarize 不会在 researchAgent 刚返回时就提前开始工作。它会等这一步里需要汇合的结果都准备好,再拿着已经合并过的 results 往下走。
6. 如果没有 reducer,并行结果会互相覆盖
这一点很重要,因为它决定了「并行协作」到底是真的协作,还是看起来在并行,最后只剩下一份结果。
假设这里的 results 只是一个普通数组字段,没有 reducer。那么 researchAgent、outlineAgent 和 riskAgent 同时写回状态时,后返回的那份结果很可能会把前一份盖掉。
这一篇里之所以一直把 results 写成追加数组,就是因为并行节点最常见的需求,就是把多份结果收回来,而不是只保留最后一份。
所以把「层级团队」和「并行协作」放在一起看时,会很容易发现:前者决定谁分工,后者决定结果怎么汇合。
7. 再往前走一步:上层也可以继续分层
如果任务再复杂一点,最上面那层负责人自己也可能不会直接面对所有执行角色。
比如一份更大的内容项目,不只是查资料和梳理风险,还包括读者画像、SEO 方向、案例设计、技术准确性。这个时候,最上面可能只有一个总负责人,下面先拆成两个小负责人:
- 一个管内容策略和选题
- 一个管事实核对和技术边界
再往下,才是具体执行角色。
这时候图的感觉就会很像一个层级团队:
总负责人先拆大块,子负责人各自再拆小块,小块里能并行的继续并行,最后层层往上汇总。
你不一定非要一开始就把图写成三层,但一旦开始遇到「一个负责人也开始忙不过来」的情况,这种层级拆分就会很自然。
8. 写这种模式时最容易出的问题
最常见的问题,是把并行写成了串行。嘴上说「资料、结构和风险一起做」,代码里却还是一条边接一条边地往下串,那最后只是多了几个角色,并没有真的并行起来。
第二个问题,是汇总节点写得太早。两个执行节点还没把结果都交回来,汇总节点就已经开始生成最终答案,这样出来的结果往往会缺一块。
还有一种情况,是上层角色什么都想管。明明已经拆了资料员、结构编辑、事实核查员、子负责人,结果最上面的负责人还是把每一层细节都握在自己手里。这样图虽然层级变多了,实际决策压力还是没被拆开。
9. 总结
到了这里,多 Agent 这条线又往前走了一步。
前面的 Supervisor 解决的是总控怎么派活,handoff 和 swarm 解决的是控制权怎么流动,这一篇开始处理团队协作:上层怎么分工,哪些任务可以并行,结果又该怎么汇回来。
下一篇讲 Store:跨会话的长期记忆。到那时,问题会从「多个角色怎么一起做事」转到「这些角色跨会话以后,还能不能记住之前留下来的东西」。