第12章 多Agent协作------从单体到集群
引言
在传统的编程范式中,我们习惯于思考"如何让一个程序完成一个任务"。但在 Claude Code 的世界里,思考方式发生了根本性的转变:如何让多个智能体协同完成复杂任务。这不仅仅是数量的增加,而是质的飞跃------就像从独奏到交响乐团的转变。
本章将深入探讨 Claude Code 的多 Agent 协作机制,揭示如何通过精心设计的架构,让多个智能体像乐团成员一样各司其职、默契配合,共同演奏出复杂的代码交响曲。
概念讲解
Agent 即工具:统一的设计哲学
在 Claude Code 中,Agent 和工具之间没有本质的区别。一个 Agent 可以被另一个 Agent 调用,就像调用一个 Bash 工具或文件编辑工具一样自然。这种"Agent 即工具"的设计理念,打破了传统编程中函数调用和进程调用的界限,实现了真正的统一抽象。
这种设计的核心优势在于:
- 递归组合:Agent 可以调用 Agent,形成任意深度的调用链
- 透明交互:调用者无需关心被调用者是本地工具还是远程 Agent
- 资源隔离:每个 Agent 都有独立的上下文和权限边界
- 并行执行:多个 Agent 可以同时工作,互不干扰
任务系统:状态机与生命周期
Claude Code 的任务系统是整个多 Agent 协作的基石。每个 Agent 的执行都被封装为一个 Task,Task 有明确的类型和状态,形成了一个完整的状态机。
任务类型分为 7 种:
local_bash:本地 Shell 命令执行local_agent:本地 Agent 执行remote_agent:远程 Agent 执行in_process_teammate:进程内队友local_workflow:本地工作流monitor_mcp:MCP 监控dream:梦境任务(特殊用途)
任务状态有 5 种:
pending:等待执行running:正在执行completed:执行完成failed:执行失败killed:被终止
递归深度控制
为了避免无限递归和资源耗尽,Claude Code 实现了递归深度控制机制。每个 Agent 调用链都有深度限制,超过限制时会自动终止,确保系统稳定性。
源码分析
1. 任务类型定义
让我们从 src/Task.ts 开始,看看任务类型和状态是如何定义的:
typescript
export type TaskType =
| 'local_bash'
| 'local_agent'
| 'remote_agent'
| 'in_process_teammate'
| 'local_workflow'
| 'monitor_mcp'
| 'dream'
export type TaskStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'killed'
这 7 种任务类型覆盖了 Claude Code 中所有可能的执行场景。从最简单的本地命令执行到复杂的远程 Agent 调用,每种类型都有其特定的用途和实现。
2. 终止状态守卫函数
一个关键的设计是 isTerminalTaskStatus() 函数:
typescript
/**
* True when a task is in a terminal state and will not transition further.
* Used to guard against injecting messages into dead teammates, evicting
* finished tasks from AppState, and orphan-cleanup paths.
*/
export function isTerminalTaskStatus(status: TaskStatus): boolean {
return status === 'completed' || status === 'failed' || status === 'killed'
}
这个函数是一个典型的守卫函数(Guard Function),用于判断任务是否已经处于终止状态。它的作用包括:
- 防止向已终止的队友发送消息
- 从 AppState 中清理已完成的任务
- 处理孤儿清理路径
这种设计体现了防御性编程的思想------在关键操作前先检查前置条件,避免错误状态下的操作。
3. 任务 ID 生成机制
任务 ID 的生成是一个精巧的设计,结合了类型前缀和随机字符:
typescript
// Task ID prefixes
const TASK_ID_PREFIXES: Record<string, string> = {
local_bash: 'b', // Keep as 'b' for backward compatibility
local_agent: 'a',
remote_agent: 'r',
in_process_teammate: 't',
local_workflow: 'w',
monitor_mcp: 'm',
dream: 'd',
}
// Get task ID prefix
function getTaskIdPrefix(type: TaskType): string {
return TASK_ID_PREFIXES[type] ?? 'x'
}
// Case-insensitive-safe alphabet (digits + lowercase) for task IDs.
// 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks.
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type)
const bytes = randomBytes(8)
let id = prefix
for (let i = 0; i < 8; i++) {
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
}
return id
}
这个设计的精妙之处:
- 类型识别:通过前缀可以快速识别任务类型,便于调试和日志分析
- 安全考虑:使用 8 字节随机数,产生约 2.8 万亿种组合,足以抵抗暴力破解的符号链接攻击
- 大小写安全:使用数字和小写字母,避免大小写敏感带来的问题
- 向后兼容 :保留
local_bash的 'b' 前缀,确保不破坏现有功能
4. 任务注册机制
在 src/tasks.ts 中,我们看到了任务注册的模式:
typescript
import { feature } from 'bun:bundle'
import type { Task, TaskType } from './Task.js'
import { DreamTask } from './tasks/DreamTask/DreamTask.js'
import { LocalAgentTask } from './tasks/LocalAgentTask/LocalAgentTask.js'
import { LocalShellTask } from './tasks/LocalShellTask/LocalShellTask.js'
import { RemoteAgentTask } from './tasks/RemoteAgentTask/RemoteAgentTask.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const LocalWorkflowTask: Task | null = feature('WORKFLOW_SCRIPTS')
? require('./tasks/LocalWorkflowTask/LocalWorkflowTask.js').LocalWorkflowTask
: null
const MonitorMcpTask: Task | null = feature('MONITOR_TOOL')
? require('./tasks/MonitorMcpTask/MonitorMcpTask.js').MonitorMcpTask
: null
/* eslint-enable @typescript-eslint/no-require-imports */
/**
* Get all tasks.
* Mirrors the pattern from tools.ts
* Note: Returns array inline to avoid circular dependency issues with top-level const
*/
export function getAllTasks(): Task[] {
const tasks: Task[] = [
LocalShellTask,
LocalAgentTask,
RemoteAgentTask,
DreamTask,
]
if (LocalWorkflowTask) tasks.push(LocalWorkflowTask)
if (MonitorMcpTask) tasks.push(MonitorMcpTask)
return tasks
}
/**
* Get a task by its type.
*/
export function getTaskByType(type: TaskType): Task | undefined {
return getAllTasks().find(t => t.type === type)
}
这里有几个值得注意的设计:
- Feature Flags :使用
feature()函数进行条件导入,实现死代码消除 - 懒加载 :通过
require()动态导入,减少启动时的内存占用 - 避免循环依赖:使用函数返回数组而不是顶层常量,避免循环依赖问题
- 类型安全:使用 TypeScript 的类型系统确保任务类型的正确性
5. 工具注册与 Agent 工具
在 src/tools.ts 中,我们看到了工具注册的模式,特别是与 Agent 相关的工具:
typescript
import { toolMatchesName, type Tool, type Tools } from './Tool.js'
import { AgentTool } from './tools/AgentTool/AgentTool.js'
import { SkillTool } from './tools/SkillTool/SkillTool.js'
// ... 其他工具导入
// Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
/* eslint-disable @typescript-eslint/no-require-imports */
const getTeamCreateTool = () =>
require('./tools/TeamCreateTool/TeamCreateTool.js')
.TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
const getTeamDeleteTool = () =>
require('./tools/TeamDeleteTool/TeamDeleteTool.js')
.TeamDeleteTool as typeof import('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
const getSendMessageTool = () =>
require('./tools/SendMessageTool/SendMessageTool.js')
.SendMessageTool as typeof import('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
/* eslint-enable @typescript-eslint/no-require-imports */
这里的关键点:
- AgentTool:Agent 被封装为工具,可以被其他 Agent 调用
- TeamCreateTool:创建团队级并行工作
- SendMessageTool:实现 Agent 间通信
- 懒加载函数:使用函数返回工具而不是直接导入,打破循环依赖
6. 上下文缓存机制
在 src/context.ts 中,我们看到了上下文收集的缓存机制:
typescript
import memoize from 'lodash-es/memoize.js'
export const getGitStatus = memoize(async (): Promise<string | null> => {
if (process.env.NODE_ENV === 'test') {
// Avoid cycles in tests
return null
}
const startTime = Date.now()
logForDiagnosticsNoPII('info', 'git_status_started')
const isGitStart = Date.now()
const isGit = await getIsGit()
logForDiagnosticsNoPII('info', 'git_is_git_check_completed', {
duration_ms: Date.now() - isGitStart,
is_git: isGit,
})
if (!isGit) {
logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', {
duration_ms: Date.now() - startTime,
})
return null
}
try {
const gitCmdsStart = Date.now()
const [branch, mainBranch, status, log, userName] = await Promise.all([
getBranch(),
getDefaultBranch(),
execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], {
preserveOutputOnError: false,
}).then(({ stdout }) => stdout.trim()),
execFileNoThrow(
gitExe(),
['--no-optional-locks', 'log', '--oneline', '-n', '5'],
{
preserveOutputOnError: false,
},
).then(({ stdout }) => stdout.trim()),
execFileNoThrow(gitExe(), ['config', 'user.name'], {
preserveOutputOnError: false,
}).then(({ stdout }) => stdout.trim()),
])
logForDiagnosticsNoPII('info', 'git_commands_completed', {
duration_ms: Date.now() - gitCmdsStart,
status_length: status.length,
})
// Check if status exceeds character limit
const truncatedStatus =
status.length > MAX_STATUS_CHARS
? status.substring(0, MAX_STATUS_CHARS) +
'\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)'
: status
logForDiagnosticsNoPII('info', 'git_status_completed', {
duration_ms: Date.now() - startTime,
truncated: status.length > MAX_STATUS_CHARS,
})
return [
`This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`,
`Current branch: ${branch}`,
`Main branch (you will usually use this for PRs): ${mainBranch}`,
...(userName ? [`Git user: ${userName}`] : []),
`Status:\n${truncatedStatus || '(clean)'}`,
`Recent commits:\n${log}`,
].join('\n\n')
} catch (error) {
logForDiagnosticsNoPII('error', 'git_status_failed', {
duration_ms: Date.now() - startTime,
})
logError(error)
return null
}
})
/**
* This context is prepended to each conversation, and cached for the duration of the conversation.
*/
export const getSystemContext = memoize(
async (): Promise<{
[k: string]: string
}> => {
const startTime = Date.now()
logForDiagnosticsNoPII('info', 'system_context_started')
// Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled
const gitStatus =
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
!shouldIncludeGitInstructions()
? null
: await getGitStatus()
// Include system prompt injection if set (for cache breaking, ant-only)
const injection = feature('BREAK_CACHE_COMMAND')
? getSystemPromptInjection()
: null
logForDiagnosticsNoPII('info', 'system_context_completed', {
duration_ms: Date.now() - startTime,
has_git_status: gitStatus !== null,
has_injection: injection !== null,
})
return {
...(gitStatus && { gitStatus }),
...(feature('BREAK_CACHE_COMMAND') && injection
? {
cacheBreaker: `[CACHE_BREAKER: ${injection}]`,
}
: {}),
}
},
)
/**
* This context is prepended to each conversation, and cached for the duration of the conversation.
*/
export const getUserContext = memoize(
async (): Promise<{
[k: string]: string
}> => {
const startTime = Date.now()
logForDiagnosticsNoPII('info', 'user_context_started')
// CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always.
// --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir.
// --bare means "skip what I didn't ask for", not "ignore what I asked for".
const shouldDisableClaudeMd =
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
// Await the async I/O (readFile/readdir directory walk) so the event
// loop yields naturally at the first fs.readFile.
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
// Cache for the auto-mode classifier (yoloClassifier.ts reads this
// instead of importing claudemd.ts directly, which would create a
// cycle through permissions/filesystem → permissions → yoloClassifier).
setCachedClaudeMdContent(claudeMd || null)
logForDiagnosticsNoPII('info', 'user_context_completed', {
duration_ms: Date.now() - startTime,
claudemd_length: claudeMd?.length ?? 0,
claudemd_disabled: Boolean(shouldDisableClaudeMd),
})
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
},
)
这个设计的精妙之处:
- Memoize 缓存 :使用 lodash 的
memoize函数缓存结果,避免重复计算 - 并行执行 :使用
Promise.all并行执行多个 git 命令,提高效率 - 日志记录:详细的性能日志,便于诊断和优化
- 条件缓存:根据环境变量和配置决定是否缓存某些上下文
- 缓存破坏 :通过
cacheBreaker机制支持强制刷新缓存
7. 初始化流程中的并行预取
在 src/setup.ts 中,我们看到了初始化流程中的并行预取策略:
typescript
profileCheckpoint('setup_before_prefetch')
// Pre-fetch promises - only items needed before render
logForDiagnosticsNoPII('info', 'setup_prefetch_starting')
// When CLAUDE_CODE_SYNC_PLUGIN_INSTALL is set, skip all plugin prefetch.
// The sync install path in print.ts calls refreshPluginState() after
// installing, which reloads commands, hooks, and agents. Prefetching here
// races with the install (concurrent copyPluginToVersionedCache / cachePlugin
// on the same directories), and the hot-reload handler fires clearPluginCache()
// mid-install when policySettings arrives.
const skipPluginPrefetch =
(getIsNonInteractiveSession() &&
isEnvTruthy(process.env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL)) ||
// --bare: loadPluginHooks → loadAllPlugins is filesystem work that's
// wasted when executeHooks early-returns under --bare anyway.
isBareMode()
if (!skipPluginPrefetch) {
void getCommands(getProjectRoot())
}
void import('./utils/plugins/loadPluginHooks.js').then(m => {
if (!skipPluginPrefetch) {
void m.loadPluginHooks() // Pre-load plugin hooks (consumed by processSessionStartHooks before render)
m.setupPluginHookHotReload() // Set up hot reload for plugin hooks when settings change
}
})
这里的关键点:
- 并行预取 :使用
void关键字启动并行任务,不等待完成 - 条件跳过:根据环境变量和模式决定是否跳过某些预取
- 性能检查点 :使用
profileCheckpoint标记关键时间点 - 热重载支持:预取时同时设置热重载机制
8. 查询引擎中的性能优化
在 src/QueryEngine.ts 中,我们看到了查询引擎的性能优化:
typescript
import { feature } from 'bun:bundle'
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
import { randomUUID } from 'crypto'
import last from 'lodash-es/last.js'
import {
getSessionId,
isSessionPersistenceDisabled,
} from 'src/bootstrap/state.js'
import type {
PermissionMode,
SDKCompactBoundaryMessage,
SDKMessage,
SDKPermissionDenial,
SDKStatus,
SDKUserMessageReplay,
} from 'src/entrypoints/agentSdkTypes.js'
import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'
import type { NonNullableUsage } from 'src/services/api/logging.js'
import { EMPTY_USAGE } from 'src/services/api/logging.js'
import stripAnsi from 'strip-ansi'
import type { Command } from './commands.js'
import { getSlashCommandToolSkills } from './commands.js'
import {
LOCAL_COMMAND_STDERR_TAG,
LOCAL_COMMAND_STDOUT_TAG,
} from './constants/xml.js'
import {
getModelUsage,
getTotalAPIDuration,
getTotalCost,
} from './cost-tracker.js'
import type { CanUseToolFn } from './hooks/useCanUseTool.js'
import { loadMemoryPrompt } from './memdir/memdir.js'
import { hasAutoMemPathOverride } from './memdir/paths.js'
import { query } from './query.js'
import { categorizeRetryableAPIError } from './services/api/errors.js'
import type { MCPServerConnection } from './services/mcp/types.js'
import type { AppState } from './state/AppState.js'
import { type Tools, type ToolUseContext, toolMatchesName } from './Tool.js'
import type { AgentDefinition } from './tools/AgentTool/loadAgentsDir.js'
import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
import type { Message } from './types/message.js'
import type { OrphanedPermission } from './types/textInputTypes.js'
import { createAbortController } from './utils/abortController.js'
import type { AttributionState } from './utils/commitAttribution.js'
import { getGlobalConfig } from './utils/config.js'
import { getCwd } from './utils/cwd.js'
import { isBareMode, isEnvTruthy } from './utils/envUtils.js'
import { getFastModeState } from './utils/fastMode.js'
import {
type FileHistoryState,
fileHistoryEnabled,
fileHistoryMakeSnapshot,
} from './utils/fileHistory.js'
import {
cloneFileStateCache,
type FileStateCache,
} from './utils/fileStateCache.js'
import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js'
import { registerStructuredOutputEnforcement } from './utils/hooks/hookHelpers.js'
import { getInMemoryErrors } from './utils/log.js'
import { countToolCalls, SYNTHETIC_MESSAGES } from './utils/messages.js'
import {
getMainLoopModel,
parseUserSpecifiedModel,
} from './utils/model/model.js'
import { loadAllPluginsCacheOnly } from './utils/plugins/pluginLoader.js'
import {
type ProcessUserInputContext,
processUserInput,
} from './utils/processUserInput/processUserInput.js'
import { fetchSystemPromptParts } from './utils/queryContext.js'
import { setCwd } from './utils/Shell.js'
import {
flushSessionStorage,
recordTranscript,
} from './utils/sessionStorage.js'
import { asSystemPrompt } from './utils/systemPromptType.js'
import { resolveThemeSetting } from './utils/systemTheme.js'
这里的关键优化点:
- Feature Flags :使用
feature()函数进行条件导入,实现死代码消除 - 类型导入:只导入类型,避免运行时开销
- 性能分析 :使用
headlessProfilerCheckpoint进行性能检查点标记 - 文件状态缓存 :使用
FileStateCache缓存文件状态,减少重复计算
设计启示
1. 统一抽象的力量
Claude Code 的核心设计哲学是"Agent 即工具"。这种统一抽象带来了巨大的优势:
- 简单性:开发者只需理解一种抽象,不需要区分函数调用和进程调用
- 可组合性:Agent 可以递归调用 Agent,形成任意复杂的协作模式
- 可扩展性:新的 Agent 类型可以轻松添加,不影响现有代码
这种设计类似于 Unix 的"一切皆文件"哲学------通过统一的抽象,实现强大的组合能力。
2. 状态机的严谨性
任务系统使用严谨的状态机设计,每个状态转换都有明确的规则:
typescript
export type TaskStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'killed'
export function isTerminalTaskStatus(status: TaskStatus): boolean {
return status === 'completed' || status === 'failed' || status === 'killed'
}
这种设计的优势:
- 可预测性:状态转换规则明确,行为可预测
- 可调试性:状态变化清晰可见,便于调试
- 安全性:守卫函数防止错误操作
3. 缓存策略的艺术
Claude Code 使用了多层缓存策略:
- Memoize 缓存:函数级别的缓存
- 上下文缓存:会话级别的缓存
- 文件状态缓存:文件系统级别的缓存
- 性能分析缓存:诊断数据的缓存
这种分层缓存策略,确保了不同粒度的数据都有合适的缓存机制。
4. 并行执行的智慧
Claude Code 在多个层面使用了并行执行:
- 初始化并行预取:在启动时并行加载必要资源
- Git 命令并行 :使用
Promise.all并行执行多个 git 命令 - Agent 并行执行:多个 Agent 可以同时工作
这种并行策略,充分利用了现代计算机的多核能力,显著提升了性能。
5. 防御性编程
Claude Code 代码中充满了防御性编程的实践:
- 守卫函数 :
isTerminalTaskStatus()防止向已终止的任务发送消息 - 错误处理:每个异步操作都有完善的错误处理
- 边界检查:检查状态长度是否超过限制,进行截断处理
- 环境检查:根据环境变量和配置调整行为
这种防御性编程的思想,确保了系统的健壮性和可靠性。
类比:交响乐团的协作
多 Agent 协作就像一个交响乐团:
- 指挥:主 Agent 负责整体协调和调度
- 乐器组:不同类型的 Agent 就像不同的乐器组,各司其职
- 乐谱:任务定义和状态机就像乐谱,规定了演奏的规则
- 节奏:并行执行和调度机制就像节奏控制,确保和谐
- 和声:Agent 间的通信和协作就像和声,产生美妙的效果
每个 Agent 都是一个独立的"乐手",有自己的专长和职责。指挥(主 Agent)通过"乐谱"(任务定义)协调所有乐手,共同演奏出复杂的"交响曲"(完成复杂任务)。
思考题
-
设计一个多 Agent 系统:假设你要设计一个代码审查系统,需要多个 Agent 协作工作。你会如何划分 Agent 的职责?如何设计它们之间的通信机制?
-
状态机扩展 :如果要在现有的任务状态机中添加一个新的状态
paused(暂停),需要修改哪些代码?如何确保状态转换的正确性? -
缓存策略优化:在什么情况下应该清除缓存?如何设计一个智能的缓存失效策略?
-
递归深度控制:如何实现递归深度控制?如果递归深度超过限制,应该如何处理?
-
性能权衡:并行执行可以提高性能,但也会增加复杂度。在实际项目中,如何平衡性能和可维护性?
小结
本章深入探讨了 Claude Code 的多 Agent 协作机制,从任务系统到工具注册,从上下文缓存到性能优化。我们看到了统一抽象的力量、状态机的严谨性、缓存策略的艺术、并行执行的智慧,以及防御性编程的重要性。
多 Agent 协作不仅仅是技术的实现,更是一种设计哲学的体现------通过精心设计的架构,让多个智能体协同工作,完成单个智能体无法完成的复杂任务。这种设计思想,不仅适用于 AI 系统,也适用于任何需要协同工作的软件系统。
在下一章中,我们将探讨性能优化的艺术,看看 Claude Code 如何通过毫秒级的优化,实现快速启动和流畅运行。