Back to Blog

Claude Code Harness 第20章:Agent 集群与多 Agent 编排

2026-04-05
Claude Code Multi-Agent Agent Orchestration Distributed AI

Claude Code Harness 第20章:Agent 集群与多 Agent 编排

系列说明:本文是《Claude Code Harness:从提示词到 AI 工程化实践》第20章,标志着我们进入第六部分:高级子系统。完整目录请参考系列导读

在之前的章节中,我们探索了 Claude Code 的核心构建块:工具系统、权限控制、会话管理、Hooks 系统,以及 CLAUDE.md 指令覆盖层。这些章节展现了一个主题:如何让单个 Agent 更智能、更可控地工作

本章将探讨一个更宏大的主题:当单个 Agent 不够用时,如何让多个 Agent 协同工作

这是一个根本性的架构转变。从单 Agent 到多 Agent,不是简单的数量叠加,而是系统复杂度的质的飞跃。想象一下:一个开发者可以同时进行代码探索、架构设计、实现编写、测试验证——这在人类世界需要团队协作,在 AI 世界同样需要多 Agent 编排。

Claude Code 提供了三种递进的多 Agent 模式,从轻量到重量分别是:子 Agent(Subagent)Fork 模式协调者模式(Coordinator Mode)。它们共享同一个入口——AgentTool,但在上下文继承、执行模型和生命周期管理上有根本差异。

本章将逐层解剖这三种模式,以及围绕它们构建的队友系统(Agent Swarms)、验证 Agent,以及最新的远程多代理规划系统 Ultraplan。


20.1 为什么需要多 Agent

在深入技术实现之前,让我们先理解多 Agent 架构解决的核心问题。

单 Agent 的局限性

单个 Agent Loop 的上下文窗口是有限资源。当任务规模超过单次对话所能承载的信息量时,单 Agent 面临三难选择:

困境 描述 后果
上下文膨胀 在对话中塞满中间结果、日志输出、临时文件内容 快速触及 token 上限,成本飙升
信息压缩 不断总结和压缩历史对话 丢失关键细节,导致错误决策
顺序执行 所有任务串行执行,无法利用并行性 浪费时间,效率低下

更本质的问题是:单 Agent 无法并行,而软件工程任务天然适合分治。

多 Agent 的优势

多 Agent 架构通过空间换时间专业化分工解决这些问题:

graph TB
    subgraph SingleAgent["单 Agent 模式"]
        SA1["任务 1:代码探索
耗时不定"] SA2["任务 2:架构设计
等待任务 1"] SA3["任务 3:实现编写
等待任务 2"] SA4["任务 4:测试验证
等待任务 3"] SA1 --> SA2 --> SA3 --> SA4 end subgraph MultiAgent["多 Agent 模式"] MA1["Agent A:代码探索
独立上下文"] MA2["Agent B:架构设计
独立上下文"] MA3["Agent C:实现编写
独立上下文"] MA4["Agent D:测试验证
独立上下文"] MA1 -.并行.-> MA2 MA3 -.并行.-> MA4 MA2 --> MA3 end SingleAgent -->|"总耗时:
T1 + T2 + T3 + T4"| SingleTime["顺序执行时间"] MultiAgent -->|"总耗时:
max(T1, T2) + T3 + T4"| MultiTime["并行执行时间"] style MultiAgent fill:#e6f3e6,stroke:#2d862d,stroke-width:2px style SingleAgent fill:#f3e6e6,stroke:#862d2d,stroke-width:2px

核心优势

  1. 上下文隔离:每个 Agent 有独立的对话历史,不会互相污染
  2. 并行执行:多个 Agent 可以同时工作,充分利用时间
  3. 专业化分工:不同 Agent 可以有不同的工具集、提示词、模型选择
  4. 容错能力:单个 Agent 失败不会影响其他 Agent
  5. 成本控制:可以为简单任务使用更便宜的模型(如 Haiku)

Claude Code 的三层多 Agent 架构

Claude Code 不是简单地"允许多个 Agent",而是提供了三层递进的多 Agent 模式,每一层都有明确的使用场景和设计权衡:

模式 复杂度 上下文共享 并行能力 典型场景
标准子 Agent 无(全新对话) 支持 独立小任务、代码搜索、简单分析
Fork 模式 完整继承父级 强制并行 需要上下文的并行探索、缓存优化
协调者模式 协调者全局视图 强制并行 复杂多阶段项目、需要全局协调

这种分层设计反映了深刻的工程洞察:不存在一种通用的多 Agent 方案能同时满足所有场景。不同的任务需要不同的上下文共享程度和协调复杂度。


20.2 AgentTool:统一的 Agent 派生入口

所有 Agent 派生都通过同一个工具完成。这个统一入口是 AgentTool,它在 tools/AgentTool/AgentTool.tsx 中定义。

工具定义与别名

// tools/AgentTool/AgentTool.tsx:226-228
export const AgentTool: ToolDefinition = {
  name: 'Agent',
  aliases: ['Task'],  // 向后兼容旧版本
  // ...
}

工具名称是 'Agent',同时保留了别名 'Task' 以兼容旧版本的调用方式。这种设计允许模型使用自然语言表达:"启动一个 agent 来做 X" 或 "创建一个 task 来完成 Y"。

输入 Schema 的动态组合

AgentTool 的输入 Schema 不是静态的——它根据 Feature Flag 和运行时条件动态组合。这是一个精妙的设计,确保模型看到的参数列表精确反映它当前可以使用的能力

基础 Schema

// tools/AgentTool/AgentTool.tsx:82-88
const baseInputSchema = lazySchema(() => z.object({
  description: z.string().describe('A short (3-5 word) description of the task'),
  prompt: z.string().describe('The task for the agent to perform'),
  subagent_type: z.string().optional(),
  model: z.enum(['sonnet', 'opus', 'haiku']).optional(),
  run_in_background: z.boolean().optional()
}));

基础 Schema 包含五个字段:

字段 类型 用途 示例
description string 简短的任务描述(3-5词) "Search for API definitions"
prompt string 详细的任务指令 "Find all REST API endpoints in the src/api directory"
subagent_type string(可选) Agent 类型 "Explore", "Plan", "general-purpose"
model enum(可选) 模型选择 "sonnet", "opus", "haiku"
run_in_background boolean(可选) 是否后台运行 true / false

条件性字段合并

当不同的 Feature Flag 启用时,额外的字段会被合并到 Schema 中:

// tools/AgentTool/AgentTool.tsx:93-97
const agentSwarmsInputSchema = lazySchema(() =>
  feature('AGENT_SWARMS')
    ? z.object({
        name: z.string().optional().describe('Name for this agent'),
        team_name: z.string().optional().describe('Team to join'),
        mode: z.enum(['create', 'spawn']).optional(),
      })
    : z.object({})
);

// tools/AgentTool/AgentTool.tsx:122-124
const backgroundInputSchema = feature('FOR K_SUBAGENT') || !backgroundAgentEnabled()
  ? z.object({})
  : z.object({ run_in_background: z.boolean().optional() });

动态组合逻辑

flowchart TD
    A[基础 Schema
5个字段] --> B{启用 AGENT_SWARMS?} B -->|是| C[合并 name, team_name, mode] B -->|否| D[跳过] C --> E{启用 FORK_SUBAGENT?} D --> E E -->|是| F[移除 run_in_background
Fork 模式强制后台] E -->|否| G{启用后台 Agent?} G -->|是| H[保留 run_in_background] G -->|否| F F --> I[最终 Schema] H --> I style I fill:#e6f3e6,stroke:#2d862d,stroke-width:3px

设计意图

  1. 精确能力匹配:当 Fork 模式开启时,模型不会看到 run_in_background,因为 Fork 模式下所有 Agent 都自动后台化,模型无需也不应显式控制
  2. 降低困惑:模型不会尝试使用当前不可用的功能,减少无效调用
  3. 版本兼容:通过 Feature Flag 逐步推出新功能,保持向后兼容

AsyncLocalStorage 上下文隔离

当多个 Agent 在同一进程中并发运行时(例如用户按 Ctrl+B 将一个 Agent 放入后台后立即启动另一个),如何隔离它们的身份信息?

答案是 Node.js 的 AsyncLocalStorage API。

为什么不用 AppState

源码注释直接解释了为什么不用全局 AppState

// utils/agentContext.ts:17-21
/**
 * When agents are backgrounded (ctrl+b), multiple agents can run
 * concurrently in the same process. AppState is a single shared
 * state that would be overwritten, causing Agent A's events to
 * incorrectly use Agent B's context. AsyncLocalStorage isolates
 * each async execution chain, so concurrent agents don't interfere.
 */

问题场景

// ❌ 使用 AppState 的错误场景
const appState = {
  currentAgentId: 'agent-123',  // 全局共享
  currentAgentName: 'Explorer'
};

// Agent A 启动后台 Agent B
appState.currentAgentId = 'agent-456';  // 覆盖了 A 的 ID!

// Agent A 的后续事件
logEvent('Agent A completed task');  
// ❌ 错误:日志显示 agent-456,实际上是 agent-123

AsyncLocalStorage 解决方案

// utils/agentContext.ts:24
import { AsyncLocalStorage } from 'async_hooks'

// utils/agentContext.ts:93
const agentContextStorage = new AsyncLocalStorage()

// utils/agentContext.ts:108-109
export function runWithAgentContext(context: AgentContext, fn: () => T): T {
  return agentContextStorage.run(context, fn)
}

// 使用示例
await runWithAgentContext(
  { agentId: 'agent-123', agentType: 'subagent' },
  async () => {
    // 这个异步调用链中的所有代码
    // 都能访问到正确的 agent-123 上下文
    await performTask();
    await logEvent('Task completed');
  }
);

// 同时,另一个 Agent 也在运行
await runWithAgentContext(
  { agentId: 'agent-456', agentType: 'subagent' },
  async () => {
    // 这个调用链独立于上面的,
    // 不会互相干扰
    await performAnotherTask();
  }
);

AgentContext 类型系统

AgentContext 是一个判别联合类型(discriminated union),通过 agentType 字段区分两种上下文:

// utils/agentContext.ts:26-85
export type AgentContext = SubagentContext | TeammateAgentContext;

export type SubagentContext = {
  agentId: string;              // UUID,如 'a1b2c3d4-...'
  subagentName?: string;        // 显示名称,如 'Explorer'
  isBuiltIn: boolean;           // 是否是内置 Agent
  invokingRequestId?: string;   // 派生者的请求 ID
  agentType: 'subagent';        // 判别字段
};

export type TeammateAgentContext = {
  agentId: string;              // 格式 'name@team-name'
  agentName: string;            // 显示名称,如 'researcher'
  teamName: string;             // 所属团队
  agentColor?: string;          // UI 颜色
  planModeRequired: boolean;    // 是否需要计划审批
  parentSessionId: string;      // Leader 的会话 ID
  isTeamLead: boolean;          // 是否是 Leader
  agentType: 'teammate';        // 判别字段
};

两种上下文对比

特性 SubagentContext TeammateAgentContext
ID 格式 UUID name@team-name
命名 subagentName agentName
团队归属 teamName
层级关系 父子关系 平面团队结构
追踪字段 invokingRequestId parentSessionId
判别字段 agentType: 'subagent' agentType: 'teammate'

稀疏边语义:consumeInvokingRequestId

invokingRequestId 字段用于追踪"谁派生了我",但它有一个特殊的行为:稀疏边语义(sparse edge semantics)。

// utils/agentContext.ts:163-178
let invokingRequestIdConsumed = false;

export function consumeInvokingRequestId(): string | undefined {
  const context = agentContextStorage.getStore();
  if (context?.agentType === 'subagent' && context.invokingRequestId) {
    if (!invokingRequestIdConsumed) {
      invokingRequestIdConsumed = true;
      return context.invokingRequestId;
    }
  }
  return undefined;
}

设计意图

每次 spawn/resume 只在第一个 API 事件上发出一次 invokingRequestId,之后返回 undefined,避免重复标记。

sequenceDiagram
    participant Parent as 父 Agent
    participant Child as 子 Agent
    participant API as Claude API
    
    Parent->>Child: spawn Agent(prompt)
    activate Child
    
    Note over Child: 首次 API 调用
    Child->>API: POST /v1/messages
invokingRequestId: "req-123" API-->>Child: 响应 Note over Child: 第二次 API 调用 Child->>API: POST /v1/messages
invokingRequestId: undefined API-->>Child: 响应 Note over Child: 第三次 API 调用 Child->>API: POST /v1/messages
invokingRequestId: undefined API-->>Child: 响应 Child-->>Parent: 完成 deactivate Child

这种设计确保:

  1. 第一跳可见:可以通过 invokingRequestId 追踪到派生关系
  2. 后续跳简洁:不会在每个 API 事件上都带这个字段,节省 token
  3. 语义清晰undefined 明确表示"这不是第一次调用"

20.3 三种 Agent 模式深度解析

现在让我们深入探索三种 Agent 模式的具体实现和使用场景。

模式一:标准子 Agent(Standard Subagent)

这是最基本的模式。模型在调用 Agent 工具时指定 subagent_type,AgentTool 从已注册的 Agent 定义中查找匹配项,然后启动一个全新的对话。

路由逻辑

// tools/AgentTool/AgentTool.tsx:322-356
function resolveSubagentType(subagent_type: string | undefined) {
  // 1. 显式指定的类型优先
  if (subagent_type) {
    const agent = builtInAgents.find(a => a.agentType === subagent_type);
    if (agent) return agent;
    throw new Error(`Unknown subagent type: ${subagent_type}`);
  }
  
  // 2. Fork 模式关闭时,默认使用 general-purpose
  if (!isForkSubagentEnabled()) {
    return GENERAL_PURPOSE_AGENT;
  }
  
  // 3. Fork 模式开启时,返回 undefined(触发 Fork 逻辑)
  return undefined;
}

内置 Agent 注册表

内置 Agent 定义在 builtInAgents.ts 中注册:

// built-in/builtInAgents.ts:45-72
const builtInAgents: BuiltInAgentDefinition[] = [
  {
    agentType: 'general-purpose',
    tools: ['*'],  // 所有工具
    maxTurns: 50,
    model: 'inherit',  // 继承父级模型
    permissionMode: 'bubble',
    source: 'built-in',
    baseDir: 'built-in',
    getSystemPrompt: () => 'You are a helpful assistant...'
  },
  {
    agentType: 'verification',
    tools: ['*'],
    maxTurns: 50,
    model: 'inherit',
    permissionMode: 'bubble',
    source: 'built-in',
    baseDir: 'built-in',
    getSystemPrompt: () => getVerificationSystemPrompt(),
    disallowedTools: [  // 验证 Agent 禁止修改项目
      'Edit',
      'Write',
      'Agent',
      'ExitPlanMode'
    ]
  },
  {
    agentType: 'Explore',
    tools: ['Glob', 'Grep', 'Read', 'Bash'],
    maxTurns: 25,
    model: 'haiku',  // 轻量级模型
    permissionMode: 'read-only',
    source: 'built-in',
    baseDir: 'built-in',
    getSystemPrompt: () => getExploreSystemPrompt()
  },
  {
    agentType: 'Plan',
    tools: ['Glob', 'Grep', 'Read', 'Bash'],
    maxTurns: 50,
    model: 'inherit',
    permissionMode: 'read-only',
    source: 'built-in',
    baseDir: 'built-in',
    getSystemPrompt: () => getPlanSystemPrompt()
  }
];

内置 Agent 对比

Agent 类型 用途 工具限制 模型 最大轮次 特殊约束
general-purpose 通用任务:搜索、分析、多步骤操作 所有工具 继承 50
verification 验证实现正确性 禁止编辑工具 继承 50 只读 + 对抗性探测
Explore 快速代码探索 只读工具 Haiku 25 轻量快速
Plan 规划任务 只读工具 继承 50 输出结构化计划

上下文隔离机制

子 Agent 的关键特征是上下文隔离:它从零开始,只看到父 Agent 传入的 prompt

graph TB
    subgraph ParentAgent["父 Agent 上下文"]
        P1["用户消息: 修复登录 bug"]
        P2["Assistant: 我会先调查问题..."]
        P3["ToolUse: Glob **/*login*"]
        P4["ToolResult: 找到 5 个文件"]
    end
    
    subgraph ChildAgent["子 Agent 上下文"]
        C1["用户消息: 找出所有 API endpoint 定义"]
        C2["Assistant: 我会搜索代码..."]
        C3["ToolUse: Grep 'app.get'"]
    end
    
    ParentAgent -->|"spawn Agent(prompt)"| ChildAgent
    
    style ChildAgent fill:#e6f3ff,stroke:#0066cc
    style ParentAgent fill:#f3e6e6,stroke:#862d2d
    
    note1["子 Agent 看不到父 Agent 的
历史消息、工具调用、上下文"] note1 -.-> ChildAgent

系统提示词也是独立生成的:

// tools/AgentTool/AgentTool.tsx:518-534
async function buildSubagentMessages(agentDefinition, prompt) {
  const systemPrompt = agentDefinition.getSystemPrompt();
  
  // 子 Agent 从零开始,只有一条用户消息
  const messages = [
    {
      role: 'user',
      content: [{
        type: 'text',
        text: prompt  // 只有传入的 prompt,没有父对话历史
      }]
    }
  ];
  
  return { systemPrompt, messages };
}

使用场景

  1. 独立代码搜索:"找出所有使用 useState 的组件"
  2. 简单分析任务:"分析这个函数的复杂度"
  3. 文档生成:"为这个 API 生成文档"
  4. 测试探索:"找出没有测试覆盖的文件"

优势

  • ✅ 上下文隔离,不会污染主对话
  • ✅ 独立的 token 计数和成本追踪
  • ✅ 可以使用更便宜的模型(如 Explore 使用 Haiku)
  • ✅ 失败不会影响主 Agent

局限

  • ❌ 无法访问父 Agent 的上下文
  • ❌ 需要在 prompt 中重复提供背景信息
  • ❌ 不适合需要深入理解的任务

模式二:Fork 模式(Fork Mode)

Fork 模式是一个实验性特性,通过 feature('FORK_SUBAGENT') 构建时门控和运行时条件共同控制。

激活条件

// tools/AgentTool/forkSubagent.ts:32-39
export function isForkSubagentEnabled(): boolean {
  if (feature('FORK_SUBAGENT')) {
    if (isCoordinatorMode()) return false;  // 协调者模式互斥
    if (getIsNonInteractiveSession()) return false;  // 非交互会话禁用
    return true;
  }
  return false;
}

激活条件检查清单

条件 要求 原因
Feature Flag FORK_SUBAGENT = true 构建时门控
协调者模式 未启用 功能互斥
会话类型 交互式会话 非交互会话不需要后台化

核心特性:上下文继承

Fork 模式与标准子 Agent 的根本区别在于上下文继承。Fork 子进程继承父 Agent 的完整对话上下文和系统提示词。

// tools/AgentTool/forkSubagent.ts:60-71
export const FORK_AGENT = {
  agentType: FORK_SUBAGENT_TYPE,
  tools: ['*'],  // 所有工具(保持工具池一致)
  maxTurns: 200,  // 更高的轮次限制
  model: 'inherit',  // 继承父级模型
  permissionMode: 'bubble',
  source: 'built-in',
  baseDir: 'built-in',
  getSystemPrompt: () => '',  // 未使用——继承父级的系统提示词
} satisfies BuiltInAgentDefinition;

关键设计点

  1. model: 'inherit':使用父 Agent 的模型,保持上下文长度一致
  2. getSystemPrompt: () => '':不生成新的系统提示词,直接继承父级
  3. tools: ['*']:保留所有工具(即使父级有权限限制)

提示词缓存共享机制

Fork 模式的核心价值在于提示词缓存共享buildForkedMessages() 函数构造的消息结构确保所有 Fork 子进程产生字节相同的 API 请求前缀。

// tools/AgentTool/forkSubagent.ts:107-164
export function buildForkedMessages(
  parentMessages: Message[],
  forkInstruction: string
): Message[] {
  const result: Message[] = [];
  
  // 1. 保留父 Agent 完整的 assistant 消息
  //    (所有 tool_use 块、thinking、text)
  for (const msg of parentMessages) {
    if (msg.role === 'assistant') {
      result.push(msg);
    }
  }
  
  // 2. 为每个 tool_use 块构造相同的占位 tool_result
  const assistantMsg = parentMessages[parentMessages.length - 1];
  if (assistantMsg.role === 'assistant') {
    const toolResults = assistantMsg.content
      .filter(block => block.type === 'tool_use')
      .map(block => ({
        type: 'tool_result',
        tool_use_id: block.id,
        content: 'Fork started — processing in background',  // 固定文本
        is_error: false
      }));
    
    result.push({
      role: 'user',
      content: toolResults
    });
  }
  
  // 3. 追加 per-child 的指令文本块
  result.push({
    role: 'user',
    content: [{
      type: 'text',
      text: forkInstruction  // 只有这部分因 child 不同
    }]
  });
  
  return result;
}

消息结构可视化

graph TB
    subgraph ParentMessages["父 Agent 消息历史"]
        M1["用户: 帮我重构认证模块"]
        M2["Assistant: 我会先探索代码..."]
        M3["ToolUse: Glob src/auth/*"]
        M4["ToolUse: Grep 'authenticate'"]
        M5["ToolUse: Read src/auth/index.ts"]
    end
    
    subgraph ForkChild1["Fork 子进程 1 消息"]
        F1["Assistant: 我会先探索代码..."]
        F2["ToolUse: Glob src/auth/*"]
        F3["ToolUse: Grep 'authenticate'"]
        F4["ToolUse: Read src/auth/index.ts"]
        F5["ToolResult: Fork started — processing in background"]
        F6["用户: 调查现有认证实现"]
    end
    
    subgraph ForkChild2["Fork 子进程 2 消息"]
        FK1["Assistant: 我会先探索代码..."]
        FK2["ToolUse: Glob src/auth/*"]
        FK3["ToolUse: Grep 'authenticate'"]
        FK4["ToolUse: Read src/auth/index.ts"]
        FK5["ToolResult: Fork started — processing in background"]
        FK6["用户: 分析安全风险"]
    end
    
    ParentMessages -->|"复制"| ForkChild1
    ParentMessages -->|"复制"| ForkChild2
    
    style F5 fill:#ffe6e6,stroke:#cc0000
    style FK5 fill:#ffe6e6,stroke:#cc0000
    style F6 fill:#e6ffe6,stroke:#00cc00
    style FK6 fill:#e6ffe6,stroke:#00cc00

缓存命中分析

请求 1(父 Agent):
[系统提示词 2000 tokens] + [用户消息 100 tokens] + [Assistant 500 tokens]
= 2600 tokens(未命中缓存,首次计算)

Fork 子进程 1:
[系统提示词 2000 tokens ✓ 缓存] + [Assistant 500 tokens ✓ 缓存] + [指令 50 tokens]
= 2550 tokens(命中 2500 tokens 缓存)

Fork 子进程 2:
[系统提示词 2000 tokens ✓ 缓存] + [Assistant 500 tokens ✓ 缓存] + [指令 50 tokens]
= 2550 tokens(命中 2500 tokens 缓存)

节省成本:(2600 - 2550) × 2 = 100 tokens per child × N children

递归 Fork 防护

Fork 子进程的工具池中保留了 Agent 工具(为了缓存一致性),但在调用时会被拦截:

// tools/AgentTool/AgentTool.tsx:332-334
if (toolUseContext.options.querySource === `agent:builtin:${FORK_AGENT.agentType}`
    || isInForkChild(toolUseContext.messages)) {
  throw new Error('Fork is not available inside a forked worker.');
}

检测机制

  1. 主检查:通过 querySource(抗压缩——即使消息被 autocompact 重写也不会丢失)
  2. 备用检查:扫描消息中的 <fork-boilerplate> 标签(fallback 机制)
// tools/AgentTool/forkSubagent.ts:78-89
export function isInForkChild(messages: Message[]): boolean {
  const lastUserMsg = messages.filter(m => m.role === 'user').pop();
  if (!lastUserMsg) return false;
  
  const text = lastUserMsg.content
    .filter(block => block.type === 'text')
    .map(block => block.text)
    .join('\n');
  
  return text.includes('');
}

为什么禁止递归 Fork

  1. 成本控制:防止指数级的 Agent 派生(2^N)
  2. 调试困难:多层 Fork 会使得调用链难以追踪
  3. 性能问题:每个 Fork 都会创建后台任务,过多会耗尽资源

使用场景

  1. 并行代码探索:让多个 Fork 子进程同时探索不同模块
  2. 候选方案对比:Fork 多个子进程尝试不同的实现方案
  3. 大规模重构:将重构任务分解为多个 Fork 子进程并行处理

优势

  • ✅ 共享父 Agent 的完整上下文
  • ✅ 最大化提示词缓存命中,降低成本
  • ✅ 强制后台化,不阻塞主对话
  • ✅ 可以并行执行多个相关任务

局限

  • ❌ 不能递归 Fork
  • ❌ 需要父 Agent 有足够的上下文
  • ❌ 仍然在同一个模型上运行

模式三:协调者模式(Coordinator Mode)

协调者模式通过环境变量 CLAUDE_CODE_COORDINATOR_MODE 激活,是三种模式中最复杂的一个。

激活方式

// coordinator/coordinatorMode.ts:36-41
export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE);
  }
  return false;
}

启动方式

# 启动协调者模式
CLAUDE_CODE_COORDINATOR_MODE=true claude

# 或在 .envrc 中
export CLAUDE_CODE_COORDINATOR_MODE=true

架构设计:四阶段工作流

在协调者模式中,主 Agent 变成一个不直接编码的协调者,它的工具集缩减为指挥工具:

graph TB
    subgraph Coordinator["协调者(主 Agent)"]
        C1["研究阶段 Research"]
        C2["综合阶段 Synthesis"]
        C3["实现阶段 Implementation"]
        C4["验证阶段 Verification"]
    end
    
    subgraph Workers["Worker Agents"]
        W1["Worker A: 代码探索"]
        W2["Worker B: 架构分析"]
        W3["Worker C: 实现编写"]
        W4["Worker D: 测试验证"]
    end
    
    C1 -->|"并行启动"| W1
    C1 -->|"并行启动"| W2
    W1 -->|"返回结果"| C2
    W2 -->|"返回结果"| C2
    
    C2 -->|"生成规格"| C3
    C3 -->|"分配任务"| W3
    W3 -->|"提交代码"| C4
    C4 -->|"验证变更"| W4
    
    style Coordinator fill:#e6f3ff,stroke:#0066cc,stroke-width:3px
    style Workers fill:#f3e6e6,stroke:#862d2d

协调者工具集

工具 用途 调用对象
Agent 派生 Worker Worker Agent
SendMessage 向 Worker 发送后续指令 运行中的 Worker
TaskStop 停止 Worker 运行中的 Worker
ExitPlanMode 退出计划模式(协调者独有) 系统

Worker 工具集

Worker 拥有实际的编码工具:EditWriteBashGlobGrepRead 等。

协调者系统提示词

协调者的系统提示词是一份详尽的编排规程,定义了四阶段工作流:

// coordinator/coordinatorMode.ts:111-368
export function getCoordinatorSystemPrompt(): string {
  return `
You are the Coordinator of a multi-agent software development system.

Your role is NOT to write code directly, but to:
1. Understand the user's request
2. Dispatch worker agents to research the codebase
3. Synthesize their findings into an implementation plan
4. Dispatch workers to implement the plan
5. Verify the implementation

## Four-Phase Workflow

### Phase 1: Research
- Spawn multiple workers in parallel to investigate different aspects
- Each worker should have a focused research task
- Examples: "Find all authentication logic", "Analyze the API structure"

### Phase 2: Synthesis
- CAREFULLY READ all worker results
- Understand the problem deeply
- Write a detailed implementation specification
- NEVER write "based on your findings" — this delegates understanding

### Phase 3: Implementation
- Spawn workers to implement the specification
- Each worker should have clear, bounded tasks
- Monitor progress and provide guidance

### Phase 4: Verification
- Spawn a worker to verify the implementation
- The worker should run tests, check edge cases, and validate correctness

## Critical Rules

1. NEVER delegate understanding
   - Bad: "Based on the research, implement X"
   - Good: "The research shows that authentication is handled in src/auth/. 
            Implement X by modifying src/auth/index.ts."

2. Always read worker results completely
   - Workers may return large amounts of information
   - You must synthesize this into a coherent plan

3. Workers cannot spawn other workers
   - The team structure is flat
   - Only the coordinator can spawn workers

4. Use parallel execution
   - In Research phase, spawn all workers at once
   - Don't wait for one worker to finish before spawning the next

5. Provide clear, bounded tasks
   - Each worker should have a specific goal
   - Avoid vague instructions like "investigate the codebase"
`;
}

核心原则:"永远不要委托理解"

// coordinator/coordinatorMode.ts:256-259
const antiDelegationPrinciple = `
Never write "based on your findings" or "based on the research."
These phrases delegate understanding to the worker instead of 
doing it yourself. The coordinator MUST understand the problem 
deeply to create a coherent implementation plan.
`;

这是协调者模式最重要的设计原则。如果协调者不亲自理解问题,就无法协调出高质量的实现方案。

Worker 上下文生成

getCoordinatorUserContext() 函数生成 Worker 工具上下文信息:

// coordinator/coordinatorMode.ts:80-109
export function getCoordinatorUserContext(appState: AppState): string {
  const workerTools = getWorkerToolDescriptions(appState);
  const mcpServers = getMcpServerDescriptions(appState.mcp.tools);
  
  let context = `
You are a worker agent in a multi-agent system.
You have access to the following tools:

${workerTools}

Available MCP servers:
${mcpServers}
`;
  
  // 当 Scratchpad 功能启用时,告知协调者可以使用共享目录
  if (feature('SCRATCHPAD')) {
    context += `
You can use the scratchpad directory (${SCRATCHPAD_DIR}) to 
share information with other workers. Files written here will 
be visible to all workers in this session.
`;
  }
  
  return context;
}

Scratchpad 跨 Worker 知识共享

graph TB
    subgraph Workers["Worker Agents"]
        W1["Worker A"]
        W2["Worker B"]
        W3["Worker C"]
    end
    
    subgraph Scratchpad["共享 Scratchpad 目录"]
        S1["research-findings.md"]
        S2["architecture-notes.md"]
        S3["implementation-plan.md"]
    end
    
    W1 -->|"写入"| S1
    W2 -->|"读取 S1
写入"| S2 W3 -->|"读取 S1, S2
写入"| S3 style Scratchpad fill:#e6ffe6,stroke:#00cc00,stroke-width:2px

Worker 可以通过 Scratchpad 共享发现和计划,避免重复工作。

Worker 结果回传机制

在协调者模式中,Worker 完成任务后的结果以 <task-notification> XML 格式作为用户角色消息注入协调者的对话中:



  worker-a@session-123
  completed
  Explored authentication module and found 3 files
  
    The authentication logic is split across:
    - src/auth/index.ts (main entry point)
    - src/auth/providers.ts (OAuth providers)
    - src/auth/middleware.ts (Express middleware)
    
    Recommendations:
    1. Consolidate into a single AuthManager class
    2. Add support for API key authentication
  
  
    12450
    8
    15230
  

为什么使用用户角色

协调者的系统提示词明确要求:

They look like user messages but they're not. Distinguish them by the <task-notification> start tag.

这种设计避免了协调者把 Worker 结果当作用户输入来回应。

状态类型

状态 含义 协调者动作
completed Worker 成功完成 读取结果,继续流程
failed Worker 遇到错误 分析错误,决定重试或放弃
killed Worker 被用户停止 记录原因,继续其他任务

三种模式全面对比

graph TB
    subgraph Standard["标准子 Agent"]
        S1["上下文: 全新对话"]
        S2["提示词: Agent 定义自带"]
        S3["执行: 前台/后台"]
        S4["缓存: 无共享"]
        S5["递归: 允许"]
        S6["场景: 独立小任务"]
    end
    
    subgraph Fork["Fork 模式"]
        F1["上下文: 完整继承父级"]
        F2["提示词: 继承父级"]
        F3["执行: 强制后台"]
        F4["缓存: 共享父级缓存"]
        F5["递归: 禁止"]
        F6["场景: 需要上下文的并行探索"]
    end
    
    subgraph Coordinator["协调者模式"]
        C1["上下文: Worker 独立"]
        C2["提示词: 协调者专用"]
        C3["执行: 强制后台"]
        C4["缓存: 无共享"]
        C5["递归: Worker 不可再派生"]
        C6["场景: 复杂多步骤项目"]
    end
    
    AgentTool["AgentTool 统一入口"] --> Standard
    AgentTool --> Fork
    AgentTool --> Coordinator
    
    style AgentTool fill:#f9f,stroke:#333,stroke-width:2px

对比表格

维度 标准子 Agent Fork 模式 协调者模式
上下文继承 无(全新对话) 完整继承 无(Worker 独立)
系统提示词 Agent 定义自带 继承父级 协调者专用提示词
模型选择 可覆盖 继承父级 不可覆盖
执行方式 前台/后台 强制后台 强制后台
缓存共享 共享父级缓存
工具池 独立组装 继承父级 Worker 独立组装
递归派生 允许 禁止 Worker 不可再派生
门控方式 始终可用 构建+运行时 构建+环境变量
典型场景 独立小任务 需要上下文的并行探索 复杂多步骤项目
协调复杂度

20.4 队友 Agent(Agent Swarms)

队友系统是 Agent 编排的另一个维度。与子 Agent 的"父派生子"层级模型不同,队友系统创建一个平面结构的团队,团队中的 Agent 通过消息传递协作。

TeamCreateTool:团队创建

TeamCreateTool 用于创建新团队:

// tools/TeamCreateTool/TeamCreateTool.ts:37-49
const inputSchema = lazySchema(() =>
  z.strictObject({
    team_name: z.string().describe('Name for the new team to create.'),
    description: z.string().optional(),
    agent_type: z.string().optional()
      .describe('Type/role of the team lead'),
  }),
)

团队信息持久化

团队信息保存到 TeamFile 中:

// types/team.ts:24-42
export type TeamFile = {
  name: string;           // 团队名称
  description?: string;   // 团队描述
  members: TeamMember[];  // 成员列表
  leadAgentId?: string;   // Leader 的 ID
  createdAt: string;      // ISO 8601 时间戳
};

export type TeamMember = {
  agentId: string;        // 格式 'name@team-name'
  name: string;           // 显示名称
  agentType: string;      // Agent 类型
  color?: string;         // UI 颜色
  joinedAt: string;       // 加入时间
};

团队名称唯一性

// tools/TeamCreateTool/TeamCreateTool.ts:64-72
let teamName = input.team_name;
const existingTeams = await loadAllTeams();

if (existingTeams.some(t => t.name === teamName)) {
  // 自动生成唯一名称
  teamName = await generateUniqueSlug(teamName, existingTeams);
}

await saveTeamFile({
  name: teamName,
  description: input.description,
  members: [],
  createdAt: new Date().toISOString()
});

TeammateAgentContext:队友上下文

队友使用 TeammateAgentContext 类型,包含丰富的团队协调信息:

// utils/agentContext.ts:60-85
export type TeammateAgentContext = {
  agentId: string;          // 完整 ID,如 "researcher@my-team"
  agentName: string;        // 显示名称,如 "researcher"
  teamName: string;         // 所属团队
  agentColor?: string;      // UI 颜色
  planModeRequired: boolean; // 是否需要计划审批
  parentSessionId: string;  // Leader 的会话 ID
  isTeamLead: boolean;      // 是否是 Leader
  agentType: 'teammate';    // 判别字段
};

ID 格式设计

队友的 ID 格式是 name@team-name,这种格式使得:

  1. 在日志中一眼看出 Agent 的身份和归属
  2. 避免不同团队中同名 Agent 的冲突
  3. 便于消息路由(SendMessageTool 可以直接解析)

示例 ID

ID Agent 名称 团队名称
researcher@my-team researcher my-team
developer@my-team developer my-team
tester@qa-team tester qa-team
reviewer@backend-team reviewer backend-team

平面结构约束

队友系统有一个重要的架构约束:队友不能派生其他队友

// tools/AgentTool/AgentTool.tsx:272-274
if (isTeammate() && teamName && name) {
  throw new Error(
    'Teammates cannot spawn other teammates — the team roster is flat.'
  );
}

设计原因

这是刻意的设计——团队名册是一个扁平数组,嵌套的队友会导致:

  1. 没有来源信息:嵌套派生的队友在名册中出现时,Leader 无法知道是谁派生的
  2. 协调逻辑混乱:Leader 的协调逻辑假设所有队友都是平等的
  3. 生命周期复杂:多层嵌套的队友生命周期管理变得困难

平面 vs. 层级结构对比

graph TB
    subgraph Flat["平面结构(Agent Swarms)"]
        F1["Team Lead"]
        F2["Member A"]
        F3["Member B"]
        F4["Member C"]
        
        F1 -.协调.-> F2
        F1 -.协调.-> F3
        F1 -.协调.-> F4
        
        F2 -.消息.-> F3
        F3 -.消息.-> F4
    end
    
    subgraph Hierarchical["层级结构(被禁止)"]
        H1["Team Lead"]
        H2["Member A"]
        H3["Member A.1(A 的子队友)"]
        H4["Member A.2(A 的子队友)"]
        
        H1 --> H2
        H2 --> H3
        H2 --> H4
        
        H3 --> H4
    end
    
    style Flat fill:#e6f3e6,stroke:#2d862d,stroke-width:2px
    style Hierarchical fill:#f3e6e6,stroke:#862d2d,stroke-dasharray:5 5

进程内队友限制

同样,进程内队友(in-process teammate)不能派生后台 Agent:

// tools/AgentTool/AgentTool.tsx:278-280
if (isInProcessTeammate() && shouldRunAsync) {
  throw new Error(
    'In-process teammates cannot spawn background agents.'
  );
}

这是因为进程内队友的生命周期绑定在 Leader 的进程上,后台 Agent 会超出这个生命周期。


20.5 验证 Agent:对抗性探测

验证 Agent 是内置 Agent 中设计最精致的一个。它的系统提示词长达约 120 行,堪称一份"如何进行真正验证"的工程规范。

核心设计原则

验证 Agent 有两个明确声明的失败模式:

// built-in/verificationAgent.ts:12-13
/**
 * Verification Agent is designed to avoid two failure modes:
 * 1. Verification avoidance
 * 2. Being misled by the first 80%
 */

失败模式 1:验证回避

表现:面对检查时找理由不执行——阅读代码、叙述测试步骤、写 "PASS",然后继续。

示例

❌ 验证回避:
用户:验证登录功能是否正常工作
Agent:我会检查登录代码...(阅读代码)
      代码看起来正确,使用了 bcrypt 和 JWT。
      PASS:登录功能正常。

✅ 真正验证:
用户:验证登录功能是否正常工作
Agent:我会启动开发服务器并实际测试登录...(运行测试)
      1. 测试正确密码:✓ 返回 200 和有效 token
      2. 测试错误密码:✓ 返回 401
      3. 测试 SQL 注入:✓ 被正确转义
      4. 测试并发登录:✓ 每个 session 独立
      VERDICT: PASS

失败模式 2:被前 80% 迷惑

表现:看到漂亮的 UI 或通过的测试套件就倾向于通过,没注意到一半按钮不起作用。

示例

❌ 被前 80% 迷惑:
用户:验证这个电商网站
Agent:首页看起来很棒,产品列表正常显示。
      购物车功能可以添加商品。
      测试套件全部通过。
      PASS:网站正常。

✅ 真正验证:
Agent:我会进行系统性测试...
      1. 首页:✓ 正常
      2. 产品列表:✓ 正常
      3. 购物车:✓ 可以添加商品
      4. 结账:✗ 支付按钮不起作用
      5. 测试套件:✓ 全部通过(但没有覆盖结账)
      
      VERDICT: FAIL
      虽然测试套件通过,但实际测试发现支付按钮无法点击。
      复现步骤:
      1. 添加商品到购物车
      2. 点击"结账"按钮
      3. 填写支付信息
      4. 点击"确认支付"按钮
      5. 观察:按钮无响应,控制台有错误
      
      控制台错误:
      Uncaught ReferenceError: processPayment is not defined

严格的只读约束

验证 Agent 被明确禁止修改项目:

// built-in/verificationAgent.ts:139-145
disallowedTools: [
  AGENT_TOOL_NAME,           // 不能派生其他 Agent
  EXIT_PLAN_MODE_TOOL_NAME,  // 不能退出计划模式
  FILE_EDIT_TOOL_NAME,       // 不能编辑文件
  FILE_WRITE_TOOL_NAME,      // 不能写入文件
  NOTEBOOK_EDIT_TOOL_NAME,   // 不能编辑 notebook
],

但它可以在临时目录(/tmp)写入临时测试脚本:

// built-in/verificationAgent.ts:147-153
allowedWritePaths: [
  '/tmp',           // 临时目录
  '/tmp/claude-*',  // Claude 专用临时目录
],

这个权限足够编写临时的测试工具但不会污染项目。

临时测试脚本示例

// 验证 Agent 会在 /tmp 中创建测试脚本
await writeFile('/tmp/test-login.sh', `
  #!/bin/bash
  # Test login endpoint
  
  # Test 1: Correct credentials
  curl -X POST http://localhost:3000/api/login \\
    -H "Content-Type: application/json" \\
    -d '{"username":"test","password":"correct"}' \\
    -o /tmp/login-test-1.json
  
  # Test 2: Wrong password
  curl -X POST http://localhost:3000/api/login \\
    -H "Content-Type: application/json" \\
    -d '{"username":"test","password":"wrong"}' \\
    -o /tmp/login-test-2.json
  
  # Analyze results...
`);

await exec('bash /tmp/test-login.sh');

VERDICT 判定格式

验证 Agent 的输出必须以严格格式的判定结尾:

// built-in/verificationAgent.ts:117-128
const verdictSchema = z.object({
  verdict: z.enum(['PASS', 'FAIL', 'PARTIAL']),
  details: z.string().optional()
});
判定 含义 使用场景
VERDICT: PASS 验证通过 所有检查都正常
VERDICT: FAIL 发现问题 包含具体错误输出和复现步骤
VERDICT: PARTIAL 环境限制导致无法完全验证 非不确定,而是客观无法完成

PARTIAL 的正确用法

✓ 正确:VERDICT: PARTIAL
      无法启动开发服务器(端口 3000 被占用),
      无法进行完整测试。已检查代码逻辑,未发现明显问题。

✗ 错误:VERDICT: PARTIAL
      我不确定这是不是 bug,可能是预期行为。

PARTIAL 仅用于环境限制:

  • 没有测试框架
  • 工具不可用
  • 服务器无法启动
  • 依赖缺失

不能用于"我不确定这是不是 bug"。

对抗性探测

验证 Agent 的提示词要求至少运行一个对抗性探测:

// built-in/verificationAgent.ts:63-69
const adversarialChecks = `
You must run at least one adversarial probe:
- Concurrent requests (race conditions)
- Boundary values (empty, max, negative)
- Idempotency (calling twice)
- Orphan operations (resources left behind)
- Edge cases (null, undefined, malformed)
`;

如果所有检查都只是"返回 200"或"测试套件通过",说明只确认了快乐路径,不算真正的验证。

对抗性探测示例

// API 端点验证的对抗性探测
await writeFile('/tmp/adversarial-test.sh', `
  #!/bin/bash
  
  echo "=== 快乐路径 ==="
  curl -X POST http://localhost:3000/api/users \\
    -d '{"name":"Alice","age":25}' \\
    -w "\nStatus: %{http_code}\n"
  
  echo "\n=== 对抗性探测 ==="
  
  # 1. 并发请求(竞态条件)
  echo "Test 1: 并发创建相同用户"
  for i in {1..10}; do
    curl -X POST http://localhost:3000/api/users \\
      -d '{"name":"Bob","age":30}' &
  done
  wait
  
  # 2. 边界值
  echo "Test 2: 边界值"
  curl -X POST http://localhost:3000/api/users \\
    -d '{"name":"","age":-1}'  # 空名称、负年龄
  
  # 3. 幂等性
  echo "Test 3: 幂等性"
  USER_ID="123"
  curl -X DELETE http://localhost:3000/api/users/$USER_ID
  curl -X DELETE http://localhost:3000/api/users/$USER_ID  # 再次删除
  
  # 4. 注入攻击
  echo "Test 4: SQL 注入"
  curl -X POST http://localhost:3000/api/users \\
    -d '{"name":"admin\'' OR 1=1 --","age":25}'
  
  # 5. 超长输入
  echo "Test 5: 缓冲区溢出"
  LONG_NAME=$(python3 -c "print('A'*1000000)")
  curl -X POST http://localhost:3000/api/users \\
    -d "{\"name\":\"$LONG_NAME\",\"age\":25}"
`);

await exec('bash /tmp/adversarial-test.sh');

验证 Agent 完整流程

flowchart TD
    A["开始验证"] --> B["阅读代码理解功能"]
    B --> C["编写测试计划"]
    C --> D["编写临时测试脚本
(/tmp/test-*.sh)"] D --> E["运行测试"] E --> F{测试通过?} F -->|是| G["执行对抗性探测"] F -->|否| H["记录失败详情"] G --> I{对抗性探测通过?} I -->|是| J["VERDICT: PASS"] I -->|否| H H --> K["VERDICT: FAIL
包含复现步骤"] K --> L["清理临时文件"] J --> L L --> M["结束"] style G fill:#ffe6e6,stroke:#cc0000,stroke-width:2px style K fill:#ffe6e6,stroke:#cc0000,stroke-width:2px

20.6 Agent 间通信与消息路由

在多 Agent 系统中,Agent 之间需要通信和协作。Claude Code 提供了一套完整的消息传递机制。

SendMessageTool:消息路由

SendMessageTool 是 Agent 间通信的核心。

// tools/SendMessageTool/SendMessageTool.ts:69-76
to: z.string().describe(
  feature('UDS_INBOX')
    ? 'Recipient: teammate name, "*" for broadcast, "uds:" for a local peer, or "bridge:" for a Remote Control peer'
    : 'Recipient: teammate name, or "*" for broadcast to all teammates',
),

寻址方式

格式 目标 示例
"teammate-name" 指定队友 "SendMessage(to: "tester", ...)"
"*" 广播到所有队友 "SendMessage(to: "*", ...)"
"uds:/tmp/peer.sock" Unix Domain Socket 对等端 "SendMessage(to: "uds:/tmp/other.sock", ...)"
"bridge:session-123" Remote Control 对等端 "SendMessage(to: "bridge:abc-123", ...)"

消息类型

// tools/SendMessageTool/SendMessageTool.ts:47-65
export type SendMessageBody =
  | { type: 'text'; content: string }
  | { 
      type: 'shutdown_request'; 
      reason?: string;
    }
  | { 
      type: 'shutdown_response'; 
      accepted: boolean;
    }
  | { 
      type: 'plan_approval_response'; 
      approved: boolean;
      feedback?: string;
    };

广播机制

to"*" 时触发广播:

// tools/SendMessageTool/SendMessageTool.ts:191-266
async function handleBroadcast(
  senderName: string,
  message: SendMessageBody,
  teamName: string
): Promise<{ recipients: string[] }> {
  const team = await loadTeamFile(teamName);
  const recipients: string[] = [];
  
  for (const member of team.members) {
    if (member.name === senderName) continue;  // 跳过发送者自己
    
    await writeToMailbox(member.agentId, {
      from: senderName,
      message: message,
      timestamp: new Date().toISOString()
    });
    
    recipients.push(member.name);
  }
  
  return { recipients };
}

广播流程

sequenceDiagram
    participant Sender as 发送者 (lead)
    participant Tool as SendMessageTool
    participant M1 as 队友 A (dev)
    participant M2 as 队友 B (tester)
    participant M3 as 队友 C (reviewer)
    
    Sender->>Tool: SendMessage(to: "*", text: "Phase 1 complete")
    Tool->>M1: 写入邮箱
    Tool->>M2: 写入邮箱
    Tool->>M3: 写入邮箱
    Tool-->>Sender: { recipients: ["dev", "tester", "reviewer"] }
    
    Note over M1,M3: 队友们在下次轮询时读取消息

邮箱系统

消息实际通过 writeToMailbox() 函数写入文件系统邮箱。

// utils/mailbox.ts:24-45
export async function writeToMailbox(
  agentId: string,
  message: MailboxMessage
): Promise {
  const mailboxPath = getMailboxPath(agentId);
  const messageEntry = {
    ...message,
    id: uuidv4(),
    timestamp: message.timestamp || new Date().toISOString(),
    read: false
  };
  
  // 追加到邮箱文件
  await fs.appendFile(
    mailboxPath,
    JSON.stringify(messageEntry) + '\n'
  );
}

export function getMailboxPath(agentId: string): string {
  return path.join(
    getAgentDataDir(),
    'mailboxes',
    `${agentId}.jsonl`
  );
}

邮箱文件格式

// /var/lib/claude-code/mailboxes/researcher@my-team.jsonl
{"id":"msg-1","from":"lead","type":"text","content":"开始研究阶段","timestamp":"2026-04-05T10:00:00Z","read":true}
{"id":"msg-2","from":"tester","type":"text","content":"测试环境已准备好","timestamp":"2026-04-05T10:05:00Z","read":false}
{"id":"msg-3","from":"lead","type":"shutdown_request","reason":"研究阶段完成","timestamp":"2026-04-05T10:30:00Z","read":false}

读取邮箱

// utils/mailbox.ts:67-89
export async function readMailbox(
  agentId: string
): Promise {
  const mailboxPath = getMailboxPath(agentId);
  
  if (!await fs.pathExists(mailboxPath)) {
    return [];
  }
  
  const content = await fs.readFile(mailboxPath, 'utf-8');
  const lines = content.trim().split('\n');
  
  return lines
    .map(line => JSON.parse(line))
    .filter(msg => !msg.read);  // 只返回未读消息
}

UDS_INBOX:Unix Domain Socket 扩展

UDS_INBOX Feature Flag 启用时,SendMessageTool 的寻址能力扩展到 Unix Domain Socket。

架构

graph TB
    subgraph Team1["Team A"]
        L1["Leader"]
        M1["Member 1"]
        M2["Member 2"]
    end
    
    subgraph Team2["Team B"]
        L2["Leader"]
        M3["Member 1"]
    end
    
    subgraph Remote["Remote Control"]
        R1["Browser Session"]
    end
    
    L1 -->|"SendMessage(to: *)"| M1
    L1 -->|"SendMessage(to: *)"| M2
    
    L1 -->|"SendMessage(to: 'uds:/tmp/team-b.sock')"| L2
    L2 -->|"SendMessage(to: 'uds:/tmp/team-a.sock')"| L1
    
    L1 -->|"SendMessage(to: 'bridge:session-123')"| R1
    R1 -->|"SendMessage(to: 'bridge:session-456')"| L1
    
    style Team1 fill:#e6f3ff,stroke:#0066cc
    style Team2 fill:#ffe6f3,stroke:#cc0066
    style Remote fill:#f3e6e6,stroke:#862d2d

UDS 通信实现

// utils/udsInbox.ts:34-67
export async function sendToUdsSocket(
  socketPath: string,
  message: SendMessageBody
): Promise {
  const client = new net.Socket();
  
  await new Promise((resolve, reject) => {
    client.connect(socketPath, () => {
      const payload = JSON.stringify({
        type: 'inbox_message',
        body: message,
        timestamp: new Date().toISOString()
      });
      
      client.write(payload + '\n', (err) => {
        if (err) reject(err);
        else resolve();
      });
    });
    
    client.on('error', reject);
    setTimeout(() => {
      client.destroy();
      reject(new Error('UDS connection timeout'));
    }, 5000);
  });
}

使用场景

  1. 跨团队协作:两个独立的团队通过 UDS 通信
  2. 进程隔离:不同 tmux 会话中的 Agent 协作
  3. Remote Control:浏览器和终端之间的双向通信

20.7 异步 Agent 的生命周期管理

shouldRunAsynctrue 时,Agent 进入异步生命周期。

触发条件

// tools/AgentTool/AgentTool.tsx:567
const shouldRunAsync = 
  input.run_in_background ||           // 显式后台运行
  input.background === true ||          // 别名
  isCoordinatorMode() ||                 // 协调者模式强制后台
  isForkSubagentEnabled() ||             // Fork 模式强制后台
  isAssistantMode();                     // 助手模式强制后台

生命周期阶段

stateDiagram-v2
    [*] --> Registering: Agent() 调用
    
    Registering --> Running: registerAsyncAgent()
分配 agentId Running --> Running: updateAsyncAgentProgress()
更新进度 Running --> Completed: 任务成功完成 Running --> Failed: 任务失败 Running --> Killed: 用户停止 Completed --> Notifying: completeAsyncAgent() Failed --> Notifying: failAsyncAgent() Killed --> Notifying: failAsyncAgent() Notifying --> [*]: enqueueAgentNotification()
注入结果到调用者 note right of Running 后台 Agent 不与父 Agent 的 abortController 关联 用户按 ESC 不会停止它 end note note right of Killed 只能通过 chat:killAgents 显式终止 end note

关键函数

阶段 函数 职责
注册 registerAsyncAgent() 创建后台任务记录,分配 agentId
执行 runWithAgentContext() 在上下文隔离中运行 runAgent()
进度 updateAsyncAgentProgress() 更新状态和进度信息
完成 completeAsyncAgent() 标记任务完成
失败 failAsyncAgent() 标记任务失败
通知 enqueueAgentNotification() 将结果注入调用者的消息流

Worktree 隔离

isolation: 'worktree' 时,Agent 在临时 git worktree 中运行。

// tools/AgentTool/AgentTool.tsx:590-593
const slug = `agent-${earlyAgentId.slice(0, 8)}`;
worktreeInfo = await createAgentWorktree(slug);

Worktree 创建流程

flowchart TD
    A["Agent 启动
isolation: 'worktree'"] --> B["生成 slug
agent-a1b2c3d4"] B --> C["创建 git worktree
git worktree add .git/worktrees/"] C --> D["获取 worktree 路径
/tmp/claude-worktrees/"] D --> E["在 worktree 中执行任务"] E --> F{有变更?} F -->|否| G["自动清理
git worktree remove"] F -->|是| H["保留 worktree
返回路径和分支名"] G --> I["结束"] H --> J["用户审查变更"] J --> K["手动合并或丢弃"] K --> I style H fill:#ffe6e6,stroke:#cc0000,stroke-width:2px style G fill:#e6ffe6,stroke:#00cc00,stroke-width:2px

实现细节

// utils/git/worktree.ts:45-78
export async function createAgentWorktree(
  slug: string
): Promise {
  const repoPath = getCurrentRepoPath();
  const worktreeDir = path.join('/tmp/claude-worktrees', slug);
  const branchName = `agent/${slug}`;
  
  // 1. 创建 worktree
  await execGit([
    'worktree',
    'add',
    worktreeDir,
    '-b',
    branchName
  ], { cwd: repoPath });
  
  // 2. 记录原始 HEAD
  const originalHead = await execGit(
    ['rev-parse', 'HEAD'],
    { cwd: repoPath }
  );
  
  return {
    path: worktreeDir,
    branch: branchName,
    originalHead: originalHead.trim()
  };
}

// tools/AgentTool/AgentTool.tsx:666-679
async function cleanupWorktree(worktreeInfo: WorktreeInfo) {
  const currentHead = await execGit(
    ['rev-parse', 'HEAD'],
    { cwd: worktreeInfo.path }
  );
  
  // 如果没有变更,自动清理
  if (currentHead.trim() === worktreeInfo.originalHead) {
    await execGit(['worktree', 'remove', worktreeInfo.path]);
  } else {
    // 有变更,保留 worktree
    return {
      worktreePath: worktreeInfo.path,
      branchName: worktreeInfo.branch
    };
  }
}

使用场景

  1. 实验性重构:让 Agent 尝试重构,结果在 worktree 中审查
  2. 并行开发:多个 Agent 在不同的 worktree 中工作
  3. 安全隔离:保护主分支不被意外修改

20.8 工具池的独立组装

每个 Worker 的工具池是独立组装的,不继承父 Agent 的限制。

// tools/AgentTool/AgentTool.tsx:573-577
const workerPermissionContext = {
  ...appState.toolPermissionContext,
  mode: selectedAgent.permissionMode ?? 'acceptEdits'
};
const workerTools = assembleToolPool(
  workerPermissionContext,
  appState.mcp.tools
);

唯一的例外:Fork 模式使用父级的精确工具数组。

// tools/AgentTool/AgentTool.tsx:631-633
const forkResult = await runAgent({
  useExactTools: true,  // 使用父级工具列表
  // ...
});

这是因为工具定义的差异会破坏提示词缓存。

MCP 服务器的等待与验证

Agent 定义可以声明所需的 MCP 服务器。

// built-in/builtInAgents.ts
const agentWithMcp = {
  agentType: 'database-expert',
  requiredMcpServers: ['postgres', 'redis'],
  // ...
};

AgentTool 在启动前会检查这些服务器是否可用。

// tools/AgentTool/AgentTool.tsx:369-409
async function validateMcpServers(
  requiredServers: string[]
): Promise {
  const unavailable = [];
  
  for (const serverName of requiredServers) {
    const server = appState.mcp.tools[serverName];
    
    if (!server) {
      unavailable.push(serverName);
      continue;
    }
    
    // 等待服务器连接,最多 30 秒
    if (server.status === 'connecting') {
      await waitForMcpServer(serverName, 30000);
    }
    
    // 检查最终状态
    if (server.status !== 'connected') {
      unavailable.push(serverName);
    }
  }
  
  if (unavailable.length > 0) {
    throw new Error(
      `Required MCP servers unavailable: ${unavailable.join(', ')}`
    );
  }
}

提前退出逻辑

// tools/AgentTool/AgentTool.tsx:379-391
async function waitForMcpServers(requiredServers: string[]) {
  const promises = requiredServers.map(async (serverName) => {
    // 如果某个服务器已经失败,立即返回
    const server = appState.mcp.tools[serverName];
    if (server?.status === 'failed') {
      return { serverName, success: false };
    }
    
    // 否则等待
    const connected = await waitForMcpServer(serverName, 30000);
    return { serverName, success: connected };
  });
  
  const results = await Promise.all(promises);
  
  // 如果任何一个失败,不再等待其他服务器
  if (results.some(r => !r.success)) {
    throw new Error('Some MCP servers failed to connect');
  }
}

20.9 设计洞察与哲学

为什么三种模式而非一种?

这源于一个根本性的权衡:上下文共享 vs. 执行隔离

模式 上下文共享 执行隔离 适用场景
标准子 Agent 最大 独立小任务
Fork 模式 最大 中等 需要上下文的并行
协调者模式 协调者有全局视图 Worker 隔离 复杂多阶段项目

不存在一种通用方案能同时满足所有场景。不同的任务需要不同的上下文共享程度和协调复杂度。

平面团队结构的设计哲学

禁止队友派生队友不仅是技术约束——它反映了一种组织原则:

在一个有效的团队中,协调应该集中在一个节点(Leader),而不是形成任意深度的委托链。

这与软件工程中"避免过深的调用栈"的直觉一致。

层级 vs. 平面

graph TB
    subgraph Hierarchy["层级结构(复杂)"]
        H1["Leader"]
        H2["Member A"]
        H3["Member A.1"]
        H4["Member A.1.a"]
        H5["Member A.1.b"]
        
        H1 --> H2
        H2 --> H3
        H3 --> H4
        H3 --> H5
    end
    
    subgraph Flat["平面结构(简单)"]
        F1["Leader"]
        F2["Member A"]
        F3["Member B"]
        F4["Member C"]
        
        F1 -.协调.-> F2
        F1 -.协调.-> F3
        F1 -.协调.-> F4
        
        F2 -.消息.-> F3
        F3 -.消息.-> F4
    end
    
    style Hierarchy fill:#f3e6e6,stroke:#862d2d
    style Flat fill:#e6f3e6,stroke:#2d862d,stroke-width:2px

验证 Agent 的"反模式清单"设计

验证 Agent 的提示词显式列出了 LLM 作为验证者时的典型失败模式:

// built-in/verificationAgent.ts:53-61
const failureModes = `
Common failure modes to avoid:

1. Verification avoidance
   - Reading code instead of running tests
   - Writing "PASS" without actual testing
   - Making excuses for not testing

2. Being misled by the first 80%
   - Assuming passing tests mean correctness
   - Not checking edge cases
   - Ignoring error messages

3. Confirmation bias
   - Only testing what you expect to work
   - Not trying to break the system

RECOGNIZE YOUR RATIONALIZATIONS
When you catch yourself making excuses, 
stop and actually run the tests.
`;

这种 meta-cognition 提示是对 LLM 固有弱点的工程补偿——不是期望模型不犯这些错误,而是让模型知道它倾向于犯这些错误。


20.10 用户能做什么:最佳实践

1. 善用子 Agent 做独立调查

当需要在不干扰主对话上下文的情况下完成一个独立子任务时:

示例:
用户:找出所有使用 useState 的组件,但不污染我们的对话
Agent:我会启动一个子 Agent 来完成这个任务。
      Agent(prompt: "Find all components using useState")
      
      子 Agent 在独立上下文中运行,完成后返回摘要。
      不会在主对话中塞满中间结果。

2. 利用 Fork 模式进行并行探索

当需要多个 Agent 并行工作,且需要共享父 Agent 的上下文时:

示例:
用户:同时调查认证模块和数据库模式,找出冲突
Agent:我会 Fork 两个子进程并行工作。
      
      Fork 1: "调查认证模块的实现"
      Fork 2: "调查数据库用户表结构"
      
      两个 Fork 子进程共享父 Agent 的完整上下文,
      最大化提示词缓存命中,降低成本。

3. 理解协调者模式的四阶段流程

如果启用了协调者模式(CLAUDE_CODE_COORDINATOR_MODE=true),了解其工作流有助于更好地协作:

Research → Synthesis → Implementation → Verification
   ↓            ↓              ↓               ↓
 Worker      协调者          Worker          Worker
 并行       理解问题        实现规格        验证正确

特别注意:协调者不会直接编码,它只负责理解问题和分配任务。

4. 利用验证 Agent 进行质量把关

当完成复杂变更后,可以显式要求运行验证 Agent:

用户:实现完成后,运行验证 Agent 测试
Agent:我会启动一个验证 Agent 来全面测试。
      
      验证 Agent 会:
      1. 运行现有测试套件
      2. 执行对抗性探测
      3. 检查边界情况
      4. 尝试破坏系统
      
      最终给出 VERDICT:PASS / FAIL / PARTIAL

5. 注意 Agent 间通信的寻址方式

理解 SendMessageTool 的寻址方式有助于设计更高效的多 Agent 工作流:

- 指定队友:SendMessage(to: "tester", ...)
- 广播:SendMessage(to: "*", ...)
- UDS 通信:SendMessage(to: "uds:/tmp/other.sock", ...)
- Remote Control:SendMessage(to: "bridge:session-123", ...)

6. Worktree 隔离保护主分支

当 Agent 使用 isolation: 'worktree' 时:

- 所有修改都在临时 git worktree 中进行
- 无变更的 worktree 会自动清理
- 有变更的则保留分支供审查
      
这意味着可以放心让 Agent 尝试实验性修改,
不会污染主分支。

20.11 展望:Ultraplan 远程多代理规划系统

本章内容基于 Claude Code v2.1.88+ 源码分析。Ultraplan 是多 Agent 架构的最新演进,将计划阶段卸载到远程,让用户终端保持可用。

为什么需要 Ultraplan

本章前文描述的多 Agent 编排都是本地的——Agent 在用户终端中运行,占用终端的输入输出,上下文窗口与用户共享。

Ultraplan 解决的问题是:把计划阶段卸载到远程,让用户终端保持可用。

维度 本地 Plan Mode Ultraplan
运行位置 本地终端 CCR(Claude Code on the web)远程容器
模型 当前会话模型 强制 Opus 4.6
探索方式 单 Agent 顺序探索 可选多 Agent 并行探索
超时 无硬超时 30 分钟
用户终端 被阻塞 保持可用,可继续其他工作
结果交付 直接在会话中执行 "远程执行并创建 PR"或"传送回本地终端执行"

架构总览

graph TB
    subgraph Local["用户终端(本地)"]
        L1["PromptInput: 检测 'ultraplan' 关键字"]
        L2["processUserInput: 替换为 /ultraplan 命令"]
        L3["launchUltraplan: 创建远程会话"]
        L4["startDetachedPoll: 后台轮询"]
        L5["任务 Pill 显示状态"]
        
        L1 --> L2 --> L3 --> L4 --> L5
    end
    
    subgraph Remote["CCR 远程容器"]
        R1["Opus 4.6 + plan mode 权限"]
        R2["探索代码库(Glob/Grep/Read)"]
        R3["可选:Task 工具生成并行子代理"]
        R4["调用 ExitPlanMode 提交计划"]
        R5["等待用户审批"]
        
        R1 --> R2 --> R3 --> R4 --> R5
    end
    
    L3 -.HTTP 创建.-> R1
    L4 -.HTTP 轮询.-> R5
    R5 -.计划就绪.-> L5
    
    style Local fill:#e6f3ff,stroke:#0066cc
    style Remote fill:#ffe6e6,stroke:#cc0000

触发方式

关键字触发

用户:ultraplan 重构认证模块,支持 OAuth2 和 API key

如果关键字出现彩虹高亮并弹出通知"This prompt will launch an ultraplan session in Claude Code on the web",说明功能已开启。

前提条件

  1. 已通过 OAuth 登录 Claude Code
  2. 订阅级别支持远程 Agent(Pro/Max/Team/Enterprise)
  3. Feature Flag ULTRAPLAN 已对账户开启

使用流程

1. 输入包含 "ultraplan" 的提示词
2. 确认启动对话框
3. 终端显示 CCR URL,你可以继续其他工作
4. 任务栏 Pill 显示进度:
   ◇ ultraplan               → 远程探索代码库中
   ◇ ultraplan needs your input → 需要你在浏览器中操作
   ◆ ultraplan ready          → 计划就绪,等待审批
5. 在浏览器中审批计划:
   a. 批准 → 远程执行并创建 Pull Request
   b. 拒绝 + 反馈 → 远程根据反馈修改后重新提交
   c. 传送回本地 → 计划回到你的终端中执行
6. 如需中途停止,通过任务系统取消即可

关键字检测系统

用户不需要输入 /ultraplan——只要在提示词中自然地写出 "ultraplan" 即可触发。

排除规则

上下文 示例 原因
引号/反引号中 `ultraplan` 代码引用
路径中 src/ultraplan/foo.ts 文件路径
标识符中 --ultraplan-mode CLI 参数
文件扩展名前 ultraplan.tsx 文件名
问号后 ultraplan? 询问功能而非触发

状态机与生命周期

Ultraplan 使用 5 个 AppState 字段管理生命周期:

ultraplanLaunching?: boolean         // 启动中
ultraplanSessionUrl?: string         // 活跃会话 URL
ultraplanPendingChoice?: {           // 已审批的计划等待选择
  plan: string
  sessionId: string
  taskId: string
}
ultraplanLaunchPending?: {           // 启动前确认
  blurb: string
}
isUltraplanMode?: boolean            // 远程端标志

状态转换图

stateDiagram-v2
    [*] --> IDLE
    
    IDLE --> LAUNCHING: 用户输入 "ultraplan"
    
    LAUNCHING --> RUNNING: teleportToRemote() 成功
    LAUNCHING --> IDLE: 启动失败
    
    RUNNING --> RUNNING: phase=running
    RUNNING --> NEEDS_INPUT: phase=needs_input
    RUNNING --> PLAN_READY: phase=plan_ready
    
    NEEDS_INPUT --> RUNNING: 远程恢复工作
    NEEDS_INPUT --> PLAN_READY: ExitPlanMode 调用
    
    PLAN_READY --> REMOTE_EXEC: 批准 → 远程执行
    PLAN_READY --> PENDING_CHOICE: 拒绝 + 传送标记
    PLAN_READY --> RUNNING: 拒绝 + 反馈
    
    REMOTE_EXEC --> IDLE: 任务完成
    PENDING_CHOICE --> IDLE: 本地执行
    PENDING_CHOICE --> RUNNING: 继续远程
    
    RUNNING --> IDLE: 超时/失败/停止

轮询与阶段检测

startDetachedPoll() 以后台 async IIFE 运行,不阻塞终端:

const POLL_INTERVAL_MS = 3000             // 每 3 秒轮询
const MAX_CONSECUTIVE_FAILURES = 5        // 连续 5 次网络错误后放弃
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000  // 30 分钟超时

阶段检测逻辑

const quietIdle =
  (sessionStatus === 'idle' || sessionStatus === 'requires_action') &&
  newEvents.length === 0

const phase: UltraplanPhase = scanner.hasPendingPlan
  ? 'plan_ready'      // ExitPlanMode 已调用,等待审批
  : quietIdle
    ? 'needs_input'    // 远程空闲
    : 'running'        // 正常工作中

远程卸载模式(Remote Offloading Pattern)

Ultraplan 体现了一种可复用的架构模式——远程卸载

本地终端                        远程容器
┌──────────┐                   ┌──────────────┐
│ 快速反馈  │───创建会话──→      │ 长时间运行    │
│ 继续可用  │                   │ 高算力模型    │
│          │←──轮询状态──       │ 多代理并行    │
│ Pill 显示 │                   │              │
│ ◇/◆ 状态 │←──计划就绪──      │ ExitPlanMode │
│          │                   │              │
│ 选择执行  │───批准/传送──→    │ 执行/停止     │
└──────────┘                   └──────────────┘

核心设计决策

  1. 异步分离startDetachedPoll() 以 async IIFE 启动,立即返回
  2. 状态机驱动 UI:三相映射到任务 Pill 的视觉状态(◇/◆)
  3. 哨兵协议__ULTRAPLAN_TELEPORT_LOCAL__ 作为进程间通信通道
  4. GrowthBook 驱动:模型、超时、提示词变体均为远程可配
  5. 孤儿防护:所有错误路径都执行归档,防止会话泄漏

20.12 总结:多 Agent 编排的艺术

本章深入探索了 Claude Code 的多 Agent 架构,从三种递进的 Agent 模式到队友系统、验证 Agent,再到最新的 Ultraplan 远程规划系统。

核心要点回顾

  1. 三种 Agent 模式各有场景

    • 标准子 Agent:独立小任务
    • Fork 模式:需要上下文的并行探索
    • 协调者模式:复杂多阶段项目
  2. AsyncLocalStorage 是关键:解决了多 Agent 并发时的上下文隔离问题

  3. 平面团队结构反映组织原则:协调集中在一个节点,避免过深的委托链

  4. 验证 Agent 的反模式清单:显式列出 LLM 的典型失败,让模型自我识别

  5. Agent 间通信支持多种寻址:名称、广播、UDS、Remote Control

  6. Ultraplan 代表远程卸载模式:长时间运行的任务卸载到远程,终端保持可用

设计哲学

Claude Code 的多 Agent 架构体现了一个深刻的工程洞察:

不存在一种通用的多 Agent 方案能同时满足所有场景。

不同的任务需要不同的上下文共享程度、协调复杂度和执行模型。Claude Code 通过提供三种递进的模式、灵活的通信机制和强大的生命周期管理,让用户可以根据具体需求选择最合适的方案。

这正是"Harness"(驾驭)的精髓——不是让 AI 替代人类,而是提供一套精妙的工具和模式,让人类能够有效地驾驭多个 AI Agent,共同完成复杂的软件工程任务。


参考文献

  • Claude Code 源码https://github.com/anthropics/claude-code
  • AgentTool 实现tools/AgentTool/AgentTool.tsx
  • 协调者模式coordinator/coordinatorMode.ts
  • 验证 Agentbuilt-in/verificationAgent.ts
  • Ultraplan 系统commands/ultraplan.tsx, utils/ultraplan/ccrSession.ts

下一章预告:第21章将探索 Claude Code 的另一个高级子系统——持久化存储与状态管理,深入了解 Agent 如何在多个会话之间保持知识和记忆。

Enjoyed this article? Share it with others!