环境
- CyberStrikeAI 1.7.17(源码包
CyberStrikeAI-main)
- Go 依赖:
github.com/cloudwego/eino v0.9.14
- 编排模式:
supervisor
- 模型:本地部署 LLM(OpenAI 兼容接口)
一、问题描述
supervisor 模式下,主代理完全无法执行任何委派,且工具列表中不存在 transfer_to_agent。
全流程 transfer_to_agent 调用次数 = 0;模型在推理中尝试委派 8 次,8 次放弃,最终结论:
Since transfer isn't in my available tools, I can't delegate. So I proceed directly.
根因:CyberStrikeAI 自研的适配器类 einoAgenticMessageAgentAdapter 未实现 Eino 的 OnSubAgents 接口。当该适配器被用作 supervisor.New 的 Supervisor 时,Eino SetSubAgents 的类型断言静默失败,OnSetSubAgents 从未被调用,导致主代理内部 subAgents 始终为空,transfer_to_agent 工具与 transfer 指令双双不被生成。
这是静默失效 —— 无报错、无日志、无回退,仅表现为「委派能力不存在」。
二、复现步骤
最小验证(推荐)
- 使用 supervisor 模式开启一个会话(需已配置至少一个 sub_agent)。
- 向主代理提问:
- 预期错误行为:返回的列表不包含
transfer_to_agent,模型会明确回答「我没有委派工具」。
- 正确行为应为:列表包含
transfer_to_agent(参数 agent_name)。
对照组:切换到 deep 模式执行同样提问 —— 列表包含 task,且主代理会主动使用它委派子代理。
完整场景复现
- 使用 supervisor 模式发起一个需要多专家分工的复杂任务(如:对单个 Web 目标做系统性渗透测试,含侦察 / 枚举 / 漏洞验证 / 报告多阶段)。
- 观察主代理推理:可见它反复考虑委派但最终放弃。
- 统计工具调用:
transfer_to_agent = 0。
实际日志摘录(同一会话,按轮次):
# 第 10 轮 —— 开始考虑委派
Let me be pragmatic. This is an enormous surface with ~150 distinct endpoints.
Given the supervisor mode, I could delegate to sub-agents.
Actually, given the complexity and the "supervisor" role, the guidance says:
use transfer when task genuinely needs different specialist roles.
# 中间多轮反复纠结(共 8 处)
This is huge. Given the supervisor mode and the 2000+ step expectation,
I should delegate significant chunks to sub-agents (penetration expert).
Given the supervisor architecture and to parallelize the enormous remaining work,
I think NOW is the right time to delegate.
# ★ 关键转折 —— 模型检查自己的工具列表
Hmm, but the harness's `transfer` — do I even have it?
Looking at my function list... I see no `transfer` function defined.
The prompt mentions transfer but it's not in the functions.
So maybe transfer isn't actually available.
Let me check: the functions list includes exit, but not transfer.
So I cannot actually call transfer!
# ★ 最终定论 —— 彻底放弃委派
Since `transfer` isn't in my available tools, I can't delegate.
So I proceed directly.
此后主代理独自执行全部工作,未产生任何子代理调用。后果:单会话内被迫承担全部工作,上下文耗尽后开始伪造中间产出(程序化生成状态文件)以通过后续校验。
三、根因分析:完整的失效链路
环节 1 · Eino 要求主代理实现 OnSubAgents 接口才能接收子代理
github.com/cloudwego/eino/adk/flow.go:132-144:
func setSubAgents(ctx context.Context, agent Agent, subAgents []Agent) (*flowAgent, error) {
fa := toFlowAgent(ctx, agent)
if len(fa.subAgents) > 0 {
return nil, errors.New("agent's sub-agents has already been set")
}
if onAgent, ok_ := fa.Agent.(OnSubAgents); ok_ { // ★ 类型断言
err := onAgent.OnSetSubAgents(ctx, subAgents) // ★ 仅断言成功时执行
if err != nil {
return nil, err
}
}
// ...
}
断言失败时无报错、无日志、无回退 —— 静默跳过。 这是缺陷得以隐藏的关键。
环节 2 · 接口定义(三个方法)
github.com/cloudwego/eino/adk/interface.go:474-479:
type OnSubAgents interface {
OnSetSubAgents(ctx context.Context, subAgents []Agent) error
OnSetAsSubAgent(ctx context.Context, parent Agent) error
OnDisallowTransferToParent(ctx context.Context) error
}
环节 3 · 原生 TypedChatModelAgent 正确实现了接口
github.com/cloudwego/eino/adk/chatmodel.go:697-708:
func (a *TypedChatModelAgent[M]) OnSetSubAgents(_ context.Context, subAgents []TypedAgent[M]) error {
if atomic.LoadUint32(&a.frozen) == 1 {
return errors.New("agent has been frozen after run")
}
if len(a.subAgents) > 0 {
return errors.New("agent's sub-agents has already been set")
}
a.subAgents = subAgents // ★ 只有走到这里 subAgents 才被设置
return nil
}
环节 4 · ★ 缺陷所在:CyberStrikeAI 适配器未实现该接口
internal/multiagent/eino_agentic_agent_adapter.go 中,einoAgenticMessageAgentAdapter 实现的全部方法:
:22 func (a *einoAgenticMessageAgentAdapter) Name(ctx context.Context) string
:29 func (a *einoAgenticMessageAgentAdapter) Description(ctx context.Context) string
:36 func (a *einoAgenticMessageAgentAdapter) Run(ctx, input, opts...) *adk.AsyncIterator[*adk.AgentEvent]
:40 func (a *einoAgenticMessageAgentAdapter) Resume(ctx, info, opts...) *adk.AsyncIterator[*adk.AgentEvent]
:44 func (a *einoAgenticMessageAgentAdapter) runTyped(...)
OnSetSubAgents / OnSetAsSubAgent / OnDisallowTransferToParent —— 均未实现。
因此 fa.Agent.(OnSubAgents) 的断言恒为 false,适配器被当作「不支持子代理的普通 agent」处理。
环节 5 · supervisor 分支恰好传入了被适配器包装的 agent
internal/multiagent/runner.go:555-562:
superChat, serr := newEinoAgenticChatModelAgentAdapter(ctx, supCfg) // ★ 包了适配器
if serr != nil {
return nil, fmt.Errorf("supervisor agentic 主代理: %w", serr)
}
supRoot, serr := supervisor.New(ctx, &supervisor.Config{
Supervisor: superChat, // ★ Supervisor 是适配器实例
SubAgents: supervisorSubAgents,
})
而 supervisor.New(Eino)内部正是调用 SetSubAgents:
// github.com/cloudwego/eino/adk/prebuilt/supervisor/supervisor.go
func New(ctx context.Context, conf *Config) (adk.ResumableAgent, error) {
subAgents := make([]adk.Agent, 0, len(conf.SubAgents))
supervisorName := conf.Supervisor.Name(ctx)
for _, subAgent := range conf.SubAgents {
subAgents = append(subAgents, adk.AgentWithDeterministicTransferTo(ctx, &adk.DeterministicTransferConfig{
Agent: subAgent,
ToAgentNames: []string{supervisorName},
}))
}
inner, err := adk.SetSubAgents(ctx, conf.Supervisor, subAgents) // ★ 触发环节 1 的静默跳过
if err != nil {
return nil, err
}
// ...
}
断链闭合:SetSubAgents 断言失败 → OnSetSubAgents 不被调用 → 内层 TypedChatModelAgent.subAgents 保持 nil。
环节 6 · subAgents 为空 → 工具与指令双双不生成
github.com/cloudwego/eino/adk/chatmodel.go:881-889:
transferToAgents := a.subAgents
if a.parentAgent != nil && !a.disallowTransferToParent {
transferToAgents = append(transferToAgents, a.parentAgent)
}
if len(transferToAgents) > 0 { // ★ 恒为 false
transferInstruction := genTransferToAgentInstruction(ctx, transferToAgents)
instruction = concatInstructions(instruction, transferInstruction)
toolsNodeConf.Tools = append(toolsNodeConf.Tools, &transferToAgent{}) // ★ 不注册
returnDirectly[TransferToAgentToolName] = true
}
两处后果:
transfer_to_agent 工具未加入工具列表 → 模型的函数列表中确实没有它
genTransferToAgentInstruction 生成的子代理说明也未拼入提示词 → 模型连「有哪些子代理可转交」都不知道
四、为什么问题不易被发现
4.1 代码中存在「描述性引用」,容易被误认为「注册证据」
| 位置 |
内容 |
实际含义 |
hitl_middleware.go:55 |
strings.EqualFold(toolName, adk.TransferToAgentToolName) |
工具调用时才触发的中间件判断。工具未注册 → 永不触发 |
hitl_middleware.go:120 |
注释「transfer_to_agent 在 Eino 中标记为 returnDirectly」 |
纯注释 |
eino_middleware.go:132,169 |
reduction 排除名单含 "transfer_to_agent" |
字符串白名单,不检查工具是否存在 |
以上均不构成「工具已注册」的证据。 它们的存在反而掩盖了注册失败的事实。
4.2 关键结构体没有 SubAgents 字段
github.com/cloudwego/eino/adk/chatmodel.go:264-312,TypedChatModelAgentConfig 的全部字段:
Name string
Description string
Instruction string
Model model.BaseModel[M]
ToolsConfig ToolsConfig
GenModelInput TypedGenModelInput[M]
Exit tool.BaseTool
OutputKey string
MaxIterations int
Middlewares []AgentMiddleware
无 SubAgents 字段。 子代理只能通过 OnSubAgents 接口注入 —— 这就是适配器漏实现接口会导致能力彻底消失的原因。对照 internal/multiagent/eino_agentic_chat_model_agent.go:33-46,CyberStrikeAI 的 typedCfg 也没有(也无法)赋值 SubAgents。
4.3 transfer_to_agent 是运行时动态挂载的工具
github.com/cloudwego/eino/adk/chatmodel.go:584-600:
const (
TransferToAgentToolName = "transfer_to_agent"
TransferToAgentToolDesc = "Transfer the question to another agent."
TransferToAgentToolDescChinese = "将问题移交给其他 Agent。"
)
var (
toolInfoTransferToAgent = &schema.ToolInfo{
Name: TransferToAgentToolName,
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"agent_name": {
Desc: "the name of the agent to transfer to",
Required: true,
Type: schema.String,
},
}),
}
// ...
)
它是形式完整的工具(名称、描述、JSON Schema),但由 prepareExecContext 在运行时根据 subAgents 是否为空决定挂载与否。
五、影响范围
| 模式 |
委派工具 |
挂载通道 |
是否受影响 |
supervisor |
transfer_to_agent |
prepareExecContext 运行时挂载,依赖 OnSubAgents 接口注入 subAgents |
❌ 完全失效 |
deep |
task |
typedTaskToolMiddleware 中间件挂载,直接用 cfg.SubAgents 切片 |
✅ 正常 |
plan_execute |
不使用子代理列表 |
— |
— |
为何 deep 不受影响(对比证据)
github.com/cloudwego/eino/adk/prebuilt/deep/deep.go:130-148:
if !cfg.WithoutGeneralSubAgent || len(cfg.SubAgents) > 0 {
tt, err := typedTaskToolMiddleware( // ★ 中间件挂载,不经接口
ctx,
cfg.TaskToolDescriptionGenerator,
cfg.SubAgents, // ★ 构造时直接传入的切片
cfg.WithoutGeneralSubAgent,
cfg.ChatModel,
instruction,
cfg.ToolsConfig,
cfg.MaxIteration,
cfg.Middlewares,
append(handlers, cfg.Handlers...),
cfg.ModelFailoverConfig,
)
if err != nil {
return nil, fmt.Errorf("failed to new task tool: %w", err)
}
handlers = append(handlers, tt)
}
internal/multiagent/runner.go:568-576(deep 分支):
dcfg := &deep.TypedConfig[*schema.AgenticMessage]{
Name: orchestratorName,
Description: orchDescription,
ChatModel: mainModel,
Instruction: orchInstruction,
SubAgents: subAgents, // ★ 直接传切片
WithoutGeneralSubAgent: ma.WithoutGeneralSubAgent,
WithoutWriteTodos: ma.WithoutWriteTodos,
// ...
}
config.example.yaml:289 默认 without_general_sub_agent: false → task 工具必然被创建。
结论:deep 模式的 task 完全绕过了该缺陷,可正常分发子代理。
六、建议修复
修复 1(必须)· 让适配器实现 OnSubAgents 接口并透传
internal/multiagent/eino_agentic_agent_adapter.go 增加以下方法:
func (a *einoAgenticMessageAgentAdapter) OnSetSubAgents(ctx context.Context, subAgents []adk.Agent) error {
if a == nil || a.inner == nil {
return fmt.Errorf("agentic adapter: inner agent is nil")
}
if on, ok := a.inner.(adk.OnSubAgents); ok {
return on.OnSetSubAgents(ctx, subAgents)
}
return nil
}
func (a *einoAgenticMessageAgentAdapter) OnSetAsSubAgent(ctx context.Context, parent adk.Agent) error {
if a == nil || a.inner == nil {
return fmt.Errorf("agentic adapter: inner agent is nil")
}
if on, ok := a.inner.(adk.OnSubAgents); ok {
return on.OnSetAsSubAgent(ctx, parent)
}
return nil
}
func (a *einoAgenticMessageAgentAdapter) OnDisallowTransferToParent(ctx context.Context) error {
if a == nil || a.inner == nil {
return fmt.Errorf("agentic adapter: inner agent is nil")
}
if on, ok := a.inner.(adk.OnSubAgents); ok {
return on.OnDisallowTransferToParent(ctx)
}
return nil
}
⚠️ 实现注意:Eino 的 OnSubAgents 接口签名使用 []adk.Agent,而内层 TypedChatModelAgent[M].OnSetSubAgents 接收 []TypedAgent[M]。需确认 Eino 是否提供 []adk.Agent → []TypedAgent[M] 的转换工具;若无,需在适配器内完成类型转换。
修复 2(防御性)· 为静默跳过增加错误或日志
建议向 CloudWeGo/eino 反馈:adk/flow.go:139 在 len(subAgents) > 0 但类型断言失败时,应当返回错误或至少输出 Warn 日志,而非静默跳过。这是导致本问题难以定位的根本原因。
修复 3(文档)· 标注适配器的接口转发义务
在 eino_agentic_agent_adapter.go 文件头加注释:任何将被用作 supervisor.New 之 Supervisor 的 agent,其适配器必须实现 OnSubAgents 接口,否则委派能力将静默失效。
修复 4(次要)· 统一提示词中的工具名
以下 3 处将 transfer 改为 transfer_to_agent,避免歧义:
| 位置 |
当前文案 |
internal/multiagent/runner.go:358 |
通过 transfer 工具委派 |
internal/multiagent/orchestrator_instruction.go:133 |
通过 **transfer** 把合适的工作 |
agents/orchestrator-supervisor.md:4 |
通过 transfer 委派专家子代理 |
修复 5(可选)· 将框架工具名补入工具名索引
internal/multiagent/runner.go:338 的 injectToolNamesOnlyInstruction 只枚举显式绑定的业务工具(mainTools,来源 runner.go:155 mainDefs := ag.ToolsForRole(roleTools))。修复 1 完成后 transfer_to_agent 虽已注册,但仍不在该索引中。建议增加 extraNames 参数以显式登记。
七、上游参考:Eino 官方建议
Eino v0.9.14 在相关代码中多处标注该模式不推荐:
// adk/prebuilt/supervisor/supervisor.go:42(Config 结构体上方)
// NOT RECOMMENDED: 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 for most multi-agent scenarios.
// adk/deterministic_transfer.go:34(AgentWithDeterministicTransferTo 上方)
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
// adk/chatmodel.go:694(OnSetAsSubAgent 上方)
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. ...
Eino 官方明确建议:多数多代理场景应使用 ChatModelAgent with AgentTool 或 DeepAgent,而非 supervisor。
考虑到这一上游建议,若修复成本较高,一个合理的替代方案是在文档中将 deep 模式设为默认推荐,并说明 supervisor 模式的已知限制。
八、一句话总结
supervisor 模式下 transfer_to_agent 从未被注册。
einoAgenticMessageAgentAdapter 漏实现 Eino 的 OnSubAgents 接口 →
SetSubAgents 类型断言静默失败 → OnSetSubAgents 不被调用 →
主代理 subAgents 恒为空 → chatmodel.go:881 的 if len(transferToAgents) > 0 恒为假 →
工具不注册 + transfer 指令不生成 → 委派能力静默失效。
deep 模式不受影响(task 走 middleware 通道,SubAgents 以切片直接传入)。
环境
CyberStrikeAI-main)github.com/cloudwego/eino v0.9.14supervisor一、问题描述
supervisor 模式下,主代理完全无法执行任何委派,且工具列表中不存在
transfer_to_agent。全流程
transfer_to_agent调用次数 = 0;模型在推理中尝试委派 8 次,8 次放弃,最终结论:根因:CyberStrikeAI 自研的适配器类
einoAgenticMessageAgentAdapter未实现 Eino 的OnSubAgents接口。当该适配器被用作supervisor.New的 Supervisor 时,EinoSetSubAgents的类型断言静默失败,OnSetSubAgents从未被调用,导致主代理内部subAgents始终为空,transfer_to_agent工具与 transfer 指令双双不被生成。这是静默失效 —— 无报错、无日志、无回退,仅表现为「委派能力不存在」。
二、复现步骤
最小验证(推荐)
transfer_to_agent,模型会明确回答「我没有委派工具」。transfer_to_agent(参数agent_name)。对照组:切换到 deep 模式执行同样提问 —— 列表包含
task,且主代理会主动使用它委派子代理。完整场景复现
transfer_to_agent= 0。实际日志摘录(同一会话,按轮次):
此后主代理独自执行全部工作,未产生任何子代理调用。后果:单会话内被迫承担全部工作,上下文耗尽后开始伪造中间产出(程序化生成状态文件)以通过后续校验。
三、根因分析:完整的失效链路
环节 1 · Eino 要求主代理实现
OnSubAgents接口才能接收子代理github.com/cloudwego/eino/adk/flow.go:132-144:断言失败时无报错、无日志、无回退 —— 静默跳过。 这是缺陷得以隐藏的关键。
环节 2 · 接口定义(三个方法)
github.com/cloudwego/eino/adk/interface.go:474-479:环节 3 · 原生
TypedChatModelAgent正确实现了接口github.com/cloudwego/eino/adk/chatmodel.go:697-708:环节 4 · ★ 缺陷所在:CyberStrikeAI 适配器未实现该接口
internal/multiagent/eino_agentic_agent_adapter.go中,einoAgenticMessageAgentAdapter实现的全部方法:OnSetSubAgents/OnSetAsSubAgent/OnDisallowTransferToParent—— 均未实现。因此
fa.Agent.(OnSubAgents)的断言恒为false,适配器被当作「不支持子代理的普通 agent」处理。环节 5 · supervisor 分支恰好传入了被适配器包装的 agent
internal/multiagent/runner.go:555-562:而
supervisor.New(Eino)内部正是调用SetSubAgents:断链闭合:
SetSubAgents断言失败 →OnSetSubAgents不被调用 → 内层TypedChatModelAgent.subAgents保持nil。环节 6 ·
subAgents为空 → 工具与指令双双不生成github.com/cloudwego/eino/adk/chatmodel.go:881-889:两处后果:
transfer_to_agent工具未加入工具列表 → 模型的函数列表中确实没有它genTransferToAgentInstruction生成的子代理说明也未拼入提示词 → 模型连「有哪些子代理可转交」都不知道四、为什么问题不易被发现
4.1 代码中存在「描述性引用」,容易被误认为「注册证据」
hitl_middleware.go:55strings.EqualFold(toolName, adk.TransferToAgentToolName)hitl_middleware.go:120eino_middleware.go:132,169"transfer_to_agent"以上均不构成「工具已注册」的证据。 它们的存在反而掩盖了注册失败的事实。
4.2 关键结构体没有
SubAgents字段github.com/cloudwego/eino/adk/chatmodel.go:264-312,TypedChatModelAgentConfig的全部字段:无
SubAgents字段。 子代理只能通过OnSubAgents接口注入 —— 这就是适配器漏实现接口会导致能力彻底消失的原因。对照internal/multiagent/eino_agentic_chat_model_agent.go:33-46,CyberStrikeAI 的typedCfg也没有(也无法)赋值SubAgents。4.3
transfer_to_agent是运行时动态挂载的工具github.com/cloudwego/eino/adk/chatmodel.go:584-600:它是形式完整的工具(名称、描述、JSON Schema),但由
prepareExecContext在运行时根据subAgents是否为空决定挂载与否。五、影响范围
supervisortransfer_to_agentprepareExecContext运行时挂载,依赖OnSubAgents接口注入subAgentsdeeptasktypedTaskToolMiddleware中间件挂载,直接用cfg.SubAgents切片plan_execute为何 deep 不受影响(对比证据)
github.com/cloudwego/eino/adk/prebuilt/deep/deep.go:130-148:internal/multiagent/runner.go:568-576(deep 分支):config.example.yaml:289默认without_general_sub_agent: false→task工具必然被创建。结论:deep 模式的
task完全绕过了该缺陷,可正常分发子代理。六、建议修复
修复 1(必须)· 让适配器实现
OnSubAgents接口并透传internal/multiagent/eino_agentic_agent_adapter.go增加以下方法:修复 2(防御性)· 为静默跳过增加错误或日志
建议向 CloudWeGo/eino 反馈:
adk/flow.go:139在len(subAgents) > 0但类型断言失败时,应当返回错误或至少输出 Warn 日志,而非静默跳过。这是导致本问题难以定位的根本原因。修复 3(文档)· 标注适配器的接口转发义务
在
eino_agentic_agent_adapter.go文件头加注释:任何将被用作supervisor.New之 Supervisor 的 agent,其适配器必须实现OnSubAgents接口,否则委派能力将静默失效。修复 4(次要)· 统一提示词中的工具名
以下 3 处将
transfer改为transfer_to_agent,避免歧义:internal/multiagent/runner.go:358通过 transfer 工具委派internal/multiagent/orchestrator_instruction.go:133通过 **transfer** 把合适的工作agents/orchestrator-supervisor.md:4通过 transfer 委派专家子代理修复 5(可选)· 将框架工具名补入工具名索引
internal/multiagent/runner.go:338的injectToolNamesOnlyInstruction只枚举显式绑定的业务工具(mainTools,来源runner.go:155mainDefs := ag.ToolsForRole(roleTools))。修复 1 完成后transfer_to_agent虽已注册,但仍不在该索引中。建议增加extraNames参数以显式登记。七、上游参考:Eino 官方建议
Eino v0.9.14 在相关代码中多处标注该模式不推荐:
Eino 官方明确建议:多数多代理场景应使用
ChatModelAgent with AgentTool或DeepAgent,而非 supervisor。考虑到这一上游建议,若修复成本较高,一个合理的替代方案是在文档中将 deep 模式设为默认推荐,并说明 supervisor 模式的已知限制。
八、一句话总结