写这篇文章的动机,其实来自我在实际项目里的一次"崩溃式体验"——用 Claude Code 处理一个跨模块重构任务时,默认的 Agent 行为总是在主对话里“大包大揽”,一会儿分析代码、一会儿查文档、一会儿给建议,上下文越拖越长,最后核心任务反而跑偏了。研究了一圈才发现,这工具本身就支持子代理(subagents)机制,用户可以自定义一套属于自己项目和团队的 Agent 角色,把高频且独立的任务拆分出去。这个功能的价值远不止"加几个角色",而是重新理解了 AI 编程工具的分工逻辑。这篇文章就聊聊我怎么从"内置子代理不够用"到"自己造了几个趁手子代理"的完整过程,包括配置文件的写法、提示词的设计细节、以及我踩过的各种坑。
1. 内置子代理到底哪里不够用
1.1 子代理机制的本质是什么
要理解自定义子代理的价值,先得搞清楚 Claude Code 的 Agent 调度模型。它并不是一个单一 AI 在从头到尾干所有事,而是有一个主对话 Agent 来理解你的整体意图、拆解执行步骤,再按需调度不同的子 Agent 去完成特定任务。这些子 Agent 有独立的系统提示词、独立的上下文窗口、独立的任务边界,干完之后把结果汇总回主对话。
这个机制的设计初衷非常像大型团队的协作方式:主 Agent 是"团队负责人",子 Agent 是"专项组"。如果所有事都堆在主 Agent 的上下文里,对话一长,前面的信息会被截断,模型的注意力会被稀释,输出质量也会肉眼可见地下降。子代理存在的意义,本质上是把复杂任务做上下文隔离——各干各的,最后交付结果。
听起来很完美,但实际用了就会发现问题。
1.2 内置子代理的典型局限
Claude Code 默认会提供一些内置的子代理角色,比如负责生成提交信息的、负责生成测试用例的、负责审查代码的。我刚上手时觉得挺省心,但随着项目深入,内置子代理的短板暴露得相当明显。
最大的痛点是覆盖面有限。内置子代理的设计目标是通用场景,它不可能预料到你的项目有特殊的编码规范、特殊的目录结构、特殊的工具链。比如我们团队用的是自定义的 API 封装层,内置的代码审查子代理根本不知道我们的约定,审查出来的结果就是"正确的废话"——语法没错,但完全不符合项目实际规范。
另一个痛点是职责定位过于宽泛。内置的代码审查子代理既要检查风格、又要查逻辑、还要看是否缺少注释,什么都会的结果就是什么都浅尝辄止。我给它的任务颗粒度一旦细化,比如"只检查这个接口是否有输入校验缺失",它往往还是按照自己的泛化模板来回话,上下文消耗不小,产出却不够聚焦。
还有上下文管理的效率问题。内置子代理被调用时,会携带一部分主对话的信息,如果项目里有大量公共工具类、类型定义、配置文件,内置子代理经常反复读取同样的内容,造成 token 浪费。自定义子代理可以用精准的系统提示词告诉它"不要读哪些文件",把上下文留给真正需要的部分。
如果你只是写脚本、做小 Demo,内置子代理完全够用。但一旦进入真实项目、有明确的代码规范、有反复出现的高频任务,内置子代理就会成为瓶颈。这也是我决定"自己造"的直接原因。
2. 自定义子代理的工作原理与配置格式
2.1 存放位置与加载机制
Claude Code 的自定义子代理通常放在项目根目录的.claude/subbagents/文件夹里,也可以放在用户级别的~/.claude/subbagents/。项目级的配置只对当前项目生效,适合沉淀团队规范;用户级的配置全局可用,适合放一些与具体项目无关的通用角色,比如"通用代码审查"或"Git 操作助手"。
每个子代理是一个独立的 Markdown 文件,文件名就是子代理的名字,比如code-reviewer.md、db-analyzer.md。Claude Code 启动时会扫描这些文件,把它们注册为可调度的角色。这里有个非常重要的细节:配置改动之后需要重启当前会话才能生效,至少在我使用的版本里是这样的,改完不重启就发现子代理召唤不出来,这个坑我踩过好几次。
2.2 配置文件的字段解析
每个子代理文件有一个 YAML 格式的 frontmatter(文件头),用---包裹,里面定义元信息;下面则是系统提示词正文。核心字段有这些:
| 字段名 | 作用 | 说明 |
|---|---|---|
name | 子代理的唯一标识 | 调用时用,建议与文件名保持一致,全小写加连字符 |
description | 子代理的功能描述 | 这是最关键的一个字段,主 Agent 靠它判断何时调用这个子代理 |
tools | 允许该子代理使用的工具列表 | 不写就带默认工具集,写了就严格限制,比如只读工具不给写权限 |
model | 子代理使用的模型配置 | 可选,不写就用主对话的模型配置 |
system prompt | 子代理的系统提示词 | 文件头下面的正文部分,定义角色行为、输出格式、操作边界 |
顺便提一下model字段。Claude Code 支持给不同的子代理指定不同的模型档位,比如轻量的总结任务用较小的模型、复杂的架构分析用较大的模型。这个配置在节省成本方面很有效——不是所有任务都需要满血模型来处理,分类一下能省不少 token。
2.3 触发逻辑:description 是召唤口令
子代理的调度逻辑非常简单粗暴:主 Agent 收到用户指令后,会浏览所有子代理的description字段,根据语义判断哪个角色适合处理当前任务,然后自动调度。所以你写的description就是子代理的"召唤口令",要写得精准、可匹配、覆盖触发场景。
写这个字段最常见的问题是两个极端。一个是写得太宽泛,比如"负责所有代码相关工作",那主 Agent 什么任务都可能丢给它,不仅干扰主任务,还容易抢活。另一个是写得太狭窄,比如"负责检查 user.go 文件的错误处理",那主 Agent 基本永远想不到用它,因为触发面太小。我的经验是:用"当用户需要处理 X 任务时,这个子代理可以提供 Y 输出"的句式,明确职责和产出物,让主 Agent 能清晰判断匹配关系。
3. 自己动手:做一个"代码审查专家"子代理
3.1 需求分析与设计思路
我做第一个自定义子代理时,场景很简单:团队有一套后端服务,代码入库前大家会不定期用 Claude Code 帮忙过一遍逻辑。但这个"过一遍"很模糊——是要检查设计模式?还是要按团队的代码规范逐行审?或者是只关注性能风险?内置的代码审查子代理根本不知道这些,我需要一个能按我们项目规范审查的子代理。
我定的需求是这样的:输入是一个文件路径或一段代码,输出是三部分——按团队规范检查出的问题、潜在逻辑风险、以及优先级排序。不需要它改代码,只做分析和建议,因为改动需要主 Agent 和开发者共同确认。
3.2 系统提示词的撰写要点
系统提示词是子代理的"人设",写得越精准,输出越可控。我设计这个子代理的系统提示词时,用了几个原则。
明确角色身份和任务边界。开篇直接定义:你是一个严格的代码审查员,不负责修改代码,只输出审查结果。然后列出团队规范的具体条目,比如"所有对外接口必须做参数校验""禁止使用全局可变状态""错误处理必须向上抛出而不是吞掉"。
定义输出结构。我要求它必须按指定格式输出:问题描述、涉及文件与行号、严重程度(阻断/重要/建议)、对应规范条目、修改建议。这个输出结构看起来琐碎,但非常关键——它让结果可以被主 Agent 直接消费,也方便集成到后续流程里。
设置行为约束。明确告诉它:不要输出与审查无关的内容,不要修改任何文件,不要执行测试命令,遇到不确定的规范优先在原代码注释中寻找依据而不是猜测。
3.3 完整配置示例
下面是我的code-reviewer.md文件配置,省去了具体项目名的部分,保留了核心逻辑:
--- name: code-reviewer description: 当需要根据团队编码规范审查代码文件、检查逻辑风险或定位性能隐患时,使用此子代理。输入文件路径或代码片段,输出结构化审查意见。 tools: Read, Glob, Grep model: sonnet --- 你是一名严格的代码审查员,服务对象是使用本团队技术栈的后端开发者。 你的任务是审查用户提供的代码文件或片段,并输出结构化审查意见。 团队强制规范: 1. 所有对外 API 必须做输入参数校验,包括类型、长度、取值范围。 2. 禁止在循环体内发起网络请求或数据库查询。 3. 错误处理不得吞掉异常,必须向上抛出或记录日志后显式处理。 4. 函数体超过 60 行时需要拆分。 5. 禁止使用全局可变状态,状态必须通过参数传递。 输出格式: - 问题列表按严重程度降序排列,每个问题包含: - 严重程度:阻断 / 重要 / 建议 - 位置:文件路径与行号 - 问题描述:一句话说清楚问题是什么、为什么是问题 - 规范依据:对应上述规范条目或逻辑风险说明 - 修改建议:不超过三句话的具体改法 行为约束: - 你只做审查,绝不直接修改代码文件。 - 不执行测试命令,不运行程序。 - 如果代码中已有明确注释说明某处行为的合理性,以注释为准并跳过该问题。 - 不要把时间花在格式化或风格类问题上,优先关注逻辑与隐患。一个小细节是model: sonnet。审查任务不需要最强的推理能力,中等档位足够,成本低不少,速度也快。如果审查结果出现误判,再考虑升级模型档位。
3.4 调用方式与实测体验
配置好之后,调用方式有几种。最直接的是对话里直接说明需求,比如"用 code-reviewer 审查一下auth/service.go这个文件",主 Agent 会自动匹配并调度。另一种是让主对话在分析代码时自然触发,当它判断一个任务属于"需要详细审查"时,会自己把子代理拉进来。实测下来,第二种方式是子代理价值最大的场景——你根本不需要记着去召唤它,主对话会把它当成一个协作者来用。
第一次用的时候,我对比了一下效果:同样一份包含三个潜在空指针风险的文件,内置审查只会说"这里可能有空指针",自定义审查会直接指出具体是哪个变量在哪个分支下可能为 nil、应该加什么校验、对应了团队的哪条规范。这就是"内置不够用"和"自己造"的核心差异——输出颗粒度完全不同。
4. 再做一个:"数据库告警分析"子代理
4.1 场景与设计思路
第二个子代理是"数据库告警分析",起因是团队经常处理线上数据库的慢查询和锁等待问题。这种任务有固定的分析路径——看表结构、看索引、看 SQL 的执行计划、判断是否存在锁竞争——非常适合做成独立的子代理。
这个子代理的设计与代码审查有些不同,因为它要处理的是"片段",而不是项目内的完整文件。用户会丢给它一段 SQL、或者一段慢查询日志、或者几个相关联的表结构,它需要输出可能的原因和优化建议。而且必须设定边界:它只是分析,不直接生成修改方案,更不执行任何写操作。
4.2 上下文隔离的设计
数据库分析场景特别容易出现上下文膨胀。慢查询日志往往很长,表结构定义也动辄几百行,如果这些全塞进主对话,其他任务的效率会被大幅拖累。子代理正好解决这个问题——用户把日志片段丢给它,它的独立上下文处理完后只输出结论,主对话的窗口完全不被污染。
但这里也暴露了一个新问题:子代理的上下文是"冷启动"的,它不了解项目的表结构全貌。我的解决方式是在系统提示词里给一个引导:告诉它先读取哪些公共配置文件、数据库 schema 放在哪个目录、索引命名规则是什么,这样它能快速进入状态,而不是从零摸索。
4.3 配置与提示词示例
--- name: db-analyzer description: 当出现慢查询、数据库锁等待、索引失效、SQL 性能异常等需要分析数据库相关问题时,使用此子代理。输入 SQL、执行计划或表结构信息,输出原因分析与优化建议。 tools: Read, Glob, Grep, Bash model: sonnet --- 你是一名资深的数据库性能分析专家,只负责分析,不负责修改线上配置。 工作步骤: 1. 如果提供了日志或 SQL,先提取其中的关键信息:表名、索引名、涉及的条件字段。 2. 如果用户没有提供完整的表结构,先检查项目中是否有数据库 schema 定义文件,读取相关部分。 3. 分析可能的原因,按概率排列:缺少索引 / 索引未命中 / 锁竞争 / 大表扫描 / 数据倾斜。 4. 给出建议,每条建议必须对应具体原因,并注明需要人工确认的环节。 输出格式: - 根因判断:一句话结论 - 详细分析:按可能性排序的三到五条原因,每条附带判断依据 - 优化建议:可执行的操作列表,明确标注哪些操作需要 DBA 或核心负责人确认后再执行 行为约束: - 绝不执行任何写操作,包括 UPDATE、DELETE、ALTER。 - 如果你需要查看执行计划,可以用只读命令获取,但不得修改任何数据库对象。 - 分析内容以用户提供的日志和项目内 schema 为准,不要臆测表结构。 - 输出必须区分"确定结论"和"推测结论",不混淆两者。这里有个细节值得说:tools字段里我加了Bash,因为某些情况下需要执行只读命令(比如explain)来辅助判断。但这也意味着这个子代理有命令行能力,所以系统提示词里的"绝不执行写操作"就非常关键,这是安全边界,不能省。
4.4 实测下来最惊喜的地方
这个子代理用了几周之后,我最真实的感受是"省心"。以前遇到慢查询,要在对话里交代各种背景信息,现在直接把日志丢给子代理,它自动读取项目里的 schema 文件,给出结论和建议,整个过程几乎不需要额外指挥。原来需要三十分钟来回沟通的排查过程,压缩到了几分钟。
而且它的输出一致性很高——因为系统提示词固定了格式和步骤,十次输出有十次的格式都一样,不像主对话里人工指挥时每次给的报告结构都不一样。这种"稳定输出"在团队协作场景下价值极大,可以直接把它的报告贴进 issue 里。
5. 常见问题与排查技巧实录
5.1 子代理为什么不响应
这个我踩了太多次。排查顺序大致如下:先看文件名和name字段是否一致;再确认配置文件是否放在正确的目录;然后检查tools列表里是否包含当前子代理需要的基础工具(比如需要读文件但没加Read);最后看description是否写得太窄,导致主 Agent 根本意识不到该用它。还有一个容易忽略的:改动配置后需要重启会话,不重启就可能继续沿用旧配置,甚至完全找不到新子代理。
5.2 输出质量忽上忽下
如果同一个子代理有时候输出很准、有时候胡说八道,通常不是模型的问题,而是description触发的输入范围超过了子代理的职责边界。也就是说,主 Agent 把不太适合这个子代理的任务也丢给它了。比如我的代码审查子代理设计的是"只审查,不修复",但如果任务里出现"帮我修改这个文件里的 bug",主 Agent 可能也会调用它,结果它就瞎指挥。解决方式是明确在description里限缩触发范围,用"仅当任务是 X 时才使用"这样的措辞。
5.3 上下文太长处理不动
子代理也会有上下文压力。一个常见的错误是系统提示词里让子代理"阅读所有相关文件",一旦项目庞大,子代理会从头到尾读一堆文件,还没开始干活上下文就爆了。处理方式是给子代理设置明确的"最小读取原则":只读用户明确指出的文件,只读 schema 定义,不读测试代码,不读构建配置。把"禁止做什么"写清楚,比"应当做什么"更有效。
5.4 需要写文件但没有对应权限
如果你的子代理任务涉及创建文件或修改代码,但内置的tools列表没有包含写入类工具,它就会罢工或者只输出建议不动手。实际上,我在尝试做一个"自动生成测试文件"的子代理时,配置里忘了加写文件的工具,结果它每次都对测试内容分析半天,但文件一直没有落盘。排查之后发现就是因为权限列表里少了写入工具。反过来,如果不需要写文件,就千万不要给写权限——默认最小权限原则,能降低子代理误操作的风险。
5.5 子代理之间互相抢活
当自定义子代理数量变多之后,会出现角色职责重叠的问题。比如我同时有"代码审查"和"通用审查"两个子代理,description都包含"检查代码质量",主 Agent 就会随机选一个,输出的侧重点完全不同,导致不稳定。解决方法是做职责收敛,同一类任务只留一个子代理,或者在description里用明确的关键词区分:一个专用于"合入前逐行审查",一个专用于"安全审计",互不重叠。
5.6 常见问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 子代理从未被调用 | description 触发面太窄或不匹配 | 扩大描述范围,加入触发关键词 |
| 子代理什么任务都接 | description 写得太泛 | 限定任务边界,明确"仅当...才使用" |
| 输出格式每次都变 | 提示词未定义输出结构 | 强制指定输出模板,不允许自由发挥 |
| 子代理读了大量无关文件 | 缺少最小读取原则 | 在提示词里明确禁止读取的内容 |
| 配置改了好几次都无效 | 会话未重启 | 重启会话,重新加载配置 |
| 子代理试图修改文件 | 未限制工具权限 | 从 tools 中移除写工具,提示词声明禁止 |
6. 几个让我获益最大经验分享
自定义子代理这件事,真正让我上头的不是"配置格式怎么写",而是想清楚"什么任务值得被封装成一个子代理"。我自己总结的标准是:高频、独立、有明确产出物、不需要频繁与主对话交互。如果一个任务需要边想边问、需要不断调整方向,那它就不适合做成子代理,硬做只会增加调度成本。
另外要注意一点:子代理的职责虽然是独立分析,但最终决策权一定要收在主对话手里。我一开始尝试让子代理直接修改代码并提交,结果它一次改了四个文件,看代码后发现至少两处修改是不必要的重构,还好没造成实际损失。那之后我给所有的写类工具都加了人工确认环节,子代理分析,主对话审批,人做最终判断。这个边界如果守不住,子代理走得太远会反噬项目。
最后再分享一个小技巧:把团队的编码规范沉淀到子代理的系统提示词里,是一项几乎零成本的长期收益。每次新增一条团队规范,顺手更新对应子代理的提示词,你会发现团队的代码风格变化会非常直观地体现在 AI 协作的结果里——因为每个被调用的子代理都默认带着这份规范去工作。到这一步,自定义子代理就不仅仅是"内置不够用自己造"的程度了,而是变成了一种项目管理工具。