从零推导 Agent Team (多智能体协同)
1. 寻找"第一性原理":最朴素的硬编码协同
team 包解决的最核心、最原初的业务问题是什么?
答案是:如何让多个 LLM Agent 协同工作,解决单体 Agent 无法处理的复杂长链路任务(例如:一个 Agent 负责规划,另一个负责写代码,再一个负责测试)。
如果不使用任何框架,我们用最朴素、最硬编码的方式(大 for 循环和 if/else)实现两个 Agent 的协同对话,代码长这样:
go
// 第一性原理:用 for 循环和硬编码实现两个 Agent 的对话
func RunSimpleTeam(userInput string) {
leaderHistory := []Message{{Role: "user", Content: userInput}}
coderHistory := []Message{}
for {
// Leader 思考
leaderResp := llm.Generate(leaderHistory)
leaderHistory = append(leaderHistory, leaderResp)
// 解析 Leader 的意图,如果是分配给 Coder
if strings.Contains(leaderResp.Text, "@Coder") {
task := extractTask(leaderResp.Text)
coderHistory = append(coderHistory, Message{Role: "user", Content: task})
// Coder 思考并执行
coderResp := llm.Generate(coderHistory)
coderHistory = append(coderHistory, coderResp)
// Coder 把结果返回给 Leader
leaderHistory = append(leaderHistory, Message{Role: "user", Content: "Coder 回复: " + coderResp.Text})
} else if strings.Contains(leaderResp.Text, "Task Done") {
break
}
}
}
2. 第一次演进:应对"同步阻塞与状态纠缠"的危机
痛点与危机 :
上述最基本的结构在应对真实业务场景时,会遇到第一个致命痛点 :高度耦合的同步阻塞。
随着 Agent 数量增多和交互变复杂,硬编码的调用链路(Leader 调 Coder,Coder 回复 Leader)变得极其僵化。如果多个 Agent 要并行工作?如果系统崩溃需要恢复状态?同步的函数调用会导致整个系统阻塞,且状态分散在局部变量中,无法持久化。
引入第一次抽象:基于信箱(Mailbox)的异步事件驱动
我们需要解耦 Agent 之间的直接函数调用,引入 "信箱(Mailbox)" 和 "消息队列" 的概念。每个 Agent 都有自己的独立信箱,通过向信箱读写消息来驱动运行。
go
// 第一次演进:引入 Mailbox 和事件驱动
type Mailbox struct {
Messages []Message
}
var mailboxes = map[string]*Mailbox{
"Leader": &Mailbox{},
"Coder": &Mailbox{},
}
// 独立的 Agent 运行循环(可放入独立 Goroutine 中并行运行)
func RunAgentLoop(agentName string, llm Agent) {
history := []Message{}
for {
// 1. 从自己的信箱读取新消息
newMsgs := mailboxes[agentName].ReadNew()
if len(newMsgs) == 0 {
time.Sleep(1 * time.Second)
continue
}
history = append(history, newMsgs...)
// 2. 思考并产生输出
resp := llm.Generate(history)
history = append(history, resp)
// 3. 投递消息到目标 Agent 信箱
if target, content, ok := parseRoute(resp.Text); ok {
mailboxes[target].Write(Message{From: agentName, Content: content})
}
}
}
3. 第二次演进:应对"框架集成与动态生命周期"的挑战
新的棘手问题 :
随着异步通信体系的建立,工程化上又遇到了新的问题:
- 生命周期管理:Agent 怎么动态创建和销毁?
- 框架透明性:如何在不破坏现有单体 Agent 框架的前提下,把多智能体通信机制无缝塞进去?
引入更高级抽象:基于中间件(Middleware)的工具注入与路由网关(Router)
我们需要将团队协调逻辑封装为中间件,在 Agent 运行时动态注入团队协作工具(如 send_message 和 agent ),并引入一个统一的路由网关来分发外部输入和内部消息。
这种注入的核心效果是 "工具化伪装" :
- 实现"框架透明性"(对现有代码零侵入) :在单体 Agent 真正开始思考前,中间件作为拦截器,往其工具箱里额外塞入
send_message等特殊工具。底层的单体 Agent 完全不知道自己处于 Team 中,它只以为自己在调用一个普通工具,而实际上触发了中间件底层的 Router 逻辑,将消息投递到了另一个 Agent 的信箱。 - 实现动态的"生命周期管理"(按需调度) :中间件专门给 Leader 注入了召唤工具(如
agent)。Leader 大模型推理认为需要帮手时,调用该工具并传入角色参数(如role="coder")。该工具底层的 Go 代码会立刻为 Coder 分配新信箱,并启动一个新的后台线程(TurnLoop)。小兵的创建和销毁不再由硬编码决定,而是由 Leader 的推理按需调度。
go
// 第二次演进:引入 Router 和 Middleware,实现多 Agent 协同体系
// 1. 统一的消息路由器
type SourceRouter struct {
sources map[string]*MailboxSource
}
func (r *SourceRouter) Route(msg TurnInput) {
target := msg.TargetAgent
if target == "" { target = "team-lead" }
r.sources[target].Push(msg)
}
// 2. Team Middleware(劫持 Agent 的工具列表)
func TeamMiddleware(agentName string, isLeader bool) Middleware {
return func(ctx Context, next Agent) Agent {
// 在 Agent 执行前,动态注入团队工具
tools := []Tool{SendMessageTool(agentName)}
if isLeader {
tools = append(tools, TeamCreateTool(), AgentTool()) // Leader 专属
}
ctx.InjectTools(tools)
return next(ctx)
}
}
// 3. 统一的执行引擎
func NewTeamRunner() {
router := NewSourceRouter()
leader := BuildAgent(WithMiddleware(TeamMiddleware("team-lead", true)))
StartTurnLoop("team-lead", leader, router.GetSource("team-lead"))
}
4. 映射到真实源码 (Mapping to Reality)
回到真实的 team 源码,它极其优雅地落地了上述推导:
-
统一的消息路由器:
sourceRouter源码中定义了带有
TargetAgent的输入结构体 types.go#L69-L76 。在创建 Runner 时, team_runner.go#L100-L107 初始化了sourceRouter,它充当系统的主干,拦截所有上游输入并分发给对应的 Agent 信箱。 -
欺上瞒下的切面拦截器:
teamMiddleware在 team.go#L94-L121 中,
BeforeAgent钩子动态向 Agent 注入了团队协作工具。如果是 Leader ,则注入TeamCreateTool、TeamDeleteTool和召唤 Teammate 的AgentTool。所有人都会被注入SendMessageTool,获得跨 Agent 通信能力。 -
独立的事件驱动循环:
Runner和TurnLoop在 team_runner.go#L132-L142 中,为 Leader 包装了一个
adk.TurnLoop。当 Leader 通过工具召唤一个新的 Teammate 时,在 team_runner.go#L174-L203 中,会动态为这个 Teammate 创建独立的TurnLoop,并分配一个由文件系统支撑的信箱。 -
工程上的"脏活累活": 任务共享与并发控制
在 team_runner.go#L218-L257 中,系统强制注入了
newTeamPlantaskMiddleware接管底层任务管理,并通过TaskLock和InboxLocks实现了跨 Goroutine 的文件读写锁,防止并发读写导致信箱数据损坏。
5. 核心架构拆解:Actor 模型消息驱动
为了更清晰地说明底层的运作逻辑,我们对其核心通信算法进行拆解:
- 过程 (How) :
- 系统初始化统一的 Router 和多个独立 Agent 的 Mailbox。
- Leader 思考后调用
send_message工具,生成一条 Target 为 Teammate 的消息结构体。 - Router 拦截该消息,路由投递至 Teammate 的专属信箱文件。
- Teammate 的轮询器检测到文件变化,读取新消息并拼接至自身对话历史中,触发下一轮推理。
- 原理 (Why) :
解耦多 Agent 之间的直接同步调用。让 Agent 可以并行工作,且生命周期独立(随时销毁重建而不影响主流程),实现高可用与持久化。 - 复杂度 :
- 时间复杂度:单次消息传递开销为
O(M) + O(I/O)(其中 M 为消息体大小,I/O 为文件读写开销),主导项为文件 I/O 与轮询等待时间。 - 空间复杂度:
O(N * H)(N 为 Agent 数量,H 为各自的独立历史记录长度)。
- 时间复杂度:单次消息传递开销为
- 具象化 :
就像一个公司里的各个部门(Agent),大家不直接打电话(同步调用),而是通过写邮件(发消息至信箱)。Leader 写好需求发到 Coder 的邮箱,Coder 每天定时刷邮箱(轮询),看到需求后开始写代码,写完再回邮件给 Leader。
6. 深度对比:Team vs. Supervisor
在 Eino 的架构中, Team 和 Supervisor 都在解决 "多智能体协同" 的问题,但它们的底层哲学、通信机制和适用场景有着本质的差异。
如果用一句话总结: Supervisor 是 "接力棒模式(控制权流转)" ,而 Team 是 "信箱模式(异步消息驱动)" 。
6.1 通信哲学的差异:Transfer vs. Messaging
-
Supervisor (基于 Transfer) :
- 核心是 控制权的转移 。在 Supervisor 模式下,整个网络同一时刻只有一个 Agent 在运行(持有 "接力棒" )。
- 过程 :主管运行 → 决定派活给小兵 A → 控制权 交接 给小兵 A → 小兵 A 运行 → 任务完成后控制权 交接回 主管。
- 本质 :它是同步的、阻塞的拓扑约束。
-
Team (基于 Messaging) :
- 核心是 信息的交换 。每个 Agent 都是一个独立的 Actor,拥有自己的信箱 (Mailbox) 。
- 过程 :Leader 运行 → 调用
send_message工具发邮件给 Teammate → Leader 继续运行或等待 → Teammate 异步从信箱读到邮件 → 开始自己的运行循环。 - 本质 :它是异步的、并行的分布式系统模型。
6.2 执行模型的差异:单循环 vs. 多循环
-
Supervisor :
- 运行在 同一个
Run调用链 中。虽然有多个 Agent,但在底层看来,它们是通过Transfer机制串联起来的一条长链路。 - 生命周期 :小兵是被 "唤起" 的,干完活就退出。
- 运行在 同一个
-
Team :
- 运行在 多个独立的
TurnLoop中。Leader 和每个 Teammate 都有自己独立的执行死循环 (Event Loop) 。 - 生命周期 :Teammate 是被 "启动 (Spawn) " 的后台进程。它们可以长时间驻留,监听信箱,处理多轮对话,甚至在 Leader 没说话时自己在那 "思考" 。
- 运行在 多个独立的
6.3 状态与记忆的差异:全局上下文 vs. 独立记忆
-
Supervisor :
- 由于是控制权流转,通常共享或部分继承对话历史。小兵执行完的结果会作为
ToolResult直接塞回主管的上下文里。 - 透明度 :主管能感知到小兵执行的每一步细节(因为那是它的
ToolCall)。
- 由于是控制权流转,通常共享或部分继承对话历史。小兵执行完的结果会作为
-
Team :
- 每个 Agent 拥有 完全隔离的私有记忆 。它们只知道自己信箱里收到了什么,以及自己发出了什么。
- 透明度 :Leader 只能看到 Teammate 回复给它的最终文本,看不到 Teammate 内部的思考过程或它调用的其他工具,除非 Teammate 在回复中显式包含这些信息。
6.4 适用场景对比
- 协作复杂度 :
- Supervisor :中低。任务定义清晰,边界明确。
- Team :高。长链路、需要并行或背景工作的任务。
- 实时性 :
- Supervisor :高(同步响应)。
- Team :低(异步轮询信箱有延迟)。
- 可靠性 :
- Supervisor :依赖单体控制流,易于追踪。
- Team :极强。支持断点续传,状态完全持久化到文件。
- 动作类比 :
- Supervisor : 部门主管派活 。主管叫你去办公室,把活交代给你,你做完立刻回来汇报。
- Team : 跨部门邮件协作 。Leader 发邮件派活,你去忙你的,做完回邮件。
6.5 源码层面的印证
- Supervisor :主要通过 supervisor.go 中的
DeterministicTransferTo包装器来强制小兵回传控制权。它复用的是 flow.go 的 Transfer 逻辑。 - Team :通过 team_runner.go 启动多个
TurnLoop,并使用 mailbox_file.go 实现基于文件系统的异步消息队列。
总结建议 :
- 如果你需要一个 确定性强、逻辑紧凑 的分发器,选 Supervisor 。
- 如果你需要构建一个 拟人化、可扩展、支持复杂长异步任务 的 Agent 组织,选 Team 。
7. 批判性总结 (Critical Trade-offs)
优势:
- 完美适配单体抽象 :通过 Middleware 和 Source/Router 的设计,多 Agent 的复杂逻辑被完美封装。底层的
ChatModelAgent和TurnLoop甚至不知道自己处于一个 Team 中,实现了对原有单体框架的零侵入。 - 强大的容错与可恢复性:基于文件系统( JSON Array )的信箱机制使得团队状态完全持久化。即使进程崩溃,重启后 Agent 依然能从信箱读取历史记录恢复工作。
代价与局限:
- 基于文件信箱的延迟损耗 :
MailboxPollInterval默认轮询文件,对于追求低延迟(如语音交互)的多智能体场景,基于文件 I/O 的轮询通信会带来显著的延迟叠加。 - 上下文隔离带来的信息差 :由于每个 Agent 只看自己的信箱历史,Leader 必须通过
send_message将必要的上下文显式复制给 Teammate。这不仅增加了大模型的推理负担(需判断传什么上下文),也极易导致关键信息在传递中丢失。
更优解的探讨 :
目前的 Team 设计是经典的 "Actor Model (参与者模式)" ,每个 Agent 相当于一个 Actor,通过异步消息通信。
- 针对低延迟场景,可以抽象一层基于 Memory/Redis 的 PubSub 机制来替代文件轮询。
- 针对上下文隔离,业界(如 LangGraph )更推崇
"State Graph (状态图模型)":维护一个全局的 State 对象,各个 Agent 作为图上的节点对全局 State 进行读写。这消除了繁琐的send_message互传,大模型只需关注如何更新全局状态,心智负担更低且不易丢失上下文。