欢迎光临
我们一直在努力

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

上一篇 讲了两个 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 的图状态(map[string]any)转成 LLM 能理解的消息列表。所有 specialist 的输出也通过同样的方式回流到消息列表。

    2. 状态管理:state 结构体

    compose.go:30-47 定义了图的内部状态:

    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 定义了一个重要的辅助函数:

    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 来分流:

    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

    // 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. 统一追踪

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

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

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

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

    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 的核心:

    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)的路由逻辑很简单:

    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 模式,一张表

    维度SupervisorPlan-ExecuteDeep
    核心机制 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 机制源码。

    赞(0)
    未经允许不得转载:171主机测评 » MultiAgent Host 源码 + ADK prebuilt 三种预制模式(第92篇-E78)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址