
作者:逆境不可逃
技术永无止境
希望我的内容可以帮助到你!!!!!
大家吼 ! 我是 逆境不可逃 今天给大家带来文章《【与我学 ClaudeCode】规划与协调篇 之 Subagent:上下文隔离的极简子代理框架》.
Learn-Claude-Code 官方地址 : https://github.com/shareAI-lab/learn-claude-code
Subagent 是在 TodoWrite 基础上迭代的第 4 个版本(s04),核心解决 Agent 上下文臃肿 问题,通过「大任务拆小、每个子任务独立干净上下文」的方式,让主对话始终保持清晰,避免被大量工具调用输出污染。
学习路线:s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12
一、问题根源:为什么 Agent 越跑越慢、越跑越偏?
随着 Agent 工作时间变长,messages 数组会急剧膨胀:
- 每次
read_file、bash命令的输出都永久留在上下文中 - 比如回答「这个项目用什么测试框架?」,可能要读 5 个文件、跑 3 条命令,主 Agent 上下文会被大量无关信息填满
- 上下文窗口是有限的,冗余信息会挤占关键任务的 token 空间,稀释系统提示的影响力,导致模型后期跑偏、重复工作
传统解决方案(比如直接复制父上下文给子代理)会让问题更糟:子代理继承了父代理的所有历史对话,不仅信息过载,还会被无关细节干扰判断。
二、三大核心设计决策(图片内容详解)
Subagent 通过三个硬约束,从架构层面实现上下文隔离,同时保证系统的可控性和安全性。
1. 子代理获得全新上下文,而非共享历史
核心设计 :父代理通过 task 工具创建子代理时,子代理从全新的消息历史 启动,只包含系统提示词和委派的任务描述,不继承父代理的任何对话历史。子代理的交互过程(哪怕数十轮),最终只以一条 tool_result 摘要的形式返回给父代理。
三大关键优势:
- 子代理完全专注于当前子任务,不会被父代理过往的对话、工具输出干扰
- 父代理的上下文始终保持干净,不会被子任务的细节污染
- 大幅降低 token 消耗:子代理的完整对话历史直接丢弃,父代理只保留最终摘要
替代方案的致命缺陷:
共享父代理完整上下文会给子代理更多信息,但也会用无关细节淹没它。上下文窗口是有限的,填充父历史会挤占子代理自身工作的空间;基于 fork 的复制上下文方案是折中方案,但仍会在无关历史上浪费大量 token。
2. Explore 类型子代理强制只读权限(可选扩展)
核心设计 :针对「探索类子任务」(如查找函数使用位置、分析项目结构),子代理仅能使用只读工具:受限的 bash、read_file 和搜索工具,禁止调用 write_file/edit_file。这遵循了最小权限原则。
解决的风险:
- 消除探索过程中误修改文件的风险
- 缩小工具空间,让模型在更少的选项中做出更精准的决策(减少工具选择的困惑)
替代方案的致命缺陷:
给所有子代理完整工具权限实现最简单,但违反最小权限原则;权限申请系统(子代理向父代理请求写权限)会增加复杂度和延迟;按角色静态过滤工具是务实的折中方案 ------ 实现简单,能有效防止意外修改。
3. 子代理不能再创建子代理(禁止递归委派)
核心设计 :task 工具不包含在子代理的工具集中,子代理必须直接完成工作,不能继续委派任务。这直接防止了无限委派循环。
为什么禁止递归:
- 没有这个约束,代理可能创建子代理,子代理又创建子代理,每一层都用略微不同的措辞重新委派同一任务,白白消耗 token 却毫无进展
- 单层级委派足以处理绝大多数真实编码场景,过度分层只会增加复杂度
替代方案的致命缺陷:
允许带深度限制的递归委派可以处理深度嵌套的任务,但会增加系统复杂度和 token 失控的风险;在实践中,单层委派就能覆盖绝大多数场景,多层委派在后续版本中通过持久团队结构解决,而非递归生成。
三、系统整体架构与工作原理
1. 核心架构图
Parent agent Subagent
+------------------+ +------------------+
| messages=[...] | | messages=[] | <-- fresh
| | dispatch | |
| tool: task | ----------> | while tool_use: |
| prompt="..." | | call tools |
| | summary | append results |
| result = "..." | <---------- | return last text |
+------------------+ +------------------+
- 父代理通过
task工具发起子任务,传递任务描述 - 子代理以空上下文启动,独立执行工具调用,完成任务后返回摘要
- 子代理的完整对话历史被丢弃,父代理只收到最终结果,上下文始终保持干净
2. 关键组件与工作流程
(1) 工具分层设计
-
父代理工具集(
PARENT_TOOLS) :基础工具 +task工具(用于创建子代理) -
子代理工具集(
CHILD_TOOLS) :仅基础工具(无task工具,禁止递归委派)子代理工具集:所有基础工具,不含 task
CHILD_TOOLS = [
{"name": "bash", ...},
{"name": "read_file", ...},
{"name": "write_file", ...},
{"name": "edit_file", ...},
]父代理工具集:子代理工具 + task 工具
PARENT_TOOLS = CHILD_TOOLS + [
{"name": "task", "description": "Spawn a subagent with fresh context.", ...}
]
(2) 子代理执行函数 run_subagent
这是整个系统的核心,负责创建独立的子代理实例:
def run_subagent(prompt: str) -> str:
# 1. 初始化全新的消息上下文,仅包含任务提示
sub_messages = [{"role": "user", "content": prompt}]
# 2. 安全循环限制:最多30轮,防止无限执行
for _ in range(30):
# 调用模型,使用子代理专用系统提示
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})
# 3. 如果模型不再调用工具,说明任务完成,退出循环
if response.stop_reason != "tool_use":
break
# 4. 执行子代理的工具调用,结果加入子代理上下文
results = []
for block in response.content:
if block.type == "tool_use":
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)[:50000]})
sub_messages.append({"role": "user", "content": results})
# 5. 仅返回最终文本摘要,子代理上下文直接丢弃
return "".join(b.text for b in response.content if hasattr(b, "text")) or "(no summary)"
(3) 父代理主循环与 task 工具处理
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":
# 处理 task 工具调用:创建子代理
desc = block.input.get("description", "subtask")
prompt = block.input.get("prompt", "")
print(f"> task ({desc}): {prompt[:80]}")
# 调用 run_subagent 执行子任务,获取摘要
output = run_subagent(prompt)
else:
# 处理普通工具调用(bash/read_file等)
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})
(4) 执行流程

四、与 TodoWrite(s03)的关键变更对比
| 组件 | 之前(s03 TodoWrite) | 之后(s04 Subagent) |
|---|---|---|
| 工具集 | 5 个基础工具(含 todo) | 父端:5 基础工具 + task;子端:4 基础工具(无 task) |
| 上下文 | 单一共享上下文,所有对话和工具输出永久保留 | 父 + 子隔离:子代理独立上下文,仅摘要返回父代理 |
| 子代理 | 无 | 新增 run_subagent() 函数,实现子代理生命周期管理 |
| 返回值 | 工具输出直接加入主上下文 | 子代理仅返回最终摘要,完整交互历史丢弃 |
| 核心约束 | 单任务聚焦、计划上限 | 上下文隔离、禁止递归委派、工具分层 |
五、核心优势与创新点
- 从架构上解决上下文臃肿问题:通过进程级别的上下文隔离,让子代理的所有细节交互不污染主对话,主上下文始终保持高效
- 极致的 token 优化:子代理的完整对话历史直接丢弃,父代理只保留关键摘要,大幅降低 token 消耗
- 最小权限与安全约束:工具分层设计(子代理无 task 工具、可扩展只读权限),防止无限委派和误操作
- 模型自主性与可控性平衡:父代理负责任务拆分和委派,子代理专注执行,既保留了模型的灵活性,又通过硬约束防止失控
- 实现简单、可扩展性强:仅 154 行代码,核心逻辑清晰,后续可扩展不同角色的子代理(探索型、执行型、测试型)
六、运行示例
假设用户输入:"实现登录功能并编写单元测试",父代理的典型执行流程:
- 父代理分析任务,决定拆分两个子任务:
- 子任务 1:分析项目结构,确定登录功能的实现位置和测试框架
- 子任务 2:实现登录接口并编写单元测试
- 父代理调用
task工具,委派子任务 1:prompt="分析项目结构,找出用户认证相关的代码文件,说明项目使用的测试框架" - 子代理以空上下文启动,执行
read_file、bash ls等工具调用,完成分析后返回摘要:"认证逻辑在 auth.py,使用 pytest 作为测试框架,现有测试文件为 test_auth.py" - 父代理收到摘要后,调用
task工具委派子任务 2:prompt="在 auth.py 中实现登录接口,密码使用 bcrypt 加密,在 test_auth.py 中添加单元测试" - 子代理执行文件读写操作,完成后返回实现结果和测试报告
- 父代理收到两个子任务的摘要,整合后完成最终回复,整个过程主上下文仅增加两条摘要,无冗余信息
