上一篇 讲了两个 Agent 怎么协作------Host MultiAgent 和 DeepFlux Subagent 两种模式,以及"Agent 就是 Tool"这个核心设计。那篇侧重"怎么用",这篇拆"里面怎么实现"。
三个问题串全篇:
- Host MultiAgent 的 Graph 内部怎么跑?(状态、流式、多意图汇总)
- ADK 三种预制模式(supervisor/planexecute/deep)各自怎么实现?
- 为什么 supervisor 标注 NOT RECOMMENDED,而 deep 是推荐方案?
(一)Host MultiAgent 源码拆解
上一篇 讲过 Host MultiAgent 的 Graph 拓扑和 specialist 注册。这里深入源码内部,看几个关键机制。
1. 输入的注入:map2list 把消息喂给 host
compose.go:49-83 的入口适配器把 Eino 的图状态(mapstringany)转成 LLM 能理解的消息列表。所有 specialist 的输出也通过同样的方式回流到消息列表。
2. 状态管理:state 结构体
compose.go:30-47 定义了图的内部状态:
go
type state struct {
Messages []*schema.Message
IsMultipleIntents bool
SpecialistResults map[string]string
}
Messages:当前消息列表,host LLM 和 specialist 都往里写IsMultipleIntents:多意图标记,host 一次产出多个 tool call 时为 trueSpecialistResults:各 specialist 的原始结果,供 summarizer 汇总
状态通过 ProcessState 在节点间流转。每个节点(host/specialist/summarizer)都能读写这个状态。
3. 流式处理:firstChunkStreamToolCallChecker
types.go:182-204 定义了一个重要的辅助函数:
go
type firstChunkStreamToolCallChecker struct {
done bool
hasCall bool
}
func (c *firstChunkStreamToolCallChecker) Check(chunk *schema.Message) bool {
if c.done {
return c.hasCall
}
c.done = true
c.hasCall = len(chunk.ToolCalls) > 0
return c.hasCall
}
只检查第一个 chunk 是否有 tool call。为什么只看第一个?因为流式场景下,host LLM 要么在第一个 chunk 就决定调 tool,要么就是不调。不需要等全部 chunk 到了再判断------这是性能优化,也避免了"等全部流式结果再决定"的延迟。
multiSpecialistsBranch(:222-244)用这个 checker 来分流:
go
if checker.Check(msg) {
// 有 tool call → 走 specialist 分支
} else {
// 无 tool call → 直接回答
}
4. 多意图汇总:Summarizer
multiIntentSummarizeNode(:286-335)处理多意图的汇总。逻辑分两层:
- 有 Summarizer 配置 (
:303-318):用 LLM 汇总各 specialist 的结果。把 specialist 结果拼成消息列表,喂给 summarizer ChatModel,产出最终回答。 - 无 Summarizer 配置 (
:319-333):纯拼接------[weather]: 结果1\n[flight]: 结果2------不做 LLM 汇总。
这个设计很实用:不是所有场景都需要 LLM 汇总------简单场景下纯拼接就够了,省一次 LLM 调用。
5. 回调体系:HandOff 的三层设计
callback.go 定义了三层回调:
- MultiAgentCallback :用户注册的回调接口,
OnHandOff(HandOffInfo)在每次 host 把任务交给 specialist 时触发 - ConvertCallbackHandlers :把 MultiAgentCallback 转成 Eino 通用回调(
OnStart/OnEnd),注入到 Graph 的节点回调中 - HandOffInfo :
ToAgentName+Argument,记录"谁交给了谁、带着什么理由"
三层设计的好处:用户只需关心 HandOff 事件,不需要理解 Eino 内部的回调机制。
(二)ADK Supervisor 模式:transfer 机制
supervisor/supervisor.go 只有 121 行,核心就两个动作:
1. 限制子 agent 只能回 supervisor
go
// supervisor.go:101-108
for _, subAgent := range conf.SubAgents {
subAgents = append(subAgents, adk.AgentWithDeterministicTransferTo(ctx, &adk.DeterministicTransferConfig{
Agent: subAgent,
ToAgentNames: []string{supervisorName},
}))
}
AgentWithDeterministicTransferTo 在每个子 agent 外面包一层,限制它只能 transfer 到 ToAgentNames 列表中的 agent。这里的 ToAgentNames 只有 supervisor 的名字。
效果:子 agent 之间不能直接通信,所有通信必须经过 supervisor。这是 supervisor 模式的核心约束------supervisor 是唯一的协调者。
2. 统一追踪
go
// supervisor.go:53-84
type supervisorContainer struct {
name string
inner adk.ResumableAgent
}
supervisorContainer 把整个 supervisor 结构(supervisor + 所有子 agent)包装成一个 agent。当 callback 注册时,OnStart/OnEnd 只触发一次,创建单一 trace root。所有 agent 共享同一个 trace 上下文。
3. NOT RECOMMENDED 的原因
源码注释直言:
Supervisor is built on agent transfer with full context sharing, which has not proven to be more effective empirically. Consider using ChatModelAgent with AgentTool or DeepAgent instead.
"经验证明这个方向不如另一个方向"。具体原因:
- 共享完整上下文:transfer 时 supervisor 和子 agent 共享完整的消息历史,上下文膨胀快
- 缺乏隔离:子 agent 能"看到" supervisor 的所有历史,包括不该它关心的信息
- 替代方案更好:AgentTool(把 agent 当 tool 调,独立 session)和 DeepAgent(内置 task tool 调度)在经验上更有效
(三)ADK Plan-Execute 模式:三阶段循环
plan_execute.go 有 881 行,是三个 prebuilt 中代码量最大的。核心是 New 函数(:862-880):
go
func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) {
loop, _ := adk.NewLoopAgent(ctx, &adk.LoopAgentConfig{
Name: "execute_replan",
SubAgents: []adk.Agent{cfg.Executor, cfg.Replanner},
MaxIterations: maxIterations,
})
return adk.NewSequentialAgent(ctx, &adk.SequentialAgentConfig{
Name: "plan_execute_replan",
SubAgents: []adk.Agent{cfg.Planner, loop},
})
}
结构是 SequentialAgent(Planner, LoopAgent(Executor, Replanner)):
markdown
Planner(生成计划)
↓
LoopAgent(循环直到完成)
├─ Executor(执行第一步)
└─ Replanner(决定:继续 or 完成)
├─ 继续 → 更新计划 → 回到 Executor
└─ 完成 → 退出
状态传递:Session Value
四个 Session Key 在不同 agent 之间传递状态:
| Key | 写入者 | 读取者 | 内容 |
|---|---|---|---|
UserInputSessionKey |
Planner(:329) |
Executor、Replanner | 用户原始输入 |
PlanSessionKey |
Planner(:403)、Replanner(:762) |
Executor、Replanner | 当前计划 |
ExecutedStepSessionKey |
Executor(通过 OutputKey) | Replanner | 最新执行结果 |
ExecutedStepsSessionKey |
Replanner(:671) |
Executor、Replanner | 所有已执行步骤 |
关键细节:ExecutedStepsSessionKey 的累积发生在 Replanner(:667-671),不是在 Executor。Replanner 把当前步骤的结果追加到历史列表,然后决定下一步。
工具调用:PlanTool + RespondTool
Replanner 有两个工具(:110-142):
- PlanTool :生成/更新计划,参数
steps[](步骤列表) - RespondTool :生成最终响应,参数
response(回复文本)
Replanner 的 prompt(:192-238)告诉 LLM 二选一:要么调 respond_tool 结束,要么调 plan_tool 更新计划继续。这是一个经典的"工具驱动流程控制"------LLM 通过选择不同的工具来决定流程走向。
循环终止
Replanner 调 respond_tool 时触发 BreakLoopAction(:748):
go
if msg.ToolCalls[0].Function.Name == r.respondTool.Name {
action := adk.NewBreakLoopAction(r.Name(ctx))
generator.Send(&adk.AgentEvent{Action: action})
return msg, nil
}
BreakLoopAction 告诉 LoopAgent "这个循环该停了",LoopAgent 收到后退出循环。
(四)ADK Deep 模式:内置工具 + task tool 调度
deep/deep.go 只有 268 行,但构造了一个完整的 agent 脚手架。核心是 NewTyped(:116-166):
go
func NewTyped[M adk.MessageType](ctx context.Context, cfg *TypedConfig[M]) (adk.TypedResumableAgent[M], error) {
// 1. 构建内置中间件
handlers, _ := buildTypedBuiltinAgentMiddlewares(ctx, cfg)
// 2. 构建 task tool(子 agent 调度)
tt, _ := typedTaskToolMiddleware(ctx, ...)
handlers = append(handlers, tt)
// 3. 创建 ChatModelAgent
return adk.NewTypedChatModelAgent(ctx, &adk.TypedChatModelAgentConfig[M]{...})
}
内置工具链
Deep agent 启动时自动装配三个中间件(buildTypedBuiltinAgentMiddlewares,:209-232):
-
write_todos (
:244-267):任务管理工具。参数todos[](每个 todo 含 content/activeForm/status),状态三态:pending → in_progress → completed。这是 Claude Code 的 TaskCreate 模式的复刻。 -
filesystem 工具 (
:219-229):如果配置了 Backend/Shell/StreamingShell,自动注册文件读写、glob、grep、shell 执行等工具。这些工具通过filesystem2.NewTyped中间件注入。 -
task tool (
task_tool.go:35-58):子 agent 调度工具。把每个子 agent 包装为 AgentTool,通过subagent_type参数路由。同时注入一个 prompt,告诉主 agent 什么时候用 task tool。
Task Tool:把 Agent 当 Tool 调
task_tool.go:61-124 的 typedNewTaskTool 是 Deep agent 的核心:
go
func typedNewTaskTool[M](...) (tool.InvokableTool, error) {
t := &typedTaskTool[M]{
subAgents: map[string]tool.InvokableTool{},
}
// 1. 如果未禁用通用子 agent,创建一个
if !withoutGeneralSubAgent {
generalAgent, _ := adk.NewTypedChatModelAgent(ctx, ...)
t.subAgents[generalAgent.Name(ctx)] = adk.NewTypedAgentTool(ctx, generalAgent)
}
// 2. 把用户提供的子 agent 也包装成 AgentTool
for _, a := range subAgents {
t.subAgents[a.Name(ctx)] = adk.NewTypedAgentTool(ctx, a)
}
return t, nil
}
InvokableRun(:156-175)的路由逻辑很简单:
go
func (t *typedTaskTool[M]) InvokableRun(ctx, argumentsInJSON string, ...) (string, error) {
input := &taskToolArgument{}
json.Unmarshal([]byte(argumentsInJSON), input)
// 按 subagent_type 找对应的 AgentTool
a := t.subAgents[input.SubagentType]
// 把 description 作为参数传给子 agent
return a.InvokableRun(ctx, marshal(map[string]string{"request": input.Description}))
}
参数只有两个字段:subagent_type(选哪个子 agent)和 description(子 agent 的任务描述)。主 agent 调 task tool 时,就像调度一个短生命周期的工作进程。
通用子 agent
general-purpose agent(:86-112)是一个特殊设计:它共享主 agent 的 instruction、tools、middlewares、handlers。这意味着通用子 agent 和主 agent 有相同的能力,但有自己的独立 session。
用途:主 agent 把复杂子任务委托给这个"分身",自己聚焦在协调上。
中英文双语 Prompt
Deep agent 的 prompt 是四种模式中最丰富的(prompt.go 有 685 行),所有 prompt 都有中英文两套:
baseAgentInstruction/baseAgentInstructionChinese:主 agent 的系统指令(~110 行),包含语气风格、安全策略、编码规范、工具使用策略taskPrompt/taskPromptChinese:task tool 的使用说明,告诉主 agent 什么时候该用 task tooltaskToolDescription/taskToolDescriptionChinese:task tool 的描述,模板变量{other_agents}填入可用子 agent 列表writeTodosToolDescription/writeTodosToolDescriptionChinese:write_todos 工具的使用说明(~180 行),大量示例
语言选择通过 internal.SelectPrompt 自动判断,根据上下文语言选择对应版本。
Deep Agent 的 prompt 来源
Deep agent 的 prompt 设计借鉴了 LangChain 的 DeepAgents 项目和 Claude Code(prompt.go:21-23):
This file contains prompt templates and tool descriptions adapted from the DeepAgents project and ClaudeCode.
这说明 Deep agent 不是凭空设计------它复刻了 Claude Code 在编码场景中验证过的 prompt 模式,包括任务管理(write_todos)、子进程调度(task tool)、文件系统操作等。
(五)三种 prebuilt 模式,一张表
| 维度 | Supervisor | Plan-Execute | Deep |
|---|---|---|---|
| 核心机制 | transfer 交接 | 计划→执行→重规划循环 | task tool 子 agent 调度 |
| 子 agent 通信 | 只能和 supervisor | 通过 session state | 通过 task tool 参数 |
| 上下文共享 | 完整共享(问题所在) | 通过 session value 选择性传递 | task tool 启动独立 session |
| 流程控制 | supervisor LLM 决定 | Replanner 二选一工具 | 主 agent LLM 决定调哪个 task |
| 内置工具 | 无 | 无 | write_todos + filesystem + task |
| 推荐程度 | NOT RECOMMENDED | 推荐 | 推荐 |
| 代码量 | 121 行 | 881 行 | 268 行(+685 行 prompt) |
小结
| 问题 | 答案 | 关键源码 |
|---|---|---|
| Host MultiAgent 图状态怎么传 | state 结构体(Messages+IsMultipleIntents+SpecialistResults),通过 ProcessState 流转 | compose.go:30-47 |
| 流式怎么判断 tool call | 只看第一个 chunk(firstChunkStreamToolCallChecker),不等全量 | types.go:182-204 |
| 多意图怎么汇总 | 有 Summarizer→LLM 汇总,无→纯拼接 | compose.go:286-335 |
| Supervisor 为什么 NOT RECOMMENDED | transfer 共享完整上下文,经验证明不如 AgentTool/DeepAgent | supervisor.go:42-44 |
| Plan-Execute 怎么循环 | SequentialAgent(Planner, LoopAgent(Executor, Replanner)),Replanner 二选一工具 | plan_execute.go:862-880 |
| Deep agent 怎么调度子 agent | task tool 把子 agent 包装为 AgentTool,通过 subagent_type 参数路由 | task_tool.go:61-175 |
| Deep agent 有哪些内置工具 | write_todos + filesystem(r/w/glob/grep/shell) + task tool | deep.go:209-232 |
几条设计判断:
-
Transfer 共享上下文不如 AgentTool 独立 session。 Supervisor 的 NOT RECOMMENDED 标注不是功能不好用,而是"经验证明这个方向不如另一个方向"。AgentTool 给每个子 agent 独立 session 和 checkpoint,隔离性更好。
-
Plan-Execute 的本质是"工具驱动流程控制"。 Replanner 不是通过代码分支决定"继续 or 完成",而是通过给 LLM 两个工具(plan_tool / respond_tool),让 LLM 自己选。这是一种"把控制流交给模型"的设计。
-
Deep agent 是 Claude Code 的复刻。 从 write_todos 的任务管理到 task tool 的子进程调度,再到 filesystem 工具链,Deep agent 把 Claude Code 在编码场景中验证过的模式搬到了 Eino ADK。
-
prompt 就是代码。 Deep agent 的 685 行 prompt 不是"文档",而是 agent 行为的核心逻辑。prompt 控制着主 agent 什么时候用 task tool、什么时候并行、什么时候写 todos------这些行为不是硬编码的,而是"软编码"在 prompt 里。
下一篇讲 Agent 间 Transfer 交接------用户在多个 Agent 间无缝切换,以及 Eino ADK 的 transfer 机制源码。