Skip to content

[Bug] supervisor 模式下 transfer_to_agent 工具从未注册:适配器未实现 OnSubAgents 接口导致委派能力静默失效 #320

Description

@RongSec

环境

  • 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 指令双双不被生成

这是静默失效 —— 无报错、无日志、无回退,仅表现为「委派能力不存在」。


二、复现步骤

最小验证(推荐)

  1. 使用 supervisor 模式开启一个会话(需已配置至少一个 sub_agent)。
  2. 向主代理提问:
列出你当前可调用的全部工具名称。
  1. 预期错误行为:返回的列表不包含 transfer_to_agent,模型会明确回答「我没有委派工具」。
  2. 正确行为应为:列表包含 transfer_to_agent(参数 agent_name)。
Image

对照组:切换到 deep 模式执行同样提问 —— 列表包含 task,且主代理会主动使用它委派子代理。

Image

完整场景复现

  1. 使用 supervisor 模式发起一个需要多专家分工的复杂任务(如:对单个 Web 目标做系统性渗透测试,含侦察 / 枚举 / 漏洞验证 / 报告多阶段)。
  2. 观察主代理推理:可见它反复考虑委派但最终放弃。
  3. 统计工具调用: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
}

两处后果

  1. transfer_to_agent 工具未加入工具列表 → 模型的函数列表中确实没有它
  2. 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-312TypedChatModelAgentConfig 的全部字段:

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: falsetask 工具必然被创建。

结论: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:139len(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:338injectToolNamesOnlyInstruction 只枚举显式绑定的业务工具(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 AgentToolDeepAgent,而非 supervisor。

考虑到这一上游建议,若修复成本较高,一个合理的替代方案是在文档中将 deep 模式设为默认推荐,并说明 supervisor 模式的已知限制


八、一句话总结

supervisor 模式下 transfer_to_agent 从未被注册

einoAgenticMessageAgentAdapter 漏实现 Eino 的 OnSubAgents 接口 →
SetSubAgents 类型断言静默失败 → OnSetSubAgents 不被调用 →
主代理 subAgents 恒为空 → chatmodel.go:881if len(transferToAgents) > 0 恒为假 →
工具不注册 + transfer 指令不生成 → 委派能力静默失效。

deep 模式不受影响(task 走 middleware 通道,SubAgents 以切片直接传入)。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions