核心摘要
在 Eino v0.9.21 中,多 Agent 协作最稳妥的默认方案不是无限制的 Swarm。责任归属可以由策略确定时使用确定性路由;必须让模型动态委派时,使用 ChatModelAgent,并通过 adk.NewAgentTool 暴露专家 Agent。Eino 仍保留基于 Transfer 的 Supervisor、Sequential、Parallel 和 Loop API,但源码已将它们标记为不推荐,原因是全上下文移交尚未证明有经验上的优势。每个 Agent 边界都应成为有类型契约、有预算、可观测的信任边界。
目录
Eino v0.9.21 的关键变化
Eino 当前 ADK 做出了一个重要设计区分:把专家 Agent 当作 Tool 调用是推荐的组合路径,全上下文 Agent Transfer 则是只适合特定需求的兼容路径。
v0.9.21 提供以下核心能力:
adk.NewChatModelAgent:构建模型驱动的 Agent 循环adk.NewAgentTool:把adk.Agent包装为 Eino Tooladk.NewRunner:运行 Agent 并消费事件流adk.WithCallbacks:注入单次执行范围的可观测回调- Checkpoint 与 Resume:支持可中断、可恢复执行
- Transfer、Supervisor、Sequential、Parallel 与 Loop:API 仍存在,但源码包含明确的“不推荐”说明
因此,旧式 Router、Supervisor、Swarm 教程不能再被理解为三个同等推荐的默认选项。它们描述的是协作形态,而不是统一最佳实践。
| 模式 | 控制方 | 传递上下文 | 适合场景 | 主要风险 |
|---|---|---|---|---|
| 确定性 Router | 应用代码 | 显式请求契约 | 领域稳定、可按策略路由 | 规则陈旧或覆盖不全 |
| Orchestrator + AgentTool | 父 Agent | 显式工具参数,必要时带历史 | 需要动态但受控的专家委派 | 嵌套成本与工具选择不透明 |
| 全上下文 Handoff | 当前 Agent | 会话历史 | 确实需要动态切换会话所有者 | 上下文、权限与故障耦合 |
可先通过 AI Agent 术语理解单 Agent 循环,再通过多智能体术语建立协作模型。
三种协作模式
确定性路由
当可信字段、权限、语言、产品域或经过验证的分类器可以决定处理者时,应优先使用确定性路由。它成本低、可测试,也最容易审计。
不要让 LLM 处理精确规则已经能够决定的问题。只有标签存在语义歧义,并且分类错误已在代表性数据上被量化时,模型路由才有价值。
Orchestrator 配合 AgentTool
adk.NewAgentTool 为父 Agent 提供标准 Tool 边界。子 Agent 必须提供非空的 Name 和 Description;默认输入是包含 request 字符串的 JSON 对象。可以用 adk.WithAgentInputSchema 替换输入契约,也可以在确有必要时用 adk.WithFullChatHistoryAsInput 传递历史。
这个边界的执行语义是:
- 父 Agent 根据工具元数据选择专家。
- 子 Agent 默认接收受限输入,而不是继承全部消息。
- 子 Agent 通过 Tool Result 返回文本。
- 子 Agent 的
Exit、Transfer 和 BreakLoop 动作被限制在 Tool 边界内;Interrupt 可向上传播以支持 Checkpoint/Resume。
该模式更容易落实最小权限:每个专家只获得角色所需的工具、凭证、数据范围和执行预算。
全上下文移交
Handoff 会改变当前对话的所有者。它适合专家必须直接延续长对话的场景,但不应被当成通用协作原语。
完整历史会产生三类耦合风险:
- 权限扩大:接收方可能看到角色范围之外的数据或指令。
- 上下文稀释:无关历史与当前任务真正需要的事实争夺窗口。
- 恢复歧义:重试与恢复必须知道每个副作用归哪个 Agent 所有。
如果保留 Handoff,必须定义允许的目标、最大移交次数、终止规则、上下文过滤和明确的所有权记录。
推荐架构
应用代码负责不可违反的约束,Agent 只负责确实需要语义判断的决策。
父 Agent 应只看到一组规模较小、描述彼此可区分的工具。对子 Agent 的输入应是任务契约,而不是“帮忙处理一下”之类的模糊要求。
有效的委派契约至少包含:
| 字段 | 作用 |
|---|---|
| objective | 子 Agent 必须产出的精确结果 |
| evidence | 已知标识符与来源材料 |
| constraints | Scope、Tenant、Region 与禁止动作 |
| output schema | 输出字段与最大尺寸 |
| deadline | 剩余端到端时间预算 |
| call budget | 剩余模型与工具调用次数 |
| idempotency key | 对副作用进行去重 |
可运行的 AgentTool 示例
下面的程序基于 Eino v0.9.21,不依赖外部模型。它实现两个确定性专家 Agent,用 NewAgentTool 包装,再路由并调用选中的 Tool。需要模型动态委派时,可以把同样的 Tool 边界放进 ChatModelAgent.ToolsConfig。
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
type specialist struct {
name string
description string
prefix string
}
func (s *specialist) Name(context.Context) string {
return s.name
}
func (s *specialist) Description(context.Context) string {
return s.description
}
func (s *specialist) Run(
ctx context.Context,
input *adk.AgentInput,
_ ...adk.AgentRunOption,
) *adk.AsyncIterator[*adk.AgentEvent] {
iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
go func() {
defer gen.Close()
if err := ctx.Err(); err != nil {
gen.Send(&adk.AgentEvent{Err: err})
return
}
request := ""
if len(input.Messages) > 0 {
request = input.Messages[len(input.Messages)-1].Content
}
reply := fmt.Sprintf("%s handled: %s", s.prefix, request)
gen.Send(adk.EventFromMessage(
schema.AssistantMessage(reply, nil),
nil,
schema.Assistant,
"",
))
}()
return iter
}
func chooseTool(input string, tools map[string]tool.InvokableTool) tool.InvokableTool {
if strings.Contains(strings.ToLower(input), "invoice") {
return tools["billing"]
}
return tools["technical"]
}
func main() {
ctx := context.Background()
tools := map[string]tool.InvokableTool{
"billing": adk.NewAgentTool(ctx, &specialist{
name: "billing",
description: "Resolve invoice and payment questions.",
prefix: "billing",
}).(tool.InvokableTool),
"technical": adk.NewAgentTool(ctx, &specialist{
name: "technical",
description: "Diagnose API and integration failures.",
prefix: "technical",
}).(tool.InvokableTool),
}
request := "Why was invoice INV-42 charged twice?"
selected := chooseTool(request, tools)
result, err := selected.InvokableRun(
ctx,
`{"request":"Why was invoice INV-42 charged twice?"}`,
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
// Output: billing handled: Why was invoice INV-42 charged twice?
}
模块依赖固定为:
require github.com/cloudwego/eino v0.9.21
需要模型驱动的 Orchestrator 时,把 Tool 放入 adk.ChatModelAgentConfig.ToolsConfig.Tools,设置有限的 MaxIterations,用 adk.NewChatModelAgent 创建 Agent,再通过 adk.NewRunner 执行。模型实现必须支持 Eino 的 model.WithTools Option。
上下文、状态与权限
多 Agent 状态应按语义拆分,而不是全部塞进共享对话。
| 状态类别 | 推荐归属 | 示例 |
|---|---|---|
| 不可变请求事实 | Request Context 或类型化输入 | Tenant、Locale、Trace ID |
| 对话推理 | 各个 Agent | Messages 与中间假设 |
| 持久工作流状态 | 事务性存储 | 审批、任务状态、Checkpoint |
| 证据产物 | 对象或文档存储 | 搜索结果、报告、代码补丁 |
| 安全决策 | Policy Engine | Grant、Scope、对象级权限 |
context.Context 适合传递取消、Deadline、Trace 和小型请求级值,不是持久业务状态仓库。Eino Session Value 可以支持运行时指令格式化,但授权仍必须来自可信的服务端策略。
不要把父 Agent 的完整 Transcript 复制给每个专家。只传递子任务必需的证据,附带来源,再返回紧凑的结构化结果。这样既减少 Prompt Injection 的传播,也更容易定位评测结果。
故障与成本预算
多 Agent 会放大依赖数量,因此每次执行必须共享一份总预算。
deadline = min(request deadline, workflow SLA)
remaining calls = global call budget - completed calls
remaining tokens = global token budget - measured usage
allowed depth = configured maximum - current depth
至少需要执行:
- 通过
context.Context传播端到端 Deadline - 模型与 Tool 超时短于父级 Deadline
- 限制 Agent 深度、模型迭代次数和工具调用次数
- 在模型供应商与下游服务边界限制并发
- 子结果重新进入模型上下文前限制字节数
- 根据幂等性和错误类型决定是否重试
- 为长任务定义取消传播和 Checkpoint 规则
不能给每个子 Agent 独立的完整预算。五个子 Agent 各有十次调用额度,会把看似十次的父级预算放大成五十次。
评测协议
所有协作模式都应在同一个冻结任务集上比较。必须保留单 Agent 基线,因为多 Agent 可能只增加 Token 消耗而没有改善最终结果。
| 维度 | 示例指标 |
|---|---|
| 结果质量 | 任务 Pass Rate 或 Rubric Score |
| 路由质量 | Owner 命中率、Fallback Rate、拒答质量 |
| 证据质量 | 可支持结论比例与引用精度 |
| 效率 | Wall Time、模型 Token、Tool Call、子 Agent 数 |
| 可靠性 | Timeout、Retry、重复副作用、恢复成功率 |
| 安全 | 未授权调用拦截率、跨租户泄漏 |
| 可运维性 | Parent-Child Trace 归因完整度 |
Anthropic 的生产报告显示,多 Agent 对广度优先的研究任务有明显收益,但也报告了显著更高的 Token 消耗,以及对强依赖任务的较弱适配性。这些数字只证明对应工作负载,不能直接套用到其他系统;你需要在自己的任务分布上复现实验。
常见失败模式
每个角色都能看到所有工具
工具过载会损害选择质量并扩大爆炸半径。每个专家只应获得最小 Tool Allowlist,而且白名单必须由代码执行。
Router 静默走默认分支
Fallback 会掩盖分类漂移。应记录原因、置信度、最终路由,以及人工或确定性规则是否覆盖了模型选择。
重试整个工作流
全局重试可能重复已完成的副作用。持久化步骤状态和 Idempotency Key,并只在契约允许时重试失败操作。
委派没有完成契约
可以无限搜索的子 Agent 会耗尽总预算。必须规定目标、输出 Schema、停止条件和最大调用数。
把 Handoff 当成授权
获得控制权不等于获得数据权限。每次工具调用都要重新校验身份、Tenant、对象和动作权限。
常见问题
路由应该写在 Eino Graph 还是应用代码中?
可信字段或稳定规则可以决定路由时,直接使用应用代码。路由属于更大规模的类型化工作流,并涉及 Branch、持久化或组件复用时,可使用 Eino Graph。只有语义歧义足以抵消模型成本和错误率时,才使用模型分类。
什么时候适合向 AgentTool 传递完整历史?
只有子 Agent 必须理解对话本身时,才使用 adk.WithFullChatHistoryAsInput。对于数据查询、计算、审阅或抽取任务,窄而明确的类型化请求通常更安全,也更省 Token。
并行 Agent 一定更快吗?
不一定。只有子任务足够独立时,并行才有价值。共享依赖、限流、汇总成本和最慢子任务都可能抵消延迟收益。
应该追踪哪些信息?
应追踪父 Run、路由决策、子 Agent 调用、模型调用、Tool 调用、Retry 和最终校验。记录标识符和有界指标,不要记录原始 Prompt、凭证、个人信息和未经限制的 Tool 输出。
总结
可靠的 Eino 多 Agent 系统依赖明确边界,而不是 Agent 数量。先使用确定性路由,再用 AgentTool 实现受控委派;只有实测工作负载能够证明价值时,才采用全上下文 Transfer。共享事实与总预算,隔离推理和权限,并始终与更简单的单 Agent 基线比较。