本教程带你用 Claude Code 的自定义子代理(subagents)搭建一个"探索者→实现者→评审者"三角色接力流水线,把一个 CommonJS 小工程无人值守地迁移到 ESM。你将掌握 .claude/agents/ 子代理文件的写法、frontmatter 字段、Agent(type) 协调者白名单、以及用 -p / --agent 无头模式跑通整条自动化链路。适合已写过一两个 Skill 或 MCP、想进入"多代理协作"阶段的 Claude Code 中级用户。
manual 权限别名、模型继承扩展思考均已就位;本教程用到 --agent、--agents、Agent() 白名单语法,建议直接用最新版)node --check 和跑测试)CLAUDE.md、斜杠命令、-p 无头模式的基本用法.claude/agents/ 自定义子代理,与内置 Explore/Plan 子代理、Skills、MCP 是互补关系,无需先掌握它们很多人第一次接触"多代理"会以为是开几个终端窗口。Claude Code 的子代理完全是另一回事:它在同一个会话里,派生出一个拥有独立上下文窗口、独立系统提示、独立工具权限的"分身",干完活只把摘要回传给主会话。
这个机制解决三个真实痛点:
Read, Grep, Glob,让它"只能看不能改",从结构上杜绝误删。.md 文件,团队所有人都能用。本次要造的三角色分工如下:
| 角色 | 文件名 | 能力 | 模型策略 |
|---|---|---|---|
| 探索者 | cjs-explorer.md |
只读扫描,产出迁移清单 | haiku 省钱 |
| 实现者 | esm-implementer.md |
改文件、装依赖 | sonnet,acceptEdits |
| 评审者 | migration-reviewer.md |
只读 + 跑测试 | sonnet |
| 协调者 | migration-coordinator.md |
调度上面三个 | inherit |
先造一个最小可迁移的 CommonJS 工程,这样后面的代理有真东西可改。
mkdir esm-migrate-demo && cd esm-migrate-demo
git init -q
mkdir -p src
package.json:
{ "name": "esm-migrate-demo", "version": "1.0.0", "type": "commonjs" }
src/math.js:
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
module.exports = { add, multiply };
src/index.js:
const { add, multiply } = require('./math');
function compute(x, y) { return multiply(add(x, y), 2); }
module.exports = { compute };
test.js(根目录):
const { compute } = require('./index');
const assert = require('assert');
assert.strictEqual(compute(2, 3), 10);
console.log('all tests passed');
验证起点是通的:
node test.js # 期望输出: all tests passed
常见问题:如果 node test.js 报 Cannot find module,说明 require('./index') 的路径解析有误——确认 src/index.js 里导出的是 compute,且 test.js 与 src/ 同级。
在工程根目录建子代理目录与第一个文件:
mkdir -p .claude/agents
.claude/agents/cjs-explorer.md:
---
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:在子代理面板里给探索者一个颜色,多代理并发时一眼区分。常见问题:子代理输出带了 Markdown 代码块包裹(```json)。这是 Haiku 常见习惯。解决方法是在正文里用"严格 JSON 清单(不要代码块包裹、不要多余解释)"这种强约束,或者让协调者做一次剥离。本教程让协调者解析。
.claude/agents/esm-implementer.md:
---
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:
---
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:
---
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/ 目录后需要重启才能加载;之后改动文件会自动热加载,无需重启):
claude
进入会话后输入一句点名协调者:
用 migration-coordinator 把 src/ 和 test.js 从 CommonJS 迁移到 ESM,评审通过为止。
预期行为:主会话识别到 migration-coordinator,通过 Agent 工具依次派生三个子代理。你会看到子代理面板里出现三个不同颜色的行(青/绿/黄),协调者行(紫)带 (+N) 后代计数。
成功跑完后,文件已变成 ESM:
// src/index.js(迁移后)
import { add, multiply } from './math.js';
export function compute(x, y) { return multiply(add(x, y), 2); }
// test.js(迁移后)
import { compute } from './index.js';
import assert from 'node:assert';
assert.strictEqual(compute(2, 3), 10);
console.log('all tests passed');
验证:
node test.js # 期望: all tests passed
常见问题:若主会话没有自动派生协调者,改用 @ 提及强制点名:@"migration-coordinator (agent)" 把 src/ 迁移到 ESM。@ 提及保证"这个子代理一定会跑",把选择权从模型手里拿过来。
交互式确认无误后,把它变成一条可放进 CI 的命令。--agent 让整个会话以协调者的系统提示启动,-p 一次性投递任务:
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):
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 命令就把输出存档"的钩子:
# 在 migration-reviewer.md 的 frontmatter 里追加
hooks:
PostToolUse:
- matcher: Bash
hooks:
- type: command
command: "mkdir -p .review-logs && cat >> .review-logs/run.log"
要点:
hooks: 写在子代理 frontmatter 里,只在该子代理的生命周期内触发,不污染主会话或其他子代理。Stop 钩子会被自动转成 SubagentStop 事件——这正是"子代理跑完做收尾"的天然挂载点。hooks、mcpServers、permissionMode 这三个字段,会被忽略;这三个字段只在 .claude/agents/、~/.claude/agents/ 和 --agents 里有效。常见问题:钩子命令的 stdin 是事件 JSON,所以上面用 cat >> run.log 能把每次 Bash 工具调用的元数据(含退出码、命令)追加进日志。调试钩子时先 cat > /tmp/hook.json 看一眼真实载荷结构再写处理逻辑。
把整条链路画成两张图。第一张是数据流(谁产出什么交给谁):
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 起子代理默认在后台跑,可并发推进不互相阻塞的任务):
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 里改,无改动时自动清理。
这是最容易被忽视的一层判断,选错机制事倍功半:
一句话记忆:子代理=独立房间+独立钥匙;Skill=主房间里的一张操作手册。
-p 模式、权限模式有基本认知。.md 文件 60 分钟、交互式跑通与排错 45 分钟、无头自动化与钩子增强 30–45 分钟。memory: project,让它跨会话积累常见失败模式;③ 用 --agents JSON 在一条命令里临时组装"探索者+评审者"两角色做代码体检;④ 引入嵌套子代理(评审者对每个失败项再派一个验证者),体会"中间输出不回主会话"的纵深隔离。Agent(type) 白名单把协调者关进笼子;能否产出一条放进 CI 的 --agent ... -p ... 无人值守命令。Agent(type) 白名单、权限模式、链式与嵌套派生、v2.1.198 后台默认等核心机制。description/tools/model 写法与触发准确率实践。[!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 等场景全覆盖。
