当一个 Agent 需要处理复杂任务时,它如何避免把自己「撑死」?答案是:生出一个儿子,让儿子去干,自己只看结果。
问题的起点
Agent 在执行任务时,消息历史会不断增长。假设父代理需要「调查一下项目用的是什么测试框架」,这个子任务本身可能需要十几次工具调用:读文件、搜索、分析......
如果所有这些中间步骤都堆在主对话里,会发生什么?
-
Token 快速消耗:每次 LLM 调用都要携带全量历史
-
推理质量下降:大量无关的中间信息干扰后续判断
-
上下文被稀释:主任务的目标越来越模糊
子代理(Subagent)就是解决这个问题的核心机制。
核心设计:子代理就是一个 Tool
Claude Code 里,子代理不是一个独立进程,而是通过 AgentTool 实现的------它本质上是一个嵌套的 Agent Loop,和 BashTool、FileReadTool 并列注册在工具列表里。
javascript
// src/tools/AgentTool/AgentTool.ts - 简化
export const AgentTool = buildTool({
name: 'Agent',
maxResultSizeChars: 50_000,
inputSchema: z.object({
prompt: z.string(), // 子代理的任务描述
description: z.string(), // 短描述 (3-5 词)
subagent_type: z.string().optional(), // 代理类型
model: z.enum(['sonnet', 'opus', 'haiku']).optional(),
run_in_background: z.boolean().optional(),
isolation: z.enum(['worktree']).optional(),
}),
// 子代理可以并发执行
isConcurrencySafe() { return true },
// 子代理本身是只读的 (它内部的工具调用有自己的权限检查)
isReadOnly() { return true },
async call(input, context) {
// 创建一个新的 QueryEngine 实例 ------ 独立的消息列表
const childEngine = new QueryEngine({
tools: getChildTools(input.subagent_type),
systemPrompt: buildSubagentPrompt(input),
// 关键: messages 从空开始
messages: [],
})
// 运行子代理的 Agent Loop
let result = ''
for await (const message of childEngine.submitMessage(input.prompt)) {
if (message.type === 'text') {
result += message.text
}
}
// 只返回最终文本给父代理
return { data: result }
},
})
隔离与共享:父子代理的边界
子代理和父代理之间有清晰的边界划分:
1.完全隔离(不共享)
|----------|-------------------------------|
| 状态 | 说明 |
| 消息历史 | 子代理 messages 从空开始,父代理的历史对它不可见 |
| Token 计费 | 独立追踪,子代理的消耗不合并到父代理 |
| 压缩状态 | 子代理有自己的 Micro/Auto-Compact 周期 |
2.共享(父子均可访问)
|------|---------------------|
| 状态 | 说明 |
| 文件系统 | 子代理读写的文件,父代理下一步就能看到 |
| 工作目录 | 相同的 cwd |
| 权限规则 | 子代理继承父代理的权限设置 |
这是典型的「进程内线程」思路:共享内存(文件系统),但各自有独立的执行栈(消息历史)。

子代理工具集
1.防止无限递归:
最关键的设计细节:子代理的工具列表里没有 AgentTool。
javascript
function getChildTools(subagentType?: string): Tool[] {
// 子代理的基础工具集 ------ 没有 AgentTool!
const baseTools = [
BashTool,
FileReadTool,
FileWriteTool,
FileEditTool,
GlobTool,
GrepTool,
WebFetchTool,
// 注意:没有 AgentTool → 防止无限递归
]
// 不同类型的子代理有不同工具
switch (subagentType) {
case 'Explore':
// 探索型:只有搜索和读取工具
return [FileReadTool, GlobTool, GrepTool, WebFetchTool]
case 'Plan':
// 规划型:只有读取工具 + 任务工具
return [FileReadTool, GlobTool, GrepTool, TaskCreateTool]
default:
return baseTools
}
}
2.子代理类型
Claude Code 预定义了多种子代理类型,每种类型的工具集不同:
|-----------------|-------------------------------|------------|
| 类型 | 可用工具 | 适用场景 |
| general-purpose | 所有工具(除 Agent) | 复杂多步骤任务 |
| Explore | Read / Glob / Grep / WebFetch | 快速探索代码库,只读 |
| Plan | Read / Glob / Grep | 设计方案,不允许修改 |
| code-reviewer | 所有工具 | 代码审查 |
Explore 类型为什么没有 Write/Edit?因为「找信息」不需要修改能力,减少工具集也减少了犯错的可能。这是最小权限原则的工程实践。
源码示例如下:
javascript
// 从系统提示词中提取的子代理类型
const SUBAGENT_TYPES = {
'general-purpose': {
description: '通用代理,适合复杂多步骤任务',
tools: '*', // 所有工具 (除了 Agent)
},
'Explore': {
description: '快速探索代码库',
tools: ['Read', 'Glob', 'Grep', 'WebFetch'],
// 不能编辑文件
},
'Plan': {
description: '设计实现方案',
tools: ['Read', 'Glob', 'Grep'],
// 只读 + 任务工具
},
'code-reviewer': {
description: '代码审查',
tools: '*',
},
// ... 更多专用类型
}
执行模式:
前台执行(默认):父代理等待子代理完成后继续。适合结果强依赖的场景。
javascript
const result = await AgentTool.call({
prompt: "分析项目的测试覆盖率",
description: "分析测试覆盖率",
})
// 父代理等待子代理完成
后台执行:父代理立即继续,子代理异步跑,完成后通知。适合耗时长但结果不紧急的场景。
javascript
const result = await AgentTool.call({
prompt: "运行所有测试并报告结果",
run_in_background: true, // ← 父代理立即继续
})
一图总结

参考信息
深入Cluade code源码:
感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!
