learn-claude-code s04 Subagent 实战:用独立 messages[] 为子任务隔离上下文,让父 Agent 保持干净
本文基于 docs/en/s04-subagent.md 讲解 learn-claude-code 教程中 s04 课(Subagents)的核心机制:父 Agent 通过 task 工具派生一个拥有全新 messages[] 的子 Agent(subagent),子 Agent 在自己的上下文里循环调用工具,结束后只把最后一段文本作为 tool_result 返回给父级,中间可能多达 30 轮的对话历史被整体丢弃。读完本文,你将理解 subagent 为什么能保护父级上下文的"思维清晰度"、它相比单循环 Agent 多了哪些设计决策,并能结合 agents/s04_subagent.py 的源码与 tests/test_s06_subagent.py 的测试用例验证其边界行为。
一、课程定位:s04 在 learn-claude-code 中的位置
learn-claude-code 是一个"从 0 到 1 构建 Claude Code 式 agent harness"的教学仓库,其核心主张是:Agent 的能动性来自模型本身,而 harness(harness = 工具 + 知识 + 观察 + 动作接口 + 权限)是工程师真正要写的部分(见 README.md)。每一课围绕 while stop_reason == "tool_use" 这一个 agent 循环隔离讲解一种 harness 机制。
s04 属于仓库中的 legacy 12 课轨道(docs/ 与 agents/ 目录),课程进度条如下:
s01 > s02 > s03 > [ s04 ] > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12
按 README.md 中"Version Status"一节说明,当前主轨是根级 s01_agent_loop/ 到 s17_goal_loop/ 的 17 课版本,legacy 轨道保留旧版供已有读者参考,且新旧章节编号不一一对应。其中 旧 s04(Subagent)对应新轨道的 s06_subagent/。本文以旧轨道 s04 文档与 agents/s04_subagent.py 为主体,关键处会指出新轨道 s06 的演进差异,方便两条轨道对照阅读。
s04 的口号(motto)是:
"Break big tasks down; each subtask gets a clean context" —— subagent 使用独立的
messages[],保持主对话的干净。Harness 层职责:Context isolation —— 保护模型的思维清晰度。
二、要解决的问题:messages[] 只增不减
随着 Agent 工作推进,它的 messages 数组持续增长。每一次文件读取、每一段 bash 输出都会永久留在上下文里。原文档给出的典型例子是:
"这个项目用什么测试框架?" —— 回答它可能需要读 5 个文件,但父 Agent 真正需要的只有答案:"pytest。"
那 5 个文件的完整内容在得出结论之后就是纯噪音,却会继续占用上下文窗口、稀释模型注意力、推高每轮请求成本。如果所有子任务的中间过程都堆在同一个 messages 列表里,父 Agent 的"思考清晰度"就会被越拖越差。s04 的答案是:让子任务在另一个上下文里执行,只把结论带回来。
三、解决方案:父级派发 task,子级独立循环
原文档给出的整体结构图如下:
Parent agent Subagent
+------------------+ +------------------+
| messages=[...] | | messages=[] | <-- fresh
| | dispatch | |
| tool: task | ----------> | while tool_use: |
| prompt="..." | | call tools |
| | summary | append results |
| result = "..." | <---------- | return last text |
+------------------+ +------------------+
Parent context stays clean. Subagent context is discarded.
两个要点:
- 消息隔离,而非进程/文件系统隔离。父子 Agent 运行在同一个 Python 进程里,共享同一个工作目录
WORKDIR,因此子 Agent 写入的文件、执行的命令仍然作用于同一工作区——隔离的只是"对话历史"这一层。 - 返回值只有一般文本摘要。子级可能产生 30+ 次工具调用的完整历史,最终只有一个段落(final text)以普通
tool_result的形式进入父级 messages。
源码文件 agents/s04_subagent.py 的模块 docstring 把这一课的核心洞见总结为一句:"Process isolation gives context isolation for free."(进程隔离天然带来上下文隔离)——虽然本课程实际用的是"同一进程内的消息列表隔离",但思想一致:上下文边界 = 消息列表边界。
四、How It Works:task 工具 + run_subagent
4.1 父级多一个 task 工具,子级没有递归派发能力
设计的第一条规则:父 Agent 获得一个 task 工具;子 Agent 获得除 task 之外的全部基础工具(不允许子 Agent 再派生孙 Agent,杜绝递归 spawning)。原文档给出的定义方式:
PARENT_TOOLS = CHILD_TOOLS + [
{"name": "task",
"description": "Spawn a subagent with fresh context.",
"input_schema": {
"type": "object",
"properties": {"prompt": {"type": "string"}},
"required": ["prompt"],
}},
]
对照 agents/s04_subagent.py 的完整实现:CHILD_TOOLS 定义了 4 个基础工具——bash(执行 shell 命令)、read_file(读文件,可选 limit 行数)、write_file(写文件)、edit_file(精确替换文本),每个都带 input_schema;随后 PARENT_TOOLS = CHILD_TOOLS + [task],其中 task 工具实际还多了一个可选的 description 字段("Short description of the task"),仅用于终端打印派发日志:
PARENT_TOOLS = CHILD_TOOLS + [
{"name": "task",
"description": "Spawn a subagent with fresh context. It shares the filesystem but not conversation history.",
"input_schema": {"type": "object", "properties": {
"prompt": {"type": "string"},
"description": {"type": "string", "description": "Short description of the task"}},
"required": ["prompt"]}},
]
一个值得注意的细节:s03(TodoWrite,见 agents/s03_todo_write.py)的工具集是
bash / read_file / write_file / edit_file / todo共 5 个;而 s04 的子级只保留了前 4 个文件/Shell 工具,todo被移出,父级则是"4 个基础 + task"共 5 个。原文档"What Changed"表格中写的"5 (base) + task (parent)"是按总工具数口径描述的,读源码时建议以后者为准。
4.2 run_subagent:全新 messages[] + 自带循环 + 30 轮安全上限
子 Agent 的核心实现如下(原文档代码块):
def run_subagent(prompt: str) -> str:
sub_messages = [{"role": "user", "content": prompt}]
for _ in range(30): # safety limit
response = client.messages.create(
model=MODEL, system=SUBAGENT_SYSTEM,
messages=sub_messages,
tools=CHILD_TOOLS, max_tokens=8000,
)
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 = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": str(output)[:50000]})
sub_messages.append({"role": "user", "content": results})
return "".join(
b.text for b in response.content if hasattr(b, "text")
) or "(no summary)"
逐点拆解这个实现,可以看到 s04 的四个关键设计决策:
| 决策点 | 实现 | 作用 |
|---|---|---|
| 上下文起点 | sub_messages = [{"role": "user", "content": prompt}] |
子 Agent 从 prompt 开始,父级历史不复制过来 |
| 循环边界 | for _ in range(30) 安全上限 |
防止子 Agent 在工具调用中死循环耗尽资源 |
| 结果截断 | str(output)[:50000] |
单次工具输出最多 5 万字符进入子级上下文 |
| 返回值 | 拼接最终 response 的 text 块,兜底 "(no summary)" |
只有最终文本回到父级;子级全部历史(可能 30+ 次工具调用)被丢弃 |
与父级使用的 SYSTEM 提示词不同,子 Agent 有独立的系统提示词 SUBAGENT_SYSTEM(agents/s04_subagent.py#L42-L43):
SYSTEM = f"You are a coding agent at {WORKDIR}. Use the task tool to delegate exploration or subtasks."
SUBAGENT_SYSTEM = f"You are a coding subagent at {WORKDIR}. Complete the given task, then summarize your findings."
父级被引导"把探索和自包含子任务委托出去",子级被引导"完成任务后总结发现"——这正对应返回值只有摘要文本的契约:子 Agent 知道它的一切中间过程都不会被保留,必须自己收敛到一个结论。
4.3 父级 agent_loop 如何把 task 当作普通工具派发
从 agents/s04_subagent.py#L146-L168 的 agent_loop 可以看到派发逻辑:父循环与 s01/s02 的标准循环完全一致,唯一的差别是在遍历 tool_use 块时多了一个分支——如果工具名是 task,就取出 prompt 同步调用 run_subagent(prompt),把返回的摘要当作这次工具调用的 tool_result 追加进父级 messages:
for block in response.content:
if block.type == "tool_use":
if block.name == "task":
desc = block.input.get("description", "subtask")
prompt = block.input.get("prompt", "")
print(f"> task ({desc}): {prompt[:80]}")
output = run_subagent(prompt)
else:
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
print(f" {str(output)[:200]}")
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(output)})
messages.append({"role": "user", "content": results})
这个"把 subagent 注册进 handler map、当作普通工具执行"的做法非常干净:父循环不需要知道 subagent 的存在方式,加一个工具 = 加一个 handler(这正是 s02 Tool Use 一课的口号)。新轨道 s06_subagent/code.py 中甚至一行写死了这个映射:TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}。
五、What Changed From s03:逐组件对比
原文档的演进对照表如下,完整保留:
| Component | Before (s03) | After (s04) |
|---|---|---|
| Tools | 5 | 5 (base) + task (parent) |
| Context | Single shared | Parent + child isolation |
| Subagent | None | run_subagent() function |
| Return value | N/A | Summary text only |
对照源码可以进一步确认:s03(agents/s03_todo_write.py)引入了 TodoManager 与"每 3 轮不更新 todo 就注入 reminder"的 nag 机制,但所有工具调用仍共享同一个 messages 列表;s04 没有改动父循环本身,只新增了 CHILD_TOOLS、SUBAGENT_SYSTEM、run_subagent() 与 PARENT_TOOLS 四处——增量极小,却能隔离一整个子对话,这是本课程"每个机制独立成章"风格的典型例子。
六、动手运行:环境与 Try It 提示词
运行前提(以仓库实际文件为准):
- 依赖见 requirements.txt:
anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0; - 按 .env.example 配置环境变量:必填
ANTHROPIC_API_KEY与MODEL_ID(示例默认claude-sonnet-4-6),可选ANTHROPIC_BASE_URL指向任何 Anthropic 兼容的 API 网关; - 在仓库根目录启动(s04 属于 legacy 轨道,脚本在
agents/下):
cd learn-claude-code
python agents/s04_subagent.py
原文档推荐的三条验证提示词,分别考察"查询型委托""批量读取型委托""写后验证型委托"三类场景:
Use a subtask to find what testing framework this project uses—— 子 Agent 去读文件找答案,父级只收到结论;Delegate: read all .py files and summarize what each one does—— 大量读取结果被限制在子级上下文内,父级只拿摘要;Use a task to create a new module, then verify it from here—— 验证"共享文件系统"这一前提:子 Agent 写的文件父级能直接看到,但对话历史互不可见。
观察要点:父级终端打印 > task (描述): prompt 前 80 字符,随后出现子 Agent 的最终摘要;父级 messages 中只多了一条 task 的 tool_result,而不是子级的全部工具调用记录。
七、源码级纵深:测试如何锁定"新上下文 + 只回传文本"契约
新轨道的测试文件 tests/test_s06_subagent.py 用 mock 的 client.messages.create 精确验证了 subagent 的两条核心契约,对理解 s04 的实现非常有助益:
test_subagent_starts_with_fresh_messages_and_returns_final_text:断言子 Agent 第一次 API 调用的 messages 恰好只有一条用户消息(即 prompt 本身,calls[0]["messages"] == [{"role": "user", "content": "..."}]),可用工具恰好是bash/read_file/write_file/edit_file/glob(不含task),且返回值是最终文本块"The note says child input.";test_s06_is_kernel_plus_task:断言父级工具集 = 基础工具集 ∪{task},子级工具集 = 基础工具集(无task),task的input_schema.required == ["prompt"]。- 另有
test_subagent_file_tools_keep_the_kernel_permission_boundary验证子 Agent 的文件工具与父级走同一权限边界:越出工作区的write_file会返回Permission denied by user。
这几条测试等价于把本文第 4 节的表格固化为可执行断言:上下文起点、工具边界、返回形态、权限一致性四项都不可被回归破坏。
八、从 s04 到 s06:机制在课程演进中的增量
作为延伸阅读(不构成 s04 本身的内容),当前主轨的 s06_subagent/code.py 在同一骨架上叠加了后续章节的机制:
- 基础工具增加
glob(BASE_TOOLS共 5 个),子级工具集SUB_TOOLS = list(BASE_TOOLS); - 接入 Hooks 扩展系统:子级工具调用与父级走同一个
execute_tool→trigger_hooks("PreToolUse" / "PostToolUse")路径,包括 deny list 拦截(rm -rf /、sudo、shutdown等)与工作区外路径的人工确认(见 s06_subagent/README.md 中的"Tool policy: Shared Hooks"设计决策表); - 30 轮耗尽时的兜底返回值从隐含行为变成显式字符串
"Subagent stopped after 30 turns without a final answer.",且新增[Subagent started] / [Subagent done] / [sub] 工具名: 前 100 字符输出的可观测性日志; - 文本提取抽成独立的
extract_text()函数,只拼接type == "text"的块。
s04 的价值在于它证明了最小形态:不引入消息队列、不引入进程隔离,仅凭"另一个 messages 列表 + 一个 handler"就能实现上下文隔离;s06 则展示了当权限、钩子等机制就位后,子 Agent 如何自动继承全部治理策略。两条轨道对照阅读,能清楚看到 harness 的演化路径:先跑通最小机制,再逐层加边界。
九、小结:subagent 的设计边界
回到 s04 文档的三句话核心:
- 上下文:全新
messages[],父级历史不复制进子级; - 执行:同一进程、同一
WORKDIR,文件系统变更双方可见; - 返回值:仅最终文本,作为一条普通
tool_result进入父级 messages。
这一定位回答了"harness 工程师如何管理上下文"这个问题:当某个子任务的中间产物对父级只是噪音时,就把它放进一个独立的消息列表里执行,只回收结论。这也是 README.md 中 harness 定义里"Manage context"职责的第一块拼图——subagent 保专注任务的上下文干净,后续课程(如新轨道 s08 的 context compaction)再解决父级自身历史过长的压缩问题,二者互补而非替代。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00