Claude Code 子代理实战:写一个只读的测试评审员,让写的和查的分开
子代理是一份 Markdown 文件:给它一个名字、一段说明、一份工具清单,Claude Code 就能把一类任务交给它在独立的上下文里做。本站实测了一个只读的测试评审员,它查出了上一篇里补的测试的真问题。还试了让子代理换用便宜的模型,并记下通过中转站时遇到过的一条报错。
本站已实测,测试日期:2026-10-12。
这篇教程解决一个问题:有些活你不想让主对话自己干,或者不想让干活的那个自己检查自己,怎么办。
办法是子代理(subagent)。它是 Claude Code 在同一个会话里派出去的另一个“自己”:有独立的上下文、自己的一段系统提示词、一份你限定的工具清单。它做完之后只把结论交回来,中间读过的文件和跑过的命令不进你的主对话。
官方文档给的使用时机是:一件附带的任务会把你的主对话塞满搜索结果、日志、文件内容,而你之后不会再看它们,就交给子代理;同一类活你反复要派,就把它定义成一个自定义的子代理。
本站接着上一篇,写了一个只读的测试评审员,让它去审那篇里补的测试。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-11,子代理换模型一节在 10 月 12 日复测 |
| 版本 | Claude Code 2.1.296 |
| 系统 | Ubuntu 24.04 |
| 模型 | Opus 5.5,auto 模式 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 示例仓库 | Express 的源码,v5.3.0,带着上一篇新加的 6 个测试 |
你其实已经在用了
Claude Code 自带几个子代理,会在合适的时候自己派出去:
| 内置子代理 | 做什么 | 工具 |
|---|---|---|
| Explore | 搜索和分析代码库 | 只读,不能写和改 |
| Plan | plan 模式下替主对话做调研 | 只读 |
| General-purpose | 既要探索又要动手的多步任务 | 全部 |
所以你让它“看看这个项目的认证是怎么做的”,它很可能是派 Explore 去翻文件,自己只拿结论。这一篇讲的是怎么写自己的。
一个子代理就是一个文件
子代理是带一段头部信息的 Markdown 文件。放在哪决定谁能用:
| 位置 | 范围 |
|---|---|
.claude/agents/ |
这个项目,可以提交给团队 |
~/.claude/agents/ |
你的所有项目 |
我们在项目里建了 .claude/agents/test-reviewer.md:
---
name: test-reviewer
description: 审查测试代码的质量。新增或修改测试之后使用。只读,不修改任何文件。
tools: Read, Grep, Glob
model: inherit
---
你是一个只做审查的测试评审员。拿到要审查的测试文件后:
1. 读这个测试文件,以及它测的那段源码。
2. 逐个用例检查:
- 断言检查的是对外的行为,还是实现细节?
- 被测的代码如果坏了,这个用例会不会失败?有没有永远为真的断言?
- 有没有把“碰巧如此”的行为写进断言?
- 和文件里原有用例的写法是否一致?
3. 只报告确实值得改的问题。没有问题就直说没有,不要凑数。
输出格式:先用一句话给出结论,然后列出问题。每个问题写明:文件和行号、问题是什么、建议怎么改。
你不能修改文件。需要改的地方只给建议,由调用你的一方去改。
头部的几个字段:
| 字段 | 作用 |
|---|---|
name |
名字,必填。调用时用它 |
description |
必填。Claude 靠这句话判断什么时候该把任务交给它,所以要写清楚“什么情况下用” |
tools |
它能用的工具。不写就继承全部。这里只给了读文件和搜索,所以它改不了任何东西 |
model |
用哪个模型。inherit 表示和主对话一样。换成别的模型的情况见下文 |
正文就是它的系统提示词。官方文档说明,子代理拿到的只有这段提示词加上工作目录这类基本信息,没有 Claude Code 完整的系统提示词。所以它该怎么做、输出什么格式,都要在这里写明白。
两个细节,都来自官方文档:
- 一个项目里第一次建
agents目录,要重启会话才能加载。 目录已经存在时,新增和修改文件几秒内就会生效。 description占的是主对话的上下文。 我们这个子代理在/context里显示为Custom agents: 73 tokens。子代理多了,说明文字要写短。
顺带一提:老教程里常说用 /agents 命令创建子代理。我们在 2.1.296 上输入它,得到的是:
The /agents wizard has been removed.
Ask Claude to create or update subagents for you (e.g. "create a code-reviewer subagent that ..."),
or edit the files directly:
• .claude/agents/ (this project)
• ~/.claude/agents/ (all projects)
这个向导已经去掉了。现在的做法是直接写文件,或者让 Claude 替你写。
派它去干活
在对话里点名就行:
用 test-reviewer 子代理审查 test/res.redirect.js 里最近新增的三个关于弃用警告的用例。等它做完,把结论告诉我。
屏幕上是这样的:
● Bash(git diff test/res.redirect.js)
● test-reviewer(Review redirect deprecation tests)
⎿ Backgrounded agent (↓ to manage · ctrl+o to expand)
● test-reviewer 子代理已经开始审查 test/res.redirect.js 里新增的三个弃用警告用例,结果还没出来。
我让它重点核对这几件事:
- 警告文本:断言里的消息是否和实现真正发出的一致。
- 事件机制:process 上的 deprecation 事件会不会真的触发……
- 第三个用例的写法:res.redirect('http://google.com', 301) 里的 301 是数字,可能根本测不到
"status 不是数字"这个情况。
- 断言质量:assert.ok 加 indexOf 是否不够精确,是否也该检查状态码和 Location 头。
✻ Waiting for 1 background agent to finish
● main
◯ test-reviewer Review redirect deprecation tests 11s · ↓ 5.1k tokens
三点观察:
test-reviewer(...)那一行就是委派。 括号里是主对话给这次任务起的简短说明。- 它是在后台跑的。 主对话没有被卡住,底部多出一个任务列表,能看到子代理在做什么、用了多少 token。官方文档说,交互会话里默认就是后台运行。
- 任务说明是主对话写的。 我们只说了一句“审查这三个用例”,它替我们展开成了四个具体的检查点交给子代理。
1 分 8 秒后,子代理交回了结论。
它查出了什么
主对话转述的结论(有删节):
test-reviewer 审完了。三个用例都能通过,断言里的弃用消息文本也和 lib/response.js 的实现完全一致。但第 3 个
用例实际返回的是 500,测试没有发现。另外三个用例都没检查响应,也没检查到底发出了哪几条警告。下面是它读
代码得出的结论,我没有自己跑测试验证。
逐个用例
1. 缺少 url:会同时发出 'Provide a url argument' 和 'Url must be a string' 两条警告……
3. status 不是数字:只传两个参数时,'http://google.com' 会被当作 status,301 被当作 url。……
- 但这次调用还会多发一条 'Url must be a string'。随后 this.status('http://google.com') 抛出 TypeError,
最终响应是 500。
- 测试里没写 .expect(状态码),supertest 不会把 500 当成错误,所以用例照样通过,这个问题被掩盖了。
修改建议
- 断言:现在的 assert.ok(indexOf !== -1) 太宽,失败时只报 false == true。建议改成 assert.deepStrictEqual
比对完整的警告列表……
- 用例 3 最要紧:明确断言 .expect(500),并在用例名或注释里说明……
这是有分量的意见。上一篇里那三个用例通过了“改坏代码会失败”的验证,我们也复核过数字,但没有人注意到第 3 个用例的请求其实是以 500 结束的。写测试的那个会话知道这件事(它提过“参数写反会返回 500”),选择了不写进断言;评审员从另一个角度看,认为应该明确断言出来,否则这个 500 就一直藏着。
谁对可以讨论。要紧的是:这个分歧只有在换一个“人”来看的时候才会出现。
官方文档把这当作一种验证手段:让一个全新的模型去挑毛病,做事的那个就不是给自己打分的那个。
还要注意主对话加的那句话:“下面是它读代码得出的结论,我没有自己跑测试验证。”评审员是只读的,跑不了测试,它的结论来自读代码。主对话把这一点如实标了出来。
工具清单是硬约束
我们故意让它越界:
让 test-reviewer 子代理自己去改 test/res.redirect.js:给第 3 个用例加上 .expect(500)。你不要替它改。
这个没法照办:test-reviewer 被配置成只读,工具只有 Read、Grep、Glob,没有 Edit 或 Write,改不了
test/res.redirect.js。你又说了不要我替它改,所以我什么都没动,文件还是原样。
可以选一条路继续:
1. 由我来改……
2. 给 test-reviewer 加编辑权限:在 .claude/agents/test-reviewer.md 的 tools 里加上 Edit,它就能自己改了。
但这样它就不再是只读的审查代理了。这个配置得你来决定,或者你明确让我改。
3. 另开一个有编辑权限的子代理(比如 general-purpose)去改这一处。
我们用 git diff --stat 确认过,文件没有变。
这和在提示词里写“请不要修改文件”不一样。没给它编辑工具,它就没有这个能力,不取决于它听不听话。
让子代理用便宜一点的模型
官方文档建议的一个省钱办法,是把简单的活交给便宜的模型,比如在子代理里写 model: sonnet 或 model: haiku。只用改头部的一行:
model: sonnet
主对话还是 Opus 5.5。让它先自己读一遍测试文件、说出判断,再派 test-reviewer 去审,子代理返回后主对话照常汇总,接着追问也正常。结束后看 /cost,两个模型是分开算的:
Total cost: $0.3573
Usage by model:
claude-opus-5-5: 1.3k input, 2.4k output, 174.6k cache read, 38.8k cache write ($0.2823)
claude-sonnet-5-5: 242 input, 4.8k output, 18.4k cache read, 9.7k cache write ($0.0750)
评审那部分按 Sonnet 计价,这次是 0.075 美元。这一轮审的是另一个测试文件,活的大小和前面那次不一样,所以这个数不能直接拿去和前面比,只能说明子代理确实换了模型、也确实单独计了费。
model: haiku 我们没有试:这把 key 调不了 Haiku 5.5,见通过中转站用 Claude Code。
通过中转站时遇到过的一条报错
上面这个写法,我们第一次试的时候是坏的。派出子代理之后,主对话报了这个:
● test-reviewer(Review redirect deprecation tests)
⎿ Backgrounded agent (↓ to manage · ctrl+o to expand)
● API Error: 400 当前会话的 thinking 签名属于其他模型,请执行 /clear 新建空会话后重试
子代理自己跑完了,/cost 里两个模型都有用量。但主对话从派出子代理那一刻起,每一次请求都返回这条 400,再发消息也一样,子代理的结论拿不回来。
这条报错是中文的,来自中转站的网关,不是 Claude Code 也不是 Anthropic 的报错。它也不是每次都出:当时同样的配置跑了三次,两次报错,一次正常,差别在派出子代理之前主对话有没有产生过思考块。对照表放在通过中转站用 Claude Code那一篇里。
中转站一方随后做了修复。10 月 12 日我们专挑之前会出错的情况复测了三次,主对话都正常,上面那段 /cost 就是其中一次的。
如果你用的中转站出现这条报错,可以这样绕开:
- 按提示输入
/clear,清空对话。 - 把子代理文件里的
model: sonnet改成model: inherit,让它和主对话用同一个模型。
我们当时就是这么处理的,前面那次成功的评审用的就是 model: inherit,整个会话 /cost 显示 0.63 美元。代价是省不了钱,子代理和主对话一样用 Opus。
要说明这个结论的范围:
- 只在酷站AI 这一家中转站上试过。 别家会不会这样,要看各自网关的实现。直连官方时,官方文档明确支持子代理用不同的模型。
- 根因我们不知道,只确认了现象、它和思考块的关系、绕开的办法,以及修复之后三次都正常。
- 官方文档里还有一个环境变量
CLAUDE_CODE_SUBAGENT_MODEL可以给所有子代理指定模型,我们没有测它。
另外两种调用方式
按官方文档,除了在话里点名,还有:
用 @ 指定。 点名时由 Claude 决定要不要委派;用 @ 则保证是这个子代理来做:
@agent-test-reviewer 看一下 test/req.is.js 新增的用例
整个会话都用它。 启动时加参数,主线程自己就带上这个子代理的工具限制和模型:
claude --agent test-reviewer
这两种我们没有实测。
想让 Claude 更主动地用某个子代理,官方的建议是在它的 description 里写上“use proactively”这类说法。
什么时候用子代理,什么时候不用
官方文档的划分:
| 用主对话 | 用子代理 |
|---|---|
| 要来回讨论、反复调整 | 会产生一大堆你不需要留在主对话里的输出 |
| 计划、实现、测试几个阶段共用很多上下文 | 想硬性限制它能用的工具或权限 |
| 很快的小改动 | 任务自成一体,交回一个总结就够 |
| 在意等待时间:子代理是从零开始的,要花时间重新了解情况 |
几种相近的东西怎么分:
| 你想要的 | 用什么 |
|---|---|
| 一套可以反复用的步骤,在主对话里执行 | skill |
| 一个独立上下文里干活的角色,可以限制工具 | 子代理 |
| 对已经在对话里的内容问一句,不想留痕 | /btw |
还有一点成本上的提醒:子代理发的是它自己的请求,和主对话算在同一份用量里。它帮你省的是主对话的上下文,不是总的 token。
这次实测没有覆盖到的
- 子代理换模型只在这一家中转站上测过,修复后跑了三次。
model: haiku没有测,直连官方时的表现我们也没有条件测。 - 只测了点名调用。
@指定和--agent没有测。 - 自动委派没有测。 我们每次都点了名,没有观察它会不会自己想起来用这个子代理。
- 头部的其他字段没有测:
permissionMode、maxTurns、skills、isolation: worktree等,官方文档里有完整的列表。 - 只跑了一次成功的评审。 它这次查出了真问题,不代表每次都有这个水平。