写一份自定义 subagent,本质上是写一个带 YAML frontmatter 的 Markdown 文件,frontmatter 决定”它是谁、能用什么工具、跑在哪个模型上”,正文成为它的系统提示。规范层面只有两个字段是必填的:name 与 description;其余字段按需出现,但都有自己的语义约束。model 字段尤其值得拆开看——它的可选值、默认值、以及在 Explore 子代理与用户级 subagent 两种场景下的解析路径都不相同,理解清楚再写能省掉大量”为什么没生效”的踩坑。
一、一份最简 subagent 长什么样
---
name: code-reviewer
description: 审查代码质量与可维护性问题
tools: Read, Glob, Grep
model: sonnet
---
你是一名代码审查专家,针对变更提供具体可执行的反馈。
把它放进 .claude/agents/(项目级)或 ~/.claude/agents/(用户级),Claude Code 启动时或调用 /agents 后即会识别。name 是调用时的标识符,description 是 Claude 决定”是否自动委托”的关键依据。tools 与 model 不写就走继承。
二、frontmatter 字段清单与约束
| 字段 | 是否必填 | 取值 | 默认 | 关键约束 |
|---|---|---|---|---|
name |
是 | 小写字母 + 连字符 | — | 不能含 :(保留给插件命名空间,如 my-plugin:reviewer),不能以 - 开头 |
description |
是 | 自由文本 | — | 写”何时委托”,措辞越精准,自动委托越准 |
tools |
否 | 工具名列表 | 继承所有可用工具 | 解析不到的工具会让 subagent 启动失败并报错 |
disallowedTools |
否 | 工具名列表 | 不拒绝任何工具 | 从继承或显式 tools 中再次扣除 |
model |
否 | 别名或完整模型 ID 或 inherit |
inherit |
别名包含 sonnet / opus / haiku / fable,也可写 claude-opus-4-6 这样的完整 ID |
permissionMode |
否 | default / acceptEdits / auto / dontAsk / bypassPermissions / plan / manual |
default |
插件 subagent 上的同名字段会被忽略 |
maxTurns |
否 | 整数 | 不限 | 到达上限 subagent 自动停止 |
skills |
否 | 技能名列表 | 不预加载 | 列出后会把完整技能内容注入到启动上下文 |
mcpServers |
否 | MCP 服务器名或内联配置 | 不引入 | 插件 subagent 上同样被忽略 |
hooks |
否 | 生命周期 hook 配置 | 不挂 hook | 仅作用于本 subagent,插件 subagent 上被忽略 |
memory |
否 | user / project / local |
不启用 | 启用后可跨会话持续学习 |
background |
否 | true / false |
false |
true 时即便主会话要求前台也保持后台运行 |
effort |
否 | low / medium / high / xhigh / max |
继承会话 | 可用档位取决于模型 |
isolation |
否 | worktree |
不隔离 | 启用后在临时 git worktree 中运行 |
color |
否 | 预设颜色名 | — | 决定任务列表与转写中的显示色 |
initialPrompt |
否 | 自由文本 | — | 作为 subagent 作为主会话代理时的首条用户消息 |
字段被遗漏时的兜底逻辑是”写有写无的语义”:name 缺失,文件被当作普通文档;name 有但 description 缺失,文件被跳过并写入 debug 日志;YAML 解析失败则完全不读字段、跳过文件。
三、model 字段的取值与默认值
model 字段接受三类写法:
- 别名:
sonnet / opus / haiku / fable; - 完整模型 ID:例如
claude-opus-4-6、claude-sonnet-4-6; inherit:明确告诉 Claude Code”沿用上一层解析出的模型”。
默认值是 inherit,这意味着不在 frontmatter 里写 model,subagent 就会跟随”主会话”或”调用方”使用的模型。inherit 与”留空”在 v2.1.196 之后行为完全一致,更早版本里 CLAUDE_CODE_SUBAGENT_MODEL=inherit 会强制把每个 subagent 锁在主模型上,忽略 frontmatter 与调用参数——这是历史踩坑点。
四、model 字段的解析优先级
model 的最终值由四层按优先级解析,从高到低依次为:
- 环境变量
CLAUDE_CODE_SUBAGENT_MODEL; - Agent 工具调用时传入的 per-invocation
model参数; - subagent frontmatter 里的
model字段; - 主会话的当前模型。
换句话说:环境变量是”全场开关”,frontmatter 是”个体默认”,per-invocation 参数是”这一次调用单独覆盖”。前两层是真正可控的入口;per-invocation 参数没有面向用户的 prompt 级语法,靠 Agent 工具内部传递。
五、Explore 子代理 vs 用户级 subagent:model 的差异
内置 Explore 子代理(快速低延迟,只读工具,专门做代码搜索)的 model 字段硬编码为 haiku——目的是把广撒网的探索丢给最便宜的模型,把”读结果、做判断”留给主会话。用户级 subagent 则不同,它的 model 字段遵循上文四层优先级,可被环境变量、调用参数、frontmatter 三种方式覆盖。
下面对比两条路径:
| 场景 | 能否修改 model | 推荐做法 |
|---|---|---|
| 内置 Explore 子代理 | 不建议覆盖 | 直接用默认 haiku 跑广撒网搜索 |
用户级 subagent(项目级 .claude/agents/) |
可改 | frontmatter 写 model: sonnet 或完整 ID |
用户级 subagent(~/.claude/agents/,跨项目) |
可改 | frontmatter 写 model: sonnet 或完整 ID |
临时通过 Agent 工具传入 model 参数 |
可覆盖 | 调用方控制”这一次”跑哪个模型 |
六、name 与 description 之外的语义约束
name 不只是标识符,还是 hook 在 agent_type 字段收到的值,文件名不必与 name 相同。description 是 Claude 决定”是否自动委托”的关键依据,写得含糊会同时带来误触发与漏触发;写得越具体,自动路由的命中率越高。tools 列表中若含无法解析的条目,subagent 启动时会直接报错并列出未解析项名;想预加载 Skills 内容请用 skills 字段,而不是把 Skill 名塞进 tools——后者不会展开完整内容。
permissionMode 在插件 subagent 上会被忽略,因为插件已经预设了权限上下文;hooks 与 mcpServers 同理被插件 subagent 忽略。memory 启用后能让 subagent 跨会话积累经验,配合 user 级 subagent 适合做”长期调优的领域专家”。
七、落地时的三个易错点
第一,别让 name 含 :。这是保留给插件命名空间(如 my-plugin:reviewer)的字符,含冒号的名字会被静默跳过、写入 debug 日志,前台没有任何提示。第二,别把 disallowedTools 写在 tools 之外又与 tools 重复声明的同一类工具上——行为可预测,但容易在维护时出现”我以为我禁了某个工具”的错觉,统一从一个口进。第三,model: haiku 写在用户级 subagent 上要慎重:haiku 适合”快速、字面”的探索任务,写”做架构判断”的 subagent 仍应给 sonnet 或 opus。
常见问题(FAQ)
Q1:subagent 的 model 字段默认值是什么?
inherit,即沿用上层解析出的模型;不写与写 inherit 等价(v2.1.196 起)。
Q2:能让一次调用临时换模型吗?
可以,由 Agent 工具调用方传 model 参数覆盖;该值优先级高于 frontmatter 但低于环境变量。
Q3:用户级 subagent 和项目级 subagent 的 model 解析一样吗?
一样,二者都走相同的四层优先级;区别在于加载位置与适用范围,解析逻辑共用。