第 13 章:多 Agent 与任务调度

# 第 13 章:多 Agent 与任务调度

> 最优雅的任务调度,不是框架,而是一个工具。

---

## 13.1 核心问题

单个 Agent 面对复杂任务时有两个瓶颈:

**瓶颈 A:Token 预算限制**

```
用户:帮我把 src/ 下的 200 个文件都做一遍安全审查
Agent:好的(读第 1 个文件……读第 2 个……)
[20 个文件后 context 满了]
Agent:抱歉,上下文超出限制,无法完成任务
```

一个 Agent 的上下文窗口(200K tokens)无法容纳大规模任务的所有中间状态。

**瓶颈 B:串行速度**

```
子任务 A:分析前端代码 (需要 30 秒)
子任务 B:分析后端代码 (需要 30 秒)
子任务 C:分析数据库层 (需要 30 秒)

串行执行:90 秒总耗时
并行执行:30 秒总耗时(父 Agent 同时派生 3 个子 Agent)
```

独立的子任务本可并行,但单 Agent 只能串行执行。

**解决方案:Agent as Tool**  
把"启动一个新 Agent"本身包装成一个工具(`Agent` 工具),LLM 通过 `tool_use` 调用它就能派生子 Agent。父 Agent 发出多个 `tool_use` → 多个子 Agent 并行执行 → 通过 `tool_result` 返回结果。

---

## 13.2 原理讲解

### 13.2.1 任务类型体系

`src/Task.ts` 定义了 Claude Code 的任务分类:

```typescript
type TaskType =
  | 'local_bash'       // Shell 命令(第 4 章的 BashTool)
  | 'local_agent'      // 本机子 Agent(主线)
  | 'remote_agent'     // 远端 CCR 云 Agent
  | 'in_process_teammate'  // 同进程团队成员(Agent Swarms)
  | 'local_workflow'   // Workflow 脚本任务
  | 'monitor_mcp'      // MCP 监控任务
  | 'dream'            // 后台思考任务(进阶特性)

type TaskStatus =
  | 'pending'     // 已创建,尚未启动
  | 'running'     // 正在执行
  | 'completed'   // 成功完成
  | 'failed'      // 执行失败
  | 'killed'      // 被中止
```

任务 ID 带类型前缀(`b` = bash,`a` = local_agent,`r` = remote_agent),便于在日志中区分:

```
b_x7k3m9p2  ← 一个 Bash 任务
a_q4w8v1z6  ← 一个本机 Agent 任务
```

### 13.2.2 Agent as Tool:AgentTool 的接口

`AgentTool`(工具名:`Agent`,旧名:`Task`)的输入 Schema:

```typescript
const inputSchema = z.object({
  description: z.string(),     // 3-5 词任务描述(UI 展示用)
  prompt: z.string(),          // 子 Agent 的完整任务说明
  subagent_type: z.string().optional(),  // 指定使用哪种 Agent(Explore/Plan/...)
  model: z.enum(['sonnet', 'opus', 'haiku']).optional(),  // 覆盖默认模型
  run_in_background: z.boolean().optional(),  // true = 后台异步执行
  isolation: z.enum(['worktree']).optional(), // 在独立 git worktree 中运行
})
```

当父 Agent 的 LLM 响应包含多个 `AgentTool` 的 `tool_use`,它们会被**并行**执行:

```json
// 父 Agent 的 LLM 响应(简化)
{
  "content": [
    {"type": "tool_use", "name": "Agent", "input": {"description": "分析前端", "prompt": "..."}},
    {"type": "tool_use", "name": "Agent", "input": {"description": "分析后端", "prompt": "..."}},
    {"type": "tool_use", "name": "Agent", "input": {"description": "分析数据库", "prompt": "..."}}
  ]
}
```

三个 `tool_use` 在同一次响应中 → 三个子 Agent 并行启动 → 结果通过 `tool_result` 返回。

### 13.2.3 Agent 定义:AgentDefinition

自定义 Agent 可以通过 Markdown 文件定义(类似 Skills 的 frontmatter 格式),放在 `.claude/agents/` 目录下:

```markdown
---
description: 专门用于代码安全审查的 Agent
when_to_use: 当需要分析代码安全漏洞时
tools: Read, Grep, Bash
model: sonnet
permission_mode: readonly
---

你是一位专注于代码安全审查的专家 Agent。
对于给定的代码,你需要:
1. 检查常见的安全漏洞(SQL注入、XSS、CSRF等)
2. 评估认证和授权逻辑
3. 给出具体的修复建议
```

Claude Code 内置了几种专用 Agent 类型:

| Agent 类型 | 用途 | 特点 |
|-----------|------|------|
| `Explore` | 快速代码探索,返回报告 | 一次性(One-Shot),不需要父 Agent 继续交互 |
| `Plan` | 制定执行计划 | 一次性,只读工具,不执行修改 |
| `verification` | 验证任务结果 | 在独立上下文中二次确认 |
| General Purpose | 通用子任务 | 有完整工具集 |

`ONE_SHOT_BUILTIN_AGENT_TYPES`(Explore/Plan)不需要 `agentId` 和 SendMessage 能力,省去了这些 header,节省约 135 字符的 token 消耗(× 每周 3400 万次调用 = 可观的节省)。

### 13.2.4 子 Agent 的完整生命周期

```
父 Agent 调用 AgentTool.call(input)
    │
    ├─ 1. 生成子 agentId(UUID)
    ├─ 2. 构建子 Agent 上下文
    │       ├─ 继承父 Agent 的 MCP 连接
    │       ├─ 继承 / 覆盖权限模式(permissionMode)
    │       └─ 构建子 Agent 的 system prompt(可覆盖父 Agent 的 CLAUDE.md)
    │
    ├─ 3. 写入 metadata 到磁盘
    │       └─ ~/.claude/projects/.../sessionId/subagents/<agentId>.meta.json
    │
    ├─ 4. 启动 runAgent()(AsyncGenerator)
    │       └─ 进入独立的 query() Agentic Loop
    │              ├─ 子 Agent 调用工具(读文件、运行 Bash...)
    │              └─ 子 Agent 可再次调用 AgentTool 派生孙 Agent(递归)
    │
    ├─ 5. 子 Agent 完成时,将结果文本序列化为 tool_result
    │
    └─ 6. 父 Agent 收到 tool_result,继续自己的 Agentic Loop
```

子 Agent 的对话历史写入独立的 JSONL 文件:

```
~/.claude/projects/<project>/
└── <session-id>/
    └── subagents/
        ├── <agent-id-1>.jsonl   ← 子 Agent A 的完整对话记录
        ├── <agent-id-2>.jsonl   ← 子 Agent B 的完整对话记录
        └── <agent-id-3>.jsonl   ← 子 Agent C 的完整对话记录
```

### 13.2.5 异步 Agent(后台执行)

设置 `run_in_background: true` 时,子 Agent 进入后台执行:

```
同步模式(默认):
  父 Agent → 调用 AgentTool → 等待子 Agent 完成 → 收到结果 → 继续

异步模式(background):
  父 Agent → 调用 AgentTool → 立即返回 task_id → 继续其他工作
                                          │
                                          └─ 子 Agent 在后台运行
                                             完成后通过通知告知父 Agent
```

异步 Agent 有更严格的工具限制(`ASYNC_AGENT_ALLOWED_TOOLS`),不能使用会阻塞 UI 的工具(如需要用户确认的权限对话框)。

### 13.2.6 隔离模式:worktree isolation

设置 `isolation: "worktree"` 时,子 Agent 在独立的 git worktree 中运行:

```bash
# Claude Code 内部等效操作:
git worktree add /tmp/claude-agent-abc123 HEAD  # 创建独立工作区
# 子 Agent 在 /tmp/claude-agent-abc123/ 中工作
# 主工作区的文件不受影响
git worktree remove /tmp/claude-agent-abc123    # 完成后清理
```

用途:让多个子 Agent 并行修改代码,而不会产生文件冲突。父 Agent 收集所有结果后,决定如何合并。

### 13.2.7 循环检测

子 Agent 也有 `AgentTool`,理论上可以无限递归:

```
父 Agent → 子 Agent A → 孙 Agent B → 曾孙 Agent C → ...
```

Claude Code 通过以下机制防止失控:
- `maxTurns`:每个 Agent 的最大对话轮数(AgentDefinition 中可配置)
- `filterDeniedAgents()`:权限层面阻止调用被拒绝的 Agent 类型
- 异步 Agent 不能再派生新的异步 Agent(`isAsync && IN_PROCESS_TEAMMATE_ALLOWED_TOOLS` 限制)

---

## 13.3 源码索引

| 文件 | 关键函数/类型 | 作用 |
|------|-------------|------|
| `src/Task.ts` | `TaskType`、`TaskStatus`、`generateTaskId()` | 任务类型体系和 ID 生成 |
| `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | `LocalAgentTaskState`、`ProgressTracker` | 本机 Agent 任务状态和进度追踪 |
| `src/tools/AgentTool/AgentTool.tsx` | `inputSchema`、`AgentTool.call()` | AgentTool 工具定义和调用入口 |
| `src/tools/AgentTool/runAgent.ts` | `runAgent()` | 子 Agent 的完整运行逻辑(AsyncGenerator) |
| `src/tools/AgentTool/loadAgentsDir.ts` | `AgentDefinition`、`getAgentDirAgents()` | Agent 定义加载(Markdown → 对象) |
| `src/tools/AgentTool/constants.ts` | `AGENT_TOOL_NAME`、`ONE_SHOT_BUILTIN_AGENT_TYPES` | 工具名称和一次性 Agent 类型集合 |
| `src/tools/AgentTool/agentToolUtils.ts` | `filterToolsForAgent()` | 按 Agent 类型过滤可用工具 |
| `src/tools/AgentTool/forkSubagent.ts` | `buildForkedMessages()` | fork 模式下的上下文消息共享 |

---

## 13.4 最小化产出物

> 代码骨架位于 `../chapters/13/src/`,参考实现位于 `../chapters/13/solution/`。
> **前置条件**:需要 `ANTHROPIC_API_KEY` 环境变量(`npm start` 需要)。

### 本章要实现什么

在 `../chapters/13/src/agent-tool.ts` 中完成多 Agent 调度。

**接口规范**(已提供,不要修改):

```typescript
export const MAX_AGENT_DEPTH = 3
export const MAX_AGENT_TURNS = 10
export const AGENT_TOOLS: Anthropic.Tool[]  // 含 dispatch_agent、bash、read_file

export async function runSubAgent(client: Anthropic, task: AgentTask): Promise<string>
// 独立的 Agentic Loop,直到任务完成或超出轮数

export class MultiAgentCoordinator {
  async chat(userInput: string): Promise<string>
  // 支持 dispatch_agent 工具,并行派发子 Agent
}
```

**你需要实现**:
1. `runSubAgent()`:独立 Agentic Loop,处理 dispatch_agent(递归)、bash、read_file 工具
2. `MultiAgentCoordinator.chat()`:父 Agent 循环,遇到 dispatch_agent 时调用 runSubAgent

**关键约束**:
- 深度 >= MAX_AGENT_DEPTH 时不能再派发子 Agent(过滤掉 dispatch_agent 工具)
- 工具执行失败不退出循环,以错误字符串作为 tool_result

### 验收

```bash
cd docs/chapters/13
npm install
npm test
```

卡住时查看 `../chapters/13/solution/agent-tool.ts`。

### src/main.ts

```typescript
/**
 * 第 13 章产出物:多 Agent 与任务调度
 *
 * 核心原理:
 *   - dispatch_agent 工具:父 Agent 通过 tool_use 调用,派生独立子 Agent
 *   - 子 Agent 有自己独立的 messages[],完整运行 Agentic Loop
 *   - 同一次父 LLM 响应中多个 dispatch_agent 调用 → 并行执行
 *   - 子 Agent 结果通过 tool_result 返回给父 Agent
 *   - 子 Agent 可递归调用 dispatch_agent(受深度限制)
 *
 * 验收:
 *   npx tsx src/main.ts
 *   输入:"帮我分析一下当前目录的代码结构,包括:1) 列出所有文件 2) 统计行数"
 *   → 父 Agent 应派发 2 个子 Agent 分别处理两个子任务
 */

import Anthropic from '@anthropic-ai/sdk'
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as child_process from 'node:child_process'
import * as readline from 'node:readline'

// ─────────────────────────────────────────────────────────────────────────────
// 类型定义
// ─────────────────────────────────────────────────────────────────────────────

interface ApiMessage {
  role: 'user' | 'assistant'
  content: string | ContentBlock[]
}

interface ContentBlock {
  type: string
  [key: string]: unknown
}

interface ToolUseBlock {
  type: 'tool_use'
  id: string
  name: string
  input: Record<string, unknown>
}

interface ToolResultBlock {
  type: 'tool_result'
  tool_use_id: string
  content: string
}

// 对应 src/Task.ts 的 TaskStatus(简化版)
type AgentStatus = 'pending' | 'running' | 'completed' | 'failed'

interface AgentTask {
  id: string
  description: string
  prompt: string
  status: AgentStatus
  result?: string
  error?: string
  depth: number  // 递归深度,防止无限循环
}

// ─────────────────────────────────────────────────────────────────────────────
// 工具定义
// ─────────────────────────────────────────────────────────────────────────────

/** dispatch_agent 工具的输入类型 */
interface DispatchAgentInput {
  description: string   // 3-5 词任务描述
  prompt: string        // 子 Agent 的完整任务
}

/** bash 工具的输入类型 */
interface BashInput {
  command: string
}

/** 工具定义(发给 Claude API 的 schema) */
const TOOLS: Anthropic.Tool[] = [
  {
    name: 'dispatch_agent',
    description: [
      'Launch a sub-agent to handle an independent subtask.',
      'The sub-agent runs its own Agentic Loop with access to bash and file tools.',
      'Use this when you can decompose a task into independent parts that can be done separately.',
      'Multiple dispatch_agent calls in the same response run in PARALLEL.',
      'Returns the sub-agent\'s final result as a string.',
    ].join('\n'),
    input_schema: {
      type: 'object',
      properties: {
        description: {
          type: 'string',
          description: 'A short (3-5 word) description of what this sub-agent will do',
        },
        prompt: {
          type: 'string',
          description: 'The complete task description for the sub-agent to perform',
        },
      },
      required: ['description', 'prompt'],
    },
  },
  {
    name: 'bash',
    description: 'Run a bash command and return its output. Use for file operations, searching, and system commands.',
    input_schema: {
      type: 'object',
      properties: {
        command: {
          type: 'string',
          description: 'The bash command to run',
        },
      },
      required: ['command'],
    },
  },
  {
    name: 'read_file',
    description: 'Read the contents of a file',
    input_schema: {
      type: 'object',
      properties: {
        path: {
          type: 'string',
          description: 'Absolute or relative path to the file',
        },
      },
      required: ['path'],
    },
  },
]

// ─────────────────────────────────────────────────────────────────────────────
// 工具执行(本地工具:bash / read_file)
// ─────────────────────────────────────────────────────────────────────────────

function executeBash(command: string): string {
  try {
    // 安全:只允许读取类操作的命令(不允许写入/删除/网络请求)
    const FORBIDDEN_PATTERNS = [/\brm\b/, /\bmv\b/, /\bdd\b/, /curl/, /wget/, />\s*[^>]/, /\|\s*sh/, /eval/]
    for (const pattern of FORBIDDEN_PATTERNS) {
      if (pattern.test(command)) {
        return `[安全拦截] 不允许执行此命令:${command}`
      }
    }
    const result = child_process.execSync(command, {
      timeout: 10_000,
      maxBuffer: 1024 * 1024,  // 1MB
      encoding: 'utf-8',
    })
    return result || '(命令执行成功,无输出)'
  } catch (err) {
    const error = err as { message?: string; stderr?: string | Buffer }
    const stderr = error.stderr ? error.stderr.toString() : ''
    return `命令执行失败:${error.message}\n${stderr}`.trim()
  }
}

function executeReadFile(filePath: string): string {
  try {
    const resolved = path.resolve(filePath)
    const content = fs.readFileSync(resolved, 'utf-8')
    // 限制返回大小,防止大文件撑爆 context
    if (content.length > 10_000) {
      return content.slice(0, 10_000) + '\n[...文件过长,已截断]'
    }
    return content
  } catch (err) {
    return `读取文件失败:${(err as Error).message}`
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// 子 Agent 运行器(对应 runAgent.ts)
// ─────────────────────────────────────────────────────────────────────────────

const MAX_AGENT_DEPTH = 3   // 最大递归深度
const MAX_AGENT_TURNS = 10  // 每个 Agent 最多对话轮数

/**
 * 运行一个子 Agent:独立的 Agentic Loop,直到任务完成或超出轮数限制
 * 对应 Claude Code 中的 runAgent() AsyncGenerator
 */
async function runSubAgent(
  client: Anthropic,
  task: AgentTask,
): Promise<string> {
  const prefix = '  '.repeat(task.depth)  // 缩进显示 Agent 层级
  console.log(`${prefix}[子 Agent ${task.id}] 开始:${task.description}`)

  const messages: ApiMessage[] = [
    {
      role: 'user',
      content: task.prompt,
    },
  ]

  let turns = 0

  while (turns < MAX_AGENT_TURNS) {
    turns++

    const response = await client.messages.create({
      model: 'claude-opus-4-5',
      max_tokens: 4096,
      system: [
        '你是一个专注于单一子任务的 Agent。',
        '当任务完成时,直接返回结果文本,不要调用任何工具。',
        '保持简洁,直接给出结果,无需解释过程。',
        `当前递归深度:${task.depth}/${MAX_AGENT_DEPTH}(${task.depth >= MAX_AGENT_DEPTH - 1 ? '不要再派发子 Agent' : '必要时可派发子 Agent'})`,
      ].join('\n'),
      tools: task.depth < MAX_AGENT_DEPTH ? TOOLS : TOOLS.filter(t => t.name !== 'dispatch_agent'),
      messages: messages as Anthropic.MessageParam[],
    })

    const assistantContent = response.content

    // 收集本轮的 tool_use 调用
    const toolUses = assistantContent.filter(
      (b): b is Anthropic.ToolUseBlock => b.type === 'tool_use'
    )

    // 无工具调用 = Agent 认为任务完成,返回最终文本
    if (toolUses.length === 0 || response.stop_reason === 'end_turn') {
      const finalText = assistantContent
        .filter(b => b.type === 'text')
        .map(b => (b as Anthropic.TextBlock).text)
        .join('')
      console.log(`${prefix}[子 Agent ${task.id}] 完成(${turns} 轮)`)
      return finalText || '(子 Agent 未返回文本结果)'
    }

    // 有工具调用:添加 assistant 消息,处理工具
    messages.push({
      role: 'assistant',
      content: assistantContent as ContentBlock[],
    })

    // 并行执行所有工具调用(对应 Claude Code 的并行 tool execution)
    const toolResults = await Promise.all(
      toolUses.map(async (toolUse): Promise<ToolResultBlock> => {
        const input = toolUse.input as Record<string, unknown>
        let result: string

        console.log(`${prefix}  [工具] ${toolUse.name}(${JSON.stringify(input).slice(0, 80)})`)

        if (toolUse.name === 'dispatch_agent') {
          const dispatchInput = input as unknown as DispatchAgentInput
          if (task.depth >= MAX_AGENT_DEPTH) {
            result = '[错误] 达到最大递归深度,无法再派发子 Agent'
          } else {
            // 递归派发子 Agent
            const childTask: AgentTask = {
              id: `${task.id}.${toolUses.indexOf(toolUse) + 1}`,
              description: dispatchInput.description,
              prompt: dispatchInput.prompt,
              status: 'running',
              depth: task.depth + 1,
            }
            try {
              result = await runSubAgent(client, childTask)
            } catch (err) {
              result = `子 Agent 执行失败:${(err as Error).message}`
            }
          }
        } else if (toolUse.name === 'bash') {
          result = executeBash((input as unknown as BashInput).command)
        } else if (toolUse.name === 'read_file') {
          result = executeReadFile(String(input.path ?? ''))
        } else {
          result = `未知工具:${toolUse.name}`
        }

        return {
          type: 'tool_result',
          tool_use_id: toolUse.id,
          content: result,
        }
      })
    )

    // 将工具结果添加到消息历史
    messages.push({
      role: 'user',
      content: toolResults as ContentBlock[],
    })
  }

  return `(达到最大轮数 ${MAX_AGENT_TURNS},子 Agent 未能完成任务)`
}

// ─────────────────────────────────────────────────────────────────────────────
// 父 Agent(主循环)
// ─────────────────────────────────────────────────────────────────────────────

class MultiAgentCoordinator {
  private client: Anthropic
  private messages: ApiMessage[] = []

  constructor() {
    if (!process.env.ANTHROPIC_API_KEY) {
      throw new Error('请设置 ANTHROPIC_API_KEY 环境变量')
    }
    this.client = new Anthropic()
  }

  async chat(userInput: string): Promise<string> {
    this.messages.push({ role: 'user', content: userInput })

    let turns = 0
    const MAX_TURNS = 20

    while (turns < MAX_TURNS) {
      turns++

      const response = await this.client.messages.create({
        model: 'claude-opus-4-5',
        max_tokens: 4096,
        system: [
          '你是一个多 Agent 协调者。',
          '对于可以分解为独立子任务的复杂请求,使用 dispatch_agent 工具并行派发子 Agent。',
          '例如:分析多个文件时,每个文件派发一个子 Agent;执行相互独立的操作时,同时派发多个 Agent。',
          '每个子 Agent 会独立完成任务并返回结果,你再汇总这些结果给用户。',
          '对于简单的单步任务,直接使用 bash 或 read_file 工具。',
        ].join('\n'),
        tools: TOOLS,
        messages: this.messages as Anthropic.MessageParam[],
      })

      const content = response.content

      // 无工具调用 = 任务完成
      const toolUses = content.filter(
        (b): b is Anthropic.ToolUseBlock => b.type === 'tool_use'
      )

      if (toolUses.length === 0 || response.stop_reason === 'end_turn') {
        const finalText = content
          .filter(b => b.type === 'text')
          .map(b => (b as Anthropic.TextBlock).text)
          .join('')

        this.messages.push({ role: 'assistant', content: finalText })
        return finalText
      }

      // 添加 assistant 消息(含工具调用)
      this.messages.push({
        role: 'assistant',
        content: content as ContentBlock[],
      })

      // 并行执行所有工具(包括 dispatch_agent)
      console.log(`\n[父 Agent] 并行执行 ${toolUses.length} 个工具调用…`)

      const toolResults = await Promise.all(
        toolUses.map(async (toolUse): Promise<ToolResultBlock> => {
          const input = toolUse.input as Record<string, unknown>
          let result: string

          if (toolUse.name === 'dispatch_agent') {
            const dispatchInput = input as unknown as DispatchAgentInput
            const taskId = `root.${toolUses.indexOf(toolUse) + 1}`
            const task: AgentTask = {
              id: taskId,
              description: dispatchInput.description,
              prompt: dispatchInput.prompt,
              status: 'running',
              depth: 1,
            }
            try {
              result = await runSubAgent(this.client, task)
            } catch (err) {
              result = `子 Agent 失败:${(err as Error).message}`
            }
          } else if (toolUse.name === 'bash') {
            result = executeBash((input as unknown as BashInput).command)
          } else if (toolUse.name === 'read_file') {
            result = executeReadFile(String(input.path ?? ''))
          } else {
            result = `未知工具:${toolUse.name}`
          }

          return {
            type: 'tool_result',
            tool_use_id: toolUse.id,
            content: result,
          }
        })
      )

      // 将工具结果添加到消息历史
      this.messages.push({
        role: 'user',
        content: toolResults as ContentBlock[],
      })
    }

    return '(达到最大轮数,任务未能完成)'
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// CLI 入口
// ─────────────────────────────────────────────────────────────────────────────

async function main(): Promise<void> {
  const coordinator = new MultiAgentCoordinator()

  console.log('\nClaude 多 Agent 协调者')
  console.log(`配置:最大子 Agent 深度=${MAX_AGENT_DEPTH},每 Agent 最多 ${MAX_AGENT_TURNS} 轮\n`)
  console.log('示例任务(可以试试):')
  console.log('  "列出当前目录下的文件,并统计每种扩展名的数量"')
  console.log('  "找出当前目录所有 .json 文件,并显示各自的第一行"\n')

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  })

  const ask = (prompt: string): Promise<string> =>
    new Promise(resolve => rl.question(prompt, resolve))

  while (true) {
    const input = await ask('你: ')
    if (!input.trim()) continue
    if (input.trim() === '/exit') {
      console.log('\n再见!')
      process.exit(0)
    }

    console.log('\nClaude: (思考中…)')
    try {
      const result = await coordinator.chat(input)
      console.log('\n' + result + '\n')
    } catch (err) {
      console.error(`\n错误:${err instanceof Error ? err.message : String(err)}\n`)
    }
  }
}

main().catch(err => {
  console.error(err)
  process.exit(1)
})
```

---

## 13.5 本章小结

### 核心机制回顾

**Agent as Tool**:把"启动子 Agent"包装成普通工具,让 LLM 通过 `tool_use` 自然地派发子任务。这是 Claude Code 最关键的设计决策之一——没有专门的任务调度框架,只是一个工具。

**并行来自多 tool_use**:LLM 在同一响应中发出多个 `dispatch_agent` 调用 → Agent 框架并行执行它们 → 通过 `tool_result` 聚合结果。并行不是显式编排的,而是 LLM 自然选择的。

**独立上下文**:每个子 Agent 有自己独立的 `messages[]` 数组和 JSONL 文件,不共享父 Agent 的对话历史。父子 Agent 只通过 `tool_use` 输入和 `tool_result` 输出通信。

**递归控制**:`maxTurns`(每 Agent 对话轮数上限)+ 深度限制(`MAX_AGENT_DEPTH`)防止无限递归。Claude Code 还通过工具过滤(异步 Agent 不能调用 `dispatch_agent`)进行额外约束。

### Claude Code vs 本章产出物的差异

| 特性 | Claude Code | 本章产出物 |
|------|------------|-----------|
| Agent 类型系统 | 5 种 TaskType(含 remote/in_process/workflow/dream) | 仅 local_agent |
| 工具过滤 | 按 Agent 类型/权限模式精细过滤 | 仅深度限制 |
| 异步执行 | `run_in_background: true` + 通知系统 | 全部同步 |
| 进度追踪 | `ProgressTracker`(工具调用数 + token 数 + 最近活动) | 仅打印日志 |
| 上下文隔离 | worktree 隔离(独立 git 工作区) | 无隔离 |
| 元数据持久化 | `writeAgentMetadata()` + JSONL 子目录 | 无持久化 |
| 子 Agent 恢复 | `--resume` 可恢复中断的子 Agent | 不支持 |

### 架构演进

```
第 7 章    Agentic Loop ──────────────────────────────┐
                                                       │ 子 Agent = 独立的 Agentic Loop
第 13 章   多 Agent 调度 ◄──────────────────────────── ┘
              ├── dispatch_agent 工具(启动子 Agent)
              ├── 父子间通信:tool_use → tool_result
              ├── 并行执行:同一响应多个 tool_use
              └── 递归控制:深度限制 + maxTurns
```

---

## 验收命令

```bash
cd chapters/13
npm install
npx tsx src/main.ts

# 验证点一:父 Agent 派发子 Agent
# 输入:"列出当前目录的文件,并统计行数最多的文件"
# 预期:父 Agent 派发 2 个子 Agent(一个列文件,一个统计行数)

# 验证点二:并行执行
# 输入:"分别找出 package.json 和 README.md 的内容摘要"
# 预期:父 Agent 同时派发 2 个子 Agent,两者并行运行(观察日志顺序)

# 验证点三:递归限制
# 要求子 Agent 再派发子 Agent 时,深度应被限制在 MAX_AGENT_DEPTH 以内
```

---

## 下一章

→ [第 14 章:插件与 MCP 集成](14-plugins-mcp)