从源码梳理 Claude Code 包含的几种子 Agent 实现机制------同一进程内的、后台异步的、tmux 多进程的、以及 git worktree 隔离的。不逐行贴源码,把每种机制的核心区别讲清楚。
总览:Claude Code 有多少种"子代理"?
Claude Code 的子代理不是一个单一概念。从源码看,至少可以按四个维度来区分:
按隔离程度:从最轻的"同一进程的 async generator 协程"到最重的"tmux 独立进程"
按运行模式:sync 同步(阻塞父级,结果回来才继续)和 async 异步(后台跑,通知回来)
按 agent 类型:general-purpose、Explore、Plan、verification 等,每种有不同的工具权限和系统提示词
按来源 :内置(built-in)、项目自定义(.claude/agents/)、插件(plugin agents)、fork 模式
一个 Agent 工具调用可以按不同维度组合出多种形态。比如 isolation: "worktree" + subagent_type: "Explore" = 在独立 worktree 里跑只读 Explore agent;run_in_background: true + subagent_type: "general-purpose" = 后台跑全功能 agent。
维度一:运行模式------sync、async、background
这是最基本的区分。由 runAgent() 调用时的 isAsync 参数决定。
sync 模式 (isAsync: false):
scss
主循环 ──→ 发出 Agent 调用 ──→ 子代理跑 query() 循环 ──→ tool_result 返回
↑ │
└──────────────── 一直阻塞等待 ────────────────────────────────┘
- 共享父级
AbortController------按 Escape 全停 setAppState共享父级(子代理能写入状态)/simplify的三个审查 Agent 就是 sync 模式
async 模式 (isAsync: true):
- 独立
AbortController------按 Escape 不影响它 setAppState为空函数(禁止写入父子代理状态)- 消息通过
SendMessage工具后续发送给子代理 - 子代理完成后自动通知
background 运行 :AgentTool 的参数里有 run_in_background: true。runAgent() 收到后走 async 路径,主模型不需要等结果。提示词里专门有一段指导 LLM:"When an agent runs in the background, you will be automatically notified when it completes --- do NOT sleep, poll, or proactively check on its progress."
源码里决定 isAsync 的关键逻辑在 AgentTool.tsx 的 call() 方法:
typescript
const shouldRunAsync = (
run_in_background === true ||
selectedAgent.background === true ||
isCoordinator ||
forceAsync ||
assistantForceAsync
) && !isBackgroundTasksDisabled;
FYI:还有一种 fork subagent (实验性 feature gate FORK_SUBAGENT),不指定 subagent_type 时触发。它强制所有子代理走 async 路径,子代理继承父级的完整对话上下文和系统提示词,通过 prompt cache 共享降低 token 开销。不展开,但知道有这么个东西就行。
维度二:隔离程度------四种级别的"隔开"
Level 1:纯协程(无隔离)
sync 模式的子代理就是典型的纯协程------同一个进程、同一个堆、同一个 event loop、同一个文件系统。资源隔离全靠 createSubagentContext() 在上层"画线":
| 属性 | 隔离方式 |
|---|---|
messages[] |
独立空数组 |
readFileState |
cloneFileStateCache() 深克隆 |
setAppState |
空函数(async)/ 共享父级(sync) |
addNotification / setToolJSX / setStreamMode |
全部 undefined |
queryTracking |
独立 chainId,depth + 1 |
子代理看不到父级的对话历史,读文件缓存互不污染,不能弹通知、不能改 UI。但它跟父级共享同一个 JS 堆------如果一个子代理的代码里写了 while(true){},整个进程就卡死了。Claude Code 能接受的原因是子代理的"代码"是 LLM 生成的工具调用,不是任意用户代码。
Level 2:worktree 隔离(文件系统级)
isolation: "worktree" 参数触发。创建临时 git worktree,子代理在独立的工作目录里跑:
typescript
// src/tools/AgentTool/AgentTool.tsx
if (effectiveIsolation === 'worktree') {
const slug = `agent-${earlyAgentId.slice(0, 8)}`
worktreeInfo = await createAgentWorktree(slug)
}
worktree 解决了两个问题:
- 多个子代理同时改文件时不会冲突(各自在不同的工作目录)
- 子代理如果改坏了东西,主工作目录不受影响
但注意:worktree 隔离的是文件系统 ,不是进程。子代理仍然在同一个 Node.js 进程里,只是 getCwd() 返回不同路径。代码里有注释专门说明这一点。如果子代理没有改动,worktree 自动清理;如果有改动,返回 worktree 路径和分支名。
Level 3:teammate 模式(真正多进程)
通过 team_name + name 参数触发,走 spawnTeammate() 路径。每个 teammate 跑在独立的 tmux pane 里,是真正的进程级隔离。
提示词里有一条硬限制:in-process 子代理不能 spawn 新 teammate,teammate 也不能 spawn teammate。"The team roster is flat"------teammate 的 roster 是平铺的,不允许嵌套。
Level 4:remote 隔离(云端 sandbox,仅 ant)
内部使用的 isolation: "remote",在远程 CCR 环境运行 agent,always background。外部版本不可用。
快速对照
| 模式 | 进程 | 文件系统 | abort 关系 | 适用 |
|---|---|---|---|---|
| sync 协程 | 共享 | 共享 | 共享父级 | 需要结果的快速子任务 |
| async 协程 | 共享 | 共享 | 独立 | 后台长任务 |
| worktree | 共享 | 隔离 | 独立 | 多 agent 并行修改文件 |
| teammate | 独立 | 共享 | 独立 | 重度并行 |
| remote | 独立 | 隔离 | 独立 | 云端 sandbox |
维度三:Agent 类型------内置和自定义
通过 subagent_type 参数指定。不指定时默认 general-purpose,或者走 fork 路径(前面说的 fork subagent)。
内置 agent 定义在 src/tools/AgentTool/built-in/ 下,每个文件 export 一个 BuiltInAgentDefinition:
| Agent | 核心特征 | 关键配置 |
|---|---|---|
| general-purpose | 默认 agent,全能力 | tools: ['*'], source: 'built-in' |
| Explore | 只读,快速搜索 | disallowedTools: [FileEdit, FileWrite, NotebookEdit, Agent, ExitPlanMode], model 定向 haiku |
| Plan | 只读,设计实现计划 | 系统提示词侧重架构设计,omitClaudeMd: true, 复用 Explore 的工具权限 |
| verification | 验证 agent 输出 | 专门用于检查和验证 |
| statusline-setup | 配置状态栏 | 专用小工具 |
| claude-code-guide | Claude Code 文档查询 | 加载官方文档作为上下文 |
Explore 就是 Claude Code 在探索代码库时派出去的搜索子代理。系统提示词第一句就是 "You are a file search specialist",核心能力只有三项:Glob 找文件、Grep 搜内容、Read 读代码。主模型收到"帮我查一下这个代码库怎么处理认证"这类任务时,就可以 subagent_type: "Explore" 把它派出去------子代理在自己的隔离上下文里大范围搜索,最后只把精简结论返回。这也是零索引方案里上下文隔离的关键支撑:大量搜索中间结果由 Explore 在自己的上下文里消化,不撑满主对话。
Explore 和 Plan 都标记为 READ-ONLY。disallowedTools 排除了所有编辑类工具。Bash 虽然保留,但提示词严格限定只读命令(ls、git status、cat、head、tail 等)。
每个内置 agent 都定义了自己的 whenToUse,这个文本会出现在主模型的系统提示词里,告诉 LLM 什么时候该切换到哪个 agent。例如 Explore 的 whenToUse:"Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns..."
Explore 还有一个特有属性 omitClaudeMd: true------它的系统提示词不加载 CLAUDE.md 的项目规则。"The main agent has full context and interprets results." 省掉约 5-15 Gtok/week 的 token。
自定义 Agent
通过 .claude/agents/*.md 或插件的 agents/ 目录定义。用 YAML frontmatter + Markdown 内容配置:
markdown
---
name: my-code-reviewer
description: 专门审查代码质量和安全性
tools: Read, Grep, Glob, Bash
permissionMode: acceptEdits
model: sonnet
---
当进行代码审查时:
1. 先读取变更的代码
2. 用 Grep 查找相关的已有实现
3. 报告发现的问题和优化建议
支持的配置项包括 tools、disallowedTools、permissionMode、model、effort、maxTurns、skills、mcpServers、hooks、background、isolation、memory、initialPrompt。自定义 agent 跟内置 agent 走同一套 AgentDefinition 类型系统,runAgent() 不区分来源。
维度四:fork 子代理(实验性)
这是一个 feature gate FORK_SUBAGENT 控制的功能。当 gate 开启且不指定 subagent_type 时触发。
跟普通子代理最大的区别:fork 子代理继承父级的完整对话上下文和系统提示词 ,而不是从干净的 "prompt + diff" 开始。每个 fork spawn 强制走 async,用户通过 <task-notification> 接收完成通知。
设计动机是 prompt cache 共享:因为 fork 子代理的系统提示词和父级完全相同,API 能复用 prompt cache,降低 token 成本。/fork <directive> slash 命令也是基于这个机制。
跟本篇关系不大,知道存在即可。
子代理怎么跑起来的------统一的 runAgent() 管线
不管哪种模式、哪种 agent 类型,最终都走同一个 runAgent() 函数。管线大致是:
scss
AgentTool.call()
↓
判断模式(sync/async,worktree?teammate?)
↓
如果是 teammate → spawnTeammate() → 返回(独立进程)
↓
如果是 subagent → 构建 runAgentParams(确定 agent 定义、组装工具池、确认 isAsync)
↓
runAgent()
├── 1. 确定模型
├── 2. 生成 agentId
├── 3. 组装初始消息(promptMessages + forkContextMessages)
├── 4. createSubagentContext() 构建隔离上下文
├── 5. resolveAgentTools() 解析工具池
├── 6. 生成系统提示词(agent.getSystemPrompt() + 环境增强)
├── 7. 运行 query() 循环(与主模型完全相同的 API 调用循环)
└── 8. finally: 7 项清理(MCP、hooks、缓存、todos、shell 任务等)
不管 sync 还是 async,不管 general-purpose 还是 Explore,全部走这条管线。区别在于调用时的参数不同。
总结
Claude Code 的子代理不是单一实现------它是一个多层面的组合系统:
- 运行模式(sync / async / background)决定父级是否等待以及 abort 关系
- 隔离程度(协程 / worktree / teammate / remote)决定资源隔离层级
- agent 类型(内置或自定义)决定工具权限、模型选择、系统提示词
- fork 模式(实验性)继承上下文,优化 prompt cache
各种组合覆盖了从"快速查一下代码"到"深度重构且不能搞坏主工作目录"的场景。核心原则是:默认最轻(in-process 协程),只在需要时才升级隔离级别。