12. Subagent
派个分身去干活:上下文隔离、终检与「理解必须收敛」
上一篇 Plan Mode 解决的是「一个 Agent 动手前先想清楚」,始终是它一个人在谋划。这一篇讲的 Subagent,是另一种组织方式:把一些又重又独立、还容易搅乱主线的活,派给「分身」去隔离地干
🪧 主 Agent 的上下文,是一张不该被弄乱的桌面
我想让 Agent 在动笔前做一轮调研,翻它几十篇素材、几十个网页,找论据、找案例
可问题来了:这些原始素材一旦都堆进主写作的上下文里,会发生什么?一是噪声淹没正事,模型的注意力被大量细节稀释,写作本身反而模糊了;二是烧 token,几十篇原文塞进去,这一轮的开销陡增;三是更隐蔽的,上下文被撑到触发压缩,而压缩很可能把我们前几篇拼命保住的画像给挤掉(第 2、4 篇的老问题)
主 Agent 的上下文,得当成一张干净的写作桌面来护着。调研是要做的,但调研那堆乱糟糟的草稿纸,不该摊在这张桌面上。理想的做法是:派个分身,去隔壁房间翻资料,翻完了,只把整理好的一页摘要递回来。 隔壁房间怎么乱是它的事,我这张桌面要始终清清爽爽
这个「隔壁房间的分身」,就是 Subagent
🧭 基础概念:Subagent = 一个跑在隔离房间里的分身
Subagent 是一个独立的 agent 实例,跑在一个全新的、隔离的上下文里,干完活,只把最终结论(那条 final message)递回给主 agent。 它的核心价值就俩字:隔离
这里要特别跟第 6 篇的 Skill 分个清楚,因为这俩我曾经搅混过:
- Skill 是「翻手边的一页手册」:共享主 agent 的上下文,轻,就地展开
- Subagent 是「另雇个人去隔壁干」:全新隔离的上下文,重,只回传结论
一轻一重、一共享一隔离,用途自然就分开了。Skill 适合「教 agent 怎么写某类文章」这种要就地用的方法论;Subagent 适合「翻资料、做质检」这种又重、中间过程又全是噪声、只有结论有用的独立活
那「隔离」到底带来了什么,值得我费这个劲?Harness Book 讲多智能体时有个观点:多个分身真正解决的,不是「更快」,而是「失败可定位」。 一个 agent 把调研、写作、检查全炖在一锅上下文里,出了问题很难按层排查;而拆成一个个隔离的分身,研究归研究、检查归检查,哪一层出岔子一目了然。所以使用 subagent,不仅仅是为了提速(纯串行分拆反而更慢),更关键的是把混乱关进隔离的房间里,让整个系统可拆解、可排查
🔩 Claude Code:主 agent 派活给子 agent 的硬规则
「隔离」的价值想明白了,具体怎么落地?先看 Claude Code 原生是怎么设计这套协作机制的。Claude Code 有一套成熟的多 agent 协作机制,Harness Book 专门有一章讲它,标题就叫「靠分工来驾驭不稳定性」。主 agent 通过一个「派活」工具,把一段任务甩给一个 sub-agent;sub-agent 在隔离上下文里跑完,结果作为这个工具的返回值回到主 agent 手里。Learn Claude Code 的源码解读系列第六篇,也专门拆了 subagent 机制,对理解这套隔离模型很有帮助
这套机制里有几条硬规则,是我后面所有定制化设计的地基,得先说清楚:
- 父传给子的,只有那一段 prompt 字符串,别无其他。 子 agent 看不到父的对话历史、看不到父之前的工具结果、也看不到父的 system prompt。它就像一个「只拿到一张任务便条、对前情一无所知」的新人。所以主 agent 派活时,必须把子 agent 干活需要的一切(文件在哪、目标是什么、有什么要求)显式写进那张便条里,漏了它就抓瞎
- 子 agent 会继承的:它自己的一份 system prompt、项目级的 CLAUDE.md、以及一套(可以裁剪的)工具集
- 子 agent 不会继承的:主 agent 那些预加载的 Skill(除非你在定义里显式点名给它)
- 一条关键限制:
AskUserQuestion(那个向用户提问的工具)在子 agent 里用不了。这意味着,凡是需要「回头问用户一句」的环节,都必须留在主 agent,不能丢给分身。分身是派出去「闷头干完」的,不是派出去「跟用户对话」的 - 主 agent 靠什么判断「这活该不该派」:完全靠每个 sub-agent 定义里那句 description。Claude Code 会拿当前任务去跟已注册 sub-agent 的 description 做匹配,够贴切就主动派活,不用你在 prompt 里手把手教它「遇到调研就去调 xxx」。这条我在 SDK 那节还会再展开,后面「描述写不清楚,等于分身白造」那个坑,根就在这
Harness Book 里反复强调,我觉得说的挺对:一个设计良好的分身机制,会把子 agent 的状态默认隔离干净,子 agent 里那些临时的、混乱的决策,绝不许回流污染父线程。用它的原话说,子代理的价值,正在于「能容纳局部的混乱」。这跟前面的比喻「隔壁房间怎么乱是它的事」是一个意思,只不过它是从系统设计的角度说的
🛠 SDK:怎么定义一个分身,以及我为什么选「用代码定义」
上面这些是 Claude Code 原生跑起来的规则;但 SmartWriter 是通过 SDK 集成写作 Agent 的,同一套隔离机制,到了开发者手上还是要自己配置、自己调用。到了 SDK 这层,定义一个 sub-agent 有两条路:
- 文件定义:在
.claude/agents/目录下放一个定义文件 - 编程定义:用一个
agents参数,在代码里传一个AgentDefinition(写明它的 prompt、能用哪些工具、用哪个模型等)
本次项目里使用的是编程定义,因为编程定义支持在运行时按场景动态地造分身:比如按当前题材给它配不同的工具、按任务的严格程度给它挂不同的模型。文件定义是死的,做不到这种「临场按需组装」
定义好了,还得保证主 agent 在该派活的时候真的会派,这里有三个要点,缺一不可:
- 那个「派活」工具,必须在允许清单里,否则主 agent 想派也派不动(会落到审批、或直接被拒,然后它就自己憋着把活干了)
- 每个分身的描述要写清楚「什么时候该用它」,因为主 agent 就是靠这句描述来判断该不该派活的。含糊的描述,等于分身白造
- 关键节点要显式点名,再用流程兜底。对「必须派」的环节,别只靠模型自觉,该在流程里硬性触发(参考第 10 篇「关键环节用 hook 焊死」)
最后一个小配置,也是个容易踩的坑:SDK 从 v2.1.198 起,把「派活是否阻塞主线」的默认值,从阻塞式改成了跟 Claude Code 本身一致的非阻塞,主 agent 不等分身干完就继续往下走。这个默认值本身没毛病:面对复杂的编程任务,主 Agent 派个分身去处理一个独立子问题,确实可以先忙别的,等分身回话了再回头跟进,效率更高。但写作场景不太一样,调研、质检、事实核查都得先拿到结论才能往下走,所以我把每个分身定义都显式改回了阻塞式,还额外挂了个 PreToolUse hook 强制注入阻塞双保险(光显式设置不一定生效,belt-and-suspenders)
👥 SmartWriter 的三个分身:各司其职
SmartWriter 里养了三个分身,各干各的活:
| 分身 | 干什么 | 配的工具 | 用的模型 | 回传什么 |
|---|---|---|---|---|
| research-assistant | 调研:翻大量素材 / 网页,找论据案例 | Read / WebSearch / WebFetch | 默认 Sonnet | 一段自由文本摘要 |
| reflection-checker | 终检:全文质检(错别字 / 逻辑 / 风格 / 画像对齐 / 技术准确 / 结构) | 只有 Read | 默认 Sonnet | 一份结构化 JSON 报告 |
| fact-checker | 事实核查:联网核实文中的数据、引文 | Read / WebSearch / WebFetch | 默认 Sonnet | 一份结构化 JSON |
这张表里有几个决策值得展开:
一、按活的分量配模型。 三个分身默认用 Claude Sonnet 5 / Deepseek V4 Flash,因为调研要综合、核查要判断、终检要挑刺,需要具备基础的推理能力,前期测试下来这两个模型基本够用。而因为使用的是编程定义,后续如果想做得更精细化,比如根据任务分量动态指派模型,也没有问题,但文件定义就做不到这种临场调整了
二、终检这个分身,工具只有一个 Read。 它的职责是发布前的兜底「检查」,不是「改稿」。所以我一个能改文件的工具(Write、Edit)都不给它,能跑命令的(Bash)、能再派活的(Agent)也统统不给。这背后是 Harness Book 那条「验证必须独立于实现」的原则:改稿的和验稿的,得是两拨「人」,一个只读、绝不下场改,才能保持它作为「第二道质量门」的独立性。「我改了」和「改对了」之间,隔着一条宽河,模型很擅长在这条河上搭纸桥自欺,因此验证类任务我更倾向于用独立 Subagent 去干
三、终检不联网做事实核查,我把它拆出去了。 早期我图省事,想让终检顺便联网核实一下文中的数据、引文。后来砍掉了,事实核查另建了一个独立的 /fact_check。原因有三:功能重叠、重复烧 token、以及用户不可控(终检时被动联网,用户没预期)。终检只保留「读稿时发现明显的年代错乱、数据自相矛盾」这类不联网的轻量提示。一个分身只干一件定义清晰的事,尽量不让它什么都沾一点,这也是「失败可定位」的前提
对了,你可能注意到画像分析不在这张表里。它看起来像个第四分身,但我没把它注册成 SDK subagent,它走的是业务层的并发 query()(首启时固定流程、或用户自主触发,不需要上下文隔离,直调 query 拿结构化结果就够了)。并不是所有「派出去的活」都要一概变成 SDK 分身,只在需要上下文隔离时才该花那个开销
🎯 研究可以分布,理解必须收敛
三个分身摆好了,但真正让我对「分工」这件事理解更深一层的,是 Harness Book 里的一条最佳实践建议:研究可以分布,理解必须收敛
你可以把「调查、研究」这类活,分布给多个分身并行去干,这没问题;但把这些分身带回来的一堆 findings,消化成一个连贯的、可执行的下一步,这个「理解」的动作,必须收敛回主 agent 一个人手里
一开始读到这句话,其实我没太注意,直到后面真正踩到坑,才明白它的重要性
🔧 避坑 · 我把「理解」这道工序,错派给了用户
在早期的调研和事实核查,链路是这样的:分身干完,产出一份报告,前端把这份报告原样展示给用户,完事,就没有然后了
但用户拿到这份报告后呢?他得自己读一遍,理解每一条的意思,然后手动敲一句 prompt:「请按上面第 2、5 条的结论,修改我的作品……」。相当于「把一份报告消化成对稿子的具体修改」这个最费脑子的活,我甩给了用户
这恰恰违反了「理解必须收敛」。用户被迫成了那个「协调者」,替我的主 agent 做了本该它自己做的消化工作,累,门槛又很高。一个正常写作用户,不会想去研究怎么把一份报告翻译成一句有效的修改指令
正解是把这道工序收回来:主 agent 拿到子 agent 的报告后,自己把它转成一份「可执行的修改 prompt」(带上每条问题的具体位置、描述、建议、证据,以及明确的「改哪、怎么改」指令),前端给用户一个「应用」按钮,点一下,修改就落到稿子上。研究的噪声散在分身那头,理解的收敛落在主 agent 这头,最后用户只需要一个动作:看一眼,点「应用」
把这个「收敛」做到位了,分工才真正省心,否则只是把复杂度从模型转嫁给了用户,治标不治本
🛡 让 LLM 稳稳吐出一个 JSON:三层防御,和一个甩不掉的上游坑
说回「结论要交给程序消费」这件事:上面提到,终检、事实核查、画像分析回传的都是「结构化 JSON」。但 LLM 是出了名的「自由散漫」,怎么才能让一个 Subagent,稳稳地、每次都吐出一个格式正确的 JSON?
SDK 本身是自带一层机制的,走结构化查询时,你可以给它绑一个 JSON schema,SDK 会在「模型吐的 JSON 不符合 schema」时自动重新提问、让它重吐,直到对为止。这本是开箱的一道保险。但问题是 Subagent 用的 AgentDefinition,它目前还不支持直接绑 schema,所以它只能靠 prompt 引导着吐 JSON,实测下来非常不稳定
于是我在 SDK 的外面又叠了三层应用层防御,自己来做兜底:
- 第一层,语义必填校验。 模型偶尔会返回一个「schema 上完全合法、但关键字段是空的」的 JSON(prompt 里明明写了必填,schema 却给了个默认空值放行)。这种「合法的空壳」得拦,命中就重试;实在拿不到就降级处理(别的字段还有用,不整个扔掉)
- 第二层,结构校验。 模型偶尔会「字段类型给错」,比如一个本该是对象的字段,它给你返回一个字符串(在输出异常长的时候尤其容易发生)。拿严格的数据模型去校验,不过就重试,末次给个友好的报错,不把一个畸形的东西往下游传
- 第三层,包裹恢复。 这是最刁钻的一个,它解决的是一个上游的已知 bug
📐 深挖 · CLI 的「包裹」bug,和我用「白名单」把它根治的思路
底层那个 CLI 有个公开已知的 bug(社区有 issue 在跟):它偶尔会抽风,把模型本该规规矩矩返回的整个结构化输出,序列化成一个字符串,塞进一个单独的占位 key 里。更坑的是,这个占位 key 的名字还不固定,我见过
$PARAMETER_NAME、__unparsedToolInput、output、description……各种奇形怪状的变体。实测触发率不低,某个版本的 CLI 上大概三次里有一次中招,换个更新的版本反而更糟。而这属于上游的 bug,等它修好,还不一定是什么时候呢我最早的应对,是列一个「已知坏 key 名」的黑名单去匹配。很快发现这是个打地鼠游戏:上游的 key 名千奇百怪、还会冒新的,黑名单永远列不全
后来我把思路反过来,不去猜「坏 key 长什么样」,只认「好 key 长什么样」。 我的 schema 里,该有哪些字段我是完全确定的。所以我立一条规则:如果收到的是一个「只有单个 key 的 dict,而这个 key 根本不在我的 schema 字段清单里」,那它大概率就是被包裹了,直接把它解开、取出里面真正的内容。这样一来,不管上游给我包一个多离谱的 key 名,都能兜得住
这个思路我觉得挺有普适性:面对一个不可控的上游 bug,与其被动地枚举它所有的错法(防不胜防),不如站在「我确定知道的东西」(我的 schema)上,去反推那些「我不确定的东西」(乱七八糟的 key),把主动权攥回自己手里
🧯 分身也会失败:把每一种失败都当回事
还有一个容易被忽略、但对真实产品很重要的点:分身是会失败的。 它可能超时、可能返回空、可能吐出一个怎么都解析不了的东西
不能让这些失败「静默」过滤掉,我的做法是把分身的各种失败模式正儿八经地列成了一张矩阵(超时、agent 报错、没输出、输出为空、解析失败……),每一种,都对应一个给用户看的友好错误态。终检、调研、事实核查这些又都是反复迭代的行为(改稿、再终检、再改),所以我把每一次执行的结果,都按序号存成独立的文件(而不是互相覆盖),这样历史上每一次终检的报告都追得回来。这些听着琐碎,但「一个分身罢工了,用户能不能得到一句人话的交代」,恰恰是 demo 和产品的分水岭
⚖️ 垂直落点:分工不是为了炫技,是为了「隔离噪声 + 收敛理解」
收个尾,回到贯穿全专栏的对照:
⚖️ 取舍现场 · 用分身这件事,通用和垂直在想什么
"偷懒"的做法 SmartWriter 为什么 调研这种重活 主 agent 自己在主上下文里翻 派给隔离的分身,只收一页摘要 护住主写作桌面,不被噪声淹没 分身的报告 原样展示给用户 主 agent 消化成「一键应用」的修改 理解必须收敛,别外包给用户 结构化结论 信 LLM 能吐对 三层防御 + schema 白名单兜底 LLM 会散漫,上游还有 bug 验证 / 终检 谁写谁顺手检查 独立分身、只读、绝不下场改 验证要独立于实现,才靠得住
写到这,「编排与分工」这个板块就整个讲完了。回头看它这两篇,其实是在两个维度上,对抗「单个 Agent 面对复杂任务时的手忙脚乱」:Plan Mode 是时间上的编排,动手之前先把顺序想清楚;Subagent 是空间上的分工,把独立又吵闹的活,隔离到别处去干,只把干净的结论收敛回来。一个管「先后」,一个管「内外」,合起来,才让一个 Agent 有能力从容地啃下一篇复杂的长文写作,而不是从头乱到尾
到这一篇,我们把 12 个机制,从引擎、记忆、驾驭、交互,一路拆到编排与分工,全都拆完了。这台机器的每一个零件,都拿出来好好看过了。但比较残酷的是,这一切精心琢磨的细节,用户是完全感知不到的。 他打开应用,看到的只是一个界面、一个输入框、一份稿子。怎么把这套精密又复杂的机器,包装成一个可能连 Agent 是什么都不知道的写作者,也能顺手用起来的产品?
这,就是最后一个板块「嵌入垂直业务」要回答的。从下一篇起,我们接着掰扯产品流程,看看这些机制是怎么被串成一条真正的写作业务动线的。咱们继续聊