零、从第一性原理思考:Agent 该怎么设计?
本节是基于源码的设计推导,不是源码本身的直接描述。每个问题先推理"应该怎样",再用源码印证"实际怎样"。
0.1 起点:一个人干不完所有活
想象你是一个项目经理,手里有一个大活要干。你自己一个人从头到尾做也行,但效率低 —— 你在跑测试的时候不能同时改代码,你在搜索代码的时候不能同时写文档。
更好的做法是:派几个人分头干,各管一摊,最后汇总结果。
Claude Code 的 Agent 系统就是这个思路。主 Claude(你对话的那个)可以"生出"子 Agent,每个子 Agent 独立干一件事,干完把结果报回来。
那该怎么设计这个"派活"系统?沿着一个 Agent 的生命周期,自然会遇到五个问题:
0.2 问题链:定义 → 创建 → 执行 → 通信 → 隔离
1. 有哪些类型的人可以派?(定义)
不同的活需要不同类型的人。搜代码需要一个"探索型",做计划需要一个"规划型",通用的活需要一个"全能型"。
源码印证:tools/AgentTool/builtInAgents.ts 注册了 6 种内置 Agent —— general-purpose(通用)、statusline-setup(状态栏)、Explore(探索)、Plan(规划)、claude-code-guide(文档向导)、verification(验证)。其中后 4 种按 feature flag 和运行环境条件加载。
2. 怎么生一个 Agent 出来?(创建)
需要一个标准化的"派活"接口:告诉系统你要派什么类型的人、让他干什么、给他什么权限。
源码印证:AgentTool.tsx定义了输入 schema ——prompt(任务描述)、subagent_type(类型)、model(模型)、isolation(隔离方式)等参数。
3. Agent 怎么干活、怎么收工?(执行)
Agent 被派出去后,需要一个独立的执行循环:读取任务 → 调用工具 → 得到结果 → 继续或结束。而且要能把中间过程"流"回来,让主 Agent 知道进展。
源码印证:runAgent.ts用async function*(异步生成器)实现流式执行,Agent 内部复用和主 Claude 相同的query()查询循环。
4. 多个 Agent 之间怎么通信?(通信)
Agent 不是孤立的。一个 Agent 可能需要告诉另一个"我做完了",或者主 Agent 需要追踪所有子 Agent 的进度。
源码印证:SendMessageTool实现了邮箱系统(writeToMailbox),Task 系统(TaskCreate/Update/Get/List)实现了任务状态追踪。
5. 多个 Agent 怎么互不干扰?(隔离)
如果两个 Agent 同时改同一个文件,就会冲突。需要让它们在各自独立的环境里工作。
源码印证:EnterWorktreeTool用 git worktree 创建独立的工作副本;forkSubagent.ts的 fork 模式继承上下文但独立执行。
一、定义:有哪些类型的 Agent?
1.1 6 种内置 Agent
Claude Code 内置了 6 种 Agent 类型,定义在 tools/AgentTool/built-in/ 目录下,由 builtInAgents.ts 注册。并非全部始终可用 —— 部分受 feature flag 或运行环境控制:
| 类型 | 用途 | 可用工具 | 模型 | 启用条件 |
|---|---|---|---|---|
| general-purpose | 通用型,处理复杂多步任务 | 全部工具(*) | 继承父模型 | 始终可用 |
| statusline-setup | 配置状态栏 | Read、Edit | Sonnet | 始终可用 |
| Explore | 快速搜索和探索代码库 | 只读(禁用 Agent/Edit/Write 等) | Haiku(外部)/ 继承(内部) | feature flag + GrowthBook |
| Plan | 设计实现方案 | 只读(同 Explore) | 继承父模型 | 同 Explore |
| claude-code-guide | Claude Code/SDK/API 使用指南 | Glob、Grep、Read、WebFetch、WebSearch | Haiku | 非 SDK 入口时可用 |
| verification | 运行构建/测试/lint 验证实现正确性 | 只读(同 Explore) | 继承父模型 | VERIFICATION_AGENT + GrowthBook |
注册逻辑在 builtInAgents.ts:45-71,始终注册 general-purpose 和 statusline-setup,其余按条件追加。
1.2 怎么选类型?看 whenToUse
每种内置 Agent 都有一个 whenToUse 字段,告诉主 Claude "什么情况下该用我"。主 Claude 读了这些描述后自己判断 —— 没有调度算法,语义匹配完全靠 LLM 推理。
whenToUse 会在 prompt.ts:43-45 中被格式化为 - type: whenToUse (Tools: ...) 注入主 Claude 的系统提示(或作为 agent_listing_delta attachment 附加到消息中),主 Claude 看到后自行决定派谁。
设计观察:好的 whenToUse 有几个共同特征:
- 给出具体的触发场景 —— 不是"用于搜索",而是"当你不确定能一次找到时"
- 给出使用示例 —— Explore 直接给了 glob 模式、关键词、问题三种例子
- 给出调用要求 —— verification 要求传入任务描述+文件列表,Explore 要求指定档位
- 给出边界条件 —— claude-code-guide 要求先检查有没有已运行的同类 Agent
1.3 whenToUse 的注入机制
built-in/*.ts 定义 whenToUse
│
▼
builtInAgents.ts 注册到 agents[] 数组
│
▼
prompt.ts:formatAgentLine() 格式化为:
"- general-purpose: General-purpose agent for... (Tools: *)"
"- Explore: Fast agent specialized for... (Tools: All tools except Agent, ...)"
│
▼
注入方式二选一(prompt.ts:196-199):
├─ 内联:直接写入 AgentTool 的工具描述(tool description)
└─ 附件:作为 agent_listing_delta attachment 追加到消息中
│ ↑ 选哪个由 shouldInjectAgentListInMessages() 决定
▼ 附件模式更省缓存——工具描述不变 → 不会 bust prompt cache
主 Claude 读到后,根据用户请求自行匹配该派哪个 Agent
附件模式是后来优化的结果(prompt.ts:51-57 注释):动态 agent 列表曾占全网 10.2% 的 cache_creation token,因为 MCP 异步连接或插件重载会导致列表变化 → 工具 schema 变化 → 缓存失效。改为附件后,工具描述保持不变,缓存命中率大幅提升。
Explore 用 Haiku(最便宜的模型),因为它只需要搜索和读取,不需要复杂推理。verification 强制后台运行(background: true),因为验证通常比较慢,不应该阻塞主对话。
1.4 还有一种特殊的:Fork 子 Agent 🚧 未启用
⚠️ 此特性尚未启用:feature('FORK_SUBAGENT')当前返回false,以下内容为源码中的设计实现,不会在运行时执行。
除了上面 6 种显式指定 subagent_type 的方式,还有一种"隐式"的 Fork 模式(forkSubagent.ts)。它不是一种可选的 Agent 类型,而是一种完全不同的创建方式 —— 省略 subagent_type 参数时自动触发,子 Agent 直接继承父 Agent 的完整上下文。
启用的三重门槛
Fork 模式的启用需要同时满足三个条件(forkSubagent.ts:32):
export function isForkSubagentEnabled(): boolean { if (feature('FORK_SUBAGENT')) { // 门槛 1:feature flag 开启 if (isCoordinatorMode()) return false // 门槛 2:不能在 Coordinator 模式下 if (getIsNonInteractiveSession()) return false // 门槛 3:必须是交互式会话 return true } return false // 当前 feature flag 关闭,此特性未启用 }
| 条件 | 原因 |
|---|---|
feature('FORK_SUBAGENT') 为 true | 实验性功能,需要 Anthropic 内部 feature flag 放行 |
| 不在 Coordinator 模式 | Coordinator 有自己的委派模型,fork 与之互斥 |
| 交互式会话 | SDK/API 等非交互模式下无法处理 fork 的异步通知 |
具体触发时机
触发逻辑在 AgentTool.tsx:318-323,是一个三路分支:
// AgentTool.tsx:322 const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType) const isForkPath = effectiveType === undefined
| 主 Claude 的调用方式 | Fork 开关状态 | effectiveType | 走哪条路 |
|---|---|---|---|
指定 subagent_type: 'Explore' | 任意 | 'Explore' | 正常路径:创建 Explore Agent |
省略 subagent_type | 开启 | undefined | Fork 路径:继承父上下文 |
省略 subagent_type | 关闭 | 'general-purpose' | 正常路径:降级为通用 Agent |
换句话说:只有 Fork 开关打开 + 主 Claude 故意省略 subagent_type 时,才会走 Fork 路径。
Fork vs 普通 Agent
| 维度 | 普通 Agent | Fork Agent |
|---|---|---|
| 上下文 | 从零开始 | 继承父 Agent 完整对话历史 |
| 系统提示 | 自己的 | 复用父 Agent 已渲染的字节(缓存一致) |
| 工具池 | 按白名单/黑名单过滤 | 复制父 Agent 完整工具池 |
| 模型 | 可独立指定 | 必须继承(不同模型会破坏缓存前缀) |
| 注册 | builtInAgents.ts 注册,主 Claude 可见 | 不注册,主 Claude 看不到 |
| 运行 | 可前台可后台 | 强制异步 |
为什么要设计 Fork?
"Fork yourself when the intermediate tool output isn't worth keeping in your context."(当中间工具输出不值得留在你的上下文里时,fork 自己。)
比如要回答"这个分支还有什么没做完?",需要跑一堆 git status、git log、grep —— 原始输出很长但只需要一个摘要。Fork 出去执行,只把摘要返回,主上下文保持干净。而且 Fork 比创建新 Agent 更省钱 —— 共享 prompt cache,因为 API 请求前缀字节完全相同。
1.5 Agent 的类型定义体系
Agent 的类型定义在 loadAgentsDir.ts:106-184,是一个三层结构:
BaseAgentDefinition(20+ 字段:agentType、whenToUse、tools、model、permissionMode 等)
│
├── BuiltInAgentDefinition — source: 'built-in',getSystemPrompt 动态生成
├── CustomAgentDefinition — source: SettingSource,用户 .claude/agents/ 下定义
└── PluginAgentDefinition — source: 'plugin',由插件提供
三者的联合类型 AgentDefinition 是代码中统一使用的类型。运行时通过 source 字段区分:isBuiltInAgent() / isCustomAgent() / isPluginAgent()。
1.6 也可以自定义 Agent
除了内置类型,用户还可以在 .claude/agents/ 目录下定义自己的 Agent(Markdown 格式),由 loadAgentsDir.ts 加载和验证。自定义 Agent 的 frontmatter 中 description 字段即为 whenToUse。
二、创建:一个 Agent 怎么被生出来的?
2.1 入口:AgentTool 的输入参数
当主 Claude 决定"派一个人去干这件事",它会调用 AgentTool。核心输入参数(AgentTool.tsx:82):
type AgentToolInput = { prompt: string; // 派给 agent 的任务描述 description: string; // 3-5 字的简短描述 subagent_type?: string; // agent 类型(不填则用默认的 general-purpose) model?: 'sonnet' | 'opus' | 'haiku'; // 可以覆盖模型 run_in_background?: boolean; // 是否后台运行 isolation?: 'worktree' | 'remote'; // 隔离方式 }
2.2 创建流程
用大白话说,创建一个 Agent 就像"组建一个新团队成员":
主 Claude 调用 AgentTool.call()
│
▼
┌─── 1. 确定类型 ──────────────────────────────┐
│ subagent_type → 在内置/自定义 Agent 中匹配 │
│ 没指定 → 使用 general-purpose 默认类型 │
└─────────────────────────────────────────────────┘
│
▼
┌─── 2. 准备上下文 ─────────────────────────────┐
│ • 创建唯一 agentId │
│ • 构建系统提示(继承父 agent + agent 自己的) │
│ • 过滤可用工具(resolveAgentTools) │
│ • 初始化 MCP 服务器连接 │
└─────────────────────────────────────────────────┘
│
▼
┌─── 3. 启动执行 ──────────────────────────────┐
│ 调用 runAgent() → 返回异步生成器 │
│ 根据 run_in_background 决定同步还是异步 │
└─────────────────────────────────────────────────┘
2.3 返回状态
Agent 创建后,会返回一个状态标记告诉主 Claude 发生了什么:
| 状态 | 含义 |
|---|---|
completed | 同步执行完成,结果已返回 |
async_launched | 后台任务已启动,稍后通知 |
teammate_spawned | 多 agent 模式,团队成员已注册 |
remote_launched | 远程环境已启动(内部功能) |
2.4 团队分工:谁来决定?
一个关键洞察:没有调度算法,LLM 自己就是调度器。
代码里没有一个函数叫 splitTaskIntoAgents() 或 calculateOptimalAgentCount()。组织者就是主 Claude 自己 —— 派几个 Agent、每人干什么(写在 prompt 字段里)、前台还是后台,全靠主 Claude 根据系统提示的指导原则自己推理。
Coordinator 模式 🚧 未启用
⚠️ 此特性尚未启用:feature('COORDINATOR_MODE')当前返回false。
Coordinator 模式是一种显式的协调角色划分(coordinatorMode.ts:116),主 Claude 变成 Coordinator,通过四阶段工作流指挥 Worker:
Research(调研)→ Synthesis(综合)→ Implementation(实现)→ Verification(验证) Workers 并行 Coordinator 自己做 Workers 按规范做 Workers 测试
当前模式下没有这种角色划分,主 Claude 既是"项目经理"也是"执行者"。
多 Agent 并行的触发条件
多 Agent 并行没有魔法关键词,也没有调度算法。系统提示中给主 Claude 注入了以下规则(AgentTool/prompt.ts:245-271):
| 规则 | 含义 |
|---|---|
| 主动并发 | 主 Claude 应主动判断,能并行就并行 —— 在一条消息中包含多个工具调用 |
| 用户说 "in parallel" | 唯一被硬编码的关键词,强制在一条消息里同时发出多个 Agent 调用 |
| 前台 vs 后台 | 需要结果才能继续 → 前台;有独立工作可做 → 后台 |
| 主动型 Agent | whenToUse 里写了"proactively"的 Agent,主 Claude 应不等用户要求就主动派出 |
代码层面的并发上限是 10 个(toolOrchestration.ts:8),可通过环境变量 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 覆盖。
Pro vs Max 订阅:Agent 功能无差异
Agent 的类型、工具、模型、并发上限、worktree 隔离等核心功能在所有订阅类型中完全一致。唯一的区别是并发提示的显示:
| 订阅类型 | "Launch multiple agents concurrently..." 提示 |
|---|---|
| Pro | 不显示 —— 不主动鼓励并发(Pro 的 rate limit 更紧) |
| Max / Team / Enterprise | 显示 —— 鼓励充分利用并发 |
Pro 用户不是"不能"并发,只是系统提示里不会主动提醒这一点。用户说 "in parallel" 照样会强制并行。
三、执行:Agent 怎么干活?
3.1 核心:生成器模式
Agent 的执行引擎在 runAgent.ts。它的核心设计是用 async function*(异步生成器)实现流式执行 —— Agent 每产生一条消息,就 yield 出去,父 Agent 可以实时看到进展。
// runAgent.ts:248 — Agent 执行引擎(简化版) export async function* runAgent({ agentDefinition, // Agent 定义(类型、工具、提示词) promptMessages, // 初始消息(任务描述) toolUseContext, // 工具执行上下文 canUseTool, // 权限检查函数 isAsync, // 是否后台运行 }): AsyncGenerator<Message, void> { // 1. 创建唯一身份 const agentId = createAgentId() // 2. 构建上下文(继承父 agent,但有自己的系统提示) const context = createSubagentContext() const systemPrompt = buildEffectiveSystemPrompt() // 3. 过滤可用工具(不同类型的 agent 能用的工具不同) const tools = resolveAgentTools() // 4. 执行查询循环(和主 Claude 用同一个 query() 函数!) for await (const message of query({ systemPrompt, tools, ... })) { yield message // 流式返回每条消息给父 agent } }
为什么用 yield 而不是 return?
yield 是 JavaScript 生成器的关键字:"产出一个值,暂停,等调用方要下一个时再继续"。
return= 一口气干完,最后返回一个结果(用户得等 Agent 跑完才能看到任何东西)yield= 干一点就吐一个中间成果出来,父 Agent 用for await...of逐条消费
效果就是流式返回 —— Agent 还在干活,用户已经能看到中间产出了。
3.2 关键洞察:Agent 和主 Claude 用同一个查询引擎
Agent 并不是一个"特殊的东西" —— 它内部调用的是和主 Claude 完全相同的 query() 函数。区别只在于:
- 系统提示不同:Agent 有自己的角色定义
- 可用工具不同:Plan/Explore 类型只能用只读工具
- 上下文不同:Agent 有自己的 agentId,但继承父 agent 的对话历史
四、通信:Agent 之间怎么说话?
Agent 之间的通信有两种机制:消息传递(实时沟通)和任务管理(状态追踪)。
4.1 消息传递:邮箱系统
SendMessageTool(918 行)实现了一个邮箱系统 —— 每个 Agent 有一个"信箱",别人可以往里面投信。
核心路由逻辑(SendMessageTool.ts:67):
const inputSchema = z.object({ to: z.string(), // 收件人(agent 名称、"*" 广播、或桥接地址) summary: z.string(), // 5-10 字预览 message: z.union([ z.string(), // 普通文本 StructuredMessage() // 结构化消息(关闭请求、计划审批等) ]) })
消息可以发给不同的目标:
| 目标格式 | 含义 |
|---|---|
agent名称 | 发给本地团队中的某个 Agent |
* | 广播给所有团队成员 |
bridge:<session-id> | 跨 Claude 会话(REPL 桥接) |
uds:<socket-path> | 本地 Unix Domain Socket |
写入邮箱的核心(SendMessageTool.ts:149):
// 把消息写入目标 agent 的邮箱文件 await writeToMailbox(recipientName, { from: senderName, // 发件人 text: content, // 消息内容 summary, // 摘要 timestamp: new Date().toISOString(), }, teamName)
4.2 结构化消息:不只是聊天
除了普通文本,Agent 之间还能发结构化消息:
| 消息类型 | 用途 |
|---|---|
shutdown_request | 请求某个 Agent 优雅终止 |
shutdown_response | 批准或拒绝终止请求 |
plan_approval_response | 计划审批反馈 |
4.3 任务管理:追踪进度
Task 系统是另一种"通信"方式 —— 不是直接对话,而是通过共享的任务状态来协调。Agent 在系统中被追踪为"任务"(Task.ts:6),类型包括 local_agent / remote_agent / in_process_teammate 等,状态为 pending → running → completed / failed / killed。
创建任务(TaskCreateTool.ts:80):
const taskId = await createTask(getTaskListId(), { subject, // 任务标题 description, // 任务描述 status: 'pending', // 初始状态 owner: undefined, // 谁在做 blocks: [], // 阻塞了谁 blockedBy: [], // 被谁阻塞 })
查询任务(TaskListTool.ts:65):
const allTasks = await listTasks(taskListId) // 过滤掉已完成任务的阻塞关系 const resolvedTaskIds = new Set( allTasks.filter(t => t.status === 'completed').map(t => t.id) )
任务系统支持依赖关系(blockedBy / blocks),一个 Agent 可以创建多个有先后顺序的任务,其他 Agent 可以查看进度。
五、隔离:Agent 怎么互不干扰?
多个 Agent 同时工作时,如果都在同一个目录里改文件,就会冲突。源码中有两种隔离机制。
5.1 Git Worktree 隔离
最主要的隔离方式。Worktree 是 git 的内置功能 —— 在同一个仓库下创建一个独立的工作副本,有自己的分支和文件。
进入 Worktree(EnterWorktreeTool.ts:77):
async call(input) { // 1. 创建 git worktree(独立的工作副本) const worktreeSession = await createWorktreeForSession( getSessionId(), slug ) // 2. 切换工作目录到 worktree process.chdir(worktreeSession.worktreePath) setCwd(worktreeSession.worktreePath) // 3. 清除缓存(系统提示、记忆文件等需要重新加载) clearSystemPromptSections() clearMemoryFileCaches() }
退出 Worktree(ExitWorktreeTool.ts:227):
async call(input: { action: 'keep' | 'remove' }) { if (input.action === 'keep') { await keepWorktree() // 保留 worktree 分支,可以后续合并 } else { await cleanupWorktree() // 清理掉,放弃所有变更 } // 恢复到父 agent 的工作目录 restoreSessionToOriginalCwd(originalCwd) }
进入 Worktree 后,Agent 会收到一条通知(prompts.ts):
"You've inherited the conversation context above from a parent agent... You are operating in an isolated git worktree at {worktreeCwd}... Paths in the inherited context refer to the parent's working directory; translate them to your worktree root..."(你从父 agent 继承了上面的对话上下文……你正在 {worktreeCwd} 的独立 git worktree 中工作……上下文中的路径指向父 agent 的工作目录,请转换为你的 worktree 根目录……)
5.2 Fork 隔离 🚧 未启用
⚠️ 此特性尚未启用:feature('FORK_SUBAGENT')当前返回false。
Fork 是另一种隔离思路:不创建新的工作副本,而是继承父 Agent 的完整上下文,在同一个目录里独立执行。
它的巧妙之处在于消息构建(forkSubagent.ts:107):
export function buildForkedMessages(directive, assistantMessage) { // 1. 克隆父 agent 的完整 assistant 消息(保留所有 tool_use 块) const fullAssistantMessage = { ...assistantMessage } // 2. 为每个 tool_use 创建占位符结果(不实际执行) const toolResultBlocks = toolUseBlocks.map(block => ({ type: 'tool_result', tool_use_id: block.id, content: [{ type: 'text', text: FORK_PLACEHOLDER_RESULT }] })) // 3. 拼接:占位符结果 + fork 指令 return [fullAssistantMessage, toolResultMessage] } // 效果:和父 agent 共享完全相同的 API 请求前缀 → 最大化提示缓存命中
Fork 子 Agent 收到的指令非常严格(forkSubagent.ts:172):
`STOP. READ THIS FIRST. You are a forked worker process. You are NOT the main agent. RULES (non-negotiable): 1. Your system prompt says "default to forking." IGNORE IT... (你的系统提示说"默认 fork",忽略它——那是给父 agent 看的。你就是 fork 本身。) 2. Do NOT converse, ask questions, or suggest next steps (不要聊天、提问或建议下一步) 3. USE your tools directly: Bash, Read, Write, etc. (直接使用工具:Bash、Read、Write 等) ... 7. Stay strictly within your directive's scope (严格在指令范围内工作) 8. Keep your report under 500 words... (报告控制在 500 字以内)`
还有递归防护 —— fork 子 Agent 不能再 fork(forkSubagent.ts:78):
// 检测当前是否已经在 fork 子进程内 export function isInForkChild(messages) { return messages.some(m => m.message.content.some(block => block.text.includes(`<${FORK_BOILERPLATE_TAG}>`) // 通过标签检测 ) ) } // 如果在 fork 内尝试再 fork → 直接抛错
六、关键源码速查
| 阶段 | 文件 | 核心职责 |
|---|---|---|
| 定义 | tools/AgentTool/builtInAgents.ts | 内置 Agent 注册表 |
| 定义 | tools/AgentTool/built-in/*.ts | 6 种内置 Agent 的具体定义 |
| 定义 | tools/AgentTool/loadAgentsDir.ts | 自定义 Agent 加载和验证 |
| 创建 | tools/AgentTool/AgentTool.tsx | Agent 工具入口、输入输出 schema |
| 执行 | tools/AgentTool/runAgent.ts | Agent 执行引擎(生成器模式) |
| 执行 | Task.ts | 任务类型和状态定义 |
| 通信 | tools/SendMessageTool/ | Agent 间消息传递(邮箱系统) |
| 通信 | tools/TaskCreateTool/ | 创建任务 |
| 通信 | tools/TaskListTool/ | 查询任务列表 |
| 通信 | tools/TaskUpdateTool/ | 更新任务状态 |
| 通信 | tools/TaskGetTool/ | 获取任务详情 |
| 隔离 | tools/EnterWorktreeTool/ | 创建 git worktree 隔离环境 |
| 隔离 | tools/ExitWorktreeTool/ | 退出 worktree(保留或清理) |
| 隔离 | tools/AgentTool/forkSubagent.ts | Fork 子 agent 🚧 未启用 |
| 辅助 | coordinator/coordinatorMode.ts | 多 agent 协调模式 🚧 未启用 |
| 辅助 | constants/prompts.ts | Agent 相关系统提示词 |