Claude Code 04:Subagent——上下文隔离的正确姿势

在构建自主 Coding Agent 的过程中,一个不可逆的物理规律是:Agent 运行的轮次越多,messages 消息数组就会越发庞大。

设想主 Agent 正在执行重构任务,期间需要确认:“当前仓库采用的单测框架是 jest 还是 vitest?”
为了得出这个结论,Agent 可能需要依次读取 package.json、tsconfig.json、vite.config.ts、检查 tests/ 目录以及执行一次试探性命令。这短短的探索过程产生了 5 次文件读取和上千行文本输出。

如果所有操作都在主对话中发生,这数千行的临时代码会永久滞留在主消息栈中。主 Agent 最终只想要一个词“vitest”,却为此背负了沉重的 Token 包袱,导致后续真正核心的代码修改因为上下文拥挤而频发幻觉。

解决这个问题的经典工程模式,就是派生子智能体(Subagent)进行上下文物理隔离。


上下文隔离模式:大任务拆解与结果单向汇聚

子智能体的设计哲学非常明确:给子任务一个纯净的独立上下文,跑完后只向父上下文返回精炼摘要,中间的所有工具探索过程直接丢弃。

子 Agent 在其私有沙箱中即使发起了 30 次工具调用、读了上万行文本,这些海量中间数据在子任务结束时会被整体垃圾回收,父 Agent 只会收到一条轻量的结果反馈。


极简 Python 实现:run_subagent

我们把派生子 Agent 封装为一个标准工具 task_agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import anthropic

client = anthropic.Anthropic()

def run_subagent(task_prompt: str, max_turns: int = 10) -> str:
"""
启动一个上下文完全隔离的子智能体
"""
# 1. 初始化属于子 Agent 的独立消息栈
sub_messages = [{"role": "user", "content": task_prompt}]

# 子 Agent 可以有更专注的 System Prompt 与裁剪过的工具集
sub_system = "你是一个专注代码探索与特定分析的子智能体。深入调研后,只向父流程返回最精准的结论摘要。"

print(f"\n[Subagent 启动] 开始独立执行: {task_prompt}")

turn = 0
while turn < max_turns:
turn += 1
resp = client.messages.create(
model="claude-3-7-sonnet-20250219",
system=sub_system,
messages=sub_messages,
tools=TOOLS, # 共享或授予部分工具权限
max_tokens=4000
)
sub_messages.append({"role": "assistant", "content": resp.content})

# 若子 Agent 不再调用工具,说明调研结束,提取最终回复
if resp.stop_reason != "tool_use":
final_text = ""
for block in resp.content:
if hasattr(block, "text"):
final_text += block.text
print(f"[Subagent 完成] 退出并回收中间上下文(共历经 {turn} 轮迭代)\n")
return final_text

# 执行子 Agent 的工具调用
tool_results = []
for block in resp.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
out = handler(**block.input) if handler else f"未知工具 {block.name}"
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": out
})
sub_messages.append({"role": "user", "content": tool_results})

return "Subagent 执行超时,未能给出完整结论。"

将子 Agent 作为工具挂载到主分发字典中:

1
TOOL_HANDLERS["run_subagent"] = lambda **kw: run_subagent(kw["prompt"])

父 Agent 在需要调研大型模块或查阅报错背景时,只需发出一句调用:run_subagent(prompt="深入分析 src/auth 目录下的鉴权流程并给出风险点清单"),主上下文就再也不会被数十次局部文件读取所污染。


生产源码探秘:Claude Code 的 AgentTool

在 Claude Code 真实的工程源码中,子 Agent 绝对不仅仅是一个简单的循环递归,而是一个支持独立模型覆盖、权限降级、后台运行与代码物理隔离的完整编排系统。

AgentTool.tsx 参数契约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/tools/AgentTool/AgentTool.tsx(生产源码 Schema 拆解)
export const fullInputSchema = z.object({
description: z.string().describe('子任务的 3-5 词极简描述'),
prompt: z.string().describe('传递给子 Agent 的明确任务指令'),
subagent_type: z.string().optional().describe('专业智能体类型标识'),

// 1. 独立模型覆盖:探索任务用轻量快模型,核心推理用大模型
model: z.enum(['sonnet', 'opus', 'haiku']).optional(),

// 2. 异步非阻塞:是否允许子 Agent 在后台常驻执行
run_in_background: z.boolean().optional(),

// 3. 多智能体可寻址:为子 Agent 分配命名,支持后续 SendMessage 点对点通信
name: z.string().optional(),
team_name: z.string().optional(),

// 4. 物理隔离模式
isolation: z.enum(['worktree', 'remote']).optional(),
// "worktree":在独立的 Git 临时工作树目录中修改代码,防止多 Agent 并发写冲突

// 5. 工作目录覆盖
cwd: z.string().optional()
})

runAgent.ts 的生命周期编排

在 src/tools/AgentTool/runAgent.ts 中,子智能体的初始化流程具备严密的工程防护:

值得特别关注的是 resolveAgentTools() 工具裁决机制:父 Agent 可以对子 Agent 进行权限降级。例如一个负责探索代码架构的子 Agent,可以被剥夺 FileWriteTool 和 BashTool 的写权限,仅授予只读工具。这种“最小特权原则”有效阻止了不可控的子任务对生产代码造成破坏。


选型思考:什么时候必须派生 Subagent?

在研发实践中,盲目派生子智能体会增加多次 LLM 调用的网络延迟与冷启动开销。我们需要清晰划定边界:

评估维度 单 Agent 原地执行 派生 Subagent 隔离
任务跨度 修改当前明确知道行号的单一文件 跨越 5 个以上未知文件的全库调研
中间日志体积 输出简短,仅需查看几行返回值 跑构建脚本或全量单测,日志成百上千行
容错敏感度 核心主逻辑修改,要求即时反馈 试探性实验,失败后允许整体丢弃
Token 预算 当前上下文非常充裕 上下文已接近压缩阈值,需避免冲击

总结

Subagent 是现代 Coding Agent 能够平稳驾驭超大代码库的工程基石:

  1. 阻断上下文雪崩:将脏活、累活、大文本探索封装在局部子进程中,保障主上下文聚焦最高优先级目标;
  2. 支持异构模型与权限降级:用轻量级模型跑低风险的调研子任务,实现成本与速度的最优配比;
  3. 单向结论沉淀:以纯文本摘要作为父子协作契约,规避复杂的跨进程消息死锁。

但是,除了临时生成的探索性上下文,Agent 往往还需要使用特定领域的专有规则(例如团队的特定安全规范或单测约定)。如果把这些静态规则也全部硬编码塞进 Prompt,上下文同样会不堪重负。

下一篇我们将探讨领域知识的按需加载策略:Skill Loading 与 SKILL.md 规范。