第 六 章 Agent 架构实操 深入学习 · 交互式 含 QA 测试

从“单兵死磕”到“分身协作”:复杂任务下 AI Agent 的工程化突围

💡 新增 task 工具:一次性 Subagent,隔离历史,共享运行边界。
把一个自包含子任务交给一次性 Subagent,让它从全新历史开始探索。父 Agent 最后只接收一句有证据的结论。历史隔离不等于沙箱:父子共享同一进程、Hook、权限、workspace 和 identity。
本章进度
0%
1 本章导读与学习目标
  • 说清「委派」里隔离的到底是什么:只有消息历史和工具注册表,其他全部共享。
  • 写一个自包含的 task 工具:description 让子 Agent 不靠父 history 也能独立完成任务。
  • 说出父 history 在委派后只多哪三条消息,以及为什么子 Agent 的推理过程无法审计。
  • 解释为什么必须禁止递归委派(三层防线),以及子 Agent 跑满 30 轮返回什么。
  • 跑通第 6 章:让父 Agent 委派一个子 Agent 干一件事,再自己给结论。
你需要先具备说明
读完第 1—5 章会看 AgentRunner、Hook、权限、tool 注册即可
多 Agent 概念不需要。本章就是第一次引入「子 Agent」
本章导读(你在这) ↓ ① 先看一眼一次真实的委派 ← 子 Agent 干活的完整日志 ↓ ② 这章解决什么问题 ← 上下文隔离,不是沙箱 ↓ ③ 严格的 task 输入 ← 自包含 description ↓ ④ SubagentTool = 执行边界 ← 复用同一个 AgentRunner ↓ ⑤ 历史隔离 + 边界共享 ← 三、四 ↓ ⑥ 禁止递归委派 ← 三层防线 ↓ ⑦ 30 轮上限 + 脱敏错误 ← 六 ↓ ⑧ 组合根 + 离线证明 ← 七、八 ↓ ⑨ 运行 + 实验 + 差异 + 小结 ← 九~十一
最该记住的一句委派不创造新路径:子 Agent 该弹的审批照样弹,该拦的越界照样拦。
2 术语速查(本章第一次出现的词)
委派(task)把一段自包含探索交给独立子 Agent,只拿回一句结论。
SubagentTool执行边界的实现:复用同一个 AgentRunner 跑一次内部 run()。
自包含 description子 Agent 只能看到 description,看不到父 history,所以描述必须完整。
历史隔离父与子各有一份独立 #history,互不可见。
边界共享Hook、权限、workspace、identity、进程全部共享一个实例。
递归委派子 Agent 自己再调 task。三层防线禁止。
evidence-based 结论因为子推理不可审计,所以要求子 Agent 给出有文件证据的结论。
subagent_turn_limit子 Agent 跑满 30 轮返回的结构化错误码。
subagent_configuration_error子工具集非法(例如包含 task)时的错误码。
toolRoundObserver父挂的「计划陈旧提醒」观察器;子 Agent 刻意没挂。
3 核心知识点
这章解决什么问题:上下文隔离

大任务塞进单一上下文会让模型注意力涣散。委派 = 把一段自包含探索搬到全新历史里跑(干净上下文),父 Agent 只拿回一句结论。注意:「隔离」= 上下文隔离,不是沙箱、不是提权、不是降权。子 Agent 与父共享 workspace、身份、权限和进程。

子 Agent 写文件还在吗在。因为它和父共享同一个 workspace 和文件系统,task 返回后文件还在磁盘上。
一 · 严格的 task 输入

只有一个入参:description(自包含任务描述)。它必须完整——因为子 Agent 看不到父 history,它只能靠这句话独立干活。描述里该带路径、该带验收标准、该带返回格式要求。

{\"description\": \"检查本项目用的测试框架:读 package.json、找到测试目录...\"}
二 · SubagentTool 只负责执行边界

SubagentTool 不是第二套 Loop——它复用同一个 AgentRunner,对同一个 context 跑一次内部 run()。只是这个内部 run 用的是全新 history 和子工具集。父 history 极少被触碰。

三 · 父 history 为什么保持干净

父调用一次 task,父 history 只多三条消息:assistant(task)tool(结论)assistant(最终回答)。子 Agent 的完整推理过程进父历史——代价是子推理无法审计,所以要求 evidence-based 结论(子 Agent 必须引用文件做证据)。

四 · 历史隔离,运行边界共享
隔离共享(同一个实例)
消息历史 #historyHookRegistry(日志因此分不清父子)
工具注册表 / 子工具集PermissionPolicy、workspace、identity、进程

共享意味着子 Agent 没有豁免:它的工具调用照样打 hook 日志、照样过权限、越界照样被硬边界 deny。审批框上不标注「这是子 Agent 发起的」——因为父子共享 identity。

五 · 为什么必须禁止递归委派

三层防线保证子 Agent 不可能再调 task:① 提示层:子 system 明确禁止;② 工厂层:子工具集构建时剔除 task;③ 运行时:即使模型自己生成了 task 调用,也返回 subagent_configuration_error。第三层保证「模型零调用」——子 Agent 压根没被创建,模型请求次数只有父那一次。

六 · 30 轮上限和脱敏错误

子 Agent 有独立 maxTurns=30,构造期校验(>30 直接拒绝启动)。跑满轮数返回结构化错误 Error [subagent_turn_limit]绝不把最后一个工具输出冒充结论。父历史仍保持「一条 tool 消息」的整洁。

// 子 Agent 异常/超限 → 结构化 ToolResult,不污染父 Loop
七 · 组合根:如何接入 P06

P06 在 bootstrap 里构造 SubagentTool 并注册为父工具集的一员。SubagentTool 需要拿到父的 AgentRunner 工厂、子工具集(不含 task)、maxTurns。父子共用 ModelClient,因此只需要一组 .env。

八 · 刻意的不对称 & 离线证明

子 Agent 的工具列表里有 todo_write,但没有挂 toolRoundObserver——所以它不会收到「计划陈旧」提醒。子 Agent 有工具但没这层机制,这是刻意的。测试用注入的假审批器证明:即使配「永远批准」,子 Agent 也写不出 workspace(硬边界拦下)。

4 先看一眼一次真实的委派
父子日志交错 + 越界写入被拦
npm run ch06 -- --prompt \"调用 task 独立检查本项目使用的测试框架,只返回有文件证据的结论\"
# stderr 上父子交错:
#   [Hook] UserPromptSubmit     (父)
#   [Hook] PreToolUse: task     (父)
#   [Hook] UserPromptSubmit     ← 子 Agent 那次 run() 也打日志!
#   [Hook] PreToolUse: read_file (子)
#   ...
# Hook 是共享实例,所以子 Agent 的 run() 也会打 UserPromptSubmit 和 Stop
# 观测局限:共享 HookRegistry 的代价是日志里分不清父子
那如果子 Agent 想越界写呢子 Agent 的 write_file 照样走第 3 章权限:工作区内弹审批、工作区外被硬边界 deny。`task` 自己是 allow(读取类),但子 Agent 的写入照样弹到你面前——审批框上不标注「这是子 Agent 发起的」。
5 运行第 6 章
0
准备环境

Set-Location code + npm ci

1
先跑离线测试

npm run test:ch06 预期 16 个文件 118 个测试。

2
观察一次真实委派

父子日志交错出现。

3
观察委派会不会被审批拦住

task 是 allow;子 Agent 写入照样弹审批。

4
统一入口

agent-tutorial -- run --chapter 6

离线测试 / 运行命令
npm run test:ch06   # 预期 16 files / 118 tests

npm run ch06 -- --prompt \"调用 task 独立检查本项目使用的测试框架,只返回有文件证据的结论\"
# 父子日志交错;Hook 共享实例,子 Agent 的 run() 也打 UserPromptSubmit/Stop

npm run ch06 -- --prompt \"调用 task 让子 Agent 把一句话写入 sub-demo.txt\"
# task 自己 allow;子 Agent 的写入照样弹审批框

# 统一入口
npm run agent-tutorial -- run --chapter 6 --prompt \"调用 task 总结 chapters/ch06/src/core 目录职责,再由父 Agent 给出结论\"
常见报错排查
现象原因 / 处理
Error [subagent_turn_limit]子 Agent 跑满 30 轮还没给最终文本。把委派任务拆小。不要调高上限——构造时会拒绝 >30
子 Agent 越界写入没弹审批框预期行为。硬边界在规则之前,越界直接 deny,压根到不了审批
子 Agent 调 task 报 subagent_configuration_error运行时防递归第三层,正常。子工具集本就不含 task
启动报 maxTurns must be at most 30构造期校验,配置错误在组合根就炸
6 验证与实验

npm run test:ch06 执行 16 个测试文件、118 个测试。四个建议动手的小实验:

实验预期学到什么
一 · maxTurns 调到 31
在 new SubagentTool 里加 maxTurns: 31
启动就抛 maxTurns must be at most 30,模型一次没被调用上限是构造期校验,配置错误应在组合根就炸
二 · 让子工具集包含 task返回 subagent_configuration_error,模型请求只有父那一次运行时防递归第三道防线,子 Agent 压根没被创建
三 · 验证子 Agent 看不到父 history子 Agent 只看到 description 那一句,必须自己再读文件隔离是真的;description 必须自包含
四 · 观察子 Agent 的 todo_write 有没有提醒没有。子 Agent 没挂 toolRoundObserver刻意的不对称:有工具但没机制
7 本章小结
三句话版本
  • task 把一段自包含探索搬到一个全新历史里跑,父 Agent 只拿回一句结论。
  • 隔离的只有消息历史和工具注册表;Hook、权限、workspace、identity、进程全部共享。
  • 委派不创造新路径:子 Agent 该弹的审批照样弹,该拦的越界照样拦。
一定要记住的八条
#结论出现在哪一节
1「隔离」= 上下文隔离,不是沙箱、不是提权、不是降权这章解决什么问题
2子 Agent 看不到父 history,所以 description 必须自包含
3SubagentTool 不是第二套 Loop,它复用同一个 AgentRunner
4父 history 只多三条:assistant(task)、tool(结论)、assistant(最终回答)
5代价是子 Agent 的推理过程无法审计——所以要求 evidence-based 结论
6禁止递归防三层:提示、工厂、运行时;第三层保证「模型零调用」
7跑满轮数返回结构化错误,绝不把最后一个工具输出冒充结论
8子 Agent 有 todo_write 但没有 toolRoundObserver——刻意的不对称
本章代码边界

P06 只加了「一层同步委派」,没加并行、没加持久化、没加沙箱。并行(第 13 章后台任务)、多 Agent 认领(第 17、18 章)、Inbox 异步协作(第 15 章)都在后面。

检查你是否真的读懂了(不看文章回答)
  • 子 Agent 在 workspace 里写的文件,task 返回后还在吗?为什么?
  • 父 Agent 调用一次 task,父 history 里多了几条消息?分别是什么?
  • 子 Agent 的 write_file 会弹审批框吗?审批框上能看出这是子 Agent 吗?
  • 给子 Agent 配「永远批准」的审批器,它能写出 workspace 吗?
  • 子模型自己生成了 task(...) 调用,它会收到什么?
  • 子 Agent 跑满 30 轮,父 Agent 收到的是第 30 轮的工具输出,还是别的东西?
  • 子 Agent 的工具列表里有 todo_write。它会收到「计划陈旧」提醒吗?
8 QA 测试环节(自测题)
已完成 0 / 7 · 答对 0
Q1. 委派里「隔离」的到底是什么?
Q2. 父 Agent 调用一次 task,父 history 多几条消息?
Q3. 为什么 task 的 description 必须自包含?
Q4. 子 Agent 的工具列表里有 todo_write,它会收到「计划陈旧」提醒吗?
Q5. 给子 Agent 配「永远批准」的审批器,它能写出 workspace 吗?
Q6. 子模型自己生成了 task(...) 调用,会发生什么?
Q7. 子 Agent 跑满 30 轮,父 Agent 收到什么?