DeepAgents(十):委派用 isolated 还是 fork
委派用 isolated 还是 fork
《task 工具怎么把活派给子 Agent》把那件工具的脸看清楚了,还剩一个更实际的问题:装配子 Agent 的时候,mode 那一格填什么。
不填就是 isolated,另一个选项叫 fork。仓库里对 fork 的说明写得很客气,说它继承你的完整对话和系统提示词。
我拿同一条父对话、同一条任务描述,派给 isolated 和 fork 各走一遍,把开局拿到的东西摊开对照。
isolated 和 fork 的差别落在子 Agent 开局那一眼:isolated 子只拿到你写的那句任务描述,fork 子会先把你这边的对话从头读一遍。选哪个,看你那句描述写不写得清楚,也看你愿不愿意付这份入场费。
全程不用 API key,读完约 6 分钟。
一、装配的时候,清单上就多一行尾巴
一个父 Agent 能同时挂好几个子 Agent,各派各的活。它们的规格写起来只差一个字段:
subagents=[
{"name": "iso1",
"description": "独立干活的"},
{"name": "fk1",
"description": "接对话的",
"mode": "fork"},
]
mode 写 fork 的那一个,会去改写 task 工具的描述。父 Agent 派活之前读的那份清单,尾巴上会多出 86 个字符,大意是「它继承你的完整对话和系统提示词,这里不用重述背景」。
这一句不是装饰。仓库源码的注释里管它叫承重结构:少了它,「每次调用都是无状态的」就是模型能看到的唯一信号,模型可能干脆不肯把活派给 fork 子。
清单之外,两边手上的工具也不一样:
isolated 子 |
fork 子 |
|
|---|---|---|
| 清单行尾巴 | 就是你写的那句 | 多 86 字符一句注 |
| 手上工具 | 7 个,没有 task |
8 个,task 在 |
| 装同一条任务时描述长度 | 1443 字符 | 1525 字符 |
少的那一个工具就是 task。这件事在《子 Agent 为什么不能继续往下派》里还会展开,这里先记一笔:isolated 子想接着派人,手上根本没这个工具。
二、开局那一眼,两边差一个数量级
现在是正题。我让父先干 2 步活,再把同一句任务描述派出去,看子 Agent 第一次被调用时,模型手上有什么。
任务描述只写了一句:「把刚才那个订单号报给我」。
isolated 子的开局是 2 条消息:一条它自己的提示词,一条任务描述。合计 12 token。
fork 子的开局是 9 条消息:父的整段对话一条不落,末尾才接上那条任务描述。合计 326 token。
isolated 子 |
fork 子 |
|
|---|---|---|
| 开局消息条数 | 2 条 | 9 条 |
| 输入合计 | 12 token | 326 token |
| 里面装的是 | 提示词 + 任务描述 | 父的整段对话 + 任务描述 |
父这边付出的代价倒是完全一样。发起调用的那条 AI 消息,两边都是 36 token,因为派出去的参数一模一样。
也就是说,从父的角度看不出任何区别,差别全在子那边热身的成本上。这笔成本具体多少,第四节算。
三、提示词是拼出来的,不是替换
第 2 条消息那个位置,fork 子拿到的是什么,值得单独看一眼。
我拿父的提示词和子自己写的提示词做了 4 组组合,结果是这样:
| 父的提示词 | 子自己写的 | fork 子拿到 |
isolated 子拿到 |
|---|---|---|---|
| 有 20 字符 | 没写 | 20 字符(父那句) | 空 |
| 有 20 字符 | 有 11 字符 | 33 字符(拼在一起) | 11 字符 |
| 没写 | 有 11 字符 | 11 字符 | 11 字符 |
| 没写 | 没写 | 空 | 空 |
fork 子的提示词是父那一份加一个空行、再接上自己那一份,33 就是 20 加 2 加 11。它没有替换掉父的提示词,两句话同时在场。
isolated 子那边完全没有继承这回事,父写什么它都看不到。
这里有个容易踩空的地方。默认那个叫 general-purpose 的子 Agent,自带 286 个字符的提示词,主题是「调用方只看得到你的最终回复,请把完整答案放进去」。很多人会把这句当成「子 Agent 都有的待遇」。
它不是。自己声明一个子 Agent 又不写 system_prompt,这段提示词就没了,子 Agent 的最终回复很可能是半截。这个坑跟选 isolated 还是 fork 无关,但既然是自己声明子 Agent,顺手写上更稳。
四、入场费:父干得越多,fork 读得越多
把父干的活从 1 步加到 8 步,两边开局的输入长这样:
| 父干了 | isolated 子 |
fork 子 |
差额 |
|---|---|---|---|
| 1 步 | 12 token | 217 token | 205 token |
| 4 步 | 12 token | 388 token | 376 token |
| 8 步 | 12 token | 616 token | 604 token |
isolated 子那一列纹丝不动,12 就是那句任务描述加一份空提示词的价钱。父干多少活,它都不知道,也不用付钱。
fork 子那一列每多 1 步就涨一点,平均一步 57 token 上下。父干过的活它全要读一遍,读多少付多少。
所以「fork 省了重述的功夫」这句话有个价钱。父这边的对话越长,这笔入场费越贵,而且它跟子 Agent 真正要干的那件事没有关系。
五、任务描述该写多细
入场费买到了什么,用一件事就看得出来。
父那边把「订单号是 A-7731」写进了一份工作文件。派出去的任务描述故意只写「把上面那个订单号报给我」,没带号。
isolated 子的开局上下文里,A-7731 出现 0 次。它手上只有那一句描述,父说过什么它一概不知,只能猜或者回头问。
fork 子的开局上下文里,A-7731 出现 1 次,就在父那次写文件的参数里。它不用你重述,自己翻得到。
一句话任务描述能不能自己站住,直接决定了这个子 Agent 该用哪种模式。写不清楚的活,用 fork;写得清楚的活,用 isolated 省 token。
顺序别搞反。挑模式这件事发生在写描述之前,而描述该写多细,取决于你挑了哪一种。
六、挡什么、塞什么、压缩掉什么
状态通道上的取舍,两边也不一样。
isolated 子那边挡掉 4 个键:对话历史、待办清单、结构化输出、还有一个内部标记。fork 子那边只挡 3 个,摘要相关的通道占了 2 个。
也就是说,待办清单在 fork 子那边是不挡的,它能看到父手上的清单。messages 这个键两边都要特殊处理,isolated 子是整段换掉,fork 子是拿父的重新拼。
fork 子还会被多塞一个键进去,一个布尔标记。它的作用是给递归委派留路标:子在内部要是想再派人,靠这个标记认出自己是子,然后被拒。isolated 子没有这个标记,因为它手上连工具都没有。
回传这边,两边一模一样。派一次活,父收到的就是一条回执,内容都是子 Agent 最后那句有正文的话。子中间看了什么、跑了几步、报了哪些错,一条都不进父的上下文。
子写的文件倒是都回传,两边我都拿到了。落盘是共用的,这一点跟模式无关。
最后一件容易忽略的事:fork 子继承的是「有效历史」。父要是压缩过上下文,子拿到的就是摘要加剩下的部分,父那边被压掉的原文它同样看不到。
好处是它不会接到一段被砍断的对话,代价是它看不到的部分跟父一样多。这个行为在源码里由摘要事件驱动,事件本身就 3 个字段:切在第几条、摘要那条消息、原文卸载去了哪。
七、怎么选
把上面的账合成一张表:
isolated 子 |
fork 子 |
|
|---|---|---|
| 开局消息 | 2 条 | 9 条(跟着父涨) |
| 继承父的提示词 | 不继承 | 继承 |
| 能看到父的来龙去脉 | 不能 | 能 |
| 能自己继续派活 | 手上没这个工具 | 有,但一调就被拒 |
| 父这边多出的痕迹 | 一条回执 | 一条回执 |
用一句话能把这件活说清楚的,用 isolated,开局 12 token,子 Agent 干净得像新拆的。说不清楚、必须让它自己翻前文的,用 fork,入场费按父的对话长度计价。
拿不定主意的时候,我习惯先用 isolated。等发现子 Agent 老是回头问同一个背景,说明该交给 fork 了。
isolated 让子 Agent 从一张白纸开始,fork 让它接着你的上下文往下走。mode 那一格填什么,说到底是对「这件活能不能用一句话说清」的一次表态。