Part 16 开篇。前面 15 个 Part 讲的是一个 Agent 怎么做安全、怎么加记忆、怎么接知识库------但单个 Agent 的注意力是有限的。一个 Agent 面对"帮我查北京天气、订机票、比价酒店"这种多维度任务,会在上下文里来回跳,工具一多就乱。 这篇回答一个问题:两个 Agent 怎么协作------Host-Worker 模式,主 Agent 分派、子 Agent 执行,注意力该拆给谁。
两个问题串全篇:
- Eino 的 Host MultiAgent 怎么让 host LLM 把 specialist 当 tool 调?(分派机制)
- DeepFlux 的 subagent 步怎么把子 Agent 嵌入 workflow?(嵌入式调用)
(一)Eino Host MultiAgent:把 Agent 当 Tool 调
eino/flow/agent/multiagent/host/compose.go 核心只有 335 行。一句话概括:host LLM 把每个 specialist 看成一个 tool,tool 名就是 specialist 名,tool 描述就是 specialist 的职责声明。
先看怎么注册(compose.go:84-105):
go
agentTools := make([]*schema.ToolInfo, 0, len(config.Specialists))
for i := range config.Specialists {
specialist := config.Specialists[i]
agentTools = append(agentTools, &schema.ToolInfo{
Name: specialist.Name, // ← tool 名就是 specialist 名
Desc: specialist.IntendedUse, // ← tool 描述就是 specialist 的用途
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"reason": {Type: schema.String, Desc: "the reason to call this tool"},
}),
})
}
Name 和 IntendedUse 来自 AgentMeta(types.go:130-134):
go
type AgentMeta struct {
Name string // 在 multi-agent 系统内唯一
IntendedUse string // 用途声明,host LLM 据此决定调哪个
}
这个设计很干净:IntendedUse 就是 specialist 的"简历"------host LLM 不需要知道 specialist 内部怎么实现,它只需要知道"这个 agent 能干什么"。和普通 tool 的区别是:普通 tool 是确定性函数(查天气 API),specialist 是完整的 Agent(有自己的 LLM + 工具链 + 推理循环)。
整个 Graph 的拓扑(compose.go:49-151,用节点名标注):
scss
START → Host(ChatModel) → Branch(有 tool_call?)
├─ 否 → END(直接回答)
└─ 是 → converter → MultiBranch(哪个 specialist?)
├─ weather → collector → AfterBranch
├─ flight → collector → AfterBranch
│ 单意图 → single_answer → END
│ 多意图 → map2list → Summarizer → END
└─ hotel → collector → AfterBranch
关键节点:
- Host (
addHostAgent, :187-203):就是带 tool list 的 ChatModel,system prompt 默认"decide which tool is best for the task and call only the best tool." - directAnswerBranch(:205-220):检查 host 输出有没有 tool call,没有就直接 END------host 自己回答了
- multiSpecialistsBranch (:222-244):读 tool call 的 function name,匹配到对应的 specialist 节点;如果多个 tool call 同时出现,标记
isMultipleIntents=true - specialist 节点 (
addSpecialistAgent, :154-185):每个 specialist 可以是一个 ChatModel,也可以是任意 Invokable/Streamable(如react.Agent) - multiIntentSummarizeNode(:286-335):多意图时把各 specialist 结果汇总。有 Summarizer 配置就用 LLM 汇总,否则纯拼接
demo(/tmp/e91demo,纯标准库复刻核心路由逻辑)用三组 specialist 验证分派:
csharp
====== 场景 A:Host MultiAgent · 注册 tool 清单 ======
tool: weather desc: 查询天气信息,包括温度、湿度、降水概率
tool: flight desc: 查询航班信息,包括航班号、起降时间、延误情况
tool: hotel desc: 查询酒店信息,包括价格、房型、评分
====== 场景 B:单意图路由(只调天气)======
host 决策: 调 weather
specialist 结果: 北京今天晴,22°C,湿度 45%
单意图=true 多意图=false 直接回答=false
====== 场景 C:多意图路由(同时调天气和航班)======
host 决策: 调了 2 个 specialist
[weather]: 北京今天晴,22°C,湿度 45%
[flight]: CA1234 北京→上海 08:00-10:30 准点
汇总结果: [weather]: 北京今天晴,22°C,湿度 45%
[flight]: CA1234 北京→上海 08:00-10:30 准点
单意图=false 多意图=true 直接回答=false
====== 场景 D:无匹配 → host 直接回答 ======
host 决策: 无匹配 specialist
host 直接回答: host direct: 我无法处理这个请求
单意图=false 多意图=false 直接回答=true
三种路径:single intent(直接返回)/ multi intent(汇总)/ direct answer(host 自己回答)。实际 Eino 中 host LLM 的 tool call 决策比 demo 的关键词匹配复杂得多,但骨架上就是这三个分支。
HandOff 回调:观测"谁交给了谁"
callback.go 定义了 MultiAgentCallback.OnHandOff,每次 host 把任务交给 specialist 时触发:
go
type HandOffInfo struct {
ToAgentName string // 交给了哪个 specialist
Argument string // 带着什么理由
}
这给观测层提供了一个干净的切面:不用翻 tool call 日志,直接看 handoff 事件就知道"host 把什么任务交给了谁"。
(二)DeepFlux Subagent:把 Agent 嵌入 Workflow
Eino Host MultiAgent 是"Agent 之间的对话"------host 和 specialist 都是独立 Agent,通过 tool call 通信。DeepFlux 走的是另一条路:subagent 是 workflow 的一个步。
orchestration/domain/model/workflow.go:26-35 定义了六种步类型:
go
type StepKind string
const (
StepKindTask StepKind = "task" // 直连工具
StepKindApproval StepKind = "approval" // HITL 审批
StepKindSubagent StepKind = "subagent" // 子 Agent
StepKindSleep StepKind = "sleep" // 定时等待
StepKindMap StepKind = "map" // 状态映射
StepKindForeach StepKind = "foreach" // 迭代
)
subagent 步的核心是 subagentLambda(einoexec/subagent.go:64-161),流程四步:
- 读图状态:workflow 的图共享状态 → JSON------这是子 agent 的上下文
- Agent First 指令 :
step.Input有值 → 作为 agent 指令,状态 JSON 附后;为空 → 回落纯状态 JSON(向后兼容旧 WorkflowDef) - 经 AgentInvoker 闭包调用 :装配根注入的闭包,同步调用 agent BC 的
RunSubAgentHandler.RunSync - 结果写回图状态 :子 agent 输出 →
state.Data[step.Ref]
demo 场景 E 验证了这个流程:
ini
====== 场景 E:DeepFlux Subagent 步 · 图状态传递 ======
step.Input: 根据用户查询,返回天气信息
step.Ref: weather_agent
子 agent 输出: [weather_agent 执行结果] 北京晴 22°C (收到上下文 142 字节)
图状态[weather_agent]: [weather_agent 执行结果] 北京晴 22°C (收...
====== 场景 F:Agent First · step.Input 为空时回落纯状态 JSON ======
step.Input 为空,回落纯状态 JSON
子 agent 输出: [flight_agent 执行结果] 北京晴 22°C (收到上下文 178 字节)
场景 E 和 F 的区别:E 有 step.Input,子 agent 收到的是 "根据用户查询,返回天气信息\n\n上游状态\n{...}";F 没有 step.Input,子 agent 收到的是纯状态 JSON。输入大小差异(142 vs 178 字节)反映了这个差异。
HITL 审批门:子 Agent 也能被叫停
subagent 步不是简单的"调完就完"。子 agent 内的 pre-tool hook 可以请求中断(subagent.go:129-144),触发 ErrSubagentInterrupt → Eino 中断 → Run→SUSPENDED。resume 时:
- 批准 → per-tool 定向放行(V5):只放行被批准的那次工具调用,已执行的不重跑
- 拒绝 → 写
denied标签供下游 Branch 分流,run 不杀
这个审批门和 approval 步用的是同一套 suspend/resume 机制,但语义不同:approval 步是"整个步等人批",subagent 审批门是"子 agent 的某个工具调用等人批"。
(三)两种模式,一张表
| 维度 | Eino Host MultiAgent | DeepFlux Subagent |
|---|---|---|
| 通信方式 | host LLM 产出 tool call → specialist 节点 | 图状态 JSON → AgentInvoker 闭包 |
| specialist 是什么 | 图的节点(ChatModel 或 Agent) | 外部 agent config(独立 session) |
| host 的角色 | 也是 LLM,自己做决策 | workflow 引擎(编译器 + 图执行) |
| 上下文传递 | 消息列表(messages) | 图状态(mapstringstring)→ JSON |
| 多 specialist | 并行调用 + summarizer 汇总 | 顺序步(DAG 编排) |
| 中断/审批 | 无内置 HITL | 子 agent 内 pre-tool hook 可中断 |
| 适用场景 | 对话式多 Agent 协作 | 工作流式多 Agent 编排 |
Host MultiAgent 适合"对话式"场景:用户一句话,host 判断该找谁,找完回来。host 和 specialist 是平等的 Agent,只是职责不同。
DeepFlux subagent 适合"工作流式"场景:多个步骤按 DAG 执行,subagent 只是其中一步,前面可能有 task 步做数据预处理,后面可能有 approval 步等人审批。
(四)Eino ADK 的另外两种多 Agent 模式
除了 Host MultiAgent,Eino ADK 还提供了两种多 Agent 模式,但文档都标注了 NOT RECOMMENDED:
Supervisor(adk/prebuilt/supervisor/supervisor.go)
建立在 agent transfer 之上,supervisor 和 sub-agents 共享完整上下文。sub-agents 只能和 supervisor 通信(不能互相直接通信)。源码注释直言:
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.
AgentTool(adk/agent_tool.go)
把任意 Agent 包装成一个 tool(NewAgentTool),让另一个 Agent 可以直接调它。这是 ADK 推荐的替代方案:不用 transfer 共享上下文,而是像普通 tool 一样调用------子 Agent 有独立的 session 和 checkpoint。
AgentTool 的设计和 Host MultiAgent 的 specialist 本质上是一个思路:Agent 就是 Tool。区别在于 AgentTool 更底层(可以作为任意 Agent 的 tool),Host MultiAgent 是更高层的封装(自带 Graph 编排)。
小结
| 问题 | 答案 | 关键源码 |
|---|---|---|
| Host 怎么分派 | specialist 注册为 tool(Name+IntendedUse→ToolInfo),host LLM 产出 tool call 匹配 | host/compose.go:84-105 |
| specialist 怎么执行 | 作为 Graph 节点,前置 handler 把消息注入 state,结果走 collector 汇聚 | host/compose.go:154-185 |
| 单意图 vs 多意图 | 单意图直接返回,多意图标记后走 summarizer 汇总 | host/compose.go:222-335 |
| DeepFlux subagent 怎么嵌入 | workflow 步类型,图状态 JSON→AgentInvoker 闭包→结果写回 | einoexec/subagent.go:64-161 |
| 上下文怎么传 | Host MultiAgent 用消息列表,DeepFlux 用图状态 JSON | subagent.go:104-117 |
| Agent First 是什么 | step.Input 有值→作 agent 指令+状态附后;空→回落纯状态 JSON | subagent.go:112-117 |
| 什么时候用哪个 | 对话式→Host MultiAgent;工作流式→DeepFlux subagent;通用→AgentTool | 三种模式对比 |
几条设计判断:
- Agent 就是 Tool。 Host MultiAgent 和 AgentTool 都在做同一件事:把 Agent 包装成可调用的工具。区别在于 Host MultiAgent 自带 Graph 编排,AgentTool 是裸 tool 留给调用方组装。
- IntendedUse 是 specialist 的"简历"。 host LLM 不需要知道 specialist 内部有什么工具、什么 prompt,它只需要知道"这个 agent 能干什么"。这和人类团队分工一样------你不需要知道同事怎么做,只需要知道该把任务交给谁。
- 上下文传递方式决定耦合度。 消息列表(Host MultiAgent)是松耦合------host 和 specialist 各自维护自己的上下文。图状态 JSON(DeepFlux)是紧耦合------subagent 能拿到 workflow 的完整状态,但也意味着状态结构变化会影响所有步。
- ADK 文档的诚实标注值得注意。 Supervisor 和 DeterministicTransfer 都标注了 NOT RECOMMENDED,并给出了替代方案(AgentTool/DeepAgent)。这不是"功能不好用",而是"经验证明这个方向不如另一个方向"------工程文档里很少见到这种诚实。
下一篇(E92)拆 MultiAgent Host 源码 + ADK prebuilt 的 supervisor/planexecute/deep 三种预制模式------从"怎么用"到"里面怎么实现的"。E93 讲 Agent 间的 Transfer 交接------用户在不同 Agent 间无缝切换。