learn-claude-code-s04_subagent.py

3 阅读1分钟

原址

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)。如果输入 qexit 或空字符串,就退出程序。(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 负责局部探索和执行。