Harness实战:Subagent——大任务拆小,上下文隔离
Agent工作越久,上下文越臃肿。Subagent机制把子任务放到独立messages[]中执行,只把精炼摘要带回父Agent。结合Plan模式,计划阶段列步骤,执行阶段委派subagent,上下文始终干净。
写在前面
上一篇我们让 Agent 学会了"先想后做"——Plan 模式。模型先输出计划,用户确认后再执行。
但执行阶段有个问题一直在恶化——上下文膨胀。
Agent 执行每一步——读文件、跑命令、查日志——输出都永久留在 messages[] 里。10 轮下来上下文可能涨到几万 token。更要命的是,这些历史输出会干扰模型的注意力。
问题:一个词的答案,315 行的代价
假设你让 Agent 回答:"这个项目用什么测试框架?"
第1轮: bash → cat package.json (输出: 200行JSON)
第2轮: bash → cat jest.config.js (输出: 30行配置)
第3轮: bash → cat tsconfig.json (输出: 50行JSON)
第4轮: bash → ls tests/ (输出: 15行文件列表)
第5轮: bash → head -20 tests/setup.ts (输出: 20行代码)5 轮下来,messages 数组里塞进了 315 行工具输出。但真正需要的答案只有一个词:"Jest"。
后续所有 API 调用都要带上这 315 行历史,反复付费,还稀释模型对当前任务的注意力。
解决方案:Subagent——独立上下文,用完即弃
核心思路:把子任务的脏活放到独立的 messages[] 里做,只把干净的结果带回来。
┌──────────────────────────────────────────────────────┐
│ 父 Agent (messages[]) │
│ │
│ User: "分析技术栈,然后更新 README" │
│ Assistant: 我先调研技术栈 → tool_use: task │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 子 Agent (sub_messages[] — 独立,用完即弃) │ │
│ │ │ │
│ │ bash → cat package.json (200行) │ │
│ │ bash → cat jest.config.js (30行) │ │
│ │ bash → ls src/ (20行) │ │
│ │ │ │
│ │ 最终回复: "TypeScript 5.3 + React 18 + Jest" │ │
│ │ ★ 整个 sub_messages[] 在这里丢弃 │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ tool_result: "TypeScript 5.3 + React 18 + Jest" │
│ Assistant: 好的,现在编辑 README...(继续干净工作) │
└──────────────────────────────────────────────────────┘父 Agent 只多了一条 tool_result,内容是精炼摘要。那 250 行中间输出?消失了。
Subagent 的三个核心价值
很多人以为 subagent 就是"压缩上下文"。这是最直接的收益,但不是全部。
1. 上下文隔离
子 Agent 用独立的 sub_messages[],所有中间工具调用和结果都在里面。执行完毕,extract_text(response) 只提取最终文本返回给父 Agent。
父 Agent 的 history 里只多了一条简短的 tool_result,而不是几十条中间步骤。
实际体验:我在测试 /plan 分析项目技术栈并更新 README 时,父 Agent 自己跑了 6 个 bash + 2 个 subagent。即便用了 subagent,父 Agent 的上下文已经膨胀到让下一次 API 调用等了很久。如果没有 subagent,那两个子任务的所有 read_file 结果也堆进父 history,会更慢。
2. 任务分解
父 Agent 只需说"分析技术栈"、"写 README",不需要关心子任务怎么做。每个 subagent 独立决策、独立执行。
这实现了关注点分离——父 Agent 负责全局协调和最终交付,子 Agent 负责具体的研究和执行细节。
3. 失败隔离
subagent 挂了或输出垃圾,父 Agent 的 history 不会被污染。父 Agent 可以重试、换策略、或跳过这一步继续。
这在生产环境至关重要——一个子任务的失败不应该搞崩整条流水线。
类比:subagent 就像你派同事去调研,他回来只给你一页总结,而不是把他查的所有资料都倒在你桌上。调研过程出了问题?你可以派另一个人重新查,自己手头的工作不受影响。
实现
在上一篇的 Plan 模式基础上,增加三样东西。
1. task 工具定义
给父 Agent 注册一个 task 工具:
TASK_TOOL = {
"name": "task",
"description": (
"Run a subtask in an isolated context. "
"The subtask gets its own clean message history, "
"only the final text summary is returned. "
"Use for research, analysis, or any work whose "
"intermediate output the parent does not need."
),
"input_schema": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "Task description for the subagent",
}
},
"required": ["prompt"],
},
}这就是父 Agent 发起子任务的唯一接口。描述里明确告诉模型——这个工具适合"不需要看中间过程"的任务。
2. run_subagent() 函数
Subagent 本质是一个迷你 Agent Loop,但有三个关键差异:
MAX_SUBAGENT_TURNS = 30
SUBAGENT_SYSTEM = f"""You are a focused agent at {WORKDIR}.
Complete the given task, then provide a clear summary of results.
Do not ask for clarification — work with what you have."""
def extract_text(response) -> str:
"""从响应中提取纯文本,丢弃工具调用块。"""
parts = [b.text for b in response.content if hasattr(b, "text")]
return "\n".join(parts) if parts else "(subagent produced no text output)"
def run_subagent(prompt: str) -> str:
"""在隔离上下文中执行子任务,仅返回最终文本。"""
sub_messages = [{"role": "user", "content": prompt}]
for _turn in range(MAX_SUBAGENT_TURNS):
response = client.messages.create(
model=MODEL,
system=SUBAGENT_SYSTEM,
messages=sub_messages,
tools=BASE_TOOLS, # ★ 没有 task 工具,不能递归
max_tokens=4096,
)
sub_messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type == "tool_use":
handler = BASE_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
sub_messages.append({"role": "user", "content": results})
# ★ 关键:只提取最终文本,sub_messages 整个丢弃
return extract_text(response)三个关键差异:
| 对比项 | 父 Agent | 子 Agent |
|---|---|---|
| 消息列表 | 全局 messages,持续累积 | 局部 sub_messages,用完即弃 |
| 可用工具 | 所有工具,包括 task | 所有工具,排除 task |
| 轮次限制 | 无硬上限 | MAX_SUBAGENT_TURNS = 30 |
3. 注册到 dispatch map
# 基础工具(子 Agent 用这个,不含 task)
BASE_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}
# 父 Agent 的工具列表和 dispatch(含 task)
TOOLS = BASE_TOOLS + [TASK_TOOL]
TOOL_HANDLERS = {
**BASE_HANDLERS,
"task": lambda **kw: run_subagent(kw["prompt"]),
}父 Agent Loop 一行不改。当模型调用 task 时,dispatch map 自动路由到 run_subagent,执行完把结果字符串作为 tool_result 追加到父 messages。
这就是 dispatch 架构的威力——加新能力只加注册,循环不碰。
Plan + Subagent:组合使用
Plan 模式生成计划,执行阶段用 subagent 委派子任务。只需在执行提示词里加一句:
# 阶段 2:执行时提示模型可以用 task 工具委派
messages.append({
"role": "user",
"content": f"{feedback} Now execute it step by step. "
f"Use the `task` tool to delegate independent subtasks."
})
agent_loop(messages)运行效果
❯ /plan 分析项目技术栈并更新 README
⏳ Generating plan...
📋 Plan:
1. 调研项目使用的语言、框架、测试工具
2. 读取当前 README 内容
3. 在 README 中补充技术栈说明
Execute? (y/n) y
⚡ Executing...
🔀 Subagent started: 调研项目的语言、框架、测试工具...
> bash: cat package.json | head -30
> bash: cat jest.config.js
> read_file: vite.config.ts
🔀 Subagent done (8 messages)
> task: TypeScript 5.3, React 18 + Vite 5, Jest + Testing Library, pnpm...
> read_file: # My Project ...
> edit_file: Edited README.md
已在 README.md 中补充了完整的技术栈说明。Plan 阶段的第 1 步(调研)被委派给 subagent,8 条消息的中间过程被隔离,父 Agent 只拿到一段摘要,然后干净地完成步骤 2 和 3。
核心机制解析
为什么禁止递归?
子 Agent 没有 task 工具,不能再派子 Agent。这是刻意的:
- 防失控:无限递归会无限消耗 API 调用和 token
- 两层就够:父 Agent 拆解的子任务粒度已经足够小,一个 Subagent 在 30 轮内能搞定
正确的做法是让父 Agent 拆出更多并列的子任务,而不是让子任务嵌套下去:
✅ 父 → [子A, 子B, 子C] (宽度扩展)
❌ 父 → 子 → 孙 → 曾孙 (深度递归)Claude Code 的生产实现也是这个设计——Task 工具的子 Agent 禁止使用 Task。
为什么丢弃子历史?
子 Agent 结束后,sub_messages[] 直接被垃圾回收。保留的成本远大于收益:
- Token 成本:留在父上下文里,后续每轮 API 调用都要反复付费
- 注意力成本:无关的历史信息稀释模型对当前任务的聚焦
父 Agent 需要的是结论,不是过程。
子 Agent 的系统提示词为什么不同?
SUBAGENT_SYSTEM = """You are a focused agent at {WORKDIR}.
Complete the given task, then provide a clear summary of results.
Do not ask for clarification — work with what you have."""关键是最后一句——"不要问澄清问题"。子 Agent 没有和用户交互的通道,它只能和工具交互,必须自行判断、自行行动、自行总结。
Claude Code 的 Subagent
Claude Code 的 Agent 工具(在 system prompt 中叫 Task)就是这个模式的生产级实现:
- 父 Agent 可以调用
Task派发子任务 - 子 Agent 在独立上下文中执行
- 子 Agent 禁止使用
Task(不能递归) - 只有最终文本返回父上下文
底层原理完全一样。
小结
- task 工具 — 父 Agent 调用它派发子任务
- 独立 messages[] — 子 Agent 用干净的上下文,用完即弃
- 禁止递归 — 子 Agent 没有
task工具,只能往宽度扩展 - 只返回文本 — 几百行中间输出压缩成几句摘要
- 三个价值 — 上下文隔离、任务分解、失败隔离
这个模式的本质是:用一个额外函数调用换取上下文的干净。 对简单任务可能多余,但对复合任务——调研+执行、分析+修改——能显著控制上下文膨胀。