MultiAgent Host 源码 + ADK prebuilt 三种预制模式(第92篇-E78)

上一篇 讲了两个 Agent 怎么协作------Host MultiAgent 和 DeepFlux Subagent 两种模式,以及"Agent 就是 Tool"这个核心设计。那篇侧重"怎么用",这篇拆"里面怎么实现"。

三个问题串全篇:

  1. Host MultiAgent 的 Graph 内部怎么跑?(状态、流式、多意图汇总)
  2. ADK 三种预制模式(supervisor/planexecute/deep)各自怎么实现?
  3. 为什么 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 时为 true
  • SpecialistResults:各 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 定义了三层回调:

  1. MultiAgentCallback :用户注册的回调接口,OnHandOff(HandOffInfo) 在每次 host 把任务交给 specialist 时触发
  2. ConvertCallbackHandlers :把 MultiAgentCallback 转成 Eino 通用回调(OnStart/OnEnd),注入到 Graph 的节点回调中
  3. HandOffInfoToAgentName + 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 上下文。

源码注释直言:

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):

  1. write_todos:244-267):任务管理工具。参数 todos[](每个 todo 含 content/activeForm/status),状态三态:pending → in_progress → completed。这是 Claude Code 的 TaskCreate 模式的复刻。

  2. filesystem 工具:219-229):如果配置了 Backend/Shell/StreamingShell,自动注册文件读写、glob、grep、shell 执行等工具。这些工具通过 filesystem2.NewTyped 中间件注入。

  3. task tooltask_tool.go:35-58):子 agent 调度工具。把每个子 agent 包装为 AgentTool,通过 subagent_type 参数路由。同时注入一个 prompt,告诉主 agent 什么时候用 task tool。

Task Tool:把 Agent 当 Tool 调

task_tool.go:61-124typedNewTaskTool 是 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 tool
  • taskToolDescription / 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 机制源码。

相关推荐
前沿在线1 小时前
WRC2026丨当机器开始理解人的意图,人机交互走向更多场景
人工智能·ai·大模型
动物园猫1 小时前
红外无人机目标检测数据集:4,500+张图像 | 目标检测
人工智能·目标检测·无人机
达子6661 小时前
AI训练师图解_6.1_让AI理解人类语言_自然语言处理
人工智能·自然语言处理
Linguwen1 小时前
中山GEO分享会回顾文
人工智能·chatgpt
武子康1 小时前
多 Agent 不是多开几个终端:Pi 的 Sub-agent 取舍
人工智能·llm·agent
俊哥V1 小时前
每日 AI 研究简报 · 2026-08-22
人工智能·ai
龙虾PRO1 小时前
数据中台 + AI 落地:2026 五大趋势与零售出海完整实操案例
人工智能·零售
wish3662 小时前
K8S 免镜像部署:通过 API 上传文件动态创建服务(以 Ollama 为例)
人工智能·云原生·容器·kubernetes·local llm