两个 Agent 怎么协作:Host-Worker 模式(第91篇-E77)

Part 16 开篇。前面 15 个 Part 讲的是一个 Agent 怎么做安全、怎么加记忆、怎么接知识库------但单个 Agent 的注意力是有限的。一个 Agent 面对"帮我查北京天气、订机票、比价酒店"这种多维度任务,会在上下文里来回跳,工具一多就乱。 这篇回答一个问题:两个 Agent 怎么协作------Host-Worker 模式,主 Agent 分派、子 Agent 执行,注意力该拆给谁。

两个问题串全篇:

  1. Eino 的 Host MultiAgent 怎么让 host LLM 把 specialist 当 tool 调?(分派机制)
  2. 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"},
        }),
    })
}

NameIntendedUse 来自 AgentMetatypes.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

关键节点:

  • HostaddHostAgent, :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 步的核心是 subagentLambdaeinoexec/subagent.go:64-161),流程四步:

  1. 读图状态:workflow 的图共享状态 → JSON------这是子 agent 的上下文
  2. Agent First 指令step.Input 有值 → 作为 agent 指令,状态 JSON 附后;为空 → 回落纯状态 JSON(向后兼容旧 WorkflowDef)
  3. 经 AgentInvoker 闭包调用 :装配根注入的闭包,同步调用 agent BC 的 RunSubAgentHandler.RunSync
  4. 结果写回图状态 :子 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 间无缝切换。

相关推荐
怕浪猫8 小时前
2026年为什么我推荐你学DeepSeek Harness?AI Agent开发入门指南
openai·agent·ai编程
MicrosoftReactor9 小时前
技术速递|Canvas 如何让 Agentic Workflow 更可见、可控、更高效
ai·agent·canvas
怕浪猫10 小时前
从 pre-execute 到 post-execute:AI Agent 调用工具时背后发生了什么
aigc·openai·ai编程
厚国兄10 小时前
大模型AI Agent记忆系统——Graphiti_源码架构与实现教程
架构·agent
canonical_entropy10 小时前
可逆不是逆向运行:DeepSeek Harness 架构的数学本质
人工智能·架构·agent
小虎AI生活11 小时前
DeepSeek Harness 插件开发实战:从最小插件到自动化日报
ai编程
冬奇Lab12 小时前
Code Agent 解剖(05):模型怎么知道有哪些工具可以用?Function Calling 如何实现?
人工智能·开源·agent
咖啡星人k13 小时前
2025 AI编程进入“自动驾驶“时代:我用MonkeyCode把Agent、MCP和AI原生工作流跑通了
人工智能·自动驾驶·prompt·aigc·ai编程·ai-native
粥里有勺糖13 小时前
分享一下最近做的AI记账 App | 欢迎体验
app·agent·ai编程