系列「企业级 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):
arduino
// NewAgentTool creates a tool that wraps an agent for invocation.
用法:把一个子 Agent 包装成工具,父 Agent 通过普通的工具调用来"雇用"它。这是 ADK 官方唯一推荐的 Multi-Agent 模式。
go
// 创建子 Agent
researchAgent, _ := 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):
arduino
// 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):
go
// eino-examples/adk/multiagent/supervisor/agent.go:192
supervisorAgent, err := supervisor.New(ctx, &supervisor.Config{
Supervisor: sv, // 主控 Agent
SubAgents: []adk.Agent{searchAgent, mathAgent}, // 子 Agent 列表
})
supervisor.New 自动把每个子 Agent 包装成 AgentTool,注入到主控 Agent 的工具列表里。你只需要定义主控 Agent 的 Instruction("你是项目经理,有两个下属......"),其余的工具调用由 ReAct 循环自动处理。
三、方案二(不推荐):LLM 驱动的 Agent 切换
这是另一套机制------让模型自己决定"我要交给哪个 Agent"。
go
// eino-examples/adk/intro/transfer/transfer.go:35
a, 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")------相当于给自己的下一步指路。
ini
用户:"北京今天天气?"
RouterAgent → 判断:这是天气相关 → 调用 transfer_to_agent(agent_name="WeatherAgent")
WeatherAgent → 调用 get_weather(city="Beijing") → 返回结果
这个模式看起来自然,但 Eino 官方在源码里明确标注(utils.go:92-95):
less
// 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)------创建 IsolatedSession:
go
isolatedSession := &runSession{
Values: parentSession.Values, // 共享 session values(键值对)
valuesMtx: parentSession.valuesMtx,
// Events: 不继承(默认为空)
}
子 Agent 有独立的事件历史(不继承父 Agent 的全部消息),但共享 session.Values(通过 AddSessionValue/GetSessionValues 存取的键值对)。这样可以在两个 Agent 间传递少量结构化状态,同时避免把全部对话历史扔过去。
四、方案三(不推荐):确定性移交
有时候移交目标不需要 AI 判断,就是固定的。AgentWithDeterministicTransferTo 用于这个场景:
go
// 执行完 agentA,固定移交给 agentB
wrappedA := adk.AgentWithDeterministicTransferTo(ctx, &adk.DeterministicTransferConfig{
Agent: agentA,
ToAgentNames: []string{"agentB"},
})
执行完 agentA 的全部逻辑后,框架自动追加两条消息(assistant 说"我要移交给 agentB" + tool 确认消息),然后触发 TransferToAgent 动作,Session 流转到 agentB。
白话:这是硬编码的流水线------A 完事了一定交给 B,不经过任何 AI 决策。适合固定流程("报告总结完毕,一定发给审阅 Agent"),不适合根据内容动态路由。
同样标注了 NOT RECOMMENDED,原因相同:全上下文共享。
五、三种方案一张表
| AgentTool(推荐) | SetSubAgents | DeterministicTransfer | |
|---|---|---|---|
| 路由决策者 | 父 Agent 的 LLM(工具调用) | Router Agent 的 LLM | 代码硬编码 |
| 上下文共享 | 隔离(只传 request 字符串) | 全部共享(IsolatedSession 折中) | 全部共享 |
| 子 Agent 动作边界 | Exit/Transfer 不传出 | 全部传出 | 全部传出 |
| 适合场景 | 绝大多数场景 | 特定路由式场景 | 固定流水线 |
| 官方立场 | ✅ 推荐 | ⚠️ 不推荐 | ⚠️ 不推荐 |
六、完整示例:Supervisor 模式(AgentTool 推荐路径)
go
// eino-examples/adk/multiagent/supervisor/agent.go(精简)
func buildSupervisor(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 工具的 Agent
mathAgent, _ := buildMathAgent(ctx) // 有 add/multiply/divide 工具的 Agent
// supervisor.New 内部自动 NewAgentTool 包装每个子 Agent
return supervisor.New(ctx, &supervisor.Config{
Supervisor: sv,
SubAgents: []adk.Agent{searchAgent, mathAgent},
})
}
运行时发生的事:
bash
用户:"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 的存在。
七、对话历史怎么处理:一个实际问题
当子 Agent(AgentTool 模式)完成任务后,父 Agent 的对话历史里只有:
- Tool 调用请求:
{"tool": "research_agent", "input": {"request": "..."}} - Tool 返回结果:
{"result": "US GDP was $29.18T..."}
子 Agent 内部的全部过程(搜索了哪些网页、中间想了什么)不进入父 Agent 的上下文 。这是 AgentTool 的设计意图:结果传递,过程隔离。
如果父 Agent 需要子 Agent 的中间事件(比如流式展示子 Agent 的思考过程),可以开启 EmitInternalEvents:
go
adk.ToolsConfig{
EmitInternalEvents: true, // 子 Agent 的事件实时推送给 Runner 的消费者
...
}
注意:这些内部事件只推给外部消费者(UI 显示),不记录在父 Agent 的 runSession 里------父 Agent 的历史依然只有工具调用和结果,不会因此膨胀。
小结
AgentTool 是唯一被官方推荐的 Multi-Agent 模式,原因很简单:上下文隔离,边界清晰 。父 Agent 通过 ReAct 工具调用驱动子 Agent,子 Agent 只看到自己的输入,不背负无关历史。supervisor.New 是在 AgentTool 之上的预制封装,适合"主控 + 多专家"的常见结构。SetSubAgents 和 DeterministicTransfer 虽然存在且功能可用,但源码里的 NOT RECOMMENDED 注释来自团队实测------在绝大多数场景下,AgentTool 效果更好,不要忽视这个提示。
下篇继续。