父 Agent 为了回答一个局部问题,可能读取大量文件、运行多条命令。这些中间过程如果全部留在父对话里,会持续占用上下文,即使最终只需要一句结论。

Subagent 提供了一条新的执行路径:把边界清楚的子任务交给一段全新的对话,子 Agent 独立探索,最后只把结果返回父 Agent。

父 Agent messages[]
       │ task(prompt)

子 Agent messages=[prompt]
       │ 独立调用基础工具

最终文本作为 tool_result


父 Agent 继续工作

核心收益是上下文压缩和任务聚焦,不是凭空增加新的执行能力。

子 Agent 本质上是嵌套 Agent Loop

在当前课程中,task 和普通工具一样被注册:

TASK_TOOL = {
    "name": "task",
    "description": "Run a subagent with fresh conversation context.",
    "input_schema": {
        "type": "object",
        "properties": {
            "prompt": {"type": "string", "minLength": 1},
        },
        "required": ["prompt"],
    },
}
 
TOOLS = [*BASE_TOOLS, TASK_TOOL]
TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}

父模型生成 task 调用后,handler 并不是直接执行某条系统命令,而是启动另一段“模型响应 → 工具调用 → 工具结果回填”的循环。

这说明 Subagent 不是特殊模型类型。它是 Harness 对同一套 Agent Loop 的再次组合:使用不同的初始消息、system prompt 和工具集合,再把输出包装成父循环的工具结果。

新上下文里有什么,没有什么

子循环从一条新消息开始:

messages = [{"role": "user", "content": prompt}]

它不会自动继承父 Agent 的完整 messages[]。因此父对话中的用户要求、已做决定、错误输出和隐藏在早期消息中的文件路径,除非被写进 prompt 或能从共享资源重新读取,否则子 Agent 并不知道。

这既是优点也是风险:

  • 优点:无关历史不会挤占子任务上下文;
  • 风险:委派提示遗漏约束时,子 Agent 会在信息不足的情况下自行推断。

一个好的委派提示至少应包含:

  • 明确目标和不在范围内的内容;
  • 可读取的权威来源或入口文件;
  • 允许使用的工具和副作用边界;
  • 期望输出格式;
  • 完成条件与需要返回的证据。

“看看这个模块”不是一个可靠子任务;“只读检查认证模块的 token 刷新路径,返回入口、关键调用链和带文件位置的风险,不修改文件”才更接近可执行契约。

隔离的是消息,不是进程和文件系统

当前教学实现中,父子循环共享:

  • Python 进程;
  • 模型客户端;
  • WORKDIR
  • 基础工具 handler;
  • 权限与生命周期 Hooks;
  • 文件和命令造成的外部副作用。

因此,子 Agent 修改文件后,父 Agent 能看到修改;子 Agent 删除或覆盖文件,影响的也是同一个工作区。

“fresh context”不能理解为安全沙箱。上下文隔离、权限隔离、进程隔离和工作区隔离是四个不同维度:

维度当前课程示例
对话消息隔离
System prompt使用独立子提示
工具集合子集化
进程共享
工作目录共享
文件副作用共享
权限 Hooks共享

如果任务可能写入同一批文件,仅有新 messages[] 不足以避免冲突。更强的隔离需要独立工作树、容器、临时目录、文件锁或由父 Agent 统一合并变更。

为什么子 Agent 没有 task 工具

课程通过两套工具集合限制委派深度:

SUB_TOOLS = list(BASE_TOOLS)
SUB_HANDLERS = dict(BASE_HANDLERS)

父 Agent 拥有 BASE_TOOLS + task,子 Agent 只有基础工具。因为子集合中没有 task,它无法继续创建孙 Agent。

这是能力收窄的简单例子:不是在 prompt 中要求“不要再委派”,而是根本不向子模型暴露该工具。

生产系统还可以按角色提供不同能力:

  • 探索型子 Agent:只读文件和搜索;
  • 验证型子 Agent:只运行允许列表中的测试;
  • 编辑型子 Agent:只写指定目录;
  • 外部研究 Agent:允许联网,但拿不到本地凭据;
  • 高风险执行 Agent:每个副作用都需要批准。

工具不可见通常比仅靠文字禁止更可靠,但 handler 仍需执行运行时授权。

当前实现是同步的,不是并行的

task handler 调用 run_subagent() 后,父循环会等待子循环返回。子 Agent 完成前,父 Agent 不会继续处理后面的推理。

即使父模型在一次响应中生成多个 task block,当前分发代码也是按顺序遍历,因此这些子任务仍会串行执行。

同步设计容易理解,也减少并发写入冲突。若要并行化,还需要额外解决:

  • 每个子任务的取消、超时和预算;
  • 结果与调用 ID 的稳定关联;
  • 多个子 Agent 同时写文件的冲突;
  • 共享终端和人工批准请求的交互竞争;
  • 部分成功、部分失败时如何汇总;
  • 父任务结束后是否取消仍在运行的子任务。

并行不是把循环放进线程池就结束了,它会改变状态所有权和失败语义。

30 轮上限限制的是什么

当前子循环使用:

for _ in range(30):
    response = client.messages.create(...)
    tool_calls = [block for block in response.content if block.type == "tool_use"]
    if not tool_calls:
        return extract_text(response.content) or "(no summary)"

一次循环对应一次模型响应。响应中可以有零个、一个或多个工具调用,所以 30 轮不等于 30 个工具调用。

上限用于避免模型持续调用工具而永不结束,但数字 30 只是教学参数。完整的资源治理还应限制:

  • 总模型调用次数与 token 预算;
  • 单工具和整项子任务的墙钟时间;
  • 命令 CPU、内存、进程数和输出大小;
  • API 重试次数和退避;
  • 用户取消信号;
  • 全局任务预算,避免每个子 Agent 都耗尽独立上限。

达到上限后返回一段“已停止”文本,只是可观察的失败结果,不应被父 Agent 当成任务成功。

只返回最终文本是一种有损压缩

子 Agent 的中间消息保存在自己的局部列表中,不会逐条加入父对话。结束时,Harness 提取最终响应中的文本,作为 tasktool_result 返回父 Agent。

这正是节省上下文的来源,但也会丢失信息:父 Agent 看不到子 Agent 为何得出结论、查过哪些文件、哪些命令失败过。

因此最终结果最好包含可核验的摘要,而不是只有一句判断。可以约定返回:

{
  "status": "completed",
  "summary": "认证刷新由 refresh_session 处理",
  "evidence": ["src/auth/session.py:84"],
  "changes": [],
  "verification": ["pytest tests/auth -q: passed"],
  "open_questions": []
}

结构化返回并不保证结论正确,但能让父 Agent 更容易核验、合并和决定下一步。对于高风险任务,父 Agent 应重新检查关键证据,而不是盲信子 Agent 的自然语言总结。

权限必须在每个执行入口生效

当前课程让父子循环共同经过 execute_tool(),因此两者触发同一组 PreToolUse 和 PostToolUse Hooks。这说明权限控制应放在实际分发路径上,而不是只写在父 Agent 的 system prompt 中。

但“共享 Hooks”仍不等于完整安全:

  • 字符串黑名单无法可靠约束任意 Shell;
  • 人工批准必须明确展示是哪个子任务发起的;
  • 子 Agent 读取的仓库内容可能包含提示注入文本;
  • 子 Agent 的总结属于不可信输入,父 Agent 不应把其中指令当作更高优先级规则;
  • 本地凭据、网络权限和环境变量仍需在进程或容器层隔离。

更稳妥的默认值是让子 Agent 拥有完成任务所需的最小能力,探索任务优先只读,写入和外部操作按任务单独授权。

Python 作用域和 Schema 的两个小问题

旧版笔记关注了两个语法点,它们可以保留为实现补充。

Python 的 for 循环不会创建新的块级作用域,因此循环内赋值的 response 在同一函数后面仍可访问。但如果循环一次都没有执行,变量就不会被赋值;使用固定正数范围虽然规避了当前情况,提前初始化或在循环内直接返回仍更清楚。

工具 Schema 中多个属性分别写 "type": "string" 也不是重复:每个属性都必须声明自己的类型。属性名、属性类型和面向模型的 description 位于不同层级,各自承担不同作用。

建议覆盖的测试

Subagent 至少应验证:

  • 子消息不包含父对话历史;
  • 委派 prompt 包含目标、范围和输出契约;
  • 子工具集合不包含 task
  • 父子循环都经过权限与审计入口;
  • 子任务正常结束、无文本结束和达到轮次上限;
  • 一轮多个工具调用时的结果关联;
  • 子 Agent 修改文件后父 Agent 可见;
  • 只读角色无法调用写入工具;
  • 多个 task 在当前实现中按顺序执行;
  • API、工具和人工批准发生异常或取消;
  • 结果包含证据,父 Agent 能识别失败状态;
  • 并行扩展时的写冲突、预算和取消传播。

小结

Subagent 的价值,是把一个边界清楚的子任务放进新对话,让中间探索留在子上下文,只把压缩后的结果交还父 Agent。它能减少父上下文污染,也便于为不同任务配置不同工具。

但新对话不等于新沙箱,当前示例也不是并行系统。父子 Agent 仍共享进程、工作目录和文件副作用。要把 Subagent 用于真实工程,需要同时设计委派契约、能力收窄、权限检查、资源预算、失败传播、证据回传和必要的执行环境隔离。

参考资料