Claude Code 源码深度拆解⑤ | 多Agent编排:Coordinator-Worker+邮箱模式详解

验证Agent的自我欺骗检测列表里,第一条写着:”reading is not verification”
一、引言:多Agent协作为什么这么难?
在前四期,我们拆解了工具系统、TAOR循环和上下文压缩。这些模块解决的是一个Agent如何高效工作的问题。
但真实世界的软件工程任务,往往需要并行处理多个子问题:一个Agent在调研代码结构,另一个在修改配置文件,第三个在编写测试用例。如果所有事情都让一个Agent串行完成,会话会长到token爆炸,上下文会乱到模型迷失。
这就是多Agent编排要解决的问题。
然而,多Agent系统是当前AI领域”讨论最多、落地最少”的话题之一。多数开源框架停留在概念验证阶段——demo能跑,但一上生产就散架。原因很简单:多个Agent同时工作会带来一系列硬问题——
- 并发冲突
:两个Agent同时修改同一个文件怎么办? - 上下文污染
:子Agent的冗长执行过程要不要塞回主Agent? - 权限失控
:子Agent能不能自主批准危险操作? - 通信开销
:Agent之间怎么传递消息而不丢失? - 结果可信
:怎么防止子Agent”谎报军情”?
Claude Code的源码揭示了一套已在生产环境运行的Coordinator-Worker多Agent架构,完整解决了这些问题。本期我们不仅展示源码,更深入解释每一个设计决策背后的权衡。
二、架构总览:Coordinator与Worker的角色分离
2.1 两种角色的明确定义
Claude Code将多Agent系统中的参与者分为两个明确的角色:
┌─────────────────────────────────────────────────────────────┐
│ Coordinator(协调器) │
│ │
│ 职责: │
│ • 理解用户意图,分解任务 │
│ • 派生Worker执行子任务 │
│ • 综合Worker返回的结果 │
│ • 与用户沟通最终答案 │
│ │
│ 拥有的工具(只有3个): │
│ • AgentTool —— 派生新的Worker │
│ • SendMessageTool —— 向已有Worker发送后续指令 │
│ • TaskStopTool —— 终止运行中的Worker │
│ │
│ 原则:**Coordinator不执行文件操作或命令** │
└─────────────────────────────────────────────────────────────┘
│
│ 派生
▼
┌─────────────────────────────────────────────────────────────┐
│ Worker(工作者) │
│ │
│ 职责: │
│ • 执行具体任务(调研、实现、验证) │
│ • 拥有完整的工具集(Bash、FileEdit、Grep等) │
│ • 在独立上下文中运行 │
│ • 完成后返回摘要给Coordinator │
│ │
│ 特点: │
│ • 异步并行执行 │
│ • 独立TAOR循环 │
│ • 独立上下文预算 │
│ • 独立compaction机制 │
└─────────────────────────────────────────────────────────────┘
2.2 系统提示词中的角色定义
源码中coordinatorMode.ts的系统提示词精确定义了Coordinator的职责边界:
// src/coordinator/coordinatorMode.ts(基于源码推断)
export function getCoordinatorSystemPrompt(): string {
return `You are Claude Code, an AI assistant that orchestrates
software engineering tasks across multiple workers.
You are a **coordinator**. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible — don't delegate work
that you can handle without tools
PARALLELISM IS YOUR SUPERPOWER.
You can launch multiple AgentTool calls in a single message.
IMPORTANT: You are NOT an executor. You don't read files or run
commands yourself. Your only tools are AgentTool, SendMessageTool,
and TaskStopTool.`
}
这段提示词的设计好在哪?
- 明确否定定义
:”You are NOT an executor”——告诉模型什么不是你的职责,比告诉它什么是你的职责更有效 - 能力声明
:”PARALLELISM IS YOUR SUPERPOWER”——用全大写强调核心能力,引导模型善用并行 - 边界清晰
:只有3个工具,模型无法越界
2.3 这个架构解决了什么问题?
问题一:单Agent的上下文爆炸
如果所有子任务都在主Agent的上下文中串行执行,token消耗是累加的。Coordinator-Worker架构中,Worker的完整执行过程不进入Coordinator的上下文——Worker完成后只返回摘要。这是第四期讲过的”Sub-Agent隔离”策略在架构层的体现。
问题二:职责混乱导致的系统脆弱
很多Agent系统让同一个模型既做决策又做执行。结果是:模型可能在执行过程中”迷失”——忘记原始目标,被细节淹没。角色分离后,Coordinator始终站在全局视角,Worker专注单点突破。
问题三:并行能力的浪费
大模型天然支持在一条消息中返回多个工具调用。Coordinator可以在一个turn中同时启动多个Worker,真正利用并行能力。
三、邮箱模式:Worker如何请求高风险操作
3.1 问题场景
假设Worker在执行任务时需要执行一条高风险命令——rm -rf ./node_modules。Worker自己能不能决定执行?
不能。 因为Worker是Coordinator派生的”下属”,不应该拥有最终审批权。
Claude Code的解决方案是邮箱模式(Mailbox Pattern):
┌─────────────────────────────────────────────────────────────┐
│ 邮箱模式工作流 │
└─────────────────────────────────────────────────────────────┘
Worker执行任务,遇到高风险操作
│
▼
┌─────────────────────────────────────┐
│ Worker向Coordinator邮箱发送审批请求 │
│ ~/.claude/teams/{team}/inboxes/ │
│ {agent-name}.json │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Coordinator在turn间隙轮询邮箱 │
│ 发现待审批请求 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Coordinator评估请求 │
│ • 是否符合任务目标? │
│ • 风险是否可接受? │
│ • 是否有更安全的替代方案? │
└─────────────────────────────────────┘
│
┌────┴────┐
▼ ▼
批准 拒绝
│ │
▼ ▼
Worker继续 Worker收到
执行 拒绝通知
3.2 邮箱模式的实现细节
// 基于 docs.rs/midtown 的邮箱协议分析(与Claude Code兼容)
interface MailboxMessage {
id: string;
from: string; // 发送方Agent名称
to: string; // 接收方Agent名称(通常是Coordinator)
type: 'approval_request' | 'notification' | 'response';
payload: {
operation: string; // 请求批准的操作
risk: 'low' | 'medium' | 'high' | 'critical';
context: string; // 为什么需要执行这个操作
};
timestamp: number;
}
// 并发控制:使用mkdir-based locking
// 匹配Claude Code使用的lockfile npm包行为
async function writeToInbox(
teamName: string,
agentName: string,
message: MailboxMessage
): Promise<void> {
const inboxPath = `~/.claude/teams/${teamName}/inboxes/${agentName}.json`;
const lockPath = `~/.claude/teams/${teamName}/inboxes/.lock`;
// 获取文件锁
await mkdirLock(lockPath);
try {
// 读取现有消息
const messages = await readJson(inboxPath) || [];
messages.push(message);
// 原子写入
await writeJson(inboxPath, messages);
} finally {
await releaseLock(lockPath);
}
}
3.3 原子认领机制:防止重复处理
一个容易被忽略但极其重要的细节是原子认领机制:
// 原子认领:防止多个Coordinator实例同时处理同一请求
async function claimPendingApproval(
inboxPath: string,
requestId: string
): Promise<boolean> {
// 使用文件锁保证原子性
const claimed = await atomicUpdate(inboxPath, (messages) => {
const request = messages.find(m => m.id === requestId);
if (request && !request.claimed) {
request.claimed = true;
request.claimedBy = process.pid;
request.claimedAt = Date.now();
return true;
}
return false;
});
return claimed;
}
这个设计好在哪里?
- 并发安全
:多个Coordinator实例或并发turn不会重复处理同一请求 - 故障恢复
:如果Coordinator在审批过程中崩溃, claimedAt超时后可以被其他实例接管 - 可审计
:每个审批请求都有完整的处理记录
3.4 为什么用文件系统而不是消息队列?
这是一个务实的工程选择:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
Claude Code选择文件系统,因为它是一个本地CLI工具,不能假设用户安装了Redis。这种”因地制宜”的务实精神,比追求技术时髦更重要。
四、AgentTool:Worker派生的一等公民
4.1 AgentTool与其他工具的关系
在第二期我们讲过,Claude Code有40+工具。但AgentTool的定位非常特殊:
// src/tools/AgentTool/AgentTool.tsx(基于源码推断)
export const AgentTool: Tool = {
name: 'Agent',
description: `Spawn a worker agent to handle complex, multi-step tasks autonomously.
WHEN TO USE:
- Research tasks that require exploring multiple files
- Implementation tasks that span several modules
- Verification tasks that need independent confirmation
THE WORKER WILL:
- Run in its own isolated context
- Have access to all standard tools (Bash, FileEdit, Grep, etc.)
- Return a summary when complete
YOU SHOULD:
- Provide a clear, specific prompt
- Set an appropriate model based on task complexity
- Consider running in background for long tasks`,
inputSchema: z.object({
description: z.string().describe('Short description for UI display'),
prompt: z.string().describe('Detailed task instructions for the worker'),
model: z.enum(['sonnet', 'opus', 'haiku']).default('sonnet'),
runInBackground: z.boolean().default(false),
isolation: z.enum(['none', 'worktree']).default('none'),
}),
execute: async (input, context) => {
// 派生Worker的核心逻辑
}
};
4.2 Worker的生命周期
┌─────────────────────────────────────────────────────────────┐
│ Worker 生命周期 │
└─────────────────────────────────────────────────────────────┘
1. 派生阶段(Spawn)
├── 选择Agent定义(system prompt、可用工具集)
├── 检查MCP依赖是否就绪
├── 组装Worker专用工具池
└── 构建Worker系统提示词
2. 执行阶段(Execute)
├── 启动独立的TAOR循环
├── 独立上下文管理(maxTurns、compaction)
├── 独立记忆系统(MEMORY.md)
└── 可能产生子Worker(嵌套派生)
3. 完成阶段(Complete)
├── 生成结构化任务摘要
├── 将摘要返回给Coordinator
├── 清理临时资源
└── (可选)写入Agent专项记忆
4. 异常阶段(Error/Timeout)
├── 超时:强制终止,返回部分结果
├── 崩溃:记录错误,通知Coordinator
└── 用户中断:优雅退出,保存状态
4.3 WorkTree隔离:解决并发修改冲突
当多个Worker同时操作同一个代码库时,最大的风险是并发修改冲突。Worker A修改auth.ts的同时Worker B也在改它,后写入的会覆盖先写入的。
Claude Code的解决方案是WorkTree隔离:
// src/tools/AgentTool/forkSubagent.ts(基于源码推断)
async function createAgentWorktree(
agentName: string,
basePath: string
): Promise<WorktreeInfo> {
// 创建git worktree
const worktreePath = `${basePath}/.claude/worktrees/${agentName}-${Date.now()}`;
await execGit(['worktree', 'add', worktreePath, 'HEAD']);
return {
path: worktreePath,
// 路径映射:将原路径请求转换为worktree路径
mapPath: (originalPath: string) => {
return originalPath.replace(basePath, worktreePath);
},
};
}
// Worker执行时的路径转换
async function executeInWorktree(
toolCall: ToolCall,
worktree: WorktreeInfo
): Promise<ToolResult> {
// 将工具调用中的文件路径映射到worktree
const mappedCall = {
...toolCall,
input: mapPathsInInput(toolCall.input, worktree.mapPath),
};
return await executeTool(mappedCall);
}
这个设计好在哪里?
- 零冲突
:每个Worker在独立的Git worktree中操作,物理隔离 - 可合并
:Worker完成后,Coordinator可以用 git merge合并结果 - 可回滚
:出错的worktree可以直接删除,不影响主工作区 - 利用现有工具
:依赖Git而非自己实现版本控制
五、验证Agent:防止”自己骗自己”
5.1 为什么需要验证Agent?
Claude Code的设计者深知LLM的一个核心弱点:容易把”读到了实现”误认为”验证了实现”。
典型场景:
- Worker A负责实现功能X
- Worker B负责验证功能X
如果Worker B也是LLM,它可能:
-
读取Worker A写的代码,看到里面有 // 实现了功能X的注释 - 看到代码逻辑”看起来正确”
- 直接返回”验证通过”
但真正的验证应该是:运行测试、执行代码、检查输出。
5.2 验证Agent的自我欺骗检测列表
源码中验证Agent的系统提示词包含一个精心设计的检测列表:
# Verification Agent Self-Deception Checklist
Before reporting "verified", confirm ALL of the following:
[ ] reading is not verification
- Did I actually EXECUTE something, or just READ code?
- Reading the implementation does NOT count as verification.
[ ] the implementer is an LLM, verify independently
- The code was written by another AI. It may be wrong.
- Trust nothing. Verify everything.
[ ] probably is not verified
- If I'm thinking "this probably works" — it's NOT verified.
- Only "I have executed and observed the expected output" counts.
[ ] tests passing is not proof of correctness
- Tests can be incomplete or wrong.
- At minimum, manually test edge cases.
[ ] one success is not reliability
- Run it multiple times.
- Vary the inputs.
- Check for flakiness.
IF ANY BOX IS UNCHECKED: DO NOT REPORT VERIFIED.
Report "Partially tested — needs manual review" instead.
这个设计的精妙之处:
- 用否定句定义正确行为
:”reading is not verification”比”you must execute”更有效,因为LLM容易忽略正面指令但会注意警告 - 承认AI的局限
:”the implementer is an LLM”——明确告诉验证Agent,你审查的是另一个AI的产物,不要给同行面子 - 阻止模糊状态
:”probably is not verified”——切断LLM最常见的”差不多”思维模式 - 可操作
:每个条目都是二元的(打勾或不打勾),不给模糊空间
5.3 验证Agent的工作流程
// src/agents/verification/VerificationAgent.ts(基于源码推断)
async function runVerification(
task: VerificationTask,
context: AgentContext
): Promise<VerificationResult> {
const agent = new Worker({
role: 'verifier',
systemPrompt: VERIFICATION_PROMPT,
tools: [
BashTool, // 用于运行测试
FileReadTool, // 用于检查代码
GrepTool, // 用于搜索
],
// 关键:验证Agent不能修改文件
writeAccess: false,
});
// 验证Agent也运行独立的TAOR循环
const result = await agent.run({
prompt: `
Verify the following claim:
"${task.claim}"
Implementation is in: ${task.files}
Tests are in: ${task.testFiles}
Follow the Self-Deception Checklist strictly.
Return a structured verification report.
`,
});
// 强制要求验证报告包含证据
return {
passed: result.passed,
evidence: result.evidence, // 必须包含命令输出、日志等
checklist: result.checklist, // 打勾状态
confidence: result.confidence,
};
}
六、并发策略:什么时候并行,什么时候串行
6.1 任务类型与并发策略
Claude Code根据任务类型制定了明确的并发策略:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6.2 上下文决策:Continue vs Spawn Fresh
当Coordinator需要Worker继续处理相关任务时,面临一个选择:
// 决策逻辑(基于源码推断)
function shouldContinueOrSpawn(
worker: Worker,
newTask: Task
): 'continue' | 'spawn' {
// 如果Worker已有相关文件上下文 → Continue
if (worker.context.includesFiles(newTask.relatedFiles)) {
return 'continue'; // 复用已有上下文,效率更高
}
// 如果新任务需要全新视角(如验证任务) → Spawn Fresh
if (newTask.type === 'verification') {
return 'spawn'; // 验证者应该是"新鲜的眼睛"
}
// 如果Worker上下文已接近token上限 → Spawn Fresh
if (worker.context.tokenUsage > 0.7 * worker.context.maxTokens) {
return 'spawn';
}
return 'continue';
}
决策权衡:
- Continue
:复用上下文,避免重复读取文件,但可能带着”偏见”继续 - Spawn Fresh
:全新视角,适合验证,但有冷启动成本
七、子Agent上下文隔离的工程价值
7.1 为什么隔离如此重要?
第四期我们讲过Sub-Agent隔离是上下文压缩的重要补充。这里从架构角度再深入:
// 主Agent vs 子Agent的上下文对比
┌─────────────────────────────────────────────────────────────┐
│ 主Agent上下文 │
│ • 用户原始请求 │
│ • 与用户的完整对话历史 │
│ • Coordinator的决策记录 │
│ • 子Agent返回的**摘要**(不是完整执行过程) │
│ │
│ 子Agent上下文(独立) │
│ • 分配给它的具体任务指令 │
│ • 执行过程中的文件读取结果 │
│ • 工具调用记录 │
│ • 独立的compaction │
│ • 任务完成后**销毁** │
└─────────────────────────────────────────────────────────────┘
核心价值:无论子任务消耗了多少token,主Agent的上下文都零污染。
7.2 子Agent的独立记忆
每个子Agent可以有自己的专项记忆:
// 子Agent记忆配置
interface SubAgentConfig {
name: string;
memory: 'none' | 'session' | 'user';
memoryPath?: string; // ~/.claude/agent-memory/{name}/MEMORY.md
}
// 下次调用时自动加载
async function loadSubAgentMemory(name: string): Promise<string> {
const memoryFile = `~/.claude/agent-memory/${name}/MEMORY.md`;
if (await fileExists(memoryFile)) {
// 只加载前200行,防止记忆膨胀
return await readFirstLines(memoryFile, 200);
}
return '';
}
这意味着每个子Agent可以越用越聪明——调研Agent记住项目结构,验证Agent记住常见错误模式。
八、完整的协作工作流
将上述机制串联起来,一个完整的Coordinator-Worker协作流程如下:
┌─────────────────────────────────────────────────────────────┐
│ 四阶段协作工作流 │
└─────────────────────────────────────────────────────────────┘
阶段1:调研(Research)
┌─────────────────────────────────────┐
│ Coordinator派生多个调研Worker │
│ Worker A:探索auth模块结构 │
│ Worker B:分析API路由定义 │
│ Worker C:检查中间件配置 │
│ │
│ 并发执行,互不干扰 │
│ 完成后各自返回结构化摘要 │
└─────────────────────────────────────┘
│
▼
阶段2:综合(Synthesis)
┌─────────────────────────────────────┐
│ Coordinator综合所有调研发现 │
│ 编写实现规范 │
│ │
│ ⚠️ 核心原则:必须自己综合 │
│ 不能写"基于研究发现"来委托给Worker │
│ 因为Coordinator是唯一有全局视角的角色 │
└─────────────────────────────────────┘
│
▼
阶段3:实现(Implementation)
┌─────────────────────────────────────┐
│ Coordinator派生实现Worker │
│ Worker D:实现登录逻辑 │
│ Worker E:实现JWT验证中间件 │
│ Worker F:实现刷新token逻辑 │
│ │
│ 使用WorkTree隔离避免冲突 │
│ 按文件重叠度决策串行或并行 │
└─────────────────────────────────────┘
│
▼
阶段4:验证(Verification)
┌─────────────────────────────────────┐
│ Coordinator派生验证Worker │
│ Worker G:运行测试、检查边界情况 │
│ │
│ 强制遵循自我欺骗检测列表 │
│ 返回带证据的验证报告 │
│ │
│ 如果验证失败 → 回到阶段3 │
│ 如果验证通过 → 向用户汇报 │
└─────────────────────────────────────┘
九、对Agent开发的核心启示
9.1 五条可迁移的设计原则
原则一:角色分离是稳定性的基础
// ❌ 错误:一个Agent什么都做
const agent = new Agent({
tools: [Bash, FileEdit, Grep, TaskDelegation], // 混乱
});
// ✅ 正确:Coordinator只管编排,Worker管执行
const coordinator = new Coordinator({
tools: [AgentTool, SendMessageTool, TaskStopTool], // 只有3个
});
原则二:邮箱模式处理高风险操作
// 任何Worker无权自主批准的危险操作,都应通过邮箱请求Coordinator审批
async function executeDangerousOp(worker: Worker, op: Operation) {
if (op.riskLevel === 'high') {
const approved = await requestApproval(worker.coordinator, op);
if (!approved) return { aborted: true };
}
return await execute(op);
}
原则三:验证者必须是”新鲜的眼睛”
// 验证任务永远使用全新派生的Worker,不带任何历史上下文
async function verify(claim: string): Promise<VerificationResult> {
const verifier = spawnFresh({ role: 'verifier' });
return await verifier.run(claim);
}
原则四:子Agent上下文隔离是规模化关键
// 子Agent的完整执行过程不进入主Agent上下文
const subResult = await subAgent.run(task);
coordinator.context.append({
type: 'subagent_summary',
content: subResult.summary, // 只加摘要
// subResult.fullLog 不加入
});
原则五:并发策略需要文件级粒度
// 根据文件重叠度决策
function canRunParallel(taskA: Task, taskB: Task): boolean {
const overlap = intersect(taskA.files, taskB.files);
if (overlap.size === 0) return true;
if (taskA.type === 'read' && taskB.type === 'read') return true;
return false; // 有写入重叠,必须串行
}
9.2 一个简化的多Agent实现
// 你可以立刻使用的简化Coordinator-Worker框架
class SimpleCoordinator {
private workers: Map<string, Worker> = new Map();
private mailbox: Mailbox = new FileSystemMailbox();
async delegate(task: Task): Promise<TaskResult> {
// 1. 决策:新Worker还是复用?
const worker = this.selectOrCreateWorker(task);
// 2. 派生执行
const result = await worker.execute(task);
// 3. 检查是否有待审批请求
const pending = await this.mailbox.getPending(worker.id);
for (const req of pending) {
const decision = await this.evaluateApproval(req);
await this.mailbox.respond(req.id, decision);
}
return result;
}
private async evaluateApproval(req: ApprovalRequest): Promise<boolean> {
// Coordinator的审批逻辑
// 可以接入人工确认
if (req.risk === 'critical') {
return await askUser(req);
}
return req.operation.match(/^git|^npm/); // 允许git和npm
}
}
十、小结与下一期预告
Coordinator-Worker+邮箱模式是Claude Code多Agent架构的核心,它告诉我们:
- 角色分离
是规模化Agent系统的第一原则——Coordinator只管编排,Worker只管执行 - 邮箱模式
用最朴素的文件系统解决了并发审批问题——务实比时髦更重要 - 验证Agent的自我欺骗检测列表
是AI审查AI的经典范例——用结构化的否定句防止幻觉 - 子Agent上下文隔离
让多Agent系统可以无限扩展——无论多少子任务,主上下文零污染
这套架构之所以值得深入学习,不是因为它用了什么高深的技术,而是因为它对分布式系统经典问题的务实解决——并发冲突用WorkTree、通信用文件邮箱、审批用原子认领。每一个都是”老技术”,但组合起来就是一套经过生产验证的可靠系统。
下一期,我们将深入安全架构,看Claude Code如何用六级权限+22个Bash验证器构建Agent的安全防线。那个解析器差异漏洞(\r绕过检查)的故事,将在第六期详细展开。
上一篇回顾:Claude Code 源码深度拆解④ | 三层压缩策略:把Context Window变成可管理资源
下一篇预告:Claude Code 源码深度拆解⑥ | 六级权限+22个验证器:Agent的安全防线怎么建
延伸思考:如果你的多Agent系统中有一个Worker持续返回错误结果,Coordinator应该如何检测并隔离这个”坏”Worker?提示:想想验证Agent的自我欺骗检测列表能否反向用于检测”撒谎的Worker”。欢迎在评论区分享你的设计。
夜雨聆风