首页
/ learn-claude-code s04 Subagent 实战:用独立 messages[] 为子任务隔离上下文,让父 Agent 保持干净

learn-claude-code s04 Subagent 实战:用独立 messages[] 为子任务隔离上下文,让父 Agent 保持干净

2026-09-05 10:11:22作者:凌朦慧Richard

本文基于 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 的测试用例验证其边界行为。

Subagent 概览:父 Agent 派发 task 后,子 Agent 在全新 messages[] 中独立循环调用工具,最终只有文本摘要返回父级

一、课程定位: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.

两个要点:

  1. 消息隔离,而非进程/文件系统隔离。父子 Agent 运行在同一个 Python 进程里,共享同一个工作目录 WORKDIR,因此子 Agent 写入的文件、执行的命令仍然作用于同一工作区——隔离的只是"对话历史"这一层。
  2. 返回值只有一般文本摘要。子级可能产生 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_SYSTEMagents/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-L168agent_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_TOOLSSUBAGENT_SYSTEMrun_subagent()PARENT_TOOLS 四处——增量极小,却能隔离一整个子对话,这是本课程"每个机制独立成章"风格的典型例子。

六、动手运行:环境与 Try It 提示词

运行前提(以仓库实际文件为准):

  1. 依赖见 requirements.txtanthropic>=0.25.0python-dotenv>=1.0.0pyyaml>=6.0
  2. .env.example 配置环境变量:必填 ANTHROPIC_API_KEYMODEL_ID(示例默认 claude-sonnet-4-6),可选 ANTHROPIC_BASE_URL 指向任何 Anthropic 兼容的 API 网关;
  3. 在仓库根目录启动(s04 属于 legacy 轨道,脚本在 agents/ 下):
cd learn-claude-code
python agents/s04_subagent.py

原文档推荐的三条验证提示词,分别考察"查询型委托""批量读取型委托""写后验证型委托"三类场景:

  1. Use a subtask to find what testing framework this project uses —— 子 Agent 去读文件找答案,父级只收到结论;
  2. Delegate: read all .py files and summarize what each one does —— 大量读取结果被限制在子级上下文内,父级只拿摘要;
  3. Use a task to create a new module, then verify it from here —— 验证"共享文件系统"这一前提:子 Agent 写的文件父级能直接看到,但对话历史互不可见。

观察要点:父级终端打印 > task (描述): prompt 前 80 字符,随后出现子 Agent 的最终摘要;父级 messages 中只多了一条 tasktool_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),taskinput_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 在同一骨架上叠加了后续章节的机制:

  1. 基础工具增加 globBASE_TOOLS 共 5 个),子级工具集 SUB_TOOLS = list(BASE_TOOLS)
  2. 接入 Hooks 扩展系统:子级工具调用与父级走同一个 execute_tooltrigger_hooks("PreToolUse" / "PostToolUse") 路径,包括 deny list 拦截(rm -rf /sudoshutdown 等)与工作区外路径的人工确认(见 s06_subagent/README.md 中的"Tool policy: Shared Hooks"设计决策表);
  3. 30 轮耗尽时的兜底返回值从隐含行为变成显式字符串 "Subagent stopped after 30 turns without a final answer.",且新增 [Subagent started] / [Subagent done] / [sub] 工具名: 前 100 字符输出 的可观测性日志;
  4. 文本提取抽成独立的 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)再解决父级自身历史过长的压缩问题,二者互补而非替代。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384