LangGraph.js Supervisor-Worker 架构解析:把它拆成四件事,并实测两个成本旋钮
Supervisor-Worker 是中心化调度最典型的形态:一个调度者负责「派活」,若干专家负责「干活」。听起来像个新概念,但把它拆开只有四件事——而且我用大约 40 行手写代码复现了官方库完全相同的执行轨迹,这就是「它内部到底做了什么」最直接的证据。
更值得一看的是后半篇:这套架构有两个成本旋钮,实测能把共享上下文从 16 条消息压到 4 条(字符数 -71%),而执行轨迹一字不变。
但请先看下面那段更正:这套架构依然有效,官方的推荐实现却换了——从
createSupervisor(worker 是图节点)换成了 subagents pattern(子 agent 包成 tool)。我在 §五 把新写法实现并实测了,结论对新写法有利。
环境:Node v24.14.1 /
@langchain/langgraph@1.4.16/@langchain/langgraph-supervisor@1.1.2。为了让执行轨迹完全确定、可复现,我用脚本化模型(按调用次序返回预置响应的自定义BaseChatModel)替代真实 LLM——无需 API Key,全部输出都是本机真实运行结果。脚本留档在source/langgraph-supervisor/。
⚠️ 更正说明(2026-09-20 追加)
本文初版把
createSupervisor当作 Supervisor-Worker 的标准做法来推荐,这一点需要更正:官方已经改了推荐路径。
- LangChain 官方文档已把新代码导向 subagents pattern——把子 agent 包成一个 tool,交给一个普通的
createAgent调度;Python 侧另有专门的迁移文档 Migrate from langgraph-supervisor,给出了逐项映射(output_mode→ 在工具包装层里自己格式化输出、create_handoff_tool→ 自己写一个调用subagent.invoke()的 tool)。- 同时,
createReactAgent→createAgent也是 JS v1 的既定迁移方向(见 JavaScript v1 迁移指南)。本文初版的示例两个都用上了,等于踩在两条旧路径上。一个精确措辞上的澄清(核实依据分两层):官方文档的原话是「no longer actively maintained」(不再积极维护)——注意它说的是「不积极维护」,不是「已废弃」。工程上的旁证是我自己核对的 npm 元数据:该包没有
deprecated标记,最近一次发版是 2026-09-12 的 1.1.2(本文写作前 8 天),仍由 LangChain 官方维护者发布。所以:
- 存量代码不必惊慌,它不是"弃用炸弹";
- 但新代码确实不该再用——这不是"选哪个都行"的偏好问题,官方已明确表态推荐变了。
我把 subagents pattern 也实现并实测了,结论对它有利:同一个任务、路由用的 LLM 调用次数完全相同(都是 3 次),但共享上下文只有 6 条消息,不到库版本默认值(12 条)的一半,而且不需要调任何参数。完整实现与对比见下文「五、官方已改推荐:subagents pattern」。
本文仍然值得读的部分:§三 把 supervisor「拆成四件事」的机制分析依然成立——理解机制才能看懂两种实现的关系;§四 的两个成本旋钮实测也依然有效,但请注意它的定位变了:在新范式里,这件事由「你的工具包装层返回什么」直接决定,比调库参数更自由。读的时候请把 §二/§三 当作"存量代码的长什么样",把 §五 当作"新代码该怎么写"。
一、Supervisor-Worker 解决什么问题
单 Agent(一个 ReAct 循环 + 一堆工具)在工具变多时会退化:
- 工具太多,选错率上升——30 个工具塞给一个模型,它经常挑错;
- 上下文被工具说明挤满——每个工具的 description 都进提示词;
- 能力无法分工——「查资料」和「写代码」需要不同的提示词与工具集,硬塞在一起互相干扰。
Supervisor-Worker 的切法是按角色拆:
┌─────────────┐
用户 ─────► │ Supervisor │◄──── 每轮都回到这里决策
└──────┬──────┘
派活 │ │ 派活
▼ ▼
┌──────────┐ ┌──────────┐
│researcher│ │ coder │
└──────────┘ └──────────┘
自己的提示词 自己的提示词
自己的工具集 自己的工具集
关键点:worker 之间不直接通信,只跟 supervisor 对话;所有信息通过共享的消息状态流动。
二、最小可运行示例与真实轨迹
用官方包起一个 supervisor,核心代码就这几行:
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { createSupervisor } from '@langchain/langgraph-supervisor';
// 两个 worker:各自是独立的 ReAct agent,有自己的工具集
const researcher = createReactAgent({ llm: researcherLlm, tools: [searchTool], name: 'researcher' });
const coder = createReactAgent({ llm: coderLlm, tools: [codeTool], name: 'coder' });
// supervisor:只负责路由
const graph = createSupervisor({
agents: [researcher, coder],
llm: supervisorLlm,
prompt: '你是调度者:把任务分给合适的专家,完成后自己收尾。',
}).compile({ name: 'supervisor_demo' });
跑一次「调研 + 出示例」的任务,真实执行轨迹:
[LLM:supervisor] 第 1 次调用 → 调用工具 transfer_to_researcher
→ 节点 supervisor 新增消息 | ToolMessage "Successfully transferred to researcher"
[LLM:researcher] 第 1 次调用 → 调用工具 search
[LLM:researcher] 第 2 次调用 → 直接回答 "研究结论:Supervisor 负责路由,Worker 负责"
→ 节点 researcher 新增消息 | ToolMessage "Successfully transferred back to supervisor"
[LLM:supervisor] 第 2 次调用 → 调用工具 transfer_to_coder
→ 节点 supervisor 新增消息 | ToolMessage "Successfully transferred to coder"
[LLM:coder] 第 1 次调用 → 调用工具 write_code
[LLM:coder] 第 2 次调用 → 直接回答 "代码骨架已给出:graph = createSupervisor({...})"
→ 节点 coder 新增消息 | ToolMessage "Successfully transferred back to supervisor"
[LLM:supervisor] 第 3 次调用 → 直接回答 "已完成:先调研架构,再给出示例。"
执行轨迹: supervisor → researcher → supervisor → coder → supervisor
三个可以直接读出来的架构事实:
transfer_to_<agentName>是自动生成的工具——supervisor 的「派活」不是靠某种特殊语法,而是让模型调用一个工具。所以 supervisor 的 LLM 必须支持 tool calling。- 每派一次活,都要经过一次 supervisor——
supervisor → researcher → supervisor → coder → supervisor。supervisor 被调用了 3 次(两次派活 + 一次收尾)。 - 交接在消息流里是「可审计的」——
ToolMessage "Successfully transferred to X"与"...back to supervisor"成对出现,一眼能看出任务流转。
循环怎么结束? 看第 3 次调用:supervisor 没有再请求转交(返回一条普通消息),图就走到 END。「不再派活」就是终止条件——这也是死循环风险的来源(详见 §六)。
三、拆解:它内部只有四件事
我把这套架构手写了一遍(约 40 行),执行轨迹与官方库完全一致。四件事分别是:
① 共享状态:一个 messages 通道(共享黑板)
const State = Annotation.Root({
messages: Annotation({ reducer: (a, b) => a.concat(b), default: () => [] }),
});
reducer: (a, b) => a.concat(b) 就是「追加」语义——所有 agent 往同一块黑板上贴消息。这也是为什么 worker 之间不需要直接通信:它们读同一块黑板。
② 交接工具:把「请求转交」表达成「调用工具」
const handoffTools = Object.keys(AGENTS).map((name) =>
tool(async ({ task }) => `Successfully transferred to ${name}, task: ${task}`, {
name: `transfer_to_${name}`,
description: `把任务转交给 ${name}`,
schema: z.object({ task: z.string() }),
}),
);
这是整套架构最巧妙的一点:不给模型发明新语法,而是复用 tool calling。模型「调用 transfer_to_」= 「我要把活交给它」。
③ 路由节点:读模型输出,决定下一步去哪
async function supervisorNode(state) {
const res = await supervisorLlm.invoke(state.messages);
const call = res.tool_calls?.[0];
if (!call) {
// 没有请求转交 = 任务结束
return new Command({ goto: END, update: { messages: [res] } });
}
const target = call.name.replace(/^transfer_to_/, '');
return new Command({
goto: target,
update: {
messages: [
res,
new ToolMessage({ content: `Successfully transferred to ${target}`, tool_call_id: call.id, name: call.name }),
],
},
});
}
Command({ goto, update }) 是 LangGraph 里「既改状态又决定去哪」的原语——这就是「动态路由」的实现方式,不需要预先画好边。
④ worker 节点:跑自己的 agent,然后交回控制权
function makeWorkerNode(name) {
return async (state) => {
const result = await AGENTS[name].invoke({ messages: state.messages });
const produced = result.messages.slice(state.messages.length); // 只取这次新增的
return new Command({
goto: 'supervisor',
update: { messages: produced },
});
};
}
注意 goto: 'supervisor'——worker 干完一定要交回控制权,这就是 §二 里那条 transfer_back_to_supervisor 的由来。
⚠️ 手写时踩到的坑:返回 Command 必须声明 ends
第一次编译直接报错:
UnreachableNodeError: Node `researcher` is not reachable.
If you are returning Command objects from your node,
make sure you are passing names of potential destination nodes as an "ends" array
into ".addNode(..., { ends: ["node1", "node2"] })".
原因:只要节点返回 Command,LangGraph 就无法静态推断它可能去哪,图的连通性校验会认为目标节点不可达。修法是显式声明:
const builder = new StateGraph(State).addNode('supervisor', supervisorNode, {
ends: [...Object.keys(AGENTS), END], // ★ 声明所有可能的去向
});
for (const name of Object.keys(AGENTS)) {
builder.addNode(name, makeWorkerNode(name), { ends: ['supervisor'] });
}
这个坑我也写进了留档脚本的注释里。用官方
createSupervisor时它替你做了这件事——这正是「用库」相对「手写」省下的第一份认知成本。
手写版的真实输出(与库版本轨迹一致):
执行轨迹节点: supervisor → researcher → supervisor → coder → supervisor(与库版本一致)
消息总数: 12
消息序列:
0. human 帮我调研 LangGraph supervisor 并给出示例
1. ai tool_calls=transfer_to_researcher
2. tool name=transfer_to_researcher Successfully transferred to resear
3. ai name=researcher tool_calls=search
4. tool name=search 搜索「LangGraph supervisor」的结果:LangGr
5. ai name=researcher 研究结论:Supervisor 负责路由,Worker 负责执行。
6. ai tool_calls=transfer_to_coder
...
11. ai 已完成:先调研架构,再给出示例。
能复现同样的轨迹,就说明这四件事就是它的全部机制。
四、★ 实测:两个成本旋钮,上下文 -71%,轨迹不变
多 Agent 架构最贵的不是模型单价,而是共享消息历史的膨胀——每次派活,worker 都带着完整历史起跑。createSupervisor 给了两个旋钮来控制它。
旋钮一:outputMode(默认 last_message)
"full_history":把 worker 的整个消息历史并入共享状态;"last_message"(默认):只把 worker 的最后一条消息带回来。
旋钮二:addHandoffMessages(默认 true)
是否把 supervisor→worker 的交接记账消息(那条 transfer_to_X 的 AIMessage + 对应 ToolMessage)放进历史。默认保留;关掉可以给「严格校验 tool-call 序列的 provider」用。
三种配置的真实对比
| 配置 | 消息数 | 上下文字符数 | 消息构成 |
|---|---|---|---|
full_history | 16 | 408 | human 1 / ai 9 / tool 6 |
last_message(默认) | 12 | 338 | human 1 / ai 7 / tool 4 |
last_message + addHandoffMessages: false | 4 | 119 | human 1 / ai 3 |
执行轨迹在三种配置下完全相同:supervisor → researcher → supervisor → coder → supervisor。
这组数字的含义要说清楚:这两个旋钮是纯成本优化,不影响行为。 调到最省,共享上下文只剩四条:
[用户问题, researcher 结论, coder 产出, supervisor 总结]——408 → 119 字符,-71%。
它们各自丢掉了什么(看消息序列)
full_history 里,每个 worker 的内部轨迹都进了共享历史:
3. ai name=researcher tool_calls=search ← worker 内部工具调用
4. tool name=search 搜索「...」的结果 ← worker 内部工具结果
5. ai name=researcher 研究结论:... ← worker 的最终产出
6. ai name=researcher tool_calls=transfer_back_to_supervisor
7. tool name=transfer_back_to_supervisor Successfully transferred back
切到 last_message 后,第 3、4 条(worker 的内部工具调用对)被丢掉了,只留结论(第 5 条)——但第 6、7 条的交接记账还在,所以总量只降了 4 条。
再关掉 addHandoffMessages,交接记账也去掉,于是每个 worker 在共享历史上只留一条结论——总数直接掉到 4 条。
这就是「省钱」与「可审计」的取舍:
- 留着 handoff 消息:历史里能看出谁把活交给了谁,便于调试和审计;代价是每次派活多 2 条消息;
- 关掉之后:上下文最省,但共享状态不再是完整的审计日志——想知道流转过程,得靠 tracing(LangSmith / Langfuse)而不是消息历史。
我的建议:开发期用默认(
addHandoffMessages: true),靠消息历史排查问题最方便;上线压成本时再考虑关掉,并把审计责任移交给 tracing。顺带一提,观测那条链路本身有个坑我单独写过:不 flush 就丢 trace。
五、官方已改推荐:subagents pattern(含实测对比)
官方为什么改
原因只有一个词:上下文控制权。回看 §三 的机制——worker 是图节点,它跑完会把自己的消息并进共享状态,于是「带多少历史回去」这件事由框架决定,你只能通过 outputMode / addHandoffMessages 这类参数间接调节(§四 实测的那两个旋钮)。
subagents pattern 把这件事翻转了:worker 不再是节点,而是一个 tool。tool 返回什么、返回多少,完全由你写的那几行决定。官方迁移文档给的第一条映射就是它:
langgraph-supervisor | 推荐替代 |
|---|---|
createSupervisor(worker 是图节点) | createAgent + 子 agent 包成 tool |
outputMode(控制历史) | 在工具包装层里自己格式化输出 |
createHandoffTool(自定义路由) | 自己写一个调用 subagent.invoke() 的 tool |
| 嵌套 supervisor | 一个 subagent tool 内部再调其他 subagent |
完整实现(可运行)
import { tool } from '@langchain/core/tools';
import { HumanMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { createAgent } from 'langchain'; // ★ 不是 createReactAgent
// 1. 子 agent:用 createAgent 建,跟普通 agent 没有区别
const researcher = createAgent({ model: researcherLlm, tools: [searchTool], name: 'researcher' });
const coder = createAgent({ model: coderLlm, tools: [codeTool], name: 'coder' });
// 2. ★ 把它包成 tool —— 这就是 subagents pattern 的全部
function asSubagentTool({ name, description, agent }) {
return tool(
async ({ task }) => {
const result = await agent.invoke({ messages: [new HumanMessage(task)] });
const last = result.messages.at(-1);
const content = typeof last.content === 'string' ? last.content : JSON.stringify(last.content);
// ★ 官方说的「outputMode 由你负责」,就是这一行:返回什么,外层就看到什么
return `${name} 的结论:${content}`;
},
{ name: `ask_${name}`, description, schema: z.object({ task: z.string() }) },
);
}
const subagentTools = [
asSubagentTool({ name: 'researcher', description: '调研类问题交给它', agent: researcher }),
asSubagentTool({ name: 'coder', description: '写代码/给示例交给它', agent: coder }),
];
// 3. 调度者:一个普通 createAgent,没有任何"supervisor 特殊机制"
const coordinator = createAgent({
model: coordinatorLlm,
tools: subagentTools,
name: 'coordinator',
prompt: '你是调度者:把任务交给合适的专家,完成后自己收尾。',
});
注意第 3 步——外层就是一个普通的 ReAct agent。所谓「Supervisor 架构」在这里退化成了一个最普通的 agent 循环,因为它本来就不需要特殊机制:调度无非是「模型在一个循环里选工具」。
真实执行轨迹
[LLM:coordinator] 第 1 次调用 → 调用 ask_researcher
→ 节点 model_request
[LLM:researcher] 第 1 次调用 → 调用 search
[LLM:researcher] 第 2 次调用 → 收尾
→ 节点 tools
[LLM:coordinator] 第 2 次调用 → 调用 ask_coder
→ 节点 model_request
[LLM:coder] 第 1 次调用 → 调用 write_code
[LLM:coder] 第 2 次调用 → 收尾
→ 节点 tools
[LLM:coordinator] 第 3 次调用 → 收尾
→ 节点 model_request
外层图只有两个节点:model_request 和 tools(就是 ReAct 循环本身),而不是 supervisor 版里的「supervisor + researcher + coder」。
消息序列(值得逐行对比)
0. human 帮我调研 LangGraph supervisor 并给出示例
1. ai name=coordinator tool_calls=ask_researcher
2. tool name=ask_researcher researcher 的结论:研究结论:Supervisor 负责路由...
3. ai name=coordinator tool_calls=ask_coder
4. tool name=ask_coder coder 的结论:代码骨架已给出:graph = createSupervisor({..
5. ai name=coordinator 已完成:先调研架构,再给出示例。
只有 6 条。 对比 §四 那张表:supervisor 默认 12 条、full_history 16 条、极限压缩后才 4 条。subagents 版本不调任何参数就是 6 条,而且——子 agent 的内部轨迹(search / write_code 那些工具调用)根本不进外层状态,因为它们在另一个图里跑,边界就是那个 tool 的返回值。
同任务实测对比
Supervisor 库(默认 last_message) | 手写 Supervisor | Subagents pattern | |
|---|---|---|---|
| 外层图节点 | supervisor + 2 个 worker | 同左 | model_request / tools |
| 路由用的 LLM 调用 | 3 次 | 3 次 | 3 次(完全相同) |
| 共享状态消息数 | 12 | 12 | 6 |
| 子 agent 内部轨迹是否进外层 | 否(靠 outputMode 裁掉) | 是 | 否(天然隔离) |
| 交接记账消息 | 有(transfer + transfer_back 各一对) | 有(仅 transfer) | 无(工具调用本身就是交接) |
| 需要调参吗 | 要(outputMode/addHandoffMessages) | — | 不用 |
两个结论:
- 路由成本完全一样——都是 3 次 LLM 调用。所以「subagents 更省」不是省在模型调用上,而是省在上下文(12 → 6)和省在配置复杂度(不用调参);
- 它的上下文更短,不是靠裁剪,而是靠天然隔离——子 agent 是另一个图,它的中间过程从来没有进过外层状态,所以不存在「先并进来再裁掉」这回事。这正是官方说「more control over context engineering」的实质。
从官方文档里要记住的三条规则
官方文档给了一节 「Requirements for interrupt propagation」,三条规则(以下为原文要点,不是我的总结):
- ★ 只给最外层图挂 checkpointer(
Compile only the outermost graph with a checkpointer); - 子 agent 一律不要写
checkpointer=...——官方给的机制是:让它们使用 per-invocation 持久化,并在运行时继承父图的 checkpointer(inherit the parent's checkpointer at runtime)。这个继承链正是「子 agent 里的interrupt()能暂停整个图」的前提; thread_id必须传——最外层invoke()/stream_events()的configurable里要带thread_id,图才有得 checkpoint 与 resume。
而且官方明确说:这三条对任意层级的嵌套都成立(These rules apply to arbitrarily nested setups),给了一个「自定义 StateGraph 外层 + 中间 create_agent 监督者 + 内层 create_agent 子 agent」的三层例子。
⚠️ 早期版本把第 2 条的后果写成「会让中断静默失效」——官方文档并没有这么说,官方给的是上面这个「继承 + per-invocation 持久化」的机制解释;第 3 条(
thread_id)也是本次核对时补上的。
另外一条容易忽略的官方限定(重要):subagents 工具范式不是万能的。官方在嵌套那一节末尾写明——如果你需要 静态子图发现(static subgraph discovery)、按层级的 checkpoint 命名空间、或层级间共享 state key,那就该改用「自定义 StateGraph + subgraph 节点」,而不是把子 agent 包成 tool。
除了上面三条,还有一条来自 JS 文档的实践建议:子 agent 的 name / description 就是路由的提示词——「模型什么时候把活交给它」由这两个字段决定,它们是一等公民的调参对象,不是随便起的名字。
补充一个 JS 侧的写法差异:官方 JS 文档里的示例倾向于只暴露一个参数化的
tasktool(schema 里带agentName+description),而不是上面每个子 agent 一个 tool。二者等价,单一tasktool 的好处是工具列表不随子 agent 数量膨胀——子 agent 多了会明显更干净。
六、它和其他多 Agent 写法怎么选
| 写法 | 控制流 | 适合 | 不适合 |
|---|---|---|---|
| 单 Agent + 多工具 | 模型自己在一个循环里选工具 | 工具数 < 10、任务单一 | 工具多、需要不同提示词/模型 |
| ★ Subagents(当前推荐) | 中心化调度,子 agent 是一个 tool | 新代码默认选它:角色分明 + 上下文天然隔离 + 不用调参 | 需要「子 agent 之间互相对话」(那要靠更复杂的编排) |
Supervisor 库(createSupervisor) | 中心化调度,worker 是图节点 | 仅存量代码维护 | 新代码(官方已改推荐) |
| 条件边路由 | 预先写死的分支边 | 流程固定(如「先查后算」) | 需要模型临场决定下一步 |
| Handoff(去中心) | agent 之间直接交接 | 平等协作、链式转接 | 需要集中审计与节流 |
判断依据:你的流程需要「一个地方统一决策」吗? 需要就走中心化调度——而中心化调度的当前实现就是 subagents pattern;不需要,条件边或 handoff 更省(每次调度都要跑一遍 LLM,这是固定成本,三种中心化写法都一样)。
注意「Supervisor-Worker」是一种架构,而
createSupervisor只是它的一种(已过时的)实现。两者别混:架构继续有效,实现换了。
七、生产上要注意的五件事
- ★ 防死循环:终止条件是「supervisor 不再派活」,但模型可能反复派活。LangGraph 有
recursionLimit兜底——别关掉它,并给 supervisor 的 prompt 写清「什么时候算完成」。 - ★ 历史会膨胀:多轮任务里共享历史线性增长。官方给了
preModelHook——在 supervisor 调 LLM 前插一个节点做裁剪或摘要。这是长任务必须加的一层。 - 接 checkpointer 才有记忆:默认跑完即忘。要跨请求续聊,得挂 checkpointer(
thread_id),做法见 LangGraph 入门那篇。 includeAgentName有 provider 限制:默认(undefined)依赖 provider 支持 AI 消息上的name属性——目前只有 OpenAI 支持。其他 provider 要设成"inline",把 agent 名以 XML 标签塞进 content。- worker 的模型可以不同:这是这套架构的隐藏红利——
researcher用便宜模型、coder用强模型,各取所需(模型分层的做法见 模型路由器)。
八、局限
- 全部输出为脚本化模型的真实运行结果:执行轨迹、消息序列、参数效果都是真实的,但不反映真实 LLM 的决策质量(比如 supervisor 会不会挑错 worker);
- demo 只用了 2 个 worker、各 2 次 LLM 调用,所以上下文差值只有 4 条消息——真实场景里 worker 工具调用越多,
last_message省下的越多,本文的绝对值不可外推; - 未测 checkpointer / 持久化 / 中断恢复,因而也未验证§五 那三条官方规则(尤其「给子 agent 单独挂 checkpointer 会让中断静默失效」——这条我只做了文档核对,没有实测复现);
- 未测真实 provider 下
addHandoffMessages: false是否真能规避 tool-call 序列校验问题(这是官方注释给出的动机); - §五 的 subagents 实现是最简单的一种形态(每个子 agent 一个 tool):未测官方 JS 文档里那个「单一参数化
tasktool」写法,也未测多轮/嵌套 subagent; - 迁移文档我只核对了 Python 版(
/oss/python/migrate/langgraph-supervisor);JS 侧对应的是 Subagents 指南 与 v1 迁移指南,没有找到 JS 专属的 "migrate from supervisor" 页面——所以「JS 与该映射逐条对应」这一层是我基于两侧文档的推断; - 版本相关:结论以文首版本为准;
langgraph-supervisor仍在小版本迭代(本文写前一天发过 1.1.2)。
关联
- LangGraph 基础:状态图与记忆、reducer 与 Annotation 讲透
- 单 Agent 与多步循环:用 LangGraph 写 ReAct Agent
- 多 Agent 的成本与容错:弹性容错五层链(模型降级、熔断、舱壁)
- 观测这条链路:Langfuse 与 LangSmith
- 另一种「中心化调度」的对照:LangChain 还是 pi-agent?
自测
- Supervisor-Worker 里的「派活」是怎么实现的?为什么要求 supervisor 的模型必须支持 tool calling?
- 为什么完整轨迹总是
supervisor → worker → supervisor?worker 之间能直接对话吗? - 这套架构的终止条件是什么?死循环风险从哪来、怎么兜?
- 手写时返回
Command为什么要声明ends?用官方库为什么不用管这个? outputMode的full_history与last_message差在哪?实测差了几条消息?- 关掉
addHandoffMessages省下了什么?代价是什么?你会怎么取舍? - 什么情况下不该用中心化调度,而该用条件边或 handoff?
- subagents pattern 里「子 agent 内部轨迹不进外层状态」是靠裁剪实现的,还是靠隔离实现的?这两者的区别为什么重要?
- 为什么官方要求「只有最外层图能挂 checkpointer」?给子 agent 单独挂会怎样?
- 实测里 subagents 与 supervisor 库的路由 LLM 调用次数相同,那 subagents 的优势体现在哪两个地方?
参考
- 实测脚本与真实输出(随仓库留档):
source/langgraph-supervisor/——supervisor.mjs(官方库 + 三组参数对比)、handrolled.mjs(手写版,约 40 行复现同样轨迹)、subagents.mjs(subagents pattern,§五 的实现与实测) - 环境:Node v24.14.1 /
@langchain/langgraph@1.4.16/@langchain/langgraph-supervisor@1.1.2/@langchain/core@1.2.12/langchain@1.5.11 - 官方推荐路径:Subagents(JS)、Migrate from langgraph-supervisor(Python)、JavaScript v1 迁移指南(含
createReactAgent→createAgent) - 存量实现的 API:langgraph-supervisor 包、LangGraph JS · Multi-agent
- 说明:全程使用脚本化模型(自定义
BaseChatModel按次序返回预置响应),无需 API Key;本机未运行该项目的单元测试
