HARNESS 104

Subagent

"大任务拆小,每个小任务干净的上下文"

给父 Agent 一个"外包"能力——把脏活累活派给子进程,
子进程干完只汇报结果,中间过程全部丢弃。

1

问题:上下文越干活越脏

Agent 每做一件事——读一个文件、跑一个命令、检查一个输出——结果都永久留在 messages 数组里。做 10 件事,就有 10 条工具结果堆积在对话中。

🧪 具体例子:一个简单问题,大量上下文垃圾

用户问父 Agent:"这个项目用什么测试框架?"

要看 requirements.txtsetup.cfgpyproject.tomlMakefile 里面有没有测试命令、再看看 tests/ 目录结构。可能要读 5 个文件。

这 5 个文件的完整内容全部进入 messages,而父 Agent 最终只需要一个词:"pytest"

messages (上下文窗口 = 有限的注意力资源):
████████████████████████████████████████ ← 5 个文件的内容占满
"pytest" ← 父 Agent 真正需要的

5 个文件的全文换来了 1 个词。上下文窗口就像桌面——放满了无关工具的说明书,你要的那把螺丝刀被埋在底下。对话越长,有效信息比例越低,Agent 的决策质量越差。

2

解决方案:派一个"临时工",干完活只汇报结果

👤 父 Agent

messages 里已经有很多历史。
调 task 工具:"帮我找出测试框架"。


🧑‍💻 子 Agent(临时工)

messages = [](全新空白上下文!)
读 5 个文件 → 分析 →
返回摘要:"pytest"

核心机制:上下文隔离

子 Agent 以 messages=[] 启动 —— 完全干净的对话历史,不继承父 Agent 的任何上下文垃圾。

子 Agent 在自己的 while 循环里干活 —— 读文件、分析、搜索,一切照常。

子 Agent 完成后,整个 messages 被丢弃 —— 只有最后一条文本回复返回给父 Agent。

父 Agent 收到的是一个普通 tool_result —— 内容是子 Agent 的摘要,就像任何其他工具调用一样。

一句话理解:子 Agent = 一个函数调用。函数内部的局部变量(子 messages)在函数返回后被回收,只有 return 值(摘要)留给了调用方(父 Agent)。
3

案例对比:同一个问题,有无 Subagent 的差异

❌ 没有 Subagent:父 Agent 亲自查

父 Agent 调 read_file 5 次,每个文件的全部内容进入 messages。对话历史迅速膨胀。后续的 Todo 列表、之前的讨论、系统提示——全部被这 5 个文件的内容稀释。

父 messages 内容构成:
██ 系统提示
██ 用户原始问题
██ Todo 列表
████████████████ requirements.txt 全文
████████████████ setup.cfg 全文
████████████████ pyproject.toml 全文
████████████████ Makefile 全文
████████████████ tests/ 目录结构
█ "pytest" ← 有用的就这一个词

✅ 有 Subagent:外包给临时工

父 Agent 调一次 task(prompt="找出这个项目的测试框架")

父 messages 内容构成:
██ 系统提示
██ 用户原始问题
██ Todo 列表
█ task_result: "这个项目使用 pytest" ← 只有这一个 tool_result
(子 Agent 的 5 次文件读取全在子 messages 里,已随函数返回而销毁)

效果:父 Agent 的上下文保持干净,后续决策质量不受影响。子 Agent 干了所有脏活,但父 Agent 只看到一个干净的摘要。

4

task 工具 & 两层工具集

父 Agent 有 task 工具,子 Agent 没有

这是 s04 最精妙的设计:两套工具列表,防止无限递归。

# CHILD_TOOLS:子 Agent 能用什么(没有 task!) CHILD_TOOLS = [bash, read_file, write_file, edit_file] # ↑ 子 Agent 只能做基础文件操作,不能再生孙子 Agent # PARENT_TOOLS:父 Agent 能用什么(CHILD_TOOLS + task) PARENT_TOOLS = CHILD_TOOLS + [ {"name": "task", "description": "Spawn a subagent with fresh context.", "input_schema": { "properties": { "prompt": {"type": "string"}, "description": {"type": "string"} }, "required": ["prompt"] }}, ]

父 Agent 的 agent_loop 里,task 是特殊处理的

其他工具走 TOOL_HANDLERS 字典,但 task 走特殊分支——因为它不是一个 handler 函数,而是启动一个完整的子 Agent:

for block in response.content: if block.type == "tool_use": if block.name == "task": # task 是特殊的 output = run_subagent(block.input["prompt"]) # 启动子 Agent else: handler = TOOL_HANDLERS.get(block.name) # 其他工具走 dispatch map output = handler(**block.input)

task 的特殊性:它不是"执行一个操作",而是"创建一个新的 Agent 实例"。所以它不走 dispatch map,而是在循环里硬编码处理——就像 s01 里硬编码 bash 一样。因为你目前只有一个"生孩子"的工具。

5

run_subagent 内部:一个缩小版的 s01 Agent Loop

run_subagent(prompt) 就是一个函数,内部跑了一个完整的 Agent Loop——和 s01 一模一样,但有三个不同:

run_subagent 的 Agent Loop(和 s01 对比)

def run_subagent(prompt): sub_messages = [{"role": "user", "content": prompt}] # ① 全新 messages,不是父的 for _ in range(30): # ② 最多 30 轮(不是无限) response = client.messages.create( system=SUBAGENT_SYSTEM, # ③ 不同的 system prompt messages=sub_messages, tools=CHILD_TOOLS, # ④ 没有 task 工具 ) sub_messages.append(...) if response.stop_reason != "tool_use": break # ... 执行工具,和 s01 完全一样 ... return "".join(b.text for b in response.content) # 只返回最后一句

四个关键不同点

① 全新 messages=[]:子 Agent 不继承父的任何上下文。它只知道自己被分配的任务(prompt)。这是隔离的核心。

② 最多 30 轮:用 for _ in range(30) 而不是 while True。安全上限——防止子 Agent 陷入无限循环。

③ 不同的 System PromptSUBAGENT_SYSTEM = "You are a coding subagent... Complete the given task, then summarize your findings." ——告诉子 Agent 你不是主角,干完活就汇报。

④ 只返回最后一条文本"".join(b.text for b in response.content)... ——子 Agent 最后一轮输出的 text block 作为返回值。中间的几十条工具调用全部丢弃。

6

为什么禁止子 Agent 再生子 Agent?

如果允许递归:1 个任务 → 10 个子任务 → 100 个孙任务

父 Agent 说"帮我研究这个项目"→ 派了一个子 Agent。
子 Agent 说"这个项目太大了,我也派几个子 Agent"→ 派了 5 个孙 Agent。
每个孙 Agent 又说"我也派几个"...

结果:API 调用量爆炸、token 消耗失控、层层嵌套的摘要越来越失真(就像传话游戏)。而且 30 轮 × N 层嵌套 → 最坏情况下调用次数指数级增长。

CHILD_TOOLS 不包含 task = 一刀切

子 Agent 的工具列表里没有 task 工具,LLM 根本看不到这个选项。它不会说"我想派子 Agent"——因为它的世界里不存在这个能力。

这并不是说多层 Agent 架构不好——s09 会讲多 Agent 团队,但那是通过不同的机制(邮箱通信、显式协调),而不是无限制递归。

7

代码拆解 & s03 vs s04

A

两套工具 + 两套 System Prompt

父 Agent 和子 Agent 有不同的身份(system)和不同的能力(tools),这是隔离的基础。

B

run_subagent() — 完整的子 Agent Loop

一个函数 = 一个 Agent。内部跑完整的 while 循环,但 messages 从空开始,结束后销毁。

C

Agent Loop 里 task 的特殊分支

task 不走 dispatch map,而是在循环里 if 判断。因为它的行为不是"执行一个函数",而是"启动一个新 Agent"。

组件s03s04
工具数量5父6(+task)子4(无 task)
上下文单一共享父子隔离,子用完即弃
System Prompt1 个2 个(SYSTEM + SUBAGENT_SYSTEM)
Agent Loop父 Agent 的循环结构不变(只在工具分发处加了一个 if 分支)
外层 REPL完全不变
8

从 s04 带走的三个核心认识

01

上下文隔离 = 函数调用隔离

子 Agent 是一个函数调用。函数的局部变量(子 messages)在 return 后自动回收。不需要手动清理,Python 帮你做了。

02

外包脏活,保护注意力

父 Agent 不是不能自己做,而是自己做会污染上下文。外包不是"没能力",是"保护自己的决策质量"。

03

递归需要显式治理

CHILD_TOOLS 不包含 task,不是功能限制,是安全边界。子不能生子 = 深度可控。多层架构是好东西,但需要治理机制。

9

落地案例:Subagent 在真实 AI 产品中的应用

🔄 场景一:Claude Code 的 Subagent(你每次用都在触发)

Claude Code 的 Task 工具就是 s04 的生产级实现。当你提出复杂需求时,Claude Code 会自动把大任务拆成子任务,每个子任务在一个 subagent 里执行。

你有没有注意过 Claude Code 有时会显示 Task: "搜索代码库中的认证逻辑"?那就是它在派 subagent。子 agent 搜完几十个文件后只汇报一个摘要:"认证逻辑在 auth.pymiddleware.py 中"。父 agent 的上下文保持干净,继续专注在主任务上。

下次 Claude Code 列出 Task 的时候,你就知道:它在用 s04 的 run_subagent()。

🧪 场景二:SWE-bench Agent — 学术界的"外包"策略

在 SWE-bench(软件工程基准测试)上,很多高分 Agent 的策略就是 subagent 模式:

定位阶段:父 Agent 阅读 issue 描述,生成需要搜索的关键词。
搜索阶段:派多个 subagent,每个搜一个关键词,各自返回相关代码片段。
修复阶段:父 Agent 汇总所有 subagent 的发现,决定修改方案,执行修改。

如果没有 subagent,父 Agent 要读完整个代码库——在大型项目中动辄几千个文件,上下文根本装不下。Subagent 让它把"大海捞针"变成了"分头找"。

🔍 场景三:Code Review Agent — 多维度并行审查

一个"代码审查 Agent"可以用 subagent 并行检查多个维度:

Subagent 1:只检查安全漏洞(SQL 注入、XSS、密钥泄露)。
Subagent 2:只检查性能问题(N+1 查询、不必要的循环、内存泄漏)。
Subagent 3:只检查代码风格(命名规范、注释质量、复杂度)。

三个 subagent 同时工作,各自在干净的上下文里专注一个维度。父 Agent 收到三份摘要后,合成一份完整的审查报告。如果没有 subagent,父 Agent 要在同一个上下文里切换三种思维模式——容易遗漏、容易混淆。

这也是你可以在 Claude Code 里用的模式——对 Claude Code 说:"帮我做 code review,用三个子任务分别检查安全性、性能、可读性。"

核心迁移:Subagent 解决的核心问题是 "上下文是一个有限的、会被污染的注意力资源"。当你用 Claude Code 做大型任务时,主动要求它"用一个子任务去查 X,另一个子任务去查 Y"——你在用 s04 的模式,让 AI 更有效地利用它有限的上下文窗口。

s04 有了 subagent,但每个 agent 的工具和能力是写死的。
s05:怎么让 Agent 按需加载知识和能力?

s05: Skill Loading →