learn-claude-code s06 深度解析:用 Subagent 的 task 工具为子任务创建独立上下文
本篇技术指南基于 learn-claude-code 仓库的课程章节 s06_subagent/README.ja.md,讲解 subagent(子代理)机制的设计与实现:当主 Agent 需要处理一个耗上下文的重型子任务(如追踪调用链、批量阅读文件)时,如何通过 task 工具启动一个持有全新 messages[] 的嵌套 Agent Loop,让中间对话留在子上下文中、仅把最终文本作为 tool result 返回父会话。读完本文,你将能够理解“消息级隔离”与“进程级隔离”的本质区别、subagent 的边界决策(工具集、返回契约、委托深度、权限策略),并可以基于 s06_subagent/code.py 在本仓库中实际运行与验证这一机制。
1. 问题背景:中间过程会持续占用父上下文
s06 章节针对的问题很具体:Agent 在修一个 bug,为了追踪调用链读了很多文件,每一次工具调用及其结果都会留在父 Agent 的 messages[] 中。等调用链搞清楚之后,这些中间细节已经不再需要,却仍然持续占用上下文窗口。
这正是本仓库课程主线("Bash is all you need" —— 从 0 到 1 构建一个 claude code 风格的 agent harness)中 harness 层的 Delegation(委托) 机制要解决的场景:把聚焦的子任务放进一个独立的会话上下文里执行。s06 在课程链中位于 s05 之后、s07 Skill Loading 之前(课程链:s01 → s02 → s03 → s04 → s05 → s06 → s07 → … → s16 → s17),此前章节已经搭建好了 Agent Loop、工具调用与 Hooks 基础设施,s06 在此之上加一个 task 工具即可。
2. 方案核心:嵌套 Agent Loop + 全新 messages[]
调用 task 后,系统会同步启动一个使用全新 messages[] 的嵌套 Agent Loop;该循环结束时,其最终文本成为父会话中的一条 tool result。
这里必须明确一个边界:被隔离的是消息,而不是进程或文件系统。父 Agent 与 subagent 运行在同一个 Python 进程中,共享 WORKDIR,因此子代理的写文件和命令执行仍然作用于同一个工作区。subagent 拥有 5 个基础工具但没有 task,并且使用与父 Agent 相同的权限 Hooks 与生命周期 Hooks。
2.1 实现代码:run_subagent
对照 s06_subagent/code.py 的源码,run_subagent 的逻辑如下:
SUB_TOOLS = list(BASE_TOOLS) # no task tool
def run_subagent(prompt: str) -> str:
messages = [{"role": "user", "content": prompt}]
for _ in range(30):
response = client.messages.create(
model=MODEL, system=SUB_SYSTEM,
messages=messages, tools=SUB_TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return extract_text(response.content) or "(no summary)"
results = []
for block in response.content:
if block.type == "tool_use":
output = execute_tool(block, SUB_HANDLERS)
results.append({... "content": output})
messages.append({"role": "user", "content": results})
return "Subagent stopped after 30 turns without a final answer."
关键实现细节(以 code.py 为准):
- 全新消息列表:
messages = [{"role": "user", "content": prompt}],父会话历史完全不复制进来,这是"上下文隔离"的全部来源; - 循环上限 30 轮:这是一个安全阀,30 轮内没有产出最终回答时返回
"Subagent stopped after 30 turns without a final answer.",避免子循环无限消耗 token; SUB_SYSTEM独立系统提示:子代理的系统提示(code.py#L49-L52)要求"Complete the given task, then return a concise final answer",与父提示"Use task for focused exploration"形成分工;extract_text兜底:从最终响应中提取text类型的内容块,若为空则返回"(no summary)",保证父循环总能拿到一个字符串;- 可观察性:运行时打印
[Subagent started]/[Subagent done],子循环内的每次工具调用以[sub] 工具名: 输出前100字符的形式打印,方便调试时确认委托确实发生。
2.2 父 Agent 视角:task 只是一个普通工具
主 Agent 调用 task 的方式与其他工具完全一致(code.py#L296-L307):
TASK_TOOL = {
"name": "task",
"description": "Run a subagent with fresh conversation context and return its final text.",
"input_schema": {
"type": "object",
"properties": {"prompt": {"type": "string", "minLength": 1}},
"required": ["prompt"],
},
}
TOOLS = [*BASE_TOOLS, TASK_TOOL]
TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
也就是说 run_subagent 被放进 TOOL_HANDLERS 映射后,父循环(code.py#L312-L340 的 agent_loop)在分发 task 工具调用时不会有任何特殊处理——task 的返回值和 bash、read_file 的返回值一样,作为一条 tool_result 追加到父 messages[] 中。父 Agent 因此只能看到 subagent 的最终文本,看不到子循环里发生过的任何工具调用。
2.3 边界决策表
文档给出的设计决策及理由如下:
| 决策点 | 选择 | 理由 |
|---|---|---|
| 会话 | 全新 messages[] |
不将父会话复制给 subagent |
| 执行 | 同一进程、同一 WORKDIR |
两个循环都能看到文件系统变更 |
| 返回值 | 仅最终文本 | 子的工具调用与结果不进入父 messages |
| 委托深度 | SUB_TOOLS 中没有 task |
本章只允许一层委托 |
| 工具策略 | 共享 Hooks | 父子使用相同的权限检查 |
从源码结构看,前四条都有明确的落点:
SUB_TOOLS = list(BASE_TOOLS)(code.py#L243-L244):子工具集直接复制 5 个基础工具(bash、read_file、write_file、edit_file、glob,见 code.py#L113-L124),不含task,从而硬性保证委托深度为 1;WORKDIR = Path.cwd()(code.py#L41):模块级常量,父循环与子循环的所有文件操作(如run_write、run_glob的root_dir=WORKDIR)与subprocess.run(..., cwd=WORKDIR)都作用于同一目录;- 第 5 条(Hooks 共享)见下一节。
3. 权限与生命周期:子代理走同一套 Hooks
run_subagent 内每次工具执行都经过统一的 execute_tool(code.py#L226-L238):先触发 PreToolUse hooks(任一 hook 返回非 None 即拦截),再调用 SUB_HANDLERS 中的处理函数,最后触发 PostToolUse hooks。由于 SUB_HANDLERS = dict(BASE_HANDLERS) 且 hooks 是模块级全局注册(code.py#L137-L223),父与子共享完全相同的权限边界:
permission_hook(PreToolUse):DENY_LIST(rm -rf /、sudo、shutdown、reboot、mkfs、dd if=)直接拦截;DESTRUCTIVE关键字(rm、> /etc/、chmod 777)需要用户交互确认;read_file/write_file/edit_file访问WORKDIR之外的路径时也会弹出确认(code.py#L156-L180)。这意味着子代理并不能借"隔离"之名绕过权限系统;log_hook/large_output_hook/summary_hook:分别记录每次工具调用、警告超过 100000 字符的大输出、在Stop事件时统计本次会话的工具调用次数。
值得注意的一个细节:Stop hook 返回非 None 时,父循环和子循环(见 agent_loop 与 run_subagent 中 force = trigger_hooks("Stop", messages))都会把该返回值作为新的 user 消息追加并继续循环——这是 harness 层的"强制继续"机制,s06 让子循环也继承了它。
4. 测试印证:契约如何被固定
tests/test_s06_subagent.py 用 mock 的 anthropic 客户端把上述设计固化为可运行的断言,是理解"实际行为"的可靠依据:
test_s06_is_kernel_plus_task(tests/test_s06_subagent.py#L66-L81):断言父工具集 = 5 个基础工具 ∪{task},子工具集 = 恰好 5 个基础工具且不含task,TASK_TOOL的input_schema.required为["prompt"];test_subagent_starts_with_fresh_messages_and_returns_final_text(tests/test_s06_subagent.py#L83-L113):模拟"先read_file再end_turn"的两轮响应,断言第一次client.messages.create收到的messages恰好是[{"role": "user", "content": "Read note.txt and report its contents."}](全新上下文)、tools只有 5 个基础工具、返回值就是最终文本"The note says child input.";test_subagent_file_tools_keep_the_kernel_permission_boundary(tests/test_s06_subagent.py#L116-L132):让子代理通过write_file写工作区外路径,模拟用户回答n,断言结果为"Permission denied by user"且目标文件确实不存在——验证了子代理与父 Agent 共享同一权限边界。
仓库另有一份旧版 12 课时的 subagent 实现 agents/s04_subagent.py(旧课程轨的 s04),其 docstring 中"Process isolation gives context isolation for free"的思路与 s06 一脉相承,但 s06 把 Hooks 权限体系也纳入了子循环,边界控制更完整。
5. 动手运行
环境依赖见 requirements.txt(anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0)。按 README.md 的 Quick Start 配置后运行:
pip install -r requirements.txt
cp .env.example .env # 配置 ANTHROPIC_API_KEY;code.py 还会读取 MODEL_ID、ANTHROPIC_BASE_URL(可选)
cd learn-claude-code
python s06_subagent/code.py
注意 code.py#L38-L43 的两处环境细节:设置了 ANTHROPIC_BASE_URL(用于指向代理/网关)时会主动 pop 掉 ANTHROPIC_AUTH_TOKEN;MODEL_ID 是必填环境变量,缺省会直接抛 KeyError。
文档推荐以下三个试验 prompt(README.ja.md "試してみよう" 一节原文继承):
Use a subtask to find what testing framework this project uses(子代理负责读文件,主 Agent 只接收结论);Delegate: read all .py files in agents/ and summarize what each one does;Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent(验证"共享 WORKDIR":子代理写入的文件,父代理可以直接验证——注意这会在你的工作区生成文件)。
观察要点:
- 是否出现
[Subagent started]/[Subagent done]标记; - 子代理的工具调用是否以
[sub] ...前缀打印(且只打印输出前 100 字符); - 父 Agent 是否只拿到
task返回的最终文本继续后续推理; - 用 prompt 3 时,父 Agent 的
messages[]中不会出现子代理逐文件读取的中间结果。
6. 局限与后续方向
当前实现是同步阻塞的委托:父循环在 task 执行期间暂停,且只允许一层委托(子代理没有 task 工具);30 轮上限与 8000 max_tokens 是写死的安全阀,没有后台任务、并行子代理或子代理间的消息传递——这些是课程后续章节(如 s11 背景任务)的方向。
此外,"把知识塞进 system prompt"并不能解决上下文膨胀问题:不同任务需要不同知识(前端组件改动需要 React 约定,写 SQL 需要表结构),全量预载必然撑爆上下文。这正是下一课 → s07 Skill Loading 的主题:按需注入技能(skill),而不是向 system prompt 堆积文档——在需要时才读取,如同读一个文件一样自然(见 s07_skill_loading/README.zh.md)。
7. 小结
s06 subagent 机制的核心可以浓缩为四条可复用的设计原则:
- 隔离单位是消息而非进程:
messages = [{"role": "user", "content": prompt}]一行代码即完成上下文隔离,同时天然共享文件系统与工作目录; - 返回值契约只有"最终文本":子循环的全部中间对话被丢弃,父上下文保持干净;
- 委托深度用工具集硬性封顶:
SUB_TOOLS不含task,杜绝递归派生; - 权限策略不可旁路:子循环复用父循环的
execute_tool+ Hooks 管线,denied/dangerous 检查对两者一视同仁。
这四个决策点使 subagent 成为 harness 中一个"低成本、强边界"的委托原语:模型决定何时委托,harness 负责隔离与回收。
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 StartedRust0622
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