← 返回文章列表← Back to posts

Claude Code 的多 Agent 机制:从 subagent 到 Agent Teams 的源码解读Claude Code's Multi-Agent Runtime: A Source Deep Dive from Subagents to Agent Teams

基于 Claude Code 2.1.88 还原源码,拆解多 Agent 的两条主线——subagent 是一次性任务委派,Agent Teams 是基于 mailbox 的持续协作,底层都复用同一个 query() 推理引擎。核心洞察:多 Agent 不是 prompt 模板,而是围绕 query() 构建的完整运行时系统,靠 ToolUseContext 隔离、Task framework 管生命周期、fork 优化 prompt cache。Based on Claude Code 2.1.88 restored source, this deep dive unpacks two parallel tracks — subagents for one-shot delegation, Agent Teams for persistent mailbox-based collaboration — both reusing the same query() engine. Key insight: multi-agent is not a prompt template but a complete runtime with ToolUseContext isolation, Task framework lifecycle management, and fork-based prompt cache optimization.

·28 分钟阅读min read

Claude Code 的多 Agent 机制:从 subagent 到 Agent Teams 的源码解读

如果只从产品体验看,多 Agent 像是“让 Claude 多叫几个帮手”。但从 Claude Code 2.1.88 的还原源码看,它不是简单地在一个上下文里扮演多个角色,而是一套真正的运行时系统:它能创建独立 Agent、把 Agent 放到后台运行、让它们通过 mailbox 通信、同步权限审批、记录 transcript,甚至把队友放进 tmux/iTerm pane 或同一个 Node.js 进程里长时间运行。

本文基于 /Users/felix/ai/其他/claude-code-sourcemap-main 2/restored-src 的 sourcemap 还原源码分析。因为源码来自还原结果,不是官方手写源码,所以本文关注架构和机制,不把它当作公开 API 文档。

先给结论

Claude Code 里的多 Agent 可以分成两条主线:

第一条是 subagent。它的核心入口是 tools/AgentTool。你可以把它理解成“为当前主 Agent 委派一个任务”。这个子 Agent 可以同步跑,也可以后台跑,还可以 fork 父会话上下文。

第二条是 Agent Teams。源码里经常叫 swarm 或 teammate。它不是一次性任务,而是“创建一组可持续协作的队友”。队友可以收发消息、等待任务、申请权限、进入 idle 状态、被 shutdown,甚至在 tmux/iTerm 里作为独立 Claude Code 进程运行。

两者共用底层能力,但语义不同:

维度 subagent Agent Teams
目标 委派一个任务 创建可持续协作的成员
生命周期 通常一次性 可长期运行、idle、继续接任务
通信 完成后返回 task notification mailbox / SendMessage 双向通信
上下文 子上下文隔离 teammate 自己维护多轮历史
权限 后台默认避免弹窗,必要时 deny/bubble leader UI 或 mailbox 统一审批
运行位置 本进程后台为主 in-process、tmux、iTerm、window

一句话概括:subagent 是“派一个人做事”,Agent Teams 是“组一个团队协作”。

一切仍然回到 query()

这个项目的核心推理循环在 src/query.ts。主会话、subagent、fork agent、in-process teammate,最终都会回到同一套逻辑:

Claude API stream
  -> assistant message
  -> tool_use
  -> tool orchestration
  -> tool_result
  -> 下一轮 query

所以多 Agent 并没有复制一套新引擎。它复用主引擎,只是在进入 query() 之前,把上下文、权限、任务状态、模型、工具列表、transcript 等包装成一个新的运行单元。

这也是整个架构最重要的设计点:多 Agent 是同一个推理引擎的多实例化,而不是多个完全不同的执行系统。

subagent:从 AgentTool 开始

subagent 的主入口是:

src/tools/AgentTool/AgentTool.tsx

它大致做几件事:

  1. 判断是否是 Team teammate 创建请求。
  2. 判断是否走 fork 子 Agent。
  3. 加载 agent definition。
  4. 解析模型、工具、MCP、skills、hooks、权限模式。
  5. 判断同步执行还是后台执行。
  6. 调用 runAgent()。

简化调用链是:

AgentTool.call()
  -> resolve agent definition
  -> resolve model/tools/permissions
  -> if async/background/fork/coordinator:
       register LocalAgentTask
       runAgent() in background
     else:
       runAgent() in foreground

runAgent() 是真正执行 Agent 的地方。它会构造子 Agent 的 system prompt、工具列表、MCP、hook、skill 环境,然后进入 query()。

这意味着自定义 Agent 本质上不是一套新执行器,而是一份配置:

  • agentType
  • whenToUse
  • tools
  • disallowedTools
  • skills
  • mcpServers
  • model
  • effort
  • permissionMode
  • maxTurns
  • initialPrompt
  • memory
  • hooks

你配置不同的 Agent,本质是给同一个 runAgent() 换一套身份、工具和约束。

子 Agent 为什么不能直接复用父上下文

关键函数在:

src/utils/forkedAgent.ts
createSubagentContext()

这个函数非常重要。它回答了一个问题:子 Agent 能不能直接拿父 Agent 的 ToolUseContext 用?

答案是:不能。

父上下文里有很多状态是危险的,比如 UI 更新、当前工具进度、文件读取缓存、权限提示、AbortController、任务注册器。如果子 Agent 原样使用父上下文,就可能出现这些问题:

  • 后台 Agent 改了父 UI。
  • 子 Agent 弹权限框,但主会话不知道怎么处理。
  • 子 Agent 的文件读取状态污染父会话。
  • 父会话 abort 时误杀不该杀的任务,或者反过来。
  • 后台 Bash 任务注册不到根任务表,留下僵尸进程。

所以 createSubagentContext() 做的是“选择性隔离”。

它默认隔离:

  • readFileState
  • memory trigger
  • skill discovery state
  • denial tracking
  • UI callbacks
  • in-progress tool IDs
  • response length mutation

但它也会保留一些必须共享的能力:

  • setAppStateForTasks
  • updateAttributionState
  • fileReadingLimits
  • options
  • messages

最值得注意的是权限提示策略。如果子 Agent 不共享父 abortController,它会把 toolPermissionContext.shouldAvoidPermissionPrompts 设置成 true。也就是说,后台 Agent 默认不要尝试弹交互式权限框。

这不是小细节,而是后台 Agent 能稳定运行的前提。

权限系统:多 Agent 安全性的中心

权限主逻辑在:

src/utils/permissions/permissions.ts
src/utils/permissions/PermissionMode.ts

权限模式包括:

  • default
  • plan
  • acceptEdits
  • bypassPermissions
  • dontAsk
  • auto
  • bubble

其中 bubble 是理解多 Agent 的关键词。它表达的是:子 Agent 遇到权限问题时,不自己处理,而是把审批冒泡给父会话或 leader。

权限判定不是一个简单开关,而是一条有很多短路点的管线:

hasPermissionsToUseTool()
  -> deny rule
  -> ask rule
  -> tool.checkPermissions()
  -> tool deny
  -> user interaction requirement
  -> content-specific ask
  -> safety check
  -> bypassPermissions
  -> always allow rule
  -> passthrough -> ask
  -> dontAsk / auto / headless transformation

这里最容易误解的是 bypassPermissions。

它不是“绝对无条件放行”。显式 deny、内容级 ask、安全检查、需要用户交互的工具,仍然可能在 bypass 前被截住。比如跨机器 bridge message 被标成 bypass-immune safety check,就必须用户明确同意。

对于后台 Agent,权限更严格。如果最终结果是 ask,但当前上下文 shouldAvoidPermissionPrompts = true:

  1. 先跑 PermissionRequest hooks。
  2. hook 给出 allow/deny 就采用。
  3. 没有 hook 决策就 deny。

这避免了后台 Agent 卡在一个用户看不到的权限弹窗上。

Task framework:后台 Agent 的运行时内核

如果说 AgentTool 是入口,query() 是推理引擎,那么 Task framework 就是后台 Agent 的运行时内核。

核心文件:

src/Task.ts
src/utils/task/framework.ts
src/tasks/LocalAgentTask/LocalAgentTask.tsx

LocalAgentTask 表示一个本地后台 Agent。它的状态里有:

  • agentId
  • prompt
  • selectedAgent
  • agentType
  • model
  • abortController
  • progress
  • messages
  • pendingMessages
  • isBackgrounded
  • retain
  • diskLoaded
  • evictAfter

一个后台 subagent 的典型生命周期是:

AgentTool
  -> registerAsyncAgent()
  -> registerTask()
  -> runAgent()
  -> completeAgentTask() / failAgentTask()
  -> enqueueAgentNotification()

完成后,它不是简单把结果塞回父上下文,而是发送结构化通知:

<task-notification>
  <task-id>...</task-id>
  <output-file>...</output-file>
  <status>completed</status>
  <summary>...</summary>
  <result>...</result>
  <usage>...</usage>
</task-notification>

这个设计很好地控制了上下文膨胀。主 Agent 不需要把子 Agent 的每个工具输出都吸进来,只需要知道结果和 output file 路径。

Task framework 还负责:

  • 轮询任务输出增量。
  • 更新 output offset。
  • 回收已通知的终态任务。
  • 避免异步 await 后用旧 snapshot 覆盖新状态。
  • 支持停止和后台化。
  • 支持 SendMessage 给运行中的 Agent queue message。

也就是说,后台 Agent 在系统里更像“可管理的进程”,而不是普通函数调用。

fork subagent:复制父上下文,但尽量不浪费 token

fork 相关文件:

src/tools/AgentTool/forkSubagent.ts
src/utils/forkedAgent.ts

fork subagent 是一个更高级的路径。它不是“开一个空白子 Agent”,而是让 child 继承父会话完整上下文。

启用条件包括:

  • FORK_SUBAGENT feature 开启。
  • 不是 coordinator mode。
  • 不是 non-interactive session。

fork 用一个 synthetic agent:

  • agentType = "fork"
  • tools = ["*"]
  • model = "inherit"
  • permissionMode = "bubble"
  • maxTurns = 200

fork 最有意思的是它对 prompt cache 的优化。

普通做法可能是给每个 child 都塞一份不同 prompt。但这样多个 child 的请求前缀会很早分叉,cache 效果差。源码里的做法是:

  1. 克隆父 assistant message,保留所有 tool_use、thinking、text。
  2. 给每个 tool_use 都补一个相同的占位 tool_result。
  3. 最后才追加每个 child 不同的 directive。

结构像这样:

...parent history
assistant(all tool_use blocks)
user(same placeholder tool_results..., child-specific directive)

这样多个 fork child 的前缀几乎完全一致,只有最后的任务指令不同,所以 prompt cache 命中最大化。

fork child 还会被明确禁止继续 fork。代码层会扫描 <fork-boilerplate> 标签,prompt 层也要求 child 不要 spawn sub-agents,而是直接使用工具。

Agent Teams:从一次性委派到持续协作

Agent Teams 的核心入口是:

src/tools/TeamCreateTool/TeamCreateTool.ts
src/tools/SendMessageTool/SendMessageTool.ts
src/tools/shared/spawnMultiAgent.ts

但源码里经常不叫 Agent Teams,而叫:

  • swarm
  • teammate
  • team
  • in_process_teammate

这也是为什么读源码时只搜 “Team” 会漏掉很多实现。

创建团队的调用链是:

TeamCreateTool.call()
  -> 检查当前 leader 是否已有 team
  -> 创建 TeamFile
  -> writeTeamFileAsync()
  -> resetTaskList()
  -> setLeaderTeamName()
  -> setAppState(teamContext)

TeamCreateTool 只创建团队上下文,不直接启动队友。队友通常是后续通过 AgentTool 带上 team_name + name 来 spawn:

AgentTool.call(input)
  if input.team_name && input.name:
    spawnTeammate(...)

这说明 Teams 不是完全独立于 subagent 的另一套系统。它复用了 AgentTool 的 agent definition、model、tool resolution,只是执行后端换成 teammate runtime。

teammate 可以跑在哪里

spawnMultiAgent.ts 会选择 teammate 后端:

spawnTeammate()
  -> handleSpawn()
    -> in-process
    -> split-pane
    -> separate-window

三种模式分别是:

  1. in-process:同一个 Node.js 进程内运行,用 AsyncLocalStorage 隔离身份。
  2. split-pane:tmux 或 iTerm2 分屏里启动新的 Claude Code。
  3. separate-window:tmux 独立 window,偏 legacy。

pane-based teammate 会真正启动一个 Claude Code 进程,并传入身份参数:

--agent-id
--agent-name
--team-name
--agent-color
--parent-session-id
--plan-mode-required
--agent-type

它不会把初始 prompt 直接放命令行,而是写入 teammate 的 mailbox。新进程启动后通过 inbox poller 读到第一条任务。

in-process teammate 则不同。它不启动新进程,而是:

handleSpawnInProcess()
  -> spawnInProcessTeammate()
  -> register InProcessTeammateTask
  -> startInProcessTeammate()
  -> runInProcessTeammate()

它的初始 prompt 直接传给 runner,不走 mailbox,避免重复消息。

in-process teammate:一个常驻 Agent 循环

in-process teammate 的核心在:

src/utils/swarm/inProcessRunner.ts

它不是跑完一次 runAgent() 就结束,而是一个长循环:

runInProcessTeammate()
  while not aborted:
    runAgent(currentPrompt)
    update progress/messages
    mark idle
    send idle notification
    wait for:
      - pending user message
      - mailbox message
      - task list claim
      - shutdown request

它有自己的 allMessages,所以可以跨多次任务保留队友自己的上下文。上下文过大时,它会调用 compact,并重置相关状态,避免长期运行导致上下文无限膨胀。

这里有一个很重要的产品语义:teammate 完成一次工作后,不会自动把所有输出汇报给 leader。 源码注释明确说,teammate 应该自己用 SendMessage 与 leader 通信。

这和 subagent 完全不同。subagent 完成后系统自动发 task notification;teammate 则像真实团队成员,需要主动汇报。

mailbox:Agent Teams 的消息总线

Teams 的通信核心是 mailbox:

~/.claude/teams/{team_name}/inboxes/{agent_name}.json

消息结构很简单:

{
  from: string
  text: string
  timestamp: string
  read: boolean
  color?: string
  summary?: string
}

写入时会用 lockfile 做并发保护:

writeToMailbox()
  -> ensure inbox dir
  -> create inbox file if missing
  -> acquire lock
  -> read current messages
  -> append read=false message
  -> write JSON
  -> release lock

这个设计朴素但有效。它让不同进程、不同 pane、leader 和 teammate 都能通过文件系统通信。

SendMessage:不只是给队友发消息

SendMessageTool 很关键,因为它有三种路由。

第一种是 Remote Control / UDS:

to = bridge:<session-id> 或 uds:<socket-path>

跨机器 bridge message 会触发 bypass-immune safety check,必须用户明确同意。

第二种是本地 subagent:

input.to 命中 appState.agentNameRegistry
  -> 如果 LocalAgentTask running:
       queuePendingMessage()
     如果 stopped:
       resumeAgentBackground()
     如果 task 被回收:
       从 transcript 尝试 resume

所以 SendMessage 不只服务 Teams,也能给本地后台 subagent 发消息或恢复它。

第三种才是 Team mailbox:

to = "*"
  -> broadcast 给 teamFile.members 里除自己外的成员

to = teammate name
  -> writeToMailbox(recipient)

结构化消息还支持:

  • shutdown_request
  • shutdown_response
  • plan_approval_response

这让 Teams 有了基本协作协议,而不是只有字符串聊天。

权限如何在 Teams 中同步

Teams 里的权限审批不能让每个 worker 自己弹 UI。否则多个 pane、多个进程、多个后台任务会非常混乱。

相关文件:

src/utils/swarm/permissionSync.ts
src/hooks/useInboxPoller.ts
src/utils/swarm/inProcessRunner.ts

pane-based teammate 的权限链路大致是:

worker tool permission ask
  -> createPermissionRequest()
  -> sendPermissionRequestViaMailbox()
  -> writeToMailbox(team-lead)
  -> leader useInboxPoller
  -> ToolUseConfirm UI
  -> sendPermissionResponseViaMailbox(worker)
  -> worker callback
  -> allow / reject

in-process teammate 更直接。如果 leader UI queue 可用:

createInProcessCanUseTool()
  -> hasPermissionsToUseTool()
  -> result ask
  -> getLeaderToolUseConfirmQueue()
  -> 直接塞进 leader 的 ToolUseConfirm UI
  -> resolve PermissionDecision

只有 leader UI queue 不可用时,它才 fallback 到 mailbox。

这套机制的核心是:权限最终收敛到 leader,而不是分散到每个 worker。

plan mode 和 shutdown:Teams 的协作协议

如果 teammate 创建时带 plan_mode_required,它初始会处于 plan mode。并且 spawn 时不会继承 leader 的 bypass permissions。这避免了一个危险情况:leader 处于高权限模式时,要求先规划的 teammate 却直接跳过审批开始改文件。

plan approval 的安全点是:teammate 侧只接受来自 team-lead 的 approval response,避免其他队友伪造批准。

shutdown 也不是简单强杀。它是一套请求/响应协议:

leader -> shutdown_request
teammate -> 模型决定 approve/reject
teammate -> shutdown_response
leader -> kill pane / abort controller / remove member

也就是说,系统默认把 teammate 当作有状态的协作者,而不是随时可无条件销毁的临时函数。

task list:除了聊天,还能抢任务

Agent Teams 还绑定了一套任务列表。

创建 team 时会:

resetTaskList(teamName)
ensureTasksDir(teamName)
setLeaderTeamName(teamName)

in-process teammate idle 时会尝试:

listTasks()
findAvailableTask()
claimTask()
updateTask(status="in_progress")
formatTaskAsPrompt()

所以 leader 可以通过两种方式派活:

  1. 用 SendMessage 给某个 teammate 发明确消息。
  2. 创建 task list,idle teammate 自动 claim 可执行任务。

这让 Agent Teams 更像一个小型协作系统,而不是聊天群。

这套设计的亮点

第一,复用统一推理引擎。无论主 Agent、subagent、fork child、teammate,最终都回到 query()。这降低了系统复杂度。

第二,用 ToolUseContext 做边界。子 Agent 的隔离不是靠“相信 prompt”,而是靠上下文对象限制它能改什么、能不能弹权限、能不能写父 UI。

第三,用 Task framework 管后台生命周期。后台 Agent 是可停止、可通知、可回收、可恢复的任务,不是失控 Promise。

第四,Teams 用 mailbox 做跨进程通信。它不优雅,但可靠、可调试、跨 tmux/iTerm/process 都能工作。

第五,fork 为 prompt cache 做了非常具体的优化。它不是抽象地“继承上下文”,而是维护字节级前缀一致性。

风险和复杂度

这套系统的复杂度也很高。

权限路径很多:规则、工具检查、mode、auto classifier、hook、headless、bubble、leader UI、mailbox 都可能参与。新增工具如果没有正确实现 checkPermissions(),在多 Agent 场景下容易出现误拒绝或误放行。

任务状态也容易出错。源码里多次避免异步 await 后写回旧 snapshot,这说明并发状态竞争是真问题。

fork 对缓存一致性非常敏感。改 system prompt、工具池、placeholder 文案、message filtering,都可能破坏 cache prefix。

Teams 依赖文件协议。mailbox 和 team file 简单直接,但要处理锁、重复消息、已读状态、进程退出、pane 被杀、task unassign 等边界。

如果要二次开发,从哪里读

建议顺序:

  1. src/tools/AgentTool/AgentTool.tsx
  2. src/tools/AgentTool/runAgent.ts
  3. src/utils/forkedAgent.ts
  4. src/tasks/LocalAgentTask/LocalAgentTask.tsx
  5. src/utils/task/framework.ts
  6. src/utils/permissions/permissions.ts
  7. src/tools/AgentTool/forkSubagent.ts
  8. src/tools/TeamCreateTool/TeamCreateTool.ts
  9. src/tools/shared/spawnMultiAgent.ts
  10. src/utils/swarm/inProcessRunner.ts
  11. src/tools/SendMessageTool/SendMessageTool.ts
  12. src/hooks/useInboxPoller.ts
  13. src/utils/swarm/permissionSync.ts
  14. src/utils/teammateMailbox.ts

读完这条线,基本就能理解 Claude Code 多 Agent 的主干。

最后的架构判断

Claude Code 的多 Agent 不是一个“多角色 prompt 模板”,而是一套围绕 query() 构建的 Agent 运行时。

subagent 负责把任务委派出去,Task framework 负责把它变成可管理后台任务,permission system 负责把安全边界收住,fork 负责高效继承父上下文,Agent Teams 则在 mailbox、task list、leader approval 和 teammate runner 之上,把一次性委派扩展成持续协作。

真正值得借鉴的是这个分层:

同一个推理引擎
  + 隔离的 ToolUseContext
  + 可管理的 TaskState
  + 可审计的 PermissionDecision
  + 可持久化的 Transcript/Mailbox
  + 可选择的运行后端

这就是它能同时支持 subagent、fork 和 Agent Teams 的原因。