Claude Code 多 Agent 图解:SubAgent 实现机制怎么做?
Claude Code 多 Agent 图解:SubAgent 实现机制怎么做?
大家好,我是小宇宙。
最近不少朋友跟我反馈,说 AI Agent 岗的面试越来越多,十有八九都要问 Multi-Agent,什么「多 agent 之间怎么通信」「一个 agent 搞不定的任务怎么拆」「并发 agent 怎么调度」。
所以这篇文章我就想带你从源码视角,把 Claude Code 的多 Agent 机制彻底讲明白,目标是让你看完能同时 get 三个问题:
第一,架构是怎么设计的,多个 agent 之间怎么隔离、各自跑各自的。
第一,架构是怎么设计的,多个 agent 之间怎么隔离、各自跑各自的。
第二,协同机制是怎么跑起来的,父子 agent 怎么分工,多个 agent 怎么并发。
第二,协同机制是怎么跑起来的,父子 agent 怎么分工,多个 agent 怎么并发。
第三,通信方式是怎么设计的,agent 之间是直接调函数,还是有别的巧妙设计。
第三,通信方式是怎么设计的,agent 之间是直接调函数,还是有别的巧妙设计。
Claude Code 里跟「多 agent」沾边的代码其实有三套不同的机制:常规 Subagent、Fork Subagent、Coordinator 协调者模式。
后面我会按由浅入深的顺序,一个个讲清楚。

一、先搞明白 Multi-Agent 到底是个啥
在扒源码之前,我想先花一点篇幅,把 Multi-Agent 这个词的底层逻辑讲清楚。因为我发现很多人连「为啥要有多 agent」都没想明白,光盯着代码看是看不懂的。
为什么一个 agent 不够用?
我们先回到最朴素的 agent 模型:一个 LLM + 一堆工具 + 一个循环。你给它一个任务,它自己决定调什么工具、调几次,直到做完。这就是经典的 agentic loop。
看起来挺强的是吧?但一到真实项目里,问题就出来了。
想象你让一个 agent 去做这么一件事:「调研下 React 18 的新特性,然后在我的项目里实现一个 useTransition 的例子,最后帮我把代码评审一遍」。
这一套下来有三个麻烦:
第一,上下文会爆炸。调研阶段要看大量文档和 StackOverflow 链接,实现阶段要读项目代码,评审阶段又要重新读实现。三个阶段的内容全塞到一个 agent 的上下文里,token 蹭蹭往上涨,后面直接塞不下。
第二,职责混乱。一个 agent 既当研究员又当程序员又当评审员,它自己都不知道现在是什么角色,容易跑偏。比如调研到一半就开始写代码了,代码写到一半又去查文档。
第三,没法并发。一个 agent 一次只能做一件事,它在查文档的时候,项目代码就在那干等着。

老板派活的思路
这时候 Multi-Agent 的思路就来了。说白了,就像一个老板带团队:
老板不自己一头扎进代码里,而是把任务拆成几块,派给不同的「专家」。研究员去调研,工程师去写代码,评审员去挑错。老板自己只负责看大方向、收结果、做决策。
这样一来:每个专家的上下文是干净的(只装自己领域的信息);职责也清楚(研究员就好好查资料别去写代码);多个专家还能同时开工。
这就是 Multi-Agent 的核心思想:把一个大任务拆给多个职责清晰的 agent 去做,它们之间通过某种方式通信和协作。
Multi-Agent 的三种常见形态
绕开花哨的术语,Multi-Agent 系统在工业界落地时,一般就三种形态。

第一种,父子型。主 agent 处理整个任务,遇到某个子问题时派一个 subagent 出去搞定,拿结果回来接着干。这是最常见的,Claude Code 里的 Task 工具就是这种。
第二种,平级协作型。几个 agent 职责对等,通过共享状态或者消息互相协作。不过这种在工程上比较难落地,状态同步很麻烦。
第三种,主从型(Coordinator-Worker)。有一个专门的「协调者 agent」,它自己不干活,只负责派 worker、收结果、做合成。worker 之间互不通信,全靠协调者调度。这种是高并发场景的标配。
Claude Code 源码里,常规 Subagent 对应父子型,Coordinator 模式对应主从型,Fork Subagent 是父子型的一个特殊优化版本(跟 cache 有关,后面讲)。

subagent 在 Claude Code 里到底长啥样?
subagent 在 Claude Code 里到底长啥样?
讲到这儿可能还有朋友有点虚:「subagent 听起来挺抽象,它在 Claude Code 里到底长啥样,看得见吗?」
我举个真实能感知的场景你就懂了。
你跟 Claude Code 说「调研一下这个项目的认证模块」,它自己判断一下:这活得派个「侦察兵」去干,而不是我亲自扎进去。于是它在内部调了一个叫 Agent 的工具(对,这个工具的名字就叫 Agent),把任务交给一个叫 Explore 的内置 subagent 去跑。
Explore 带着一套精简的工具池(只有读文件、搜代码这些只读工具),带着一份独立的上下文,跑完调研把结果打包回来交给主 agent。主 agent 收到结果后,该改代码改代码、该回答回答。
所以 subagent 不是什么玄学,说白了就是「主 agent 通过一个特定工具派出去的另一个独立 agent 实例」。每一个 subagent 都是一个真实存在的执行单元,有自己的工具池、上下文、生命周期。

明白了这些,咱们就可以进入 Claude Code 的源码了。
二、Subagent 的隔离机制
在讲通信、讲并发之前,我想先从 Claude Code 多 agent 设计里最关键的一环讲起:隔离机制。
为什么隔离最关键?你想想,多 agent 系统本质就是「一堆 agent 共处一个进程、共享一个底层运行时」。如果隔离做得不好,一个 subagent 偷偷污染了父 agent 的状态、或者调了不该调的工具,整个系统就会乱成一锅粥。
Claude Code 在 subagent 启动时,把隔离做到了两个维度:工具隔离(不给子 agent 它不该有的工具)和 上下文隔离(不让子 agent 搅乱父 agent 的运行时状态)。咱们一个一个看。

第一维度:给子 agent 发一个定制工具箱
先说工具隔离。这是 Claude Code 多 agent 设计里最容易被忽略,但又很重要的一环。
什么意思呢?主 agent 拥有一大堆工具(读文件、写文件、执行命令、派 subagent、问用户问题等等几十个),但你不能把这堆工具原封不动地丢给 subagent。为啥?
你想想,如果 subagent 也能调派新 subagent 的工具,那它就能派子子 agent,子子 agent 又派子子子 agent,层层嵌套没完没了,token 消耗直接起飞。
再比如主 agent 用来管理任务列表的工具,是给主 agent 的大脑用的,subagent 跟着瞎写会污染主 agent 的待办状态。
所以 Claude Code 给 subagent 发工具的思路是「按 agent 身份走三道准入门」:
第一道门是「所有 subagent 通用黑名单」。这道门里被禁的工具有几类:
能派新 subagent 的工具:防止子再派孙、孙再派重孙的递归嵌套
能主动问用户问题的工具:子 agent 不该抢主 agent 的对话权,用户是跟主 agent 说话的
能切换规划模式的工具:规划模式是主 agent 用来跟用户对齐方案的,子 agent 没资格切
能停止其他任务的工具:任务管理是主线程的专属权力,子 agent 乱停会天下大乱
第二道门是「自定义 agent 多套一层黑名单」。用户自己写的 agent(比如在项目里自己配的那种 Markdown agent)比内置 agent 要再严一点,因为用户写的没经过官方审核,多防一道更安全。
第三道门反过来,是「后台异步 agent 走白名单」。这类 agent 是完全后台跑的,没法跟用户交互,所以只准用事先圈定好的一小批工具(读文件、搜代码、执行命令、编辑文件这些)。白名单的哲学是「默认不准用,明确列出来的才能用」,比黑名单更保险。
三道门走下来,每个 subagent 拿到的都是一份量身定制的工具池,既够它干活,又不会越权。

这个机制在源码里其实就是一个过滤函数:
// src/tools/AgentTool/agentToolUtils.ts:70
export function filterToolsForAgent({ tools, isBuiltIn, isAsync, permissionMode }): Tools {
return tools.filter(tool => {
if (tool.name.startsWith('mcp__')) return true // MCP 工具全放行
if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
return false
}
return true
})
}
// src/tools/AgentTool/agentToolUtils.ts:70
export function filterToolsForAgent({ tools, isBuiltIn, isAsync, permissionMode }): Tools {
return tools.filter(tool => {
if (tool.name.startsWith('mcp__')) return true // MCP 工具全放行
if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
return false
}
return true
})
}
可以看到就是顺着「全局黑名单 → 自定义 agent 加严 → 异步白名单」这三道条件依次判定。最后留下来的,才是这个 subagent 能用的工具。

这个设计看着简单,其实挺有工程智慧的。我在设计自己的多 agent 系统时,就学到了一条原则:不要假设所有 agent 都能用所有工具,按 agent 类型做细粒度的权限控制。
第二维度:搭一个隔离的运行环境
说完工具,再来聊第二维度:上下文隔离。这块是 Claude Code 多 agent 设计里最精髓的一块,我觉得全篇文章最值得细读的就是这一节。
先说问题。父 agent 跑起来后有一个庞大的运行时上下文,里面装着很多东西:已经读过哪些文件、每个文件读到第几行、全局的 UI 状态、中止信号、权限状态、任务注册表等等。
现在轮到你做设计。要派一个 subagent,这份庞大上下文怎么传给它?
你脑子里很可能蹦出两个直觉方案:A 完全共享(父那份直接给子用)、或者 B 完全新建(给子一份全新空的)。先别看下面,自己想想哪个对?
…
先说 A 不行,举个具体场景你就懂:父 agent 已经读过 file.ts 的前 100 行,子 agent 拿过去接着读到 200 行。这下父 agent 那边「文件读到哪了」的缓存被刷成 200 了,下次它要读这文件就以为自己已经读过 200 行了,直接跳过。子的一次操作,把父的视图污染了。
再说 B 也不行:用户按 Ctrl+C 想中止整个任务,主线程把中止信号广播出去,结果子 agent 因为是全新上下文收不到这个信号,对外面发生啥一无所知,自顾自继续跑。子 agent 跟世界完全脱节了。
发现了吧,两个极端都走不通。那 Claude Code 怎么办?答案是一个很巧妙的折中思路:不按「整体」决策,而是按「字段」决策。每一项状态单独判断该克隆、该共享、该屏蔽,还是该新建。
我把 Claude Code 在这件事上的四个关键决策挑出来,用大白话讲一遍:
决策一:「读文件的缓存」要复制一份给子 agent
这个缓存存的是「这个文件读过没、读到第几行」。如果父子共享,子 agent 读了某个文件,父 agent 会误以为自己也读过,下次跳过不读,数据就错了。所以要复制一份独立的给子 agent,子怎么折腾都不影响父的文件视图。
决策二:「改全局状态」这件事对子 agent 直接关闭
全局 UI 状态是主线程用 React 在管的。如果异步 subagent 也能改,就会出现「两边同时改同一份状态、抢起来对不上」的问题,界面就花了。所以 Claude Code 干脆把 subagent 的「写全局状态」这个权力完全关闭掉,改成空操作,一了百了。
决策三:但「注册后台任务」这条通路得保留
这里有个小细节值得讲。既然子 agent 的写权力关掉了,那它自己起的后台进程(比如在后台跑一条 bash 命令)怎么登记到全局任务表?
Claude Code 专门开了一个小口子:其他写全局的口都堵死,唯独「注册/结束后台任务」这条路留着。不然子 agent 起的后台进程就变成「没爹的孤儿进程」,永远在后台跑没人回收。
决策四:给每个 subagent 发独立 ID、深度代代 +1
每派一个 subagent,都给它一个独立的 ID,并且在父 agent 的深度基础上 +1。这样系统能随时知道「当前这个 agent 处于嵌套的第几层」。深度超过阈值(比如 5 层)就报警甚至强制停止,防止意外嵌套失控。
这四个决策其实回答了四类问题:信息怎么传、状态怎么写、通路怎么留、身份怎么追踪。

对应到源码里,就是一个叫 createSubagentContext 的函数,我把最能说明上面四个决策的部分精简出来:
createSubagentContext
// src/utils/forkedAgent.ts:345
export function createSubagentContext(parentContext, overrides): ToolUseContext {
return {
// 决策一:文件读缓存克隆一份
readFileState: cloneFileStateCache(parentContext.readFileState),
// 决策二:写全局状态直接设为空操作
setAppState: () => {},
// 决策三:但任务注册的通路例外保留
setAppStateForTasks: parentContext.setAppStateForTasks ?? parentContext.setAppState,
// 决策四:独立 ID + 深度 +1
agentId: overrides?.agentId ?? createAgentId(),
queryTracking: {
chainId: randomUUID(),
depth: (parentContext.queryTracking?.depth ?? -1) + 1,
},
// ...其他字段略
}
}
// src/utils/forkedAgent.ts:345
export function createSubagentContext(parentContext, overrides): ToolUseContext {
return {
// 决策一:文件读缓存克隆一份
readFileState: cloneFileStateCache(parentContext.readFileState),
// 决策二:写全局状态直接设为空操作
setAppState: () => {},
// 决策三:但任务注册的通路例外保留
setAppStateForTasks: parentContext.setAppStateForTasks ?? parentContext.setAppState,
// 决策四:独立 ID + 深度 +1
agentId: overrides?.agentId ?? createAgentId(),
queryTracking: {
chainId: randomUUID(),
depth: (parentContext.queryTracking?.depth ?? -1) + 1,
},
// ...其他字段略
}
}
你看这几行代码,一一对应上面讲的四个决策:克隆缓存、关掉写权限、保留任务通路、发独立 ID。
看完这块,我的感受是:所谓上下文隔离,不是一刀切地「全隔离」或者「不隔离」,而是按每个状态的语义单独决策。这个细腻劲儿,正是 Claude Code 这种工业级产品稳定跑的根基。

走完「工具隔离」和「上下文隔离」这两道门,一个 subagent 就拿到了干净的工具池 + 干净的运行环境,可以独立跑起来了。那父 agent 和这个跑起来的 subagent,又是怎么互相说话的呢?下一章见真章。
三、父子 Agent 是怎么通信的
隔离机制搞定了,但隔离只是开始,真正决定一个多 agent 系统好不好用的,是它们之间怎么通信。
这一章我来讲 Claude Code 的通信方式。但开讲之前,得先立一个分水岭,不然很容易把人带沟里去:父子之间能怎么通信,取决于你开没开「团队(agent-teams)模式」。
默认形态和团队形态,是两条很不一样的线:
默认形态:subagent 更像一次「重型工具调用」,父 agent 派它出去、它跑完把结果交回来。这条线里,父 agent 没法中途给在跑的子 agent 插话,消息基本是子→父 单向通知。
团队(agent-teams)模式:开启之后才升级成完整的双向消息驱动,父 agent 能往子 agent 的信箱里扔字条,子 agent 也能回话,真正的双向对讲。
下面两条线分开讲,你就不会糊涂了。
配图意见:父子通信「两条线」对照图。左右两栏,左边「默认形态」画一根从子指向父的单向箭头(标注「只有子→父 完成通知」),右边「团队模式」画父子之间两根来回箭头(标注「父→子 扔字条 + 子→父 通知」)。重点用箭头数量和方向直观表达「默认单向 vs 团队双向」,配色上左栏冷色(克制)、右栏暖色(双向激活)。
配图意见:父子通信「两条线」对照图。左右两栏,左边「默认形态」画一根从子指向父的单向箭头(标注「只有子→父 完成通知」),右边「团队模式」画父子之间两根来回箭头(标注「父→子 扔字条 + 子→父 通知」)。重点用箭头数量和方向直观表达「默认单向 vs 团队双向」,配色上左栏冷色(克制)、右栏暖色(双向激活)。
默认形态:派出去,跑完把结果交回来
先看默认这条线。这里先停一下,问你一个问题:父 agent 派一个子 agent 出去调研,在子 agent 埋头跑的这段时间里,它俩还能不能说上话?父能不能临时补一句「顺便也看看权限模块」?
凭直觉你可能觉得「应该可以吧」。但默认形态下的答案是:不能。
你跟 Claude Code 说「调研一下认证模块」,它派一个 subagent 出去,这个 subagent 带着独立上下文自己跑,跑完把结果作为一次工具调用的返回值(tool_result)原样交回给父 agent。
注意这条线的关键特征:父 agent 对正在跑的子 agent 是「只能等」的,没法中途塞新指令给它。就是一次「派出去,等结果」,跟你平时调一个普通工具没啥两样。
那又冒出一个新问题:如果子任务跑得特别久,父 agent 干等着不就被卡死了吗?Claude Code 这里有个补丁,叫 auto-background。
如果 subagent 很快跑完(比如 30 秒内),父 agent 就在前台阻塞等,像一次普通工具调用,完事就拿结果继续。但如果 subagent 跑超过 2 分钟还没完,Claude Code 会自动把它转到后台,让父 agent 可以先继续干别的。子任务真完成时,再回头通知父 agent。
这个设计本质上是把同步工具调用自动降级成异步通知的优化。没有它,长任务会一直占着父 agent 的执行权,用户也没法跟父 agent 继续对话。

源码里这个「2 分钟阈值」就是一个常量开关,而且它本身也带着 feature 门控,不是无条件开的:
// src/tools/AgentTool/AgentTool.tsx:72
function getAutoBackgroundMs(): number {
if (isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS)
|| getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)) {
return 120_000; // 2 分钟
}
return 0;
}
// src/tools/AgentTool/AgentTool.tsx:72
function getAutoBackgroundMs(): number {
if (isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS)
|| getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)) {
return 120_000; // 2 分钟
}
return 0;
}
那子任务转后台之后,完成时怎么告诉父 agent「我干完了」?
最直觉的做法是:给主线程发一个「工具返回结果」事件。但 Claude Code 玩得更骚气,它的设计是:把完成通知拼成一段 XML,伪装成一条用户消息,塞给父 agent 的对话历史。
父 agent 那边看到的就像用户发了一条新消息过来,长这样:
<task-notification>
<task-id>agent-a1b</task-id>
<output-file>/tmp/xxx.txt</output-file>
<status>completed</status>
<summary>Agent "Investigate auth bug" completed</summary>
<result>Found null pointer in src/auth/validate.ts:42...</result>
<usage>
<total_tokens>12345</total_tokens>
<tool_uses>8</tool_uses>
<duration_ms>34567</duration_ms>
</usage>
</task-notification>
<task-notification>
<task-id>agent-a1b</task-id>
<output-file>/tmp/xxx.txt</output-file>
<status>completed</status>
<summary>Agent "Investigate auth bug" completed</summary>
<result>Found null pointer in src/auth/validate.ts:42...</result>
<usage>
<total_tokens>12345</total_tokens>
<tool_uses>8</tool_uses>
<duration_ms>34567</duration_ms>
</usage>
</task-notification>
📌 配图建议:task-notification XML 渲染示意,高亮各个 tag 的含义
📌 配图建议:task-notification XML 渲染示意,高亮各个 tag 的含义

为啥要搞 XML 不用结构化对象? 这个设计有它的巧妙之处,我特意想明白过。
第一,LLM 对 XML 非常友好。Anthropic 训练 Claude 的时候就强调了 XML 的结构化表达。你把 XML 塞到 prompt 里,LLM 能很自然地解析出语义,不用额外教它。
第二,XML 是纯文本,可以直接塞进对话历史。如果是结构化对象,还得额外走个「工具结果」的字段结构,流程更复杂。
第三,它伪装成用户消息,天然地复用了 agentic loop 的处理逻辑。父 agent 不需要额外的状态机去「等通知」,它就像收到一条新的用户输入一样处理。
这种「把系统事件伪装成对话」的设计思路,在 LLM 应用里是非常值得学的一招。

对应到源码里,生成这段 XML 的代码就是在拼字符串:
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:197
const message = `<${TASK_NOTIFICATION_TAG}>
<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>
<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
<${STATUS_TAG}>${status}</${STATUS_TAG}>
<${SUMMARY_TAG}>${summary}</${SUMMARY_TAG}>${resultSection}${usageSection}
</${TASK_NOTIFICATION_TAG}>`;
enqueuePendingNotification({ value: message, mode: 'task-notification' });
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:197
const message = `<${TASK_NOTIFICATION_TAG}>
<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>
<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
<${STATUS_TAG}>${status}</${STATUS_TAG}>
<${SUMMARY_TAG}>${summary}</${SUMMARY_TAG}>${resultSection}${usageSection}
</${TASK_NOTIFICATION_TAG}>`;
enqueuePendingNotification({ value: message, mode: 'task-notification' });
拼完就扔到主 agent 的待处理消息队列里,等主 agent 下一轮循环时当作一条用户消息来处理。
讲到这里,把默认这条线小结一下:父 agent 派出去、等结果,长任务转后台后子 agent 回头发个完成通知。这条线里有「消息」的,基本只有子→父这一个方向的通知,父 agent 没法主动给在跑的子 agent 发新指令。如果你只看默认形态,说「subagent 的消息队列只是用来通知父 agent 的、是单向的」,这个判断是站得住的。
团队(agent-teams)模式:父子之间才真正双向对讲
那「父→子 也能发消息」的双向对讲是什么时候出现的?答案是:开了团队(agent-teams)模式之后。这是 Claude Code 的一个实验特性,外部用户要显式开启(比如带上 --agent-teams 启动),内部默认就开着。
--agent-teams
为什么要专门为这个场景设计一套双向通信?我建议你先停个 10 秒想想:如果让你来设计「父 agent 中途给子 agent 发指令」这套通信,你会怎么写?
大概率你脑子里第一反应是「父 agent 调个函数,等 subagent 跑完返回」对吧?这跟我们平时写 RPC 调远程服务的思路一模一样,太自然了。
但我接着追问你两个问题,你看你能不能答上来:
第一个追问:如果 subagent 是个跑 5 分钟的代码评审任务,那这 5 分钟里,父 agent 想临时改个要求,怎么递进去?
第二个追问:如果父 agent 想同时指挥 5 个 subagent 并行干活、随时给它们各自补充指令,你这个「调函数等返回」的方案要怎么改?
是不是有点卡了?「调函数等返回」这种同步思路,根本没法支持「一边跑一边对讲」。Claude Code 正是看穿了这一点,才在团队模式里铺了一套完全不一样的底座:消息驱动。
想象每个 subagent 是公司里一个带「信箱」的独立员工。父 agent 要给它布置新活,就往它信箱里扔一张字条走人,不站在那里等。subagent 自己干完活了,通过另一条信道把结果送回主 agent 的案头。
这个「信箱 + 字条」的模型,本质上就是消息队列 + 异步通知。没有直接的函数返回,没有主线程阻塞,所有沟通都是消息。
配图意见:沿用原「函数调用 vs 消息驱动」对比图,无需重画。只需把图注/标题里若有「Claude Code 的通信用消息驱动」这类绝对说法,收窄成「团队模式选了消息驱动」,避免读者误以为这是默认形态的通用机制。
配图意见:沿用原「函数调用 vs 消息驱动」对比图,无需重画。只需把图注/标题里若有「Claude Code 的通信用消息驱动」这类绝对说法,收窄成「团队模式选了消息驱动」,避免读者误以为这是默认形态的通用机制。

先看每个 subagent 的「员工档案」。Claude Code 给每个 subagent 建了一份档案:一个对象,里面记着这个 subagent 的 ID、当前状态(等待中/跑步中/已完成/失败/被停了)、它的信箱(待处理消息数组)、已经产生的结果、进度信息等等。
要说明的是,这份档案本身是通用的,不是团队模式才有,默认形态下那套「转后台、发完成通知」也是靠它来追踪每个子任务的。我们这里要重点盯住的,是里面那个信箱字段,它才是团队模式真正用起来、支撑起「父→子 扔字条」的部分。
所有跟 subagent 有关的读写(父要发消息,子要改状态),都通过全局的 task 表里这份档案来进行。
对应到源码里的类型定义大致长这样:
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:116
export type LocalAgentTaskState = TaskStateBase & {
type: 'local_agent';
agentId: string; // 子 agent 唯一 ID
prompt: string; // 初始任务
agentType: string;
status: TaskStatus; // pending/running/completed/failed/killed
result?: AgentToolResult; // 完成后的结果
progress?: AgentProgress; // 进度
isBackgrounded: boolean; // 是否已转后台
pendingMessages: string[]; // 信箱:父 agent 扔进来的待处理消息
messages?: Message[];
};
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:116
export type LocalAgentTaskState = TaskStateBase & {
type: 'local_agent';
agentId: string; // 子 agent 唯一 ID
prompt: string; // 初始任务
agentType: string;
status: TaskStatus; // pending/running/completed/failed/killed
result?: AgentToolResult; // 完成后的结果
progress?: AgentProgress; // 进度
isBackgrounded: boolean; // 是否已转后台
pendingMessages: string[]; // 信箱:父 agent 扔进来的待处理消息
messages?: Message[];
};
重点关注的是 pendingMessages 数组,它就是我们说的「信箱」,父 agent 往里扔字条,子 agent 自己来捡。
pendingMessages

再看父 → 子怎么扔字条。父 agent 要给跑着的 subagent 发指令的流程,拆开看就是两步:
第一步:父往信箱扔字条。父 agent 在自己的 agentic loop 里调用一个叫 SendMessage 的工具,工具内部做的事情很简单:往目标 subagent 档案的信箱末尾追加一条消息,然后立刻返回。父 agent 扔完走人,不等子 agent 看。
这里要点一句关键的:SendMessage 这个工具本身就是团队模式才启用的。它的 isEnabled 判断挂的就是「有没有开 agent-teams」这个开关,没开团队模式,主 agent 的工具箱里压根没有 SendMessage,父→子 这条路自然就不存在。这也是前面说「默认形态是单向」的根本原因。
第二步:子在循环边界自己捡字条。subagent 自己的 agentic loop 在每一轮工具调用结束后,都会去瞄一眼自己的信箱。如果有新字条,就把这些字条作为「用户消息」注入自己的对话历史,然后带着新消息进入下一轮 LLM 调用。
这里还有个细节设计特别巧:如果子 agent 已经干完活停下来了(completed 或者被手动停了),父 agent 发 SendMessage 会怎样?
Claude Code 的做法是:自动把它唤醒。从磁盘上那份已经保存的对话 transcript 里,把子 agent 的完整对话历史恢复出来,拼上新消息,重新跑起来。这个唤醒机制很妙,意味着 subagent 即使完成了也不是「死了」,父 agent 随时可以叫醒它继续干。
配图意见:沿用原父子通信时序图,无需重画。但标题/图注要补上「团队模式下」这个前提(比如「团队模式下的父子双向通信时序」),别让读者误以为这套父→子 + 子→父 的完整来回是默认形态的通用流程。
配图意见:沿用原父子通信时序图,无需重画。但标题/图注要补上「团队模式下」这个前提(比如「团队模式下的父子双向通信时序」),别让读者误以为这套父→子 + 子→父 的完整来回是默认形态的通用流程。

对应到源码,SendMessage 工具里的核心逻辑长这样:
// src/tools/SendMessageTool/SendMessageTool.ts:800
const task = appState.tasks[agentId]
if (isLocalAgentTask(task) && !isMainSessionTask(task)) {
if (task.status === 'running') {
queuePendingMessage(agentId, input.message, context.setAppStateForTasks)
return { data: { success: true, message: 'Message queued...' } }
}
// 任务已停止,自动唤醒从 transcript 里恢复
const result = await resumeAgentBackground({ agentId, prompt: input.message, ... })
}
// src/tools/SendMessageTool/SendMessageTool.ts:800
const task = appState.tasks[agentId]
if (isLocalAgentTask(task) && !isMainSessionTask(task)) {
if (task.status === 'running') {
queuePendingMessage(agentId, input.message, context.setAppStateForTasks)
return { data: { success: true, message: 'Message queued...' } }
}
// 任务已停止,自动唤醒从 transcript 里恢复
const result = await resumeAgentBackground({ agentId, prompt: input.message, ... })
}
可以看到就是两个分支:正在跑就扔信箱,已经停了就唤醒。
「扔信箱」这个动作本身的实现就 4 行:
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:162
export function queuePendingMessage(taskId, msg, setAppState): void {
updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => ({
...task,
pendingMessages: [...task.pendingMessages, msg]
}));
}
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:162
export function queuePendingMessage(taskId, msg, setAppState): void {
updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => ({
...task,
pendingMessages: [...task.pendingMessages, msg]
}));
}
纯纯的「追加到数组末尾」。子→父 那条信道,复用的就是默认形态里讲过的那套 task-notification(伪装成用户消息)。两个方向凑齐,团队模式下的父子通信才算真正双向。
回头看通信设计的全貌
到这里我们把两条线都讲清楚了,串起来看:
默认形态:父 agent 派子 agent 出去、等结果,长任务转后台后子 agent 回头发完成通知。消息通路基本是子→父 单向通知,父 agent 不能中途插话。
团队(agent-teams)模式:在上面基础上,补齐父→子这条路(SendMessage 往信箱扔字条),凑成完整的双向消息驱动。
团队模式那套双向消息体系,落到底就两个关键字:异步 + 消息。没有直接函数调用,没有锁,没有回调地狱,全靠读写共享的任务状态和消息队列。
配图意见:这张「通信全貌」图建议重画。原图若只画了一套双向通信,现在撑不起「两条线」这个新框架了。改成上下或左右两栏:一栏「默认形态」画「父派出 → 子跑 →(超 2 分钟转后台)→ 子发完成通知」的单向链路,一栏「团队模式」在其上叠加「父→子 SendMessage 扔字条」的回路。重点让读者一眼看出:团队模式是在默认形态之上「补了父→子这一条」,而不是另起炉灶。
配图意见:这张「通信全貌」图建议重画。原图若只画了一套双向通信,现在撑不起「两条线」这个新框架了。改成上下或左右两栏:一栏「默认形态」画「父派出 → 子跑 →(超 2 分钟转后台)→ 子发完成通知」的单向链路,一栏「团队模式」在其上叠加「父→子 SendMessage 扔字条」的回路。重点让读者一眼看出:团队模式是在默认形态之上「补了父→子这一条」,而不是另起炉灶。

而且不管走哪条线,这套「不阻塞」的底子都带来一个特别大的好处:天然支持多 subagent 并发。只要父 agent 不傻等着某个子跑完,它就能同时派 5 个 subagent 出去,谁先完成谁先给它发通知,父 agent 按到达顺序处理就行。并发不是团队模式的专利,默认形态配合 auto-background 也能并发,后面要讲的 Coordinator 模式更是把并发拉到极致。

下一章,我们再讲一个特别精妙的优化:Fork Subagent。
四、Fork Subagent:省钱又省延迟的隐藏大招
前面讲的常规 subagent 已经是主流玩法了,但 Claude Code 还有一个更精妙的机制,叫 Fork Subagent。这个机制有点隐蔽,用起来是透明的,但对成本和延迟的优化非常显著。
我先抛两个问题让你估算下,先别往下翻看答案:
第一,Claude Code 的 system prompt 大概有多长?是几百 token、几千 token,还是上万 token? 第二,每派一个 subagent,如果它有自己独立的 system prompt,LLM API 那边对这段 prompt 是从头算一遍,还是有办法复用?
subagent 的隐藏成本
公布答案:Claude Code 的 system prompt 长度是上万 token,里面塞了大量的工具说明、规范约定、用户上下文。
而每派一个 subagent,如果它有独立的 system prompt(内置的 Explore、Plan 这些都有独立的),LLM API 那边就得对这一万多 token 重新从头算一遍,就跟没见过似的。
这有两个代价:钱(input token 重新算钱)和延迟(首 token 等更久)。在生产环境里,subagent 派得越频繁,这个开销线性放大,是个很可怕的成本黑洞。
Anthropic 有个 prompt 缓存机制可以缓解这事。简单说:API 请求里如果前缀跟之前某次请求一样,这段前缀可以不重新算,直接走缓存,价钱只要原来的 10%,延迟也大幅降低。
到这儿我再问你一个关键的:prompt 缓存命中的条件是「内容大致相同」就行,还是「字符级别相同」,还是「字节级别完全相同」?再猜一下。
公布:是最严格的那个,字节级别完全相同。系统 prompt 一个字不一样、工具列表顺序不一样、甚至空格位置不一样,都会直接没命中缓存。
是不是比你想的严格多了?
那既然这么严,能不能设计一种 subagent,它的 system prompt 和工具池跟父 agent 完全一样,这样就能复用父的缓存了?这就是 Fork Subagent 的起点。

Fork 的核心思路:派一个「字节级相同」的分身
Fork Subagent 的直觉是这样的:派一个子 agent 出去干活,但这个子 agent 的 API 请求前缀跟父 agent 一模一样,让 Anthropic 那边一看:「哦这个前缀我认识」,走缓存。
这里的「一模一样」要做到什么程度?字节级。一个字节不对都不行。
具体要对齐哪些东西呢?有五样必须跟父 agent 完全一致:
系统 prompt 的内容(最核心的,对齐第一位)
用户上下文(拼在消息前的那部分动态内容,比如当前项目的 CLAUDE.md 内容)
系统上下文(拼在 system prompt 后的环境信息)
工具池的顺序和定义(工具的字段结构会被序列化进 API 请求,顺序都不能变)
对话历史的前缀(决定了 user/assistant 消息序列中「从哪里开始分叉」)
这五样只要有一样跟父 agent 字节不一致,缓存就直接没了。
对应到源码里,Claude Code 专门定义了一个类型(CacheSafeParams),把这五项打包:
// src/utils/forkedAgent.ts:57
export type CacheSafeParams = {
/** System prompt - 必须跟父完全一致 */
systemPrompt: SystemPrompt
/** User context - 拼接在消息前,影响缓存 */
userContext: { [k: string]: string }
/** System context - 拼接在 system prompt 后,影响缓存 */
systemContext: { [k: string]: string }
/** 工具池、模型等所在的上下文 */
toolUseContext: ToolUseContext
/** 父 agent 的消息前缀,用于缓存共享 */
forkContextMessages: Message[]
}
// src/utils/forkedAgent.ts:57
export type CacheSafeParams = {
/** System prompt - 必须跟父完全一致 */
systemPrompt: SystemPrompt
/** User context - 拼接在消息前,影响缓存 */
userContext: { [k: string]: string }
/** System context - 拼接在 system prompt 后,影响缓存 */
systemContext: { [k: string]: string }
/** 工具池、模型等所在的上下文 */
toolUseContext: ToolUseContext
/** 父 agent 的消息前缀,用于缓存共享 */
forkContextMessages: Message[]
}
你看这个类型的意思很明显:凡是会影响缓存命中的字段,我全列在这儿,你 Fork 的时候严格按这份清单跟父 agent 对齐。

一个有意思的细节:system prompt 不重新生成
Fork Subagent 的合成定义里有个有意思的细节,值得单独说。
正常一个 subagent 有个生成 system prompt 的函数,跑的时候现生成一段 prompt 文本。但 Fork 机制用的那个 subagent 的生成函数直接返回空字符串:
// src/tools/AgentTool/forkSubagent.ts:60
export const FORK_AGENT = {
agentType: FORK_SUBAGENT_TYPE,
tools: ['*'], // 用父的完整工具池
maxTurns: 200,
model: 'inherit', // 继承父的模型
permissionMode: 'bubble', // 权限弹窗浮到父终端
source: 'built-in',
getSystemPrompt: () => '', // 返回空串!
} satisfies BuiltInAgentDefinition
// src/tools/AgentTool/forkSubagent.ts:60
export const FORK_AGENT = {
agentType: FORK_SUBAGENT_TYPE,
tools: ['*'], // 用父的完整工具池
maxTurns: 200,
model: 'inherit', // 继承父的模型
permissionMode: 'bubble', // 权限弹窗浮到父终端
source: 'built-in',
getSystemPrompt: () => '', // 返回空串!
} satisfies BuiltInAgentDefinition
这不是偷懒,而是精心设计的。
为啥要返回空串?因为 Fork subagent 的 system prompt 根本不走这个函数生成,而是直接用父 agent 已经渲染好的那份字节。
原因很简单:如果重新调一次生成函数,里面可能有些小差异(比如某个功能开关的缓存状态变了、某个动态字段的值变了),生成出来的 prompt 跟父 agent 就可能差一个字符,缓存就没了。
最稳的办法是:把父 agent 那边已经渲染出来的 prompt,作为字节原样拿过来用,一个字节都不动。
这个细节非常工业级,普通人写 agent 系统根本想不到。

什么时候用 Fork,什么时候用常规 subagent?
Fork 机制不是万能的,它的适用场景很特定:你希望子 agent 完全继承父 agent 的整个上下文(对话历史、system prompt、工具池),只是「派个分身去试试另一条路」。
比如「Ctrl+F 生成 PR 描述」「运行 /btw 命令做 post-turn 总结」,这些任务需要父 agent 的完整上下文,但又不希望污染父 agent 的主循环。
相反,如果你的任务有明确的专业分工(比如派一个专门搜代码的 agent、派一个专门做规划的 agent),那就用常规 subagent,它们的 system prompt 是定制的,Fork 机制反而不适用。
还有一个关键点:Fork 机制和 Coordinator 模式是互斥的。Coordinator 模式下主 agent 已经是个纯协调者了,它派的 worker 本来就是异步的,不需要 Fork 这种「轻量分身」机制。两个机制职责重叠,就只留一个:
// src/tools/AgentTool/forkSubagent.ts:32
export function isForkSubagentEnabled(): boolean {
if (feature('FORK_SUBAGENT')) {
if (isCoordinatorMode()) return false // 互斥!
if (getIsNonInteractiveSession()) return false
return true
}
return false
}
// src/tools/AgentTool/forkSubagent.ts:32
export function isForkSubagentEnabled(): boolean {
if (feature('FORK_SUBAGENT')) {
if (isCoordinatorMode()) return false // 互斥!
if (getIsNonInteractiveSession()) return false
return true
}
return false
}

Fork 的工程启示
Fork 机制我想单独说下它对我们的启示。
很多人做 agent 系统只关心「能不能跑起来」,不关心「跑起来要花多少钱」。但在生产环境,这两个是一回事。Claude Code 靠 Fork 机制,在缓存友好的场景下能把 subagent 的成本降到原来的 10% 左右。
这意味着什么?意味着你的 subagent 可以调得更频繁。原本成本考虑不敢派的活,现在都能派了,这反过来又让整个 agent 系统的能力边界扩大了。
所以成本优化本身就是能力的一部分。这个思路我觉得对自建 agent 系统的朋友特别重要。

好了,讲完 Fork,下面进入整篇文章最「多 agent」的一章:Coordinator 模式。
五、Coordinator 模式:真正的多 Agent 并行协作
五、Coordinator 模式:真正的多 Agent 并行协作
前面讲的 subagent(不管是常规的还是 Fork 的),本质都是父子结构:父 agent 派一个子,自己该干啥干啥,子完成了通知一声。
但如果你的任务量很大,需要一堆 agent 同时开工呢?比如一个大的代码迁移,要并行调研 10 个模块。这时候父子结构就显得单薄了。
Claude Code 为此设计了一个专门的模式:Coordinator 模式。这是 Claude Code 多 agent 设计里最「多 agent」的部分,也是最能打的地方。
Coordinator 模式的启用
这个模式不是默认开的,要显式打开。需要同时满足两个条件:编译时的功能开关和运行时的环境变量 CLAUDE_CODE_COORDINATOR_MODE=1。
CLAUDE_CODE_COORDINATOR_MODE=1
// src/coordinator/coordinatorMode.ts:36
export function isCoordinatorMode(): boolean {
if (feature('COORDINATOR_MODE')) {
return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
}
return false
}
// src/coordinator/coordinatorMode.ts:36
export function isCoordinatorMode(): boolean {
if (feature('COORDINATOR_MODE')) {
return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
}
return false
}
开启之后,主 agent 的行为模式会发生根本性变化。
核心设计:主 agent 退化成「纯协调者」
常规模式下,主 agent 是「全能型选手」:它读代码、写代码、跑测试、做规划全都干,只在需要时才派 subagent 帮一把。
Coordinator 模式下,主 agent 不干实际工作了,它只做三件事:派 worker、收结果、合成答案。
这个角色转换是通过主 agent 的 system prompt 强制约束出来的。打开源码里那段 prompt,开头就写得很明白:
You are Claude Code, an AI assistant that orchestrates software engineering
tasks across multiple workers.
## 1. Your Role
You are a **coordinator**. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible, don't delegate work
that you can handle without tools
You are Claude Code, an AI assistant that orchestrates software engineering
tasks across multiple workers.
## 1. Your Role
You are a **coordinator**. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible, don't delegate work
that you can handle without tools
翻译一下:你的身份是协调者,你的工作是指挥 worker 去做研究、实现、验证,然后自己合成结果跟用户交流。能自己回答的问题不要派人去做。

三大内部工具
既然主 agent 要协调,就得有专门的协调工具。Coordinator 模式下,主 agent 多了一套「团队管理」工具箱:
派 worker 的工具:派一个新 worker 出去干某件具体的活,派完立刻返回 worker 的 ID。
创建/解散团队的工具:批量管理 worker 组。
给 worker 发消息的工具:给已经派出去的 worker 发后续指令(也就是前面讲的 SendMessage),因为 worker 的上下文还在,续命比重新派一个更省钱。
合成最终输出的工具:协调者合成完答案后,通过这个工具把最终回复交给用户。
停止 worker 的工具:当协调者意识到某个 worker 跑错方向时,把它停掉省 token。
这套工具放在一起,协调者就有了一整套指挥团队的 API。
📌 配图建议:协调者工具箱图,把五个工具画成五个按钮,标注每个按钮的作用
📌 配图建议:协调者工具箱图,把五个工具画成五个按钮,标注每个按钮的作用

对应到源码里,有这么一组常量把这几样工具圈在一起:
// src/coordinator/coordinatorMode.ts:29
const INTERNAL_WORKER_TOOLS = new Set([
TEAM_CREATE_TOOL_NAME, // 创建 worker 团队
TEAM_DELETE_TOOL_NAME, // 解散团队
SEND_MESSAGE_TOOL_NAME, // 给 worker 发消息
SYNTHETIC_OUTPUT_TOOL_NAME, // 合成最终输出给用户
])
// src/coordinator/coordinatorMode.ts:29
const INTERNAL_WORKER_TOOLS = new Set([
TEAM_CREATE_TOOL_NAME, // 创建 worker 团队
TEAM_DELETE_TOOL_NAME, // 解散团队
SEND_MESSAGE_TOOL_NAME, // 给 worker 发消息
SYNTHETIC_OUTPUT_TOOL_NAME, // 合成最终输出给用户
])
这里得说清楚它的真实用途,免得误会:这组常量其实是一张给 worker 用的「黑名单」。Coordinator 模式下,系统会把这几样工具从 worker 的工具池里摘掉,让 worker 只管干活、没法反过来去创建团队、给别人发消息、调度别人。所以它不是「只有协调者才有这些工具」,而是「这些协调专用的工具,不发给 worker」。特别是 SendMessage,它本身并不是 Coordinator 模式专属的东西,前面第三章讲的团队(agent-teams)模式里,父 agent 用的就是它。
顺带厘清一个容易混的点:agent-teams(团队/队友)模式和 Coordinator 模式是两个独立的开关。前者管的是「父子之间双向发消息、派队友」,后者是更进一步的「主 agent 退化成纯协调者」的编排模式,别把两者当成一回事。
并行才是真本事
Coordinator 模式的 prompt 里有一句我特别喜欢:
Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible, don't serialize work that can run simultaneously and look for opportunities to fan out.
Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible, don't serialize work that can run simultaneously and look for opportunities to fan out.
翻译一下:并行是你的超能力,worker 全是异步的,能并行的绝不串行,多找机会一口气派一堆出去。
这句话背后是一个很关键的工程事实:Claude Code 的派 worker 工具调用可以在同一条 assistant 消息里出现多次,底层会一起并发执行,不是一个跑完再跑下一个。
所以协调者要做的就是在一次 LLM 回合里,一口气生成多个派 worker 的工具调用:
派 worker 调研 auth 模块
派 worker 调研 session 模块
派 worker 调研 token 模块
派 worker 调研 auth 模块
派 worker 调研 session 模块
派 worker 调研 token 模块
这三个调用同时启动,三个 worker 同时干活,协调者等通知一条条返回。

对比一下:
串行:派 worker1 → 等 → 结果 → 派 worker2 → 等 → 结果 → 派 worker3... 用户等十分钟
并行:同时派三个 worker → 三份结果陆续到 → 用户等三分钟多一点
这就是「并行是超能力」的真正含义。工业级多 agent 系统,没有并行就没有可用性。

协调者的「任务流水线」
Coordinator 模式下,一个典型的任务流程被切成四个阶段:
阶段谁来做目的调研Workers(并行)调查代码库、找文件、理解问题合成协调者本人读完发现、理解问题、写实现规格实现Workers按规格做具体修改、提交验证Workers测试改动是否真的工作
阶段谁来做目的
阶段
谁来做
目的
调研Workers(并行)调查代码库、找文件、理解问题
调研
Workers(并行)
调查代码库、找文件、理解问题
合成协调者本人读完发现、理解问题、写实现规格
合成
协调者本人
读完发现、理解问题、写实现规格
实现Workers按规格做具体修改、提交
实现
Workers
按规格做具体修改、提交
验证Workers测试改动是否真的工作
验证
Workers
测试改动是否真的工作
注意中间的「合成」阶段是协调者亲自做,这是协调者存在的意义:理解全局,做决策。prompt 里反复强调:不要偷懒让 worker「based on your findings, implement the fix」,而是自己把 findings 读懂、写成具体的规格再派下去。

这是一个非常重要的 multi-agent 设计哲学:协调者必须「理解」而不能「转发」。如果协调者只是转发,它就没有存在价值,worker 直接跟用户对话就行了。

Continue vs Spawn:老 worker 还是新 worker?
Continue vs Spawn:老 worker 还是新 worker?
协调者要持续派活,遇到一个新任务,是给老 worker 发消息续命,还是派个新 worker 从头开始?这是个有经验才能做好的决策。
Claude Code 的 prompt 里给出了一张决策表,我总结一下核心逻辑:
如果新任务跟 worker 现有上下文高度相关(比如刚查的文件现在要改),续命老 worker,因为它已经「知道」那些文件了。
如果新任务跟 worker 现有上下文没关系,或者之前 worker 的工作走偏了,派新 worker,避免旧上下文干扰判断。
验证这种需要「新鲜眼光」的工作,永远派新 worker,不能让刚写完代码的 worker 自己验自己。
这个设计其实也挺反映人类团队合作的直觉:有的活就该让懂上下文的人接着干(沟通成本低),有的活就该换个人做(避免认知偏差)。

Worker 的工具限制
Coordinator 模式下,worker 拿到的工具有什么不同?关键在于:协调者专属的那套内部工具(创建团队、发消息、合成输出等等),不给 worker 用。worker 不需要再去协调别人,它的活是干事情。
这其实是一个递归防护:如果 worker 也能派 worker,整个系统就变成递归树了,没完没了。通过工具白名单把 worker 的「派人权」收回,让系统结构保持「一个协调者 + 一堆 worker」的扁平形态。

跟常规 subagent 对比
讲完这些我们对比一下 Coordinator 模式和常规 subagent:
维度常规 subagentCoordinator 模式主 agent 角色全能选手纯协调者subagent 执行同步(2 分钟后才转后台)默认异步并发程度偶尔并发最大化并发适合场景单个任务 + 临时帮手大任务 + 高并发拆解系统形态父子树协调者 + worker 扁平层
维度常规 subagentCoordinator 模式
维度
常规 subagent
Coordinator 模式
主 agent 角色全能选手纯协调者
主 agent 角色
全能选手
纯协调者
subagent 执行同步(2 分钟后才转后台)默认异步
subagent 执行
同步(2 分钟后才转后台)
默认异步
并发程度偶尔并发最大化并发
并发程度
偶尔并发
最大化并发
适合场景单个任务 + 临时帮手大任务 + 高并发拆解
适合场景
单个任务 + 临时帮手
大任务 + 高并发拆解
系统形态父子树协调者 + worker 扁平层
系统形态
父子树
协调者 + worker 扁平层

Coordinator 模式的工程启示
讲完 Coordinator,我想提炼几条值得学的设计思想。
第一,角色分离。协调和干活是两件事,不要让同一个 agent 身兼二职。角色清晰的系统更稳定。
第二,并发优先。异步 + 消息队列是并发的基础,有了这套基础,多 agent 才能真正发挥威力。
第三,合成不转发。协调者要理解中间结果,不能把它当传话筒。这是 Multi-Agent 系统里最容易踩坑的一点。
第四,扁平不递归。通过工具权限把层级限制在两层(协调者 + worker),避免失控的递归嵌套。

六、5 条 Multi-Agent 设计原则
Claude Code 的源码扒得差不多了。我把前面讲的所有东西浓缩一下,沉淀成 5 条可以直接用到自己项目、也可以直接用到面试答案里的设计原则。

原则 1:上下文隔离要按字段粒度做
这是我最想强调的一条。很多 agent 框架的「隔离」就是粗暴地给 subagent 一个空 context,结果缺这缺那一堆 bug。
Claude Code 的做法是:每个状态单独决策。读文件缓存克隆(避免污染),写全局状态关掉(避免两边抢),任务注册通路保留(不然孤儿进程没人回收),深度计数 +1(可追踪,防失控嵌套)。
做多 agent 系统时,对着父 agent 的每项状态问一句:「子 agent 拿这个状态干啥?会不会影响父?」,就能避开大部分坑。
原则 2:通信走消息,不走函数调用
父 → 子:写入子 agent 的消息队列,子 agent 下一轮循环自己读取。
子 → 父:把完成通知包装成 XML 消息,伪装成用户消息注入父 agent 对话。
这套模型的好处:天然异步、天然支持并发、天然兼容 agentic loop、天然持久化(消息都能落盘)。
要补一句严谨的:上面这套完整的双向消息驱动,是 Claude Code 团队(agent-teams)模式打开后的形态。默认的常规 subagent 更接近「同步派发 + 子→父 完成通知」,父→子 这条主动发消息的路是团队模式才接通的。面试时把这个边界讲清楚,比笼统说「双向」更显你真读过源码。
如果你问面试官「你们的多 agent 之间怎么通信」,把这套答出来,基本就到位了。
原则 3:工具权限要分级管控
全局黑名单(防递归、防乱问用户),类型黑名单(自定义 agent 更严),异步白名单(后台 agent 只能用子集)。
每种 agent 按自己的场景配工具,不要一刀切。
原则 4:缓存友好是一种架构能力
API 成本和延迟对生产环境 agent 来说是能力的一部分。设计 subagent 的时候,考虑它的 prompt 前缀能不能复用父 agent 的缓存,能省 80-90% 的成本。
Claude Code 那套「严格锁定缓存前缀 + 复用父 agent 已渲染字节」的思路,是这方面的教科书式实现。
原则 5:并行优先 + 协调者合成
真正的多 agent 系统威力在并发。通过异步消息和消息队列做基础,通过协调者做合成,避免「大 agent 大循环什么都自己扛」的窘境。
并且协调者要亲自合成,不能当传话筒。
这 5 条原则背后,其实都能看到 Claude Code 源码里的清晰落点。我建议你别光记这些原则,下次看到 Multi-Agent 相关的东西,都拿这 5 条去对照,会迅速看出对方系统的深浅。
最后
写到这里,Claude Code 的多 Agent 机制基本就扒完了。
回过头看,Claude Code 这套系统不是简单的「一个主 agent 嵌几个 subagent」那么朴素。它在架构、通信、并发、成本、隔离每一个维度都做了精致的设计:
按字段粒度做的上下文隔离,既不让 subagent 污染父 agent,又保留了必要的通路。
按字段粒度做的上下文隔离,既不让 subagent 污染父 agent,又保留了必要的通路。
消息队列 + XML 通知支撑起异步父子通信,让并发成为可能。
消息队列 + XML 通知支撑起异步父子通信,让并发成为可能。
Fork Subagent 的缓存前缀复用,把成本打到缓存友好的极致。
Fork Subagent 的缓存前缀复用,把成本打到缓存友好的极致。
Coordinator 模式 把主 agent 彻底解放成纯协调者,让多 worker 真正并行起来。
Coordinator 模式 把主 agent 彻底解放成纯协调者,让多 worker 真正并行起来。
每一块拆开看都不是啥复杂技术,但组合在一起,就成了一个能支撑 Anthropic 这种级别产品的工业级多 agent 系统。
今天分享都到这里,我们下篇见!