learn-claude-code Subagent 深入解析:用 task 工具让子任务运行在干净的上下文里
本篇基于 learn-claude-code 仓库的 docs/zh/s04-subagent.md 展开,讲解 Agent Harness 中最核心的上下文隔离机制——Subagent(子代理):父 Agent 通过一个 task 工具派生出一个拥有全新 messages[] 的子循环,子循环独享自己的对话历史、共享同一文件系统,结束后只把最终文本作为普通 tool_result 返回。读完后,你能理解"委派"在工程上如何实现、父子上下文的边界在哪里,并能直接在本仓库中运行和验证这套机制。
一、问题:主对话上下文为什么会被"污染"
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_TOOLS(agents/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 脚本依赖 anthropic 与 python-dotenv(通过 load_dotenv(override=True) 加载配置),并强制要求环境变量 MODEL_ID(agents/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 时,脚本会主动 pop 掉 ANTHROPIC_AUTH_TOKEN 以避免认证冲突(见 agents/s04_subagent.py)。
2. 启动
cd learn-claude-code
python agents/s04_subagent.py
进入 s04 >> 交互式提示符后,输入 q 或空行退出。原文档推荐这三条测试 prompt(英文 prompt 对 LLM 效果更好,也可以用中文):
Use a subtask to find what testing framework this project usesDelegate: read all .py files and summarize what each one doesUse 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_TOOL的prompt加了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 Loop:
task工具同步启动一个全新messages[]的子循环,子循环独享对话、共享文件系统,结束后只把最终文本作为tool_result还给父端; - 隔离靠"新列表 + 只返回摘要"实现,安全靠父子共用的
safe_path沙箱与危险命令黑名单保证,递归靠"子端没有task工具"硬性禁止; - 旧轨道实现见 agents/s04_subagent.py,当前正式版见 s06_subagent/code.py 与 s06_subagent/README.zh.md,行为约束由 tests/test_s06_subagent.py 固化;
- 上下文隔离解决"子任务噪音不污染主对话",它与后续的 s08 上下文压缩(主对话自身的腾挪)共同构成 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