系列「企业级 AI Agent 实现拆解」E19 篇。上一篇 E18 讲了 ADK 的基础用法ChatModelAgent、Runner、Interrupt/Resume。这篇聚焦 Multi-Agent 协作的核心问题——一个 Agent 怎么把任务交给另一个 Agent。ADK 提供了三种机制但只有一种被官方推荐另外两种留有明确的NOT RECOMMENDED注释。读完这篇你会知道为什么需要 Agent Transfer单 Agent 处理不了哪些场景三种协作机制AgentTool推荐、SetSubAgents不推荐、Supervisor不推荐AgentTool 的边界隔离子 Agent 的 Exit/Transfer 动作为什么不传出来Transfer 模式下对话历史怎么迁移IsolatedSession 的设计确定性移交DeterministicTransfer 的用法三种机制的实测对比一、为什么一个 Agent 不够单 Agent 处理任何问题有一个根本限制上下文会越来越长。当你要求同一个 Agent 既搜索资料、又写代码、又做数学计算历史消息越积越多模型要在所有历史里找线索注意力被稀释。而且同一个系统 Prompt 很难同时把三个角色都交代清楚——“你是搜索员也是程序员也是数学家”——通常意味着什么都不精。多 Agent 的核心价值就在这里每个 Agent 只看自己需要的上下文专注自己的职责。二、方案一推荐把 Agent 包装成工具Eino 源码注释里明确写着agent_tool.go:69// NewAgentTool creates a tool that wraps an agent for invocation.用法把一个子 Agent 包装成工具父 Agent 通过普通的工具调用来雇用它。这是 ADK 官方唯一推荐的 Multi-Agent 模式。// 创建子 AgentresearchAgent,_:adk.NewChatModelAgent(ctx,adk.ChatModelAgentConfig{Name:research_agent,Description:the agent responsible to search the internet for info,Instruction:You are a research agent...,Model:m,ToolsConfig:adk.ToolsConfig{...},})// 把子 Agent 变成一个工具researchTool:adk.NewAgentTool(ctx,researchAgent)// 父 Agent 把这个工具加到自己的工具列表parentAgent,_:adk.NewChatModelAgent(ctx,adk.ChatModelAgentConfig{Name:project_manager,Model:m,ToolsConfig:adk.ToolsConfig{ToolsNodeConfig:compose.ToolsNodeConfig{Tools:[]tool.BaseTool{researchTool,codeTool,reviewTool},},},})从外部看父 Agent 还是正常的 ReAct 循环只是工具恰好是另一个 Agent。父 Agent 不关心子 Agent 内部怎么工作——它只是调用了一个名叫research_agent的工具传入{request: find 2024 US GDP}, 等待返回结果。关键特性动作边界隔离子 Agent 内部可能触发 Exit、Transfer、BreakLoop 等动作但这些动作不会传到父 Agent源码agent_tool.go:90-93// Action Scoping: // - Interrupted: Propagated via CompositeInterrupt (interrupt/resume works across boundaries) // - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool白话子 Agent 想结束任务对它自己来说任务确实结束了但父 Agent 只看到这个工具调用完成了返回了结果。子 Agent 不能意外终止父 Agent 的执行流。唯一的例外是Interrupted中断等待人工确认这个会通过CompositeInterrupt传递出去让整个系统都知道需要暂停等待用户。Supervisor 预制模式supervisor.New是 AgentTool 之上的一个预制封装源码在adk/prebuilt/supervisor// eino-examples/adk/multiagent/supervisor/agent.go:192supervisorAgent,err:supervisor.New(ctx,supervisor.Config{Supervisor:sv,// 主控 AgentSubAgents:[]adk.Agent{searchAgent,mathAgent},// 子 Agent 列表})supervisor.New自动把每个子 Agent 包装成AgentTool注入到主控 Agent 的工具列表里。你只需要定义主控 Agent 的 Instruction“你是项目经理有两个下属……”其余的工具调用由 ReAct 循环自动处理。三、方案二不推荐LLM 驱动的 Agent 切换这是另一套机制——让模型自己决定我要交给哪个 Agent。// eino-examples/adk/intro/transfer/transfer.go:35a,err:adk.SetSubAgents(ctx,routerAgent,[]adk.Agent{chatAgent,weatherAgent})SetSubAgents做了两件事把子 Agent 的名字和描述告诉 Router Agent注入到系统 Prompt给 Router Agent 加一个内置工具transfer_to_agent参数agent_name执行时RouterAgent 的模型看到用户问题自己判断该交给谁然后调用transfer_to_agent(WeatherAgent)——相当于给自己的下一步指路。用户北京今天天气 RouterAgent → 判断这是天气相关 → 调用 transfer_to_agent(agent_nameWeatherAgent) WeatherAgent → 调用 get_weather(cityBeijing) → 返回结果这个模式看起来自然但 Eino 官方在源码里明确标注utils.go:92-95// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not // proven to be more effective empirically. Consider using ChatModelAgent with AgentTool // or DeepAgent instead for most multi-agent scenarios.核心问题全上下文共享。当 RouterAgent 的会话历史传给 WeatherAgent 时WeatherAgent 要处理一堆它不需要的上下文RouterAgent 的历史消息既浪费 Token又可能干扰判断。上下文隔离机制ADK 在实现 Transfer 时做了一个折中deterministic_transfer.go:166——创建IsolatedSessionisolatedSession:runSession{Values:parentSession.Values,// 共享 session values键值对valuesMtx:parentSession.valuesMtx,// Events: 不继承默认为空}子 Agent 有独立的事件历史不继承父 Agent 的全部消息但共享session.Values通过AddSessionValue/GetSessionValues存取的键值对。这样可以在两个 Agent 间传递少量结构化状态同时避免把全部对话历史扔过去。四、方案三不推荐确定性移交有时候移交目标不需要 AI 判断就是固定的。AgentWithDeterministicTransferTo用于这个场景// 执行完 agentA固定移交给 agentBwrappedA:adk.AgentWithDeterministicTransferTo(ctx,adk.DeterministicTransferConfig{Agent:agentA,ToAgentNames:[]string{agentB},})执行完agentA的全部逻辑后框架自动追加两条消息assistant 说我要移交给 agentB tool 确认消息然后触发TransferToAgent动作Session 流转到 agentB。白话这是硬编码的流水线——A 完事了一定交给 B不经过任何 AI 决策。适合固定流程“报告总结完毕一定发给审阅 Agent”不适合根据内容动态路由。同样标注了 NOT RECOMMENDED原因相同全上下文共享。五、三种方案一张表AgentTool推荐SetSubAgentsDeterministicTransfer路由决策者父 Agent 的 LLM工具调用Router Agent 的 LLM代码硬编码上下文共享隔离只传 request 字符串全部共享IsolatedSession 折中全部共享子 Agent 动作边界Exit/Transfer 不传出全部传出全部传出适合场景绝大多数场景特定路由式场景固定流水线官方立场✅ 推荐⚠️ 不推荐⚠️ 不推荐六、完整示例Supervisor 模式AgentTool 推荐路径// eino-examples/adk/multiagent/supervisor/agent.go精简funcbuildSupervisor(ctx context.Context)(adk.Agent,error){sv,_:adk.NewChatModelAgent(ctx,adk.ChatModelAgentConfig{Name:supervisor,Instruction:You are a supervisor managing two agents: - a research agent: assign research-related tasks - a math agent: assign math-related tasks Do not do any work yourself.,Model:m,Exit:adk.ExitTool{},// 主控完成后用 exit 工具退出})searchAgent,_:buildSearchAgent(ctx)// 有 search 工具的 AgentmathAgent,_:buildMathAgent(ctx)// 有 add/multiply/divide 工具的 Agent// supervisor.New 内部自动 NewAgentTool 包装每个子 Agentreturnsupervisor.New(ctx,supervisor.Config{Supervisor:sv,SubAgents:[]adk.Agent{searchAgent,mathAgent},})}运行时发生的事用户Find US and NY GDP in 2024. What % of US GDP was NY? 1. Supervisor 收到问题 2. Supervisor 决定先搜索 → 调用 research_agent 工具传入问题 3. research_agent 内部搜索工具 → 返回 US $29.18T, NY $2.297T 4. Supervisor 收到搜索结果 5. Supervisor 决定再计算 → 调用 math_agent 工具传入数字 6. math_agent 内部divide(2.297, 29.18) → 0.0787 7. Supervisor 汇总结果调用 exit 工具结束主控 Agent 从未自己搜索或计算只做任务分配 汇总。每个子 Agent 只看自己的 request不知道其他 Agent 的存在。七、对话历史怎么处理一个实际问题当子 AgentAgentTool 模式完成任务后父 Agent 的对话历史里只有Tool 调用请求{tool: research_agent, input: {request: ...}}Tool 返回结果{result: US GDP was $29.18T...}子 Agent 内部的全部过程搜索了哪些网页、中间想了什么不进入父 Agent 的上下文。这是 AgentTool 的设计意图结果传递过程隔离。如果父 Agent 需要子 Agent 的中间事件比如流式展示子 Agent 的思考过程可以开启EmitInternalEventsadk.ToolsConfig{EmitInternalEvents:true,// 子 Agent 的事件实时推送给 Runner 的消费者...}注意这些内部事件只推给外部消费者UI 显示不记录在父 Agent 的 runSession 里——父 Agent 的历史依然只有工具调用和结果不会因此膨胀。小结AgentTool 是唯一被官方推荐的 Multi-Agent 模式原因很简单上下文隔离边界清晰。父 Agent 通过 ReAct 工具调用驱动子 Agent子 Agent 只看到自己的输入不背负无关历史。supervisor.New是在 AgentTool 之上的预制封装适合主控 多专家的常见结构。SetSubAgents和DeterministicTransfer虽然存在且功能可用但源码里的 NOT RECOMMENDED 注释来自团队实测——在绝大多数场景下AgentTool 效果更好不要忽视这个提示。下篇继续。代码来源cloudwego/eino · cloudwego/eino-examples