Day 15 / 共 20 天 · 第 3 周收官 · 加深版

子代理 / Task / coordinator

前四天(Day 11-14)都在讲"给主 Agent 加料";今天是第 3 周收官——主 Agent 怎么派生"子 Agent"分身并行干活。贴真实代码看一个惊人简单的真相:子代理就是递归调用 query()。这是"统一抽象"主线的最后一块拼图。

📍 你在整门课的位置 · 第 3 周收官(本周结论:一切扩展都归约到少数几个统一原语)
D12 MCP D13 Skills D14 Hooks D15 子代理(收官) D16 会话/记忆(W4)
L01

为什么要子代理

🤔 痛点:一个人干所有活,脑子会爆 你让主 Agent"调研代码库里所有认证相关代码"。它得读 50 个文件,这 50 个文件的内容全塞进它的上下文——很快就把宝贵的 token 占满(呼应 Day 10),而且你其实只想要一份结论,不想看它翻过的每一页。
💡 本质:子代理 = 派实习生去办分支任务 派子代理就像项目负责人派实习生去出差调研:实习生自己去翻资料、跑测试(在他自己的脑子里),最后只交你一页结论报告。你的"脑容量"(主上下文)不被细节占满,还能同时派好几个实习生并行跑。

Day 08 见过 Agent 工具(别名 Task)。当主 Agent 遇到一个"值得独立探索"的子任务时——比如"在整个代码库里搜集所有跟认证相关的代码"——它可以派一个子代理去干。好处:

  • 上下文隔离:子代理搜集过程产生的一大堆中间信息(读了 50 个文件)不污染主对话,主 Agent 只拿到最终结论。
  • 并行:可以同时派多个子代理(Day 08 讲 Agent 工具 isConcurrencySafe=true)。
  • 专注:子代理有自己的 system prompt,可专门为某类任务优化。
类比 你是项目负责人(主 Agent),遇到一个需要深挖的问题,你派个实习生(子 Agent)去调研,让他自己去翻资料、跑测试,最后只把一页结论报告给你——你不用看他翻的每一份资料。这样你的"脑容量"(上下文)不被细节占满,还能同时派好几个实习生。这份教程本身就是我派了 6 个子代理精读代码、再汇总写成的。
L02

子代理 = 递归调用 query()(真实代码)

核心洞察极简:子代理就是再跑一遍 Day 06 讲的那个 query() 循环,只是换一套输入真实代码packages/builtin-tools/src/tools/AgentTool/runAgent.ts:776):

for await (const message of query({
  messages: initialMessages,          // 子代理自己的初始消息
  systemPrompt: agentSystemPrompt,    // 子代理专属 system prompt
  userContext: resolvedUserContext,
  canUseTool,
  toolUseContext: agentToolUseContext, // ★ 带独立 agentId
  querySource,
  maxTurns: maxTurns ?? agentDefinition.maxTurns,
})) {
  onQueryProgress?.()
  // ... 消费子代理吐出的消息
}
看第一行——子代理调的还是同一个 query()(Day 06 那个循环层入口),只是传进去的是"子代理自己的 messages / systemPrompt / agentId"。主 Agent 跑 query,query 里模型调 Agent 工具,Agent 工具内部又跑一个 query——就像函数递归调用自己。
主 query() 循环 模型想调 Agent 工具 → Agent 工具内部:又跑一个 query()(换一套 messages/systemPrompt/agentId) 子代理里的模型若再调 Agent 工具 → 还能再套一个 query()… (像函数递归调用自己,可无限嵌套)
图注:多代理不是一套新框架,只是同一个 query() 循环的嵌套调用(递归)。
📝 举个例子:一次派发的参数差异 主对话 messages = 你和 Claude 的全部往来;调 Agent 工具时,子代理拿到的 initialMessages = 只有一句"调研认证相关代码"、systemPrompt = 子代理专属、agentId = 新生成 → 同一个 query() 就跑起一个隔离的子会话,跑完只把最后一句话交回主循环。
为什么这叫"惊人简单"? 你可能以为"多代理系统"需要一套专门的框架。但这里根本没有——它只是同一个 query 循环的嵌套调用。子代理里的模型如果再调 Agent 工具,还能再套一层。这是本项目又一个"用一个统一机制覆盖复杂需求"的例子——呼应 Day 11(一切是 Command)、Day 12(一切是 CoreTool)、Day 13(skill 是 prompt Command)。第 3 周的大主题就是:Claude Code 不堆砌机制,而是把复杂能力归约到少数几个统一原语。多代理 = 递归 query,就是最漂亮的一例。
L03

独立上下文:互不干扰

主 Agent(query 循环 · 主对话上下文)
子代理 A独立 agentId
独立 messages
子代理 B独立 agentId
独立 messages

每个子代理有自己独立的一套:messages、systemPrompt、agentId、abortController(就是 L02 代码里传的那些参数)。它跑它的循环,主 Agent 看不到它的中间过程。

因为 toolUseContext.agentId 存在,query.ts 里很多"面向主 UI"的分支会跳过——子代理不生成工具调用摘要、不发 profiler、不跑 Stop hook 的后台任务,因为它不直接面对你的界面。

独立上下文 = 上下文经济(呼应 Day 10):子代理读了 50 个文件、来回 10 轮,这些全在它自己的 messages 里,不进主对话。主 Agent 的上下文只增加"一份最终报告"。用独立的 messages 数组把子代理的"思考过程"关在它自己的沙盒里。子代理的 transcript 也单独存(Day 16:<sessionId>/subagents/agent-<agentId>.jsonl)。
L04

结果怎么汇回主代理

子代理跑完,怎么把结论给主 Agent?finalizeAgentToolagentToolUtils.ts:277)取子代理最后一条 assistant 消息的文本作为结果。

关键:只取"最后一条 assistant 文本"作为结果。 子代理来回 10 轮,但汇报给主 Agent 的只是它最终说的那段话(就像实习生只交一页结论,不交草稿)。这个 content 变成 Agent 工具的 tool_result,按 Day 03 的回填机制进入主 query() 的下一轮消息——于是主 Agent"看到"了子代理的报告,继续往下推理。所以子代理对主 Agent 而言,就是一个"输入任务描述、输出一段报告"的普通工具调用——只不过这个工具内部跑了一整个 Agent。这正好也解释了 Day 08 为什么 Agent 工具标 isReadOnly:true(它对主流程的直接效果就是返回一段文本)。
L05

工具裁剪:子代理能用啥

子代理可用的工具会被裁剪——有个禁用清单 ALL_AGENT_DISALLOWED_TOOLSsrc/constants/tools.ts),如 ExitPlanModeAskUserQuestionTaskStopLocalMemoryRecall 等。

为什么裁剪? 有些工具只有"主 Agent 面对用户时"才有意义:AskUserQuestion(问用户问题)——子代理是后台干活的,问了谁答?ExitPlanMode(退出规划模式)——那是主流程的状态。把这些从子代理的工具池去掉,防止它调用没意义/会捣乱的工具。这跟 gov-agents 教程里 subagent 只能用受限工具集是同一种"按角色限权"思想。

子代理调用它被允许的工具(如 Read/Grep/Bash)时,权限检查照常生效(Day 09)——回扣 Day 08:"派子代理不弹权限,子代理干的活逐个受权限管"(权限下放)。

L06

Task:后台任务的上层抽象

src/Task.ts 是比"同步子代理"更上层的后台任务抽象。它定义了几种 TaskTypelocal_bash(后台跑命令)、local_agent(后台跑子代理)、remote_agentin_process_teammate(coordinator 用)、local_workflow(Day 19)。

同步子代理 vs Task 的区别:Day 02-05 讲的 Agent 工具是同步的——主 Agent 派它、等它跑完、拿结果再继续。而 Task 是异步后台的——比如你让它"后台跑着测试",主 Agent 不等它,继续干别的,之后用 TaskOutput/TaskGet 工具去查进度/结果。run_in_background: true 的 Agent 就走 Task 路径。这让 Agent 能"边干主线边挂几个后台任务",像你开几个终端标签页并行跑东西。
L07

coordinator:一个协调者 + 一群 worker

coordinator 模式(feature('COORDINATOR_MODE'))是更成体系的多代理编排——回扣 gov-agents 教程 Day 04 的 supervisor 模式:

协调者 Coordinator
只派活、不亲自干
worker 1
worker 2
worker 3
  • coordinatorMode.tsgetCoordinatorSystemPrompt() 给协调者一段"你是协调者,用 Agent 工具派 worker"的 prompt。
  • workerAgent.ts:定义 WORKER_AGENT,worker 的工具是"异步代理允许工具集"减去编排工具。
  • 机制:协调者是主 query 循环里的模型,通过调 Agent 工具(subagent_type: "worker")派发;worker 结果以 <task-notification> 消息回到协调者。
coordinator vs 普通子代理 vs workflow:普通子代理是"临时派一个去干个具体活";coordinator 是"一种整体工作模式"——主模型化身纯协调者,自己不写代码,只拆任务、派 worker、汇总,适合大型任务分工并行。它是模型驱动(协调者自己决定派谁);而 Day 19 的 workflow engine 是脚本驱动(确定性编排)。三者都是"多代理协作"的不同强度形态,但底层都建立在 L02 的"递归 query"之上。
L08

第 3 周收官 + 动手

🧠 第 3 周(扩展能力)自测

  • 为什么要子代理?(上下文隔离、并行、专注)
  • 子代理本质是什么?(递归跑 query(),用独立参数)
  • 子代理中间过程为什么不污染主对话?结果怎么汇回?(只取最后一条 assistant 文本)
  • 为什么裁剪子代理工具?同步子代理 vs Task 的区别?
  • coordinator vs 普通子代理 vs workflow?

回顾第 3 周(都印证同一主题——统一抽象):Day 11 一切是 Command → Day 12 MCP 工具是 CoreTool → Day 13 skill 是 prompt Command → Day 14 hooks 挂生命周期 → Day 15 多代理是递归 query。Claude Code 的可扩展性 = 少数几个统一原语 + 各种来源都实现它。

✋ 动手:对着真实代码读一遍

# 1. 子代理 = 递归 query(L02,本页精华)
sed -n '773,795p' packages/builtin-tools/src/tools/AgentTool/runAgent.ts

# 2. 结果汇回(L04)
sed -n '277,320p' packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts

# 3. 子代理工具裁剪清单(L05)
grep -n "ALL_AGENT_DISALLOWED_TOOLS" src/constants/tools.ts

# 4. Task 类型 + coordinator(L06/L07)
sed -n '6,20p' src/Task.ts
grep -n "getCoordinatorSystemPrompt\|WORKER_AGENT" src/coordinator/*.ts

# 5. 实操:让主 Agent 派子代理
bun run dev   # 输入:用 3 个子代理分别调研 A/B/C,汇总给我
第 4 周预告 · Day 16:核心 + 扩展都讲完了!最后一周讲"平台化"。Day 16 讲会话/历史/记忆——对话怎么存与恢复(--continue/--resume)、CLAUDE.md 记忆、三套不同的"历史"别搞混。

← Day 14 事件 Hooks Day 16 · 会话 / 历史 / 记忆 →