<!-- title: 造三角色子代理团队接力迁移 -->
## 概述

本教程带你用 Claude Code 的**自定义子代理（subagents）**搭建一个"探索者→实现者→评审者"三角色接力流水线，把一个 CommonJS 小工程无人值守地迁移到 ESM。你将掌握 `.claude/agents/` 子代理文件的写法、frontmatter 字段、`Agent(type)` 协调者白名单、以及用 `-p` / `--agent` 无头模式跑通整条自动化链路。适合已写过一两个 Skill 或 MCP、想进入"多代理协作"阶段的 Claude Code 中级用户。

## 前置条件

- Claude Code **v2.1.198 及以上**（后台子代理默认开启、`manual` 权限别名、模型继承扩展思考均已就位；本教程用到 `--agent`、`--agents`、`Agent()` 白名单语法，建议直接用最新版）
- 一个可运行的 Node.js（≥ 18，用于 `node --check` 和跑测试）
- 已了解 `CLAUDE.md`、斜杠命令、`-p` 无头模式的基本用法
- 本教程聚焦 `.claude/agents/` 自定义子代理，与内置 `Explore`/`Plan` 子代理、Skills、MCP 是互补关系，无需先掌握它们

---

## 一、先理解子代理到底解决了什么

很多人第一次接触"多代理"会以为是开几个终端窗口。Claude Code 的子代理完全是另一回事：**它在同一个会话里，派生出一个拥有独立上下文窗口、独立系统提示、独立工具权限的"分身"**，干完活只把摘要回传给主会话。

这个机制解决三个真实痛点：

1. **上下文污染**——跑一次完整测试套件、读十几个源文件、抓一堆日志，这些会把你主会话的上下文撑爆。交给子代理，冗长输出留在它自己的窗口里，主会话只收到一句"3 个失败用例，原因分别是…"。
2. **权限收敛**——你可以给某个子代理只开 `Read, Grep, Glob`，让它"只能看不能改"，从结构上杜绝误删。
3. **角色分工与复用**——评审、实现、测试这类反复出现的角色，固化成一个 `.md` 文件，团队所有人都能用。

本次要造的三角色分工如下：

| 角色 | 文件名 | 能力 | 模型策略 |
|------|--------|------|----------|
| 探索者 | `cjs-explorer.md` | 只读扫描，产出迁移清单 | `haiku` 省钱 |
| 实现者 | `esm-implementer.md` | 改文件、装依赖 | `sonnet`，`acceptEdits` |
| 评审者 | `migration-reviewer.md` | 只读 + 跑测试 | `sonnet` |
| 协调者 | `migration-coordinator.md` | 调度上面三个 | `inherit` |

## 二、步骤一：搭好示例工程

先造一个最小可迁移的 CommonJS 工程，这样后面的代理有真东西可改。

```bash
mkdir esm-migrate-demo && cd esm-migrate-demo
git init -q
mkdir -p src
```

`package.json`：

```json
{ "name": "esm-migrate-demo", "version": "1.0.0", "type": "commonjs" }
```

`src/math.js`：

```js
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
module.exports = { add, multiply };
```

`src/index.js`：

```js
const { add, multiply } = require('./math');
function compute(x, y) { return multiply(add(x, y), 2); }
module.exports = { compute };
```

`test.js`（根目录）：

```js
const { compute } = require('./index');
const assert = require('assert');
assert.strictEqual(compute(2, 3), 10);
console.log('all tests passed');
```

验证起点是通的：

```bash
node test.js        # 期望输出: all tests passed
```

**常见问题**：如果 `node test.js` 报 `Cannot find module`，说明 `require('./index')` 的路径解析有误——确认 `src/index.js` 里导出的是 `compute`，且 `test.js` 与 `src/` 同级。

## 三、步骤二：造探索者（只读、省钱）

在工程根目录建子代理目录与第一个文件：

```bash
mkdir -p .claude/agents
```

`.claude/agents/cjs-explorer.md`：

```markdown
---
name: cjs-explorer
description: 扫描 CommonJS 工程并列出全部 require/module.exports 用法，产出 ESM 迁移清单。只读、不改任何文件。
tools: Read, Glob, Grep
model: haiku
color: cyan
---

你是 CommonJS→ESM 迁移的探索者。收到任务后：
1. 用 Glob 找出所有 `.js` 文件（含 test 文件）。
2. 用 Grep 定位每个文件里的 `require(...)`、`module.exports`、`exports.xxx`。
3. 读取相关文件确认细节。

输出**严格 JSON 清单**（不要多余解释），形如：
[
  { "file": "src/math.js", "requires": [], "exports": ["add", "multiply"], "needsExt": false },
  { "file": "src/index.js", "requires": [{ "from": "./math", "names": ["add", "multiply"], "addExt": true }], "exports": ["compute"], "needsExt": false },
  { "file": "test.js", "requires": [{ "from": "./index", "names": ["compute"], "addExt": true }, { "from": "assert", "builtin": true, "nodePrefix": true }], "exports": [], "needsExt": false }
]
规则：相对路径导入要标记 `addExt:true`（补 `.js`）；Node 内建模块标记 `nodePrefix:true`（改 `node:assert`）。
```

要点拆解：

- `tools: Read, Glob, Grep` 是**白名单**——只列这三个，子代理就没有 `Write/Edit/Bash`，物理上改不了文件。
- `model: haiku`：探索是"读+总结"的轻活，用 Haiku 能省下大量成本。
- `color: cyan`：在子代理面板里给探索者一个颜色，多代理并发时一眼区分。
- 正文是它的**系统提示**，子代理收不到完整的 Claude Code 系统提示，只看到你写的这段。

**常见问题**：子代理输出带了 Markdown 代码块包裹（```json）。这是 Haiku 常见习惯。解决方法是在正文里用"严格 JSON 清单（不要代码块包裹、不要多余解释）"这种强约束，或者让协调者做一次剥离。本教程让协调者解析。

## 四、步骤三：造实现者（可改、自动接受编辑）

`.claude/agents/esm-implementer.md`：

```markdown
---
name: esm-implementer
description: 接收探索者产出的迁移清单，把 CommonJS 改写为 ESM：require→import、module.exports→export、相对路径补 .js、内建模块加 node: 前缀、更新 package.json 的 type。
tools: Read, Edit, Write, Glob
model: sonnet
permissionMode: acceptEdits
color: green
---

你是 ESM 迁移的实现者。你会收到一份 JSON 迁移清单。逐文件执行：
- `const {a,b} = require('./x')` → `import {a,b} from './x.js'`
- `const x = require('assert')`（内建）→ `import x from 'node:assert'`
- `module.exports = {foo}` → `export {foo}`；`exports.foo = ...` → `export const foo = ...`
- `const m = require('./m')`（整体导入）→ `import * as m from './m.js'`
- 把 `package.json` 的 `"type": "commonjs"` 改为 `"type": "module"`。

完成后用 `Read` 自检每个文件语法，回传"已修改文件列表"与每个文件的关键改动行。不要运行测试——那是评审者的职责。
```

要点：

- `permissionMode: acceptEdits`：自动接受工作目录内的文件编辑与常见文件系统命令，免去逐个确认。注意：若主会话本身是 `bypassPermissions` 或 `acceptEdits`，会**强制覆盖**子代理的设置；若主会话是 `auto` 模式，子代理的 `permissionMode` 被**忽略**，直接继承 auto。
- 用 `sonnet` 而非 `haiku`：改写涉及细节判断（整体导入 vs 命名导入），需要更强的代码能力。

**常见问题**：实现者改完 `package.json` 后，旧测试还用 `require`，跑起来必然报错。**这是预期的**——本步不跑测试，把验证交给下一步。不要在实现者里塞测试逻辑，那是越界。

## 五、步骤四：造评审者（只读 + 跑测试）

`.claude/agents/migration-reviewer.md`：

```markdown
---
name: migration-reviewer
description: ESM 迁移完成后的评审者。对每个改动文件执行 node --check 语法校验，再跑测试，回传通过/失败结论与失败明细。不改正文代码。
tools: Read, Bash, Grep
disallowedTools: Write, Edit
model: sonnet
color: yellow
---

你是迁移评审者。任务：
1. 对所有被改动的 `.js` 文件执行 `node --check <file>`，收集语法错误。
2. 运行 `node test.js`（或工程定义的测试命令）。
3. 输出结构化结论：
   PASS 或 FAIL。
   若 FAIL，列出每个失败项：文件、错误信息、定位到的根因（如"相对导入缺 .js 扩展名"）。
不要修改代码。只报告。
```

要点：

- 这里同时用了 `tools` 白名单和 `disallowedTools`。当两者都设置时，`disallowedTools` 先生效（从继承池里移除），再用 `tools` 在剩余池里取交集——双保险确保评审者**绝对不会动代码**。
- 评审者有 `Bash`，所以能跑 `node --check` 和测试，但它**没有 Edit/Write**。

**常见问题**：`node --check` 对 ESM 的静态 `import` 校验很严格，"相对导入必须带扩展名"这类错误能被它第一时间抓出来，比直接跑测试定位更快。养成"先 `--check` 后测试"的两段式评审习惯。

## 六、步骤五：造协调者（编排三角色）

关键一步：协调者要能调用上面三个子代理，且**只能**调这三个。这用 `Agent(type)` 白名单语法实现。

`.claude/agents/migration-coordinator.md`：

```markdown
---
name: migration-coordinator
description: 协调 CommonJS→ESM 迁移的三角色流水线：先派 cjs-explorer 扫描，再派 esm-implementer 按清单改写，最后派 migration-reviewer 验证。失败则把评审反馈回传实现者重试，最多 2 轮。
tools: Agent(cjs-explorer, esm-implementer, migration-reviewer), Read, Bash
model: inherit
color: purple
---

你是迁移流水线的协调者。严格按下列顺序工作：
1. 派 `cjs-explorer` 扫描，拿到 JSON 迁移清单。若输出被代码块包裹，先剥离再传给下一步。
2. 把清单交给 `esm-implementer` 执行改写。
3. 派 `migration-reviewer` 校验。
4. 若评审为 FAIL：解析失败项，连同失败文件列表再次交给 `esm-implementer` 修复；重新评审。最多 2 轮重试。
5. 若 2 轮后仍 FAIL，停止并汇报剩余问题；若 PASS，汇报成功与改动摘要。

不要自己改代码——你没有 Edit/Write。你的职责是调度与传递上下文。
```

要点：

- `tools: Agent(cjs-explorer, esm-implementer, migration-reviewer), Read, Bash`：`Agent(a,b,c)` 是**允许派生的子代理类型白名单**。协调者尝试派生清单外的类型会失败。这是防止协调者"跑偏"去调内置 `general-purpose` 的护栏。
- 注意 `Agent(type)` 白名单语法**仅对以主线程身份运行**（`claude --agent`）的代理生效；作为子代理被派生时，括号里的类型列表会被忽略。本教程协调者正是用 `--agent migration-coordinator` 当主线程跑，所以白名单生效。
- `model: inherit`：协调者本身只做调度判断，用主会话模型即可。

## 七、步骤六：交互式跑通

先确认子代理被识别。重启一次会话（首次创建 `.claude/agents/` 目录后需要重启才能加载；之后改动文件会自动热加载，无需重启）：

```bash
claude
```

进入会话后输入一句点名协调者：

```
用 migration-coordinator 把 src/ 和 test.js 从 CommonJS 迁移到 ESM，评审通过为止。
```

预期行为：主会话识别到 `migration-coordinator`，通过 `Agent` 工具依次派生三个子代理。你会看到子代理面板里出现三个不同颜色的行（青/绿/黄），协调者行（紫）带 `(+N)` 后代计数。

成功跑完后，文件已变成 ESM：

```js
// src/index.js（迁移后）
import { add, multiply } from './math.js';
export function compute(x, y) { return multiply(add(x, y), 2); }
```

```js
// test.js（迁移后）
import { compute } from './index.js';
import assert from 'node:assert';
assert.strictEqual(compute(2, 3), 10);
console.log('all tests passed');
```

验证：

```bash
node test.js        # 期望: all tests passed
```

**常见问题**：若主会话没有自动派生协调者，改用 `@` 提及强制点名：`@"migration-coordinator (agent)" 把 src/ 迁移到 ESM`。`@` 提及保证"这个子代理一定会跑"，把选择权从模型手里拿过来。

## 八、步骤七：无头自动化（-p / --agent）

交互式确认无误后，把它变成一条可放进 CI 的命令。`--agent` 让整个会话以协调者的系统提示启动，`-p` 一次性投递任务：

```bash
claude --agent migration-coordinator \
  --permission-mode acceptEdits \
  -p "将 src/ 与 test.js 从 CommonJS 迁移到 ESM：相对导入补 .js，内建模块加 node: 前缀，更新 package.json 的 type。完整跑通探索→实现→评审，评审 PASS 才算完成。" \
  > migrate-report.txt 2>&1
```

要点：

- `--agent migration-coordinator`：主线程直接变成协调者，它的系统提示**替换**默认的 Claude Code 系统提示（和 `--system-prompt` 同理），但 `CLAUDE.md` 仍正常加载。
- `--permission-mode acceptEdits`：覆盖整个会话的权限模式，实现者改文件时不再逐个弹确认——这是无人值守的前提。
- 若想把它做成项目的默认代理，在 `.claude/settings.json` 里加 `"agent": "migration-coordinator"`，CLI 标志会覆盖该设置。

**临时一次性用法**：不想落盘子代理文件，可以用 `--agents` 直接传 JSON 定义。注意 JSON 里系统提示字段叫 `prompt`（等价于文件的正文 body）：

```bash
claude --agents '{
  "quick-reviewer": {
    "description": "快速只读评审，跑 node --check 与测试。",
    "tools": ["Read", "Bash", "Grep"],
    "disallowedTools": ["Write", "Edit"],
    "model": "sonnet",
    "prompt": "你是评审者，只报告 PASS/FAIL 与失败明细，不改代码。"
  }
}' -p "用 quick-reviewer 评审当前目录的 ESM 改造结果"
```

这条命令里子代理**只在本会话存活**、不写盘，适合脚本里临时组合能力。

## 九、增强：给评审者挂子代理级钩子

Claude Code 支持把生命周期钩子**作用域限定到单个子代理**（`hooks:` frontmatter 字段），这是 2026 年的新能力，比全局钩子更精准。给评审者加一个"每次跑完 Bash 命令就把输出存档"的钩子：

```yaml
# 在 migration-reviewer.md 的 frontmatter 里追加
hooks:
  PostToolUse:
    - matcher: Bash
      hooks:
        - type: command
          command: "mkdir -p .review-logs && cat >> .review-logs/run.log"
```

要点：

- `hooks:` 写在子代理 frontmatter 里，只在该子代理的生命周期内触发，不污染主会话或其他子代理。
- 一个重要自动转换规则：当你把代理当子代理派生时，它 frontmatter 里的 `Stop` 钩子会被**自动转成 `SubagentStop` 事件**——这正是"子代理跑完做收尾"的天然挂载点。
- 出于安全，**插件来源的子代理不支持** `hooks`、`mcpServers`、`permissionMode` 这三个字段，会被忽略；这三个字段只在 `.claude/agents/`、`~/.claude/agents/` 和 `--agents` 里有效。

**常见问题**：钩子命令的 `stdin` 是事件 JSON，所以上面用 `cat >> run.log` 能把每次 Bash 工具调用的元数据（含退出码、命令）追加进日志。调试钩子时先 `cat > /tmp/hook.json` 看一眼真实载荷结构再写处理逻辑。

## 十、实战案例：完整流水线总览

把整条链路画成两张图。第一张是数据流（谁产出什么交给谁）：

```mermaid
flowchart LR
    U([用户 / -p 任务]) --> CO[migration-coordinator<br/>协调者]
    CO -->|1. 扫描| EX[cjs-explorer<br/>只读·haiku]
    EX -->|JSON 迁移清单| CO
    CO -->|2. 改写+清单| IM[esm-implementer<br/>可改·sonnet]
    IM -->|改动摘要| CO
    CO -->|3. 校验| RV[migration-reviewer<br/>只读+Bash·sonnet]
    RV -->|PASS / FAIL+明细| CO
    CO -. FAIL→重试最多2轮 .-> IM
    CO -->|PASS 摘要| R([完成报告])
```

第二张是并发与时序（注意 v2.1.198 起子代理默认在后台跑，可并发推进不互相阻塞的任务）：

```mermaid
sequenceDiagram
    participant Main as 主会话
    participant CO as 协调者
    participant EX as 探索者
    participant IM as 实现者
    participant RV as 评审者
    Main->>CO: --agent + -p 派发迁移任务
    CO->>EX: 扫描工程（白名单内）
    EX-->>CO: 返回迁移清单 JSON
    CO->>IM: 传清单，执行改写
    IM-->>CO: 改动文件摘要
    CO->>RV: 校验（node --check + test）
    alt PASS
        RV-->>CO: PASS
        CO-->>Main: 成功摘要
    else FAIL
        RV-->>CO: FAIL + 失败明细
        CO->>IM: 回传失败项修复
        IM-->>CO: 二次改动
        CO->>RV: 重新校验
    end
```

## 十一、常见问题汇总

**Q1：协调者不按顺序，自己跑去读文件改代码了。**
检查 `tools` 是否误开了 `Edit/Write`，并确认 `Agent(...)` 白名单语法写对。协调者只应有 `Agent(三个名字), Read, Bash`。没有 `Edit/Write`，它物理上改不了代码。

**Q2：探索者跑得慢、成本高。**
确认 `model: haiku` 真的生效。模型解析优先级是：环境变量 `CLAUDE_CODE_SUBAGENT_MODEL` > 单次调用的 `model` 参数 > frontmatter `model` > 主会话模型。如果你设了该环境变量，会覆盖 frontmatter。

**Q3：子代理之间要不要传完整文件内容？**
不要。让探索者只回传**清单**，协调者把清单交给实现者——实现者自己去 `Read` 文件。传完整文件内容等于把上下文污染从子代理搬回协调者，违背隔离初衷。

**Q4：评审者卡在测试失败，实现者改了两轮还过不了。**
协调者上限设的是 2 轮重试。超出就停止并汇报，把剩余问题交还人类判断。这是"有界自主"的设计——避免无限循环烧钱。可按任务复杂度调整协调者正文里的重试上限。

**Q5：想并行迁移多个工程。**
子代理在**单个会话内**工作；要跑很多独立会话并行，应该用 background agents（多会话并行+集中监控），那是另一套机制，不在本教程范围。

**Q6：并发派多个探索者会不会乱套？**
不会。每个子代理有独立上下文与独立工作目录起点（默认共享主会话 cwd）。若担心并行改文件互相覆盖，给实现者加 `isolation: worktree`，让它在一个临时 git worktree 里改，无改动时自动清理。

## 十二、设计取舍：子代理 vs 主会话 vs Skills

这是最容易被忽视的一层判断，选错机制事倍功半：

- **用主会话**：需要频繁来回迭代、多阶段共享大量上下文、改动很小、延迟敏感。主会话没有"派生+回传"的开销。
- **用子代理**：产出大量你不需要留在主上下文的输出（测试日志、文档抓取）、想强制工具/权限收敛、任务是自包含的可回传摘要。本教程的迁移评审就是典型——测试输出很脏，留在评审者窗口里，主会话只收 PASS/FAIL。
- **用 Skills**：要复用的是**提示词或工作流**，且希望它跑在主会话上下文里（而非隔离窗口）。Skills 没有"独立上下文+独立权限"的隔离性。

一句话记忆：**子代理=独立房间+独立钥匙；Skill=主房间里的一张操作手册。**

## 训练营适用性评估

- **适合类型**：进阶训练营（已过入门关的 Claude Code 用户）、企业内部"AI 研发效能"专题、多代理/Agent 工作坊。不适合纯零基础——需要对 frontmatter、`-p` 模式、权限模式有基本认知。
- **教学时长**：约 **2.5–3 小时**。其中概念与三角色设计 45 分钟、手写四个 `.md` 文件 60 分钟、交互式跑通与排错 45 分钟、无头自动化与钩子增强 30–45 分钟。
- **可扩展练习**：① 把迁移目标换成"回调地狱→async/await"；② 给评审者加 `memory: project`，让它跨会话积累常见失败模式；③ 用 `--agents` JSON 在一条命令里临时组装"探索者+评审者"两角色做代码体检；④ 引入嵌套子代理（评审者对每个失败项再派一个验证者），体会"中间输出不回主会话"的纵深隔离。
- **考核点**：能否解释"为什么探索者只读+haiku、实现者可改+sonnet"的分工理由；能否用 `Agent(type)` 白名单把协调者关进笼子；能否产出一条放进 CI 的 `--agent ... -p ...` 无人值守命令。

## 参考来源

1. [Create custom subagents — Claude Code Docs](https://code.claude.com/docs/en/sub-agents) — 官方子代理权威文档，覆盖 frontmatter 全字段、`Agent(type)` 白名单、权限模式、链式与嵌套派生、v2.1.198 后台默认等核心机制。
2. [Subagents in the SDK — Claude Code Docs](https://code.claude.com/docs/en/agent-sdk/subagents) — Agent SDK 中定义与调用子代理、隔离上下文、并行任务与专项指令的官方说明。
3. [How to Build Custom Sub-Agents in Claude Code: YAML, Tools — MindStudio](https://www.mindstudio.ai/blog/build-custom-sub-agents-claude-code-yaml) — 详解子代理 YAML frontmatter 的 `description`/`tools`/`model` 写法与触发准确率实践。
4. [Claude Code Subagents: Parallelize Development — AI for Anything (2026)](https://aiforanything.io/blog/claude-code-subagents-parallel-development-guide-2026) — 2026 年视角的子代理并行开发与多代理工作流编排指南。
5. [Claude Code Subagents, Background Agents & Agent Teams in 2026 — SaaSCity](https://saascity.io/blog/claude-code-subagents-agent-teams-2026) — 区分子代理（单会话）、background agents（多会话并行）、agent teams（会话间通信）三类机制的对比。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
