首页
/ learn-claude-code Subagent 深入解析:用 task 工具让子任务运行在干净的上下文里

learn-claude-code Subagent 深入解析:用 task 工具让子任务运行在干净的上下文里

2026-09-04 09:10:06作者:郦嵘贵Just

本篇基于 learn-claude-code 仓库的 docs/zh/s04-subagent.md 展开,讲解 Agent Harness 中最核心的上下文隔离机制——Subagent(子代理):父 Agent 通过一个 task 工具派生出一个拥有全新 messages[] 的子循环,子循环独享自己的对话历史、共享同一文件系统,结束后只把最终文本作为普通 tool_result 返回。读完后,你能理解"委派"在工程上如何实现、父子上下文的边界在哪里,并能直接在本仓库中运行和验证这套机制。

Subagent 概览:父 Agent 的 task 工具同步派生一个拥有全新 messages 的子 Agent Loop,子循环的最终文本作为工具结果返回父对话

一、问题:主对话上下文为什么会被"污染"

Agent 工作越久,messages 数组越臃肿。每次读文件、跑命令的输出都永久留在上下文里。比如用户问"这个项目用什么测试框架?",回答这个问题可能要读 5 个文件,但父 Agent 最终只需要一个词:"pytest。"

中间的 5 次文件读取、几十个 KB 的工具输出,对父对话来说全是噪音。如果这些内容都堆积在主循环的 messages[] 中,会挤压后续真正需要的推理空间,也让模型更容易"走神"。这正是 s08 上下文压缩 要解决的"容量"问题,而 Subagent 解决的是"隔离"问题——从源头让中间过程根本不进入父上下文。

"大任务拆小,每个小任务干净的上下文"——Subagent 用独立的 messages[],不污染主对话。它属于 Harness 层的上下文隔离机制:守护模型的思维清晰度。

二、解决方案:父-子委派模型

原文档给出的核心架构图如下:

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.

对应到 agents/s04_subagent.py 的模块注释中,还有一句关键的洞察:"Process isolation gives context isolation for free."(进程隔离免费换来上下文隔离)。不过要注意,本仓库的 s04 实现并非真的开子进程——两个循环运行在同一进程里,隔离的是"消息列表",共享的是"文件系统"。

1. 父端:一个 task 工具 + 基础工具

父 Agent 的工具体是"子 Agent 全部工具 + 一个 task 分发器":

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 的真实实现里,task 工具比文档示例多了一个可选的 description 参数,且描述更完整:

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"]}},
]

这段描述本身就是写给模型的"契约":共享文件系统,不共享对话历史。子端工具池 CHILD_TOOLSagents/s04_subagent.py)只包含 4 个基础工具:

工具 作用 实现要点
bash 执行 shell 命令 危险命令黑名单拦截,120s 超时,输出截断 50000 字符
read_file 读文件 safe_path 校验,支持 limit 限行读取
write_file 写文件 自动创建父目录
edit_file 精确替换文本 old_text 不存在时返回错误而不是抛异常

所有文件类工具都经过同一个沙箱函数 safe_path:把相对路径解析后检查 is_relative_to(WORKDIR),逃逸工作区的路径直接抛 ValueError。这意味着子 Agent 的写操作依然受同一套边界约束——隔离的是上下文,不是安全边界。

2. 子端:全新 messages,独立循环

子 Agent 的核心就是 run_subagent(prompt):以 messages=[] 起步,跑自己的 Agent Loop,只有最终文本返回给父 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)"

对照 run_subagent 的完整源码,可以确认文档示例与实际实现完全一致,并补充几个工程细节:

  • 30 轮安全上限for _ in range(30) 防止子循环无限调用工具;
  • 独立 system prompt:子循环使用 SUBAGENT_SYSTEM"You are a coding subagent at {WORKDIR}. Complete the given task, then summarize your findings.",见 agents/s04_subagent.py),与父端的 SYSTEM 区分开——父端提示"用 task 委派",子端提示"完成任务并总结";
  • 未知工具兜底handler(**block.input) if handler else f"Unknown tool: {block.name}",模型幻觉出不存在的工具名时循环不会崩;
  • 输出截断:每个 tool_result 截断到 50000 字符,防止单次超长输出撑爆子上下文;
  • 空结果兜底:子循环结束时若没有文本块,返回 "(no summary)",父端至少知道子任务没有产出;
  • 丢弃式回收:函数返回后,sub_messages 中可能累积的 30+ 轮工具调用记录直接被垃圾回收。父 Agent 收到的只是一段摘要文本,以普通 tool_result 的身份进入它的 messages[]

3. 父循环如何分发 task

父循环与普通工具的分发方式完全一致——只是 handler map 里多了一个 task 分支(agent_loop 源码):

def agent_loop(messages: list):
    while True:
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=PARENT_TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return
        results = []
        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}"
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": str(output)})
        messages.append({"role": "user", "content": results})

注意 run_subagent(prompt)同步调用:父循环在 task 工具执行期间完全阻塞,等子循环跑完才把摘要塞进 results。这是最简单也最可控的委派方式(后续章节会引入异步团队协议来替代它)。

三、隔离边界:什么被隔离,什么被共享

把散落各处的实现细节汇总成一张决策表(s04 版本 + 当前 s06 版本通用):

维度 选择 依据
对话历史 全新的 messages[] 父对话不复制给子 Agent,见 run_subagent
执行环境 同一进程、同一 WORKDIR 父子共用 subprocess.run(cwd=WORKDIR)safe_path,文件修改互相可见
返回值 只返回最终文本 子循环的工具调用与结果不进入父消息列表
委派深度 只允许一层 子端的 CHILD_TOOLS没有 task,禁止递归生成
权限策略 父子相同 子循环执行工具时走同一组 handler 与沙箱校验

"禁止递归"这一点值得强调:如果子 Agent 也能调 task,就可能出现无限嵌套委派,上下文隔离反而变成资源泄漏。当前实现通过在工具池层面直接删掉 task 来硬性禁止。

四、相对 s03 的变更

组件 之前 (s03) 之后 (s04)
Tools 5 5 (基础) + task (仅父端)
上下文 单一共享 父 + 子隔离
Subagent run_subagent() 函数
返回值 不适用 仅摘要文本

结合源码精确说明:s03(agents/s03_todo_write.py)的 5 个工具是 bash/read_file/write_file/edit_file/todo;s04 的子端去掉了 todo(子任务不需要跨轮次计划状态),父端在 4 个基础工具上增加 task,因此父端工具总数仍是 5、子端是 4。

五、运行指南:试一试

1. 环境准备

s04 脚本依赖 anthropicpython-dotenv(通过 load_dotenv(override=True) 加载配置),并强制要求环境变量 MODEL_IDagents/s04_subagent.py)。按 README 快速开始 的步骤:

git clone https://github.com/shareAI-lab/learn-claude-code
cd learn-claude-code
pip install -r requirements.txt
cp .env.example .env   # 配置 ANTHROPIC_API_KEY 与 MODEL_ID

.env.example 默认给出 MODEL_ID=claude-sonnet-4-6,也列出了经 ANTHROPIC_BASE_URL 接入其他提供商时的取值示例;当设置了 ANTHROPIC_BASE_URL 时,脚本会主动 popANTHROPIC_AUTH_TOKEN 以避免认证冲突(见 agents/s04_subagent.py)。

2. 启动

cd learn-claude-code
python agents/s04_subagent.py

进入 s04 >> 交互式提示符后,输入 q 或空行退出。原文档推荐这三条测试 prompt(英文 prompt 对 LLM 效果更好,也可以用中文):

  1. Use a subtask to find what testing framework this project uses
  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

观察重点:父循环打印的 > task (描述): prompt 前 80 字符(来自 agent_loop 的 print 语句),以及父对话最终只把子 Agent 的摘要文本当作一条 tool_result 收进来。第 3 条 prompt 尤其能体现"共享文件系统"的语义——子 Agent 创建的模块,父 Agent 随后可以用自己的 read_file 直接验证。

六、延伸:当前章节 s06 与可验证的测试证据

需要说明版本脉络:README 指出仓库有两条课程轨道——docs/agents/旧版 12 课过渡轨道(本篇所属),根目录 s01_agent_loop/ ~ s17_goal_loop/当前 17 课正式版。按 README 的映射表,旧 s04(Subagent)对应新的 s06_subagent 章节。

当前轨道的 s06_subagent/code.py 在旧版基础上演进为:

  • 基础工具扩为 5 个(新增 glob),子端工具池 SUB_TOOLS = list(BASE_TOOLS)s06_subagent/code.py);
  • TASK_TOOLprompt 加了 minLength: 1 约束,task 与普通工具统一进 TOOL_HANDLERS 分发 map(s06_subagent/code.py);
  • 子循环接入与父循环同一组 Hooks(权限检查、日志、大输出告警),并带 [Subagent started] / [sub] ... / [Subagent done] 的可观测输出(s06_subagent/code.py);
  • 30 轮安全上限耗尽时返回显式提示 "Subagent stopped after 30 turns without a final answer.",而不是把最后一次的半成品当摘要。

这些行为都有自动化测试背书,见 tests/test_s06_subagent.py

  • test_s06_is_kernel_plus_task 断言父端工具集 = 基础工具 ∪ {task},且子端工具集里不含 task,直接验证"单层委派"约束(tests/test_s06_subagent.py);
  • test_subagent_starts_with_fresh_messages_and_returns_final_text 用 mock 客户端断言:子循环第一次请求的 messages 恰好只含 prompt 一条 user 消息,且返回值就是最终文本(tests/test_s06_subagent.py);
  • test_subagent_file_tools_keep_the_kernel_permission_boundary 验证子循环写工作区外文件会被权限边界拦截。

七、小结

  • Subagent 的本质是嵌套 Agent Looptask 工具同步启动一个全新 messages[] 的子循环,子循环独享对话、共享文件系统,结束后只把最终文本作为 tool_result 还给父端;
  • 隔离靠"新列表 + 只返回摘要"实现,安全靠父子共用的 safe_path 沙箱与危险命令黑名单保证,递归靠"子端没有 task 工具"硬性禁止;
  • 旧轨道实现见 agents/s04_subagent.py,当前正式版见 s06_subagent/code.pys06_subagent/README.zh.md,行为约束由 tests/test_s06_subagent.py 固化;
  • 上下文隔离解决"子任务噪音不污染主对话",它与后续的 s08 上下文压缩(主对话自身的腾挪)共同构成 Harness 层的上下文治理。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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