系列「企业级 AI Agent 实现拆解」E15 篇。上一篇 E14 讲了 ChatTemplate------怎么给 AI 发消息。但有些任务一个 Agent 搞不定:需要调研的同时做计算,需要写代码的同时做代码审查,需要先规划再一步步执行。这篇拆 Multi-Agent:多个 AI 怎么分工、怎么通信、怎么在失败时重新规划。
读完这篇你会知道
- 为什么要用多个 Agent 而不是加更多工具
- Host-Worker:Host 派活,Specialist 干活,Summarizer 汇总
- Plan-Execute-Replan:先规划,逐步执行,失败就重新规划
- Supervisor:为什么 eino 官方说"不推荐"
- Agent 之间靠什么通信:ToolCall 数据结构
- 任务分配的三种策略
一、为什么要多个 Agent
单个 Agent 加工具够用吗?理论上够。但实践中有两个问题:
问题 1:专业度。让一个 Agent 既懂检索、又懂计算、又懂代码审查,prompt 会越写越长,越长越混乱,效果越来越差。把职责拆开,每个 Agent 只做一件事,效果好得多。
问题 2:并行。单 Agent 串行执行,5 个任务得一个个来。多 Agent 可以并发跑,总时间缩短。
eino 提供了两种官方推荐的 Multi-Agent 模式,加一种不推荐但值得了解的模式。
二、Host-Worker:派活模式
源码:eino/flow/agent/multiagent/host/
整体结构
csharp
用户问题
↓
[Host Agent] (持有所有 Specialist 的"工具描述")
↓ 通过 ToolCall 选择一个或多个 Specialist
[Specialist A] [Specialist B] (并发执行)
↓
[Summarizer] (如果选了多个 Specialist,汇总结果)
↓
最终回复
Host 不是普通 ChatModel,它是 ToolCallingChatModel------能输出结构化的 ToolCall,告诉系统"去找哪个 Specialist、传什么参数"。每个 Specialist 的名字和描述被包装成一个"工具"注册到 Host 上,Host 看完用户问题后决定调哪个。
核心配置
go
// 源码:eino/flow/agent/multiagent/host/types.go
// Host:决策者
type Host struct {
ToolCallingModel model.ToolCallingChatModel
SystemPrompt string // "你是一个任务分发助手,根据用户需求选择合适的专家"
}
// Specialist:执行者
type Specialist struct {
AgentMeta // Name + IntendedUse(这两个字段决定 Host 怎么描述它)
ChatModel model.BaseChatModel
SystemPrompt string // "你是一个数学计算专家,只做数学题"
Invokable compose.Invoke[[]*schema.Message, *schema.Message, agent.AgentOption]
}
// Summarizer:汇总者(可选)
type Summarizer struct {
ChatModel model.BaseChatModel
SystemPrompt string // "综合多位专家的回答,给出统一答复"
}
用起来
go
// 参考:eino-examples/adk/multiagent/integration-project-manager
hostCfg := &host.Config{
Host: host.Host{
ToolCallingModel: myLLM,
SystemPrompt: "你是项目经理,根据任务类型分配给合适的专家。",
},
Specialists: []host.Specialist{
{
AgentMeta: host.AgentMeta{Name: "researcher", IntendedUse: "负责资料检索和调研"},
ChatModel: researchLLM,
SystemPrompt: "你是研究员,擅长搜索和分析信息。",
Invokable: researchAgent.Invoke,
},
{
AgentMeta: host.AgentMeta{Name: "coder", IntendedUse: "负责编写和调试代码"},
ChatModel: codingLLM,
SystemPrompt: "你是工程师,只负责写代码,不做其他事。",
Invokable: coderAgent.Invoke,
},
},
Summarizer: &host.Summarizer{
ChatModel: summaryLLM,
SystemPrompt: "综合研究员和工程师的输出,给出完整报告。",
},
}
multiAgent, _ := host.NewMultiAgent(ctx, hostCfg)
result, _ := multiAgent.Generate(ctx, userMessages)
单个 vs 多个 Specialist
Host 选了一个 Specialist → 直接返回该 Specialist 的结果(不过 Summarizer)。
Host 选了多个 Specialist → Specialist 并发执行,结果送进 Summarizer 汇总。
HandOff 事件监控
Host 每次把任务转给 Specialist,会触发一个 HandOff 事件,可以用 Callback 监听:
go
// 源码:eino/flow/agent/multiagent/host/callback.go
type HandOffInfo struct {
ToAgentName string // 转给哪个 Specialist
Argument string // 传了什么参数
}
handler := host.NewMultiAgentCallbackHandler(func(ctx context.Context, info *host.HandOffInfo) context.Context {
log.Printf("[HandOff] 转给 %s,参数:%s", info.ToAgentName, info.Argument)
return ctx
})
三、Plan-Execute-Replan:规划循环模式
源码:eino/adk/prebuilt/planexecute/plan_execute.go
这个模式适合步骤不确定的复杂任务:不知道要几步,每步成功了才知道下一步怎么走,中途可能需要调整计划。
三个角色
csharp
[Planner] → 制定计划(步骤列表)
[Executor] → 执行第一步(用工具)
[Replanner] → 看结果,决定:继续执行 or 宣布完成
如果需要调整,输出新计划;如果完成,输出最终答案
三者形成循环:Planner → (Executor → Replanner) × N,直到 Replanner 宣布完成。
状态通过 Session 传递
各阶段靠 Session 键值对共享状态,没有显式消息总线:
go
// 源码:eino/adk/prebuilt/planexecute/plan_execute.go
const (
UserInputSessionKey = "UserInput" // 用户原始问题
PlanSessionKey = "Plan" // 当前计划(步骤列表)
ExecutedStepSessionKey = "ExecutedStep" // 本次执行结果
ExecutedStepsSessionKey = "ExecutedSteps" // 所有历史执行结果
)
// Plan 接口
type Plan interface {
FirstStep() string // 取出当前要执行的第一步
json.Marshaler
json.Unmarshaler
}
// 默认实现:有序步骤列表
type defaultPlan struct {
Steps []string `json:"steps"`
}
Planner 写 Plan,Executor 读 Plan.FirstStep() 执行并写 ExecutedStep,Replanner 读所有 ExecutedSteps 判断是否完成。
Planner:用 ToolChoiceForced 强制输出结构化计划
go
// Planner 强制 LLM 调 plan() 工具输出结构化步骤,不允许输出普通文字
var PlanToolInfo = schema.ToolInfo{
Name: "plan",
Desc: "制定按序执行的步骤列表",
ParamsOneOf: NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"steps": {
Type: schema.Array,
ElemInfo: &schema.ParameterInfo{Type: schema.String},
Desc: "按顺序排列的执行步骤",
Required: true,
},
}),
}
Replanner:只能做两个选择
go
// 选择 1:任务未完,输出新计划继续
var PlanTool = ... // 同 Planner 的 PlanToolInfo
// 选择 2:目标达成,输出最终回复退出循环
var RespondToolInfo = schema.ToolInfo{
Name: "respond",
Desc: "当目标已达成时,给出最终回复",
ParamsOneOf: NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"response": {
Type: schema.String,
Desc: "给用户的完整答案",
Required: true,
},
}),
}
完整示例:旅行规划 Agent
go
// 参考:eino-examples/adk/multiagent/plan-execute-replan/
cfg := &planexecute.Config{
Planner: planexecute.NewPlannerAgent(ctx, &planexecute.PlannerConfig{
ChatModelWithFormattedOutput: llm,
}),
Executor: planexecute.NewExecutorAgent(ctx, &planexecute.ExecutorConfig{
Model: llm,
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{searchTool, weatherTool, bookingTool},
},
},
MaxIterations: 20,
}),
Replanner: planexecute.NewReplannerAgent(ctx, &planexecute.ReplannerConfig{
ChatModel: llm,
}),
MaxIterations: 10, // 最多重规划 10 次
}
agent, _ := planexecute.New(ctx, cfg)
执行过程:
vbnet
Planner: ["查询热门景点", "查询天气", "规划路线", "预订酒店"]
Executor: 调 searchTool → "故宫、长城、天坛..." → 写 ExecutedStep
Replanner: 步骤未完 → 更新计划(移除已完成步骤)
Executor: 调 weatherTool → "明天晴,适合户外" → 写 ExecutedStep
Replanner: 步骤未完 → 继续...
...
Replanner: 所有步骤完成 → 调 respond() → 输出行程 → 退出循环
四、Supervisor:为什么官方不推荐
源码:eino/adk/prebuilt/supervisor/supervisor.go
Supervisor 把整段对话上下文传给 Sub-Agent,Sub-Agent 执行后再把全部上下文还给 Supervisor:
css
[Supervisor] → 全部对话 + 任务 → [Sub-Agent A]
[Sub-Agent A] → 全部对话 + 结果 → [Supervisor]
[Supervisor] → 全部对话 + 任务 → [Sub-Agent B]
...
问题:全上下文传递经过实验没有带来更好效果,context 越来越长,每次调用越来越贵。eino 代码注释里明确标注 ⚠️ Not Recommended。
推荐替代方案:
go
// AgentTool:把 Sub-Agent 包装成工具,按需调用,不共享全上下文
agentTool := adk.NewAgentTool(ctx, subAgent)
// Parent Agent 通过 ToolCall 调用 Sub-Agent,和调普通工具一样
parentCfg := &adk.ChatModelAgentConfig{
Model: llm,
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{agentTool, otherTool},
},
},
}
五、Agent 之间靠什么通信
无论哪种模式,Agent 之间传的都是 schema.Message 列表。Host-Worker 靠 ToolCall 路由,Plan-Execute-Replan 靠 Session 键值对。
go
// 源码:eino/schema/message.go
type Message struct {
Role RoleType // assistant / user / system / tool
Content string
// Host → Specialist:派活指令
ToolCalls []ToolCall // 模型输出:调哪个 Specialist,传什么参数
// Specialist → Host:执行结果
ToolCallID string // 对应哪个 ToolCall
ToolName string // 哪个工具/Specialist 产生的结果
}
type ToolCall struct {
ID string
Function struct {
Name string // Specialist 的名字(即 AgentMeta.Name)
Arguments string // JSON 格式的任务描述
}
}
框架用 ToolCall.Function.Name 把结果路由到对应 Specialist,开发者不需要手动写路由逻辑。
六、任务分配三种策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| LLM 路由(Host-Worker) | Host LLM 通过 ToolCall 决定派给谁 | 任务类型多样,边界模糊 |
| 结构化规划(Plan-Execute-Replan) | Planner 先拆步骤,Executor 按步执行 | 步骤未知,需要迭代调整 |
| AgentTool 调用(推荐替代) | Parent 按需 ToolCall 调 Sub-Agent | 子任务边界清晰,无需共享上下文 |
小结
sql
Host-Worker(推荐)
适合:多专家并行,任务类型多样
关键:Host 通过 ToolCall 路由,Specialist 并发执行
Plan-Execute-Replan(推荐)
适合:步骤不确定的复杂任务
关键:Session 传状态,Replanner 决定继续还是结束
Supervisor(不推荐)
问题:全上下文膨胀,实验效果不及预期
替代:AgentTool(简单)/ DeepAgent(复杂)
怎么选:
| 场景 | 模式 |
|---|---|
| 问题可能需要多个专家 | Host-Worker |
| 任务复杂、步骤未知、可能失败重试 | Plan-Execute-Replan |
| 子任务边界清晰、轻量 | AgentTool |
| 多层嵌套、复杂协调 | DeepAgent |
Multi-Agent 不是越复杂越好。能用一个 Agent 加工具解决的,不要上 Multi-Agent。上了,优先选 Host-Worker 或 Plan-Execute-Replan,不要默认选 Supervisor。