DeepAgents(十三):怎么看清子 Agent 都在干什么

2026-10-19 04:570 条评论26 次阅读

怎么看清子 Agent 都在干什么

讲完怎么派活,下一个问题就来了:活派出去以后,我怎么知道它们各自在干什么。

我一开始的想法很朴素,主 Agent 都在流式输出了,子 Agent 应该也在这条流里。真跑了一段才明白,默认情况下它们不在。

主 Agent 派出去的子 Agent,默认在流里是隐身的:同一段活,关掉 subgraphs 只给 6 条事件,打开给 13 条。分清谁在干活靠一个字段,lc_agent_name。

一、同一段活,开与关差在哪

先交代我用的那段活。主 Agent 收到一句「跑一遍」,在同一条消息里派 1 个子 Agent 出去;子 Agent 先调一次工具,再回一句话;主 Agent 收到回执,收尾。

这段活跑完,流里是这样:

怎么看 收到几条 按名字归组
关掉 subgraphs 6 条 全是主 Agent
打开 subgraphs 13 条 主 Agent 6 条,子 Agent 7 条

关掉时那 6 条是 5 个模型片段加 1 条工具回执,namespace 一栏始终是空的。打开后多出来的那 7 条,namespace 全都落在一个带 tools: 前缀的新格里。

**子 Agent 的模型片段、它调的那件工具、工具给它的回执,全在那个新格里。**关掉 subgraphs,你只能看到主 Agent 收到的那一条 task 回执,中间过程一条都没有。

换句话说,主 Agent 说完「我派了个活出去」,流就沉默了,直到子 Agent 干完回来。

一段活: 派 1 个子 Agent

关掉 subgraphs: 6 条

打开 subgraphs: 13 条

主 Agent 6 条

子 Agent 7 条

二、分清谁在干活的那个字段

打开 subgraphs 之后,每条事件都带一份 metadata。我数了下,一共 11 个键。

里面有一个叫 lc_agent_name。这段活跑下来它只有 2 个取值,一个是我给主 Agent 起的名字,另一个是子 Agent spec 里的名字。

谁 名字从哪来
主 Agent 建 Agent 时挂在 config 上,用的就是 name 参数
子 Agent 每次被调起时注进去,用的是 spec 里的 name

2 个注入点分别在框架的 2 份源码里,一处给子 Agent 盖名字,一处给主 Agent 盖名字。所以不用去猜 namespace 里那串编号对应谁,直接读这个字段就行。

同一段活里,这个名字还能顺手告诉我们各层产出了多少条:

名字 模型片段条数 出现过的节点
主 Agent 6 条 model、tools
子 Agent 7 条 model、tools

两边节点名一模一样。这也是为什么光看节点名分不出主次,得看名字字段。

三、模型不吐字,流就是假的

这件事我是拿 2 个模型对照出来的。

同一个 Agent、同一段活,只换模型:一个会在内部一片一片吐 token,另一个一次给整条消息。

模型 收到几条 模型片段 整条消息
不吐 token 6 条 0 条 4 条
会吐 token 13 条 11 条 0 条

不吐 token 的那个,流里一条模型片段都没有,只有几条整块的 AIMessage。前端拿到的是一句话忽然出现,看着就像卡住了。

所以流式要成立,模型自己得真吐 token。外层接口再漂亮,模型那边一口气给整条,聚合出来还是整条。

我把真模型也接上量了一遍,用的是智谱的 GLM-5.2,让它答一句 40 字左右的话,跑 2 遍:

第几遍 分成几段 第一段什么时候来 第一段到最后一段 整条链总耗时
第 1 遍 34 段 1.4 秒 0.7 秒 2.0 秒
第 2 遍 29 段 1.8 秒 0.6 秒 2.4 秒

这 2 行有个地方值得看:第一段等的时间,比后面所有字吐完的时间加起来还长。等首字 1.4 到 1.8 秒,剩下的字 0.6 到 0.7 秒就全出来了。

做流式界面的时候,这个账决定了体验的瓶颈在哪。用户盯着屏幕不动的那段时间,花在等首字上。

四、另有一套:投影

框架还有一套更新的流式接口,开法是 await astream_events(version="v3")。它不给零散事件,给几条分好类的投影。

我数了下,一共 6 条,每条背后是一个专门的变压器:

投影 归谁管
messages MessagesTransformer
tool_calls ToolCallTransformer
subagents SubagentTransformer
subgraphs SubgraphTransformer
lifecycle LifecycleTransformer
values ValuesTransformer

同一段活,底下跑过去的协议事件是 37 条:values 8 条、messages 23 条、tools 4 条、lifecycle 2 条。

这里有个很容易误判的地方:本层投影只收本层。

我在这段活上分别读了 3 条投影。messages 给 2 条,正好是主 Agent 自己的 2 次模型调用,子 Agent 的 token 一条都没有。tool_calls 给 1 条,只有主 Agent 那次 task 调用,子 Agent 内部调的工具不在里面。

所以投影看着方便,但它是个收口的视图。要用它看子 Agent 内部,得顺着另一个入口往下钻。

五、那个 handle 给的三样东西

第 4 节说的另一个入口,是 subagents 这条投影。它每条给一个 handle,一个 handle 对应一个子 Agent 的一次执行。

handle 上有三样东西值钱:

字段 给什么
name 子 Agent 的名字,就是第 2 节那个字段
cause 派它出去的那次工具调用,带 tool_call_id
status 起止状态,跑的时候是 started,读完是 completed

cause 是这套接口最值钱的地方。原来在旧接口里,想知道「这个子 Agent 是谁派的」,得自己去主 Agent 的调用列表里对 id。现在这条因果边直接给出来。

钻进去读子 Agent 内部的写法,有一点讲究:要在遍历到它的那一刻当场读。

我试过先把手柄攒起来,等整条流跑完再回头读,结果一条都拿不到。它是个推迟订阅的流,错过那一刻,缓冲里就是空的。

name 认人

cause 认谁派

status 认起止

六、到达顺序不稳

并发这件事,讲派活那篇已经踩过一次。这次换成投影接口,结果还是不稳。

同时派 3 个子 Agent,看它们的 handle 按什么顺序到达,我跑了 5 遍:

跑第几遍 handle 的到达顺序 跟发起顺序一致
1 C、A、B 否
2 A、B、C 是
3 A、B、C 是
4 C、A、B 否
5 C、B、A 否

5 遍里 2 遍是顺的。讲派活那篇里那条远端的路,6 遍一遍都不顺。

2 处的结论一样:认人得靠 cause,不能靠位置。位置有时候看着对,那是运气。

七、子 Agent 炸了,流里还剩什么

我让子 Agent 内部那件工具抛一个异常,然后看 2 条路各自收到什么。

lifecycle 这条给得挺完整,2 条事件:先一条 started,再一条 failed,error 里就是异常原文。

handle 那条给的是状态变化:读它的时候 status 会转成 failed,error 里也是异常原文。

但有件事得留心。异常会从迭代里抛出来,把当前这一轮读打断:本层那条工具视图一条都收不到。

我第一遍就把 handle 和 lifecycle 放在同一个循环里读,异常先把整段读带走了,lifecycle 那边一条都没读着,看着像「框架什么都不给」。分成 2 个 run 单独读,failed 事件就在那儿。

所以真要审计子 Agent 的失败,单独订阅 lifecycle,别跟 handle 混在一个迭代里。

这和讲派活那篇的结论接得上:默认装配下,子 Agent 不算一层隔离。区别只是这一次,流里还留下了失败的原因。

八、怎么用

理完这些,我在「看清子 Agent 在干什么」这件事上留下 4 条。

先打开 subgraphs。 这是最便宜的一步,一行参数,同一段活从 6 条变成 13 条,子 Agent 的中间过程全出来了。

认人用 lc_agent_name,别用位置。 节点名两边一样,到达顺序也不稳,只有这个名字是可靠的。

要知道「谁派的它」,就用投影接口的 cause。 旧接口得自己对 id,新接口直接把这条因果边给你。代价是这套接口还标着实验性。

要盯失败,单独订阅 lifecycle。 别把它跟 handle 放在同一个迭代里读,异常会先把整段读带走。