s04_subagent.py 讲的是:在普通 Coding Agent 里增加一个 task 工具,让主 Agent 可以把某个探索任务/子任务交给一个“子 Agent”执行;子 Agent 拥有独立上下文,但共享同一个工作目录,最后只把总结结果返回给主 Agent。 这个文件的核心目标是:用 Subagent 保持主 Agent 上下文干净,避免把大量探索过程塞进主对话历史。 (GitHub)
1. 整体结构
可以把它理解成两层 Agent:
Parent Agent 主 Agent
├─ 可以直接用 bash / read_file / write_file / edit_file
└─ 也可以调用 task 工具
task 工具
└─ 启动 Child Agent 子 Agent
├─ 子 Agent 有自己的 messages=[]
├─ 子 Agent 可以用基础工具
├─ 子 Agent 不能继续调用 task
└─ 最后只返回一段 summary 给主 Agent
文件开头的说明已经把这个思路讲得很清楚:主 Agent 保留自己的 messages=[...],子 Agent 使用新的 messages=[],子 Agent 执行完后只返回总结,子 Agent 的上下文会被丢弃。(GitHub)
2. 初始化部分:准备模型、工作目录和系统提示词
代码先加载 .env,创建 Anthropic 客户端,然后从环境变量里读取模型 ID:
load_dotenv(override=True)
WORKDIR = Path.cwd()
client = Anthropic()
MODEL = os.environ["MODEL_ID"]
这里的 WORKDIR = Path.cwd() 表示 Agent 的工作目录就是你运行这个 Python 文件时所在的目录。后面的读文件、写文件、执行命令,默认都在这个目录下进行。(GitHub)
它定义了两个系统提示词:
SYSTEM = ...
SUBAGENT_SYSTEM = ...
区别是:
提示词
给谁用
作用
SYSTEM
主 Agent
告诉模型:你是 coding agent,可以用 task 委托子任务
SUBAGENT_SYSTEM
子 Agent
告诉模型:你是 coding subagent,完成任务后总结发现
也就是说,主 Agent 偏“调度”,子 Agent 偏“执行”。(GitHub)
3. 基础工具:bash / read / write / edit
这份代码复用了前几节的工具能力,主要有 4 个:
工具
对应函数
作用
bash
run_bash()
执行 shell 命令
read_file
run_read()
读取文件内容
write_file
run_write()
写入文件
edit_file
run_edit()
替换文件中的指定文本
其中 safe_path() 很关键:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(...)
它的作用是:防止模型读写工作目录之外的文件。比如模型想读 ../../secret.txt,这个函数会阻止。(GitHub)
run_bash() 里面还做了一层简单安全拦截:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
如果命令中包含这些危险片段,就直接返回错误,不执行。它还设置了 timeout=120,避免命令一直卡住。(GitHub)
4. CHILD_TOOLS:子 Agent 只能用基础工具
子 Agent 拿到的工具是:
CHILD_TOOLS = [
bash,
read_file,
write_file,
edit_file
]
注意:子 Agent 没有 task 工具。
这是一个很重要的设计,代码注释也写了:子 Agent 拥有所有基础工具,但不允许递归创建新的子 Agent。这样可以避免:
主 Agent
-> 子 Agent
-> 子子 Agent
-> 子子子 Agent
-> ...
否则很容易失控,造成无限递归、上下文爆炸、工具调用成本暴涨。(GitHub)
5. 核心函数:run_subagent(prompt)
这是本文件最核心的函数。
它做了几件事:
sub_messages = [{"role": "user", "content": prompt}]
这行表示:子 Agent 的上下文是全新的。它不会继承主 Agent 之前的完整聊天记录,只拿到主 Agent 通过 task 工具传进来的 prompt。(GitHub)
然后进入最多 30 轮的 Agent Loop:
for _ in range(30):
response = client.messages.create(...)
这和前面几节的 Agent Loop 类似:模型回复,如果需要工具,就执行工具;执行完后把 tool_result 塞回 sub_messages;模型继续推理。(GitHub)
流程大概是:
run_subagent(prompt)
-> 创建新的 sub_messages
-> 调 Claude
-> Claude 要用工具?
-> 执行工具
-> 把工具结果追加到 sub_messages
-> 继续调 Claude
-> Claude 不再要工具
-> 返回最终文本 summary
最后这一句很关键:
return "".join(b.text for b in response.content if hasattr(b, "text")) or "(no summary)"
它只把子 Agent 最终回复里的文本返回给主 Agent。子 Agent 中间经历了多少次读文件、跑命令、编辑文件,主 Agent 不会拿到完整过程,只拿到最后总结。(GitHub)
6. PARENT_TOOLS:主 Agent 多了一个 task 工具
主 Agent 的工具是:
PARENT_TOOLS = CHILD_TOOLS + [task]
也就是说,主 Agent 不仅能直接读写文件、执行命令,还多了一个特殊工具:
task
这个工具的输入大概是:
{
"prompt": "...",
"description": "..."
}
prompt 是交给子 Agent 的具体任务,description 是简短描述,用来打印日志。(GitHub)
7. 主 Agent 的执行循环:agent_loop(messages)
主 Agent 的循环逻辑和前几节基本一致:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=PARENT_TOOLS,
max_tokens=8000,
)
如果模型没有调用工具,就结束;如果模型调用了工具,就执行对应工具,并把结果作为 tool_result 塞回 messages。(GitHub)
特殊点在这里:
if block.name == "task":
output = run_subagent(prompt)
else:
output = handler(**block.input)
也就是说:
工具类型
执行方式
普通工具
直接调用 TOOL_HANDLERS
task 工具
调用 run_subagent(prompt),启动子 Agent
所以 task 本质上不是普通文件工具,而是一个 Agent 调度器。(GitHub)
8. 命令行入口
最后是交互式命令行:
if __name__ == "__main__":
history = []
while True:
query = input("s04 >> ")
...
用户输入一句话,就追加到 history,然后调用 agent_loop(history)。如果输入 q、exit 或空字符串,就退出程序。(GitHub)
执行完成后,它会从 history[-1]["content"] 里取出模型最后的文本块并打印出来。(GitHub)
9. 举个实际例子
假设你输入:
分析这个项目的目录结构,并找出主要入口文件
主 Agent 可能会想:
这个任务需要先探索项目结构,我可以交给子 Agent 去做。
于是主 Agent 调用:
task(prompt="探索项目目录结构,找出主要入口文件", description="project exploration")
子 Agent 会在自己的上下文里执行:
bash: ls
bash: find . -maxdepth ...
read_file: README.md
read_file: package.json
...
然后子 Agent 返回总结:
项目主要入口是 xxx,核心目录包括 xxx,README 说明 xxx。
主 Agent 只收到这段总结,而不是子 Agent 每一步的完整工具调用记录。
这就是 Subagent 的价值:让脏活、探索活、信息收集活在子上下文里完成,主上下文只保留结果。
10. 这个文件相比前几节新增了什么?
文件
核心能力
s01_agent_loop.py
最小 Agent Loop:模型回复、维护 history
s02_tool_use.py
让模型可以调用工具
s03_todo_write.py
让 Agent 有任务规划/待办意识
s04_subagent.py
让 Agent 可以委托子 Agent 执行任务,保持主上下文干净
所以 s04_subagent.py 的重点不是“新增一个文件工具”,而是新增了一种架构能力:任务委派 / 子 Agent / 上下文隔离。
11. 需要注意的几个点
第一,这份代码里的 Subagent 不是一个真正独立的操作系统进程。虽然注释里提到类似“隔离”的思想,但从实现看,run_subagent() 仍然是在同一个 Python 程序里调用的;真正隔离的是 sub_messages,也就是模型上下文。(GitHub)
第二,子 Agent 和主 Agent 共享同一个文件系统工作目录。所以子 Agent 读写文件的结果,主 Agent 后续也能看到。这就是为什么子 Agent 可以帮主 Agent 做探索、修改、生成文件。(GitHub)
第三,子 Agent 的结果只返回最终文本 summary。这样能减少主 Agent 上下文污染,但也有代价:如果子 Agent 总结得不好,主 Agent 可能拿不到完整细节。
第四,run_bash() 使用了 shell=True,虽然做了简单危险命令拦截,但这不是严格安全沙箱。真实生产环境还需要更强的权限控制、命令白名单、容器隔离、文件系统权限隔离。
最核心理解
这份代码的主线是:
主 Agent 不是什么都自己干。
遇到复杂探索任务时,
它可以调用 task 工具,
把任务交给一个全新上下文的子 Agent。
子 Agent 自己读文件、跑命令、分析问题,
最后只把总结结果返回给主 Agent。
这样主 Agent 的上下文保持干净,
不会被大量中间探索信息污染。
所以,s04_subagent.py 本质上是在演示一个更接近 Claude Code / AI Coding Agent 的真实架构能力:主 Agent 负责决策和整合,子 Agent 负责局部探索和执行。