公众号爬取学习的内容和图片来源于@小林coding
Claude Code 的源码堪称 AI Agent 工程的教科书。 前段时间源码泄露后,社区得以窥见 Anthropic 在多 Agent 设计上的工业级实践。这篇文章从源码视角,拆解 Claude Code 中三套不同的多 Agent 机制:常规 Subagent、Fork Subagent 和 Coordinator 协调者模式。
Multi-Agent 不是多个 LLM 实例的简单堆叠,而是一套围绕隔离、通信、并发和成本精心设计的工程体系。真正能打的多 Agent 系统,其工程复杂度不亚于一个分布式中间件。

一、Multi-Agent 的本质与三种形态
为什么一个 Agent 不够用?
回到最朴素的 Agent 模型:一个 LLM + 一堆工具 + 一个循环(agentic loop)。看起来够用,但遇到真实项目问题就暴露了:
上下文爆炸。 调研阶段要看大量文档,实现阶段要读写项目代码,评审阶段要重新审视。三个阶段的内容全塞到一个 Agent 的上下文里,token 蹭蹭往上涨。
职责混乱。 一个 Agent 既当研究员又当程序员又当评审员,调研到一半就开始写代码,代码写到一半又去查文档。
无法并发。 一个 Agent 一次只能做一件事,在查文档时,其他工作只能干等。
Multi-Agent 的思路很直观:把一个大任务拆给多个职责清晰的 Agent 去做,通过某种机制通信和协作。
工业界常见的三种形态
| 形态 | 模型 | 说明 |
|---|---|---|
| 父子型 | 主 Agent 派 Subagent 做事,拿结果继续 | Claude Code 的 Task 工具 |
| 平级协作型 | 多 Agent 共享状态、互相通信 | 工程上落地困难 |
| 主从型 | 协调者派 Worker,Worker 间互不通信 | 高并发场景标配 |
Claude Code 源码里,常规 Subagent 对应父子型,Coordinator 模式对应主从型,Fork Subagent 是父子型的优化变体。

二、Subagent 的隔离机制
多 Agent 系统本质是"一堆 Agent 共处一个进程、共享一个底层运行时"。隔离做不好,一个 Subagent 偷偷污染父 Agent 的状态,整个系统就乱了。Claude Code 从两个维度做隔离:工具隔离和上下文隔离。
第一维度:工具隔离——定制工具箱
主 Agent 拥有几十个工具,但不能原封不动丢给 Subagent。Claude Code 按三道准入门过滤:
第一道:全局黑名单。 所有 Subagent 都不能用的工具:
- 能派新 Subagent 的工具——防递归嵌套
- 能主动问用户的工具——Subagent 不该抢对话权
- 能切换规划模式的工具——没有资格
- 能停止其他任务的工具——任务管理是主线程专属
第二道:自定义 Agent 加严黑名单。 用户自写的 Agent 比内置 Agent 多一层防护。
第三道:后台异步 Agent 走白名单。 默认不准用,只有明确列出的才能用(读文件、搜代码、执行命令等)。
export function filterToolsForAgent(
{ tools, isBuiltIn, isAsync, permissionMode }
): Tools {
return tools.filter(tool => {
if (tool.name.startsWith('mcp__')) return true
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
})
}
第二维度:上下文隔离——按字段粒度决策
这是 Claude Code 最精髓的设计之一。两个直觉方案都走不通:
- 完全共享:子 agent 读文件读了 200 行,父的缓存被刷成 200,以为自己也读过,数据出错
- 完全新建:用户 Ctrl+C 中止,子 agent 收不到中止信号,自顾自跑
- Claude Code 的做法是:不按"整体"决策,而是按"字段"决策。 每项状态单独判断该克隆、该共享、该屏蔽,还是该新建。
四个关键决策:
- 读文件缓存 → 克隆一份。 子怎么折腾都不影响父的文件视图。
- 写全局状态 → 关闭。 子 agent 没有写 UI 状态的权限。
- 任务注册通路 → 保留。 子 agent 起的后台进程需要登记到全局任务表。
- 发独立 ID + 深度 +1。 系统随时知道当前嵌套层数。
export function createSubagentContext(parentContext, overrides): ToolUseContext {
return {
readFileState: cloneFileStateCache(parentContext.readFileState),
setAppState: () => {},
setAppStateForTasks: parentContext.setAppStateForTasks,
agentId: overrides?.agentId ?? createAgentId(),
queryTracking: {
chainId: randomUUID(),
depth: (parentContext.queryTracking?.depth ?? -1) + 1,
},
}
}

三、父子 Agent 的通信机制
为什么不用函数调用?
直觉方案是"父 Agent 调个函数,等 Subagent 跑完返回"。但有两个致命问题:
- 同步阻塞:Subagent 跑 5 分钟,父 Agent 啥也干不了,用户说话没反应
- 无法并发:要派 5 个 Subagent 调研 5 个模块,要么排队阻塞,要么手动搓并发代码
- Claude Code 换了一个完全不同的路子:消息驱动。

父 → 子:扔字条 + 子自己取
父 Agent 调 SendMessage 工具,往目标 Subagent 的信箱末尾追加一条消息,然后立刻返回。Subagent 在每轮工具调用结束后,瞄一眼自己的信箱,有新消息就注入对话历史。
如果 Subagent 已经完成,Claude Code 会自动唤醒它——从 transcript 恢复对话历史,拼上新消息重新跑起来。
子 → 父:把通知伪装成用户消息
Subagent 跑完后,把完成通知拼成 XML 消息,伪装成用户消息塞给父 Agent 的对话历史:
<task-notification>
<task-id>agent-a1b</task-id>
<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>
为什么要用 XML?Anthropic 训练 Claude 时就强调 XML 结构化表达;XML 是纯文本可塞进对话历史;伪装成用户消息,天然复用 Agent 的 agentic loop 处理逻辑。

Auto-background:同步到异步的自动降级
Subagent 跑起来后,如果 30 秒内完成,父在前台阻塞等;超过 2 分钟,自动转到后台,父 Agent 先干别的。2 分钟后 Subagent 完成,通过 task-notification 把结果送回。
整个通信体系就两个关键字:异步 + 消息。没有直接函数调用,没有锁,没有回调地狱。
四、Fork Subagent:省钱又省延迟的隐藏大招
隐藏成本
Claude Code 的 system prompt 上万 token。每派一个 Subagent,LLM API 要对这一万多 token 重新计算。Anthropic 的 prompt 缓存机制可以缓解——如果请求前缀跟之前一致,可以走缓存,只需原来 10% 的价格。
但缓存命中的条件是:字节级别完全相同。 一个字、一个空格不一样,缓存就不命中。
Fork 的核心思路
Fork Subagent 让 Subagent 的 API 请求前缀跟父 Agent 字节级一致,从而复用父的缓存。
要对齐五样东西:
1. 系统 prompt — 最核心
2. 用户上下文 — 当前项目的 CLAUDE.md 等
3. 系统上下文 — system prompt 后的环境信息
4. 工具池顺序和定义 — 序列化顺序都不能变
5. 对话历史前缀 — 决定"从哪里开始分叉"

一个特别有工程智慧的细节:Fork Subagent 的 getSystemPrompt 直接返回空字符串——因为 Fork 的 system prompt 根本不是通过这个函数生成的,而是直接用父 Agent 已经渲染好的那字节。重新调一次生成函数可能会有微小差异,缓存就没了。
export const FORK_AGENT = {
agentType: FORK_SUBAGENT_TYPE,
tools: ['*'],
model: 'inherit',
getSystemPrompt: () => '', // 返回空串!
} satisfies BuiltInAgentDefinition
工程启示: 成本优化本身就是能力的一部分。靠 Fork 机制,Subagent 的成本降到原来的 10% 左右。

五、Coordinator 模式:真正的多 Agent 并行
前面讲的 Subagent 本质是父子结构。如果要并行调研 10 个模块,就需要更强大的机制。Claude Code 的 Coordinator 模式 就是为此设计的。
主 Agent 退化成"纯协调者"
开启后,主 Agent 不干实际工作了,只做三件事:派 Worker、收结果、合成答案。
五大内部工具:
| 工具 | 作用 |
|---|---|
| 派 Worker | 派一个新 Worker 出去干活,派完立刻返回 ID |
| 创建/解散团队 | 批量管理 Worker 组 |
| 发消息 | 给已派出的 Worker 发后续指令 |
| 合成输出 | 协调者把最终回复交给用户 |
| 停止 Worker | 跑错方向时停掉省 token |
Coordinator 的 prompt 里有一句金句:"Parallelism is your superpower."

派 Worker 的工具调用可以在同一条 assistant 消息里出现多次,底层一起并发执行:
- 串行:派 Worker1 → 等 → 结果 → 派 Worker2 → 等 → 结果 → 用户等十分钟
- 并行:同时派三个 Worker → 三份结果陆续到 → 用户等三分钟多一点

中间的"合成"阶段是协调者亲自做——协调者必须"理解"而不能"转发"。这是多 Agent 系统里最容易踩坑的一点。
与常规 Subagent 对比
| 维度 | 常规 Subagent | Coordinator 模式 |
|---|---|---|
| 主 Agent 角色 | 全能选手 | 纯协调者 |
| 执行方式 | 同步(2 分钟转后台) | 默认异步 |
| 并发程度 | 偶尔并发 | 最大化并发 |
| 适合场景 | 单个任务 + 临时帮手 | 大任务 + 高并发拆解 |
| 系统形态 | 父子树 | 协调者 + Worker 扁平层 |

六、五条设计原则
原则 1:上下文隔离要按字段粒度做
不要一刀切地"全隔离"或"不隔离"。对着父 Agent 的每项状态问一句"子 Agent 拿这个状态干啥?会不会影响父?",就能避开大部分坑。
原则 2:通信走消息,不走函数调用
父 → 子写消息队列,子 → 父用 XML 伪装用户消息。天然异步、天然支持并发、天然兼容 agentic loop。
原则 3:工具权限要分级管控
全局黑名单 → 类型黑名单 → 异步白名单。每种 Agent 按自己的场景配工具。
原则 4:缓存友好是一种架构能力
设计 Subagent 时考虑它的 prompt 前缀能否复用父 Agent 的缓存。
原则 5:并行优先 + 协调者合成
通过异步消息做基础,通过协调者做合成。协调者要亲自合成,不能当传话筒。
Claude Code 的 Multi-Agent 系统不是一个简单的"主 Agent 嵌几个 Subagent",它在架构、通信、并发、成本、隔离每一个维度都做了精致的设计。每一块拆开看都不是复杂技术,但组合在一起,就成了支撑 Anthropic 级别产品的工业级多 Agent 系统。
如果你在自建 Agent 系统,建议把这 5 条原则拿去做对照,每次看到 Multi-Agent 相关设计时都拿它们去衡量——会迅速看出对方系统的深浅。
夜雨聆风