Agent 间 Transfer 交接:用户在不同 Agent 间无缝切换(第93篇-E79)

上一篇 拆了三种 ADK prebuilt 模式,其中 Supervisor 用 transfer 机制让 agent 之间交接任务。第91篇 讲 Host MultiAgent 时,host 把任务交给 specialist 也是一种"交接"。

这篇聚焦一个问题:Agent 之间怎么交接? 三个具体问题:

  1. transfer 是怎么发生的?(从 LLM 调 tool 到 flowAgent 启动子 agent 的完整链路)
  2. 交接时上下文怎么传?(rewriteMessage 重写历史 + session 共享)
  3. 为什么 transfer 被标注 NOT RECOMMENDED,AgentTool 又怎么做?

(一)Transfer 的完整链路:从 LLM 决策到 flowAgent 启动

第一步:ChatModelAgent 自动注入 transfer_to_agent 工具

chatmodel.go:874-927prepareExecContext 在每个 agent 启动时运行:

go 复制代码
// chatmodel.go:881-891
transferToAgents := a.subAgents
if a.parentAgent != nil && !a.disallowTransferToParent {
    transferToAgents = append(transferToAgents, a.parentAgent)
}

if len(transferToAgents) > 0 {
    transferInstruction := genTransferToAgentInstruction(ctx, transferToAgents)
    instruction = concatInstructions(instruction, transferInstruction)

    toolsNodeConf.Tools = append(toolsNodeConf.Tools, &transferToAgent{})
    returnDirectly[TransferToAgentToolName] = true
}

三个关键动作:

  1. 收集可 transfer 的 agent:subAgents + parentAgent(如果没禁止 transfer 回 parent)
  2. 注入 instruction:告诉 LLM 有哪些 agent 可以 transfer 到,以及什么时候该 transfer
  3. 注册 transfer_to_agent 工具:让 LLM 能通过 tool call 发起 transfer
  4. 设置 ReturnDirectly:transfer 工具调用后直接返回,不再经过 LLM

第二步:LLM 决定 transfer

instruction.go:28-42 定义了 transfer 的 instruction(中英双语):

diff 复制代码
可用的其他 agent:
- Agent 名字: MathAgent
  Agent 描述: 执行数学计算

决策规则:
- 如果根据你的职责描述,你最适合回答这个问题:ANSWER
- 如果根据其职责描述,另一个 agent 更适合:调用 transfer_to_agent 函数,并传入该 agent 的名称

当进行移交时:只输出函数调用,不要输出其他任何内容

LLM 看到这段 instruction 后,如果判断自己不合适,会调用 transfer_to_agent 工具,参数 agent_name 填目标 agent 名。

第三步:transfer_to_agent 工具执行

chatmodel.go:661-677transferToAgent.InvokableRun

go 复制代码
func (tta transferToAgent) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
    params := &transferParams{}
    sonic.UnmarshalString(argumentsInJSON, params)

    // 关键:通过 SendToolGenAction 把 TransferToAgentAction 附加到工具事件
    err = SendToolGenAction(ctx, TransferToAgentToolName, NewTransferToAgentAction(params.AgentName))

    return transferToAgentToolOutput(params.AgentName), nil
}

SendToolGenActionreact.go:274-285)把 TransferToAgentAction 存到 Eino 图状态中,当工具事件发出时,这个 action 会被附加到事件上。

第四步:flowAgent 处理 transfer

flow.go:481-582flowAgent.run() 是 transfer 的核心处理逻辑:

go 复制代码
// flow.go:554-581
var destName string
if lastAction != nil {
    if lastAction.TransferToAgent != nil {
        destName = lastAction.TransferToAgent.DestAgentName
    }
}

if destName != "" {
    agentToRun := a.getAgent(ctxForSubAgents, destName)
    if agentToRun == nil {
        generator.Send(&AgentEvent{Err: fmt.Errorf("transfer failed: agent '%s' not found", destName)})
        return
    }

    subAIter := agentToRun.Run(ctxForSubAgents, nil, ...)
    for {
        subEvent, ok_ := subAIter.Next()
        if !ok_ { break }
        generator.Send(subEvent)
    }
}

流程:agent 运行完 → 检查最后一个 action 是否有 TransferToAgent → 通过 getAgent() 在 subAgents 和 parentAgent 中查找 → 找到后调用 agentToRun.Run() 启动目标 agent。

getAgent:去哪儿找?

flow.go:174-186

go 复制代码
func (a *flowAgent) getAgent(ctx context.Context, name string) *flowAgent {
    for _, subAgent := range a.subAgents {
        if subAgent.Name(ctx) == name {
            return subAgent
        }
    }
    if a.parentAgent != nil && a.parentAgent.Name(ctx) == name {
        return a.parentAgent
    }
    return nil
}

先在 subAgents 里找,再在 parentAgent 里找。这意味着一个 agent 可以 transfer 到它的子 agent(下属)或父 agent(上级),但不能 transfer 到 sibling(同级)。

(二)交接时上下文怎么传:rewriteMessage + Session 共享

transfer 的核心问题是:目标 agent 怎么知道之前发生了什么?

Session 共享

所有 agent 共享同一个 runSessionflow.go:352-396)。每个 agent 产出的事件都写入同一个 session,后续 agent 通过 genAgentInputflow.go:275-328)从 session 读取事件历史来构建自己的输入。

rewriteMessage:标注来源

flow.go:188-253rewriteMessage 是关键:

go 复制代码
func rewriteMessage(msg Message, agentName string) Message {
    var sb strings.Builder
    sb.WriteString("For context:")
    if msg.Role == schema.Assistant {
        if msg.Content != "" {
            sb.WriteString(fmt.Sprintf(" [%s] said: %s.", agentName, msg.Content))
        }
        // tool call 也标注
        for i := range msg.ToolCalls {
            f := msg.ToolCalls[i].Function
            sb.WriteString(fmt.Sprintf(" [%s] called tool: `%s` with arguments: %s.",
                agentName, f.Name, f.Arguments))
        }
    } else if msg.Role == schema.Tool && msg.Content != "" {
        sb.WriteString(fmt.Sprintf(" [%s] `%s` tool returned result: %s.",
            agentName, msg.ToolName, msg.Content))
    }
    rewritten := schema.UserMessage(sb.String())
    return rewritten
}

作用:把非本 agent 产出的消息重写为 "For context: AgentX said: ..." 格式,注入到目标 agent 的输入中。目标 agent 就知道"这是别人说的,不是我的记忆"。

genAgentInput:构建目标 agent 的输入

flow.go:275-328

go 复制代码
func (a *flowAgent) genAgentInput(ctx context.Context, runCtx *runContext, skipTransferMessages bool) (*AgentInput, error) {
    input := deepCopyAgentInput(runCtx.RootInput)
    events := runCtx.Session.getEvents()
    historyEntries := make([]*HistoryEntry, 0)

    // 先放用户原始输入
    for _, m := range input.Messages {
        historyEntries = append(historyEntries, &HistoryEntry{IsUserInput: true, Message: m})
    }

    // 再放 session 中的所有事件
    for _, event := range events {
        // 跳过 transfer 消息(如果 skipTransferMessages=true)
        if skipTransferMessages && event.Action != nil && event.Action.TransferToAgent != nil {
            continue
        }
        msg, err := getMessageFromWrappedEvent(event)
        historyEntries = append(historyEntries, &HistoryEntry{AgentName: event.AgentName, Message: msg})
    }

    // 调用 historyRewriter 重写
    messages, err := a.historyRewriter(ctx, historyEntries)
    input.Messages = messages
    return input, nil
}

buildDefaultHistoryRewriterflow.go:330-350)对每条非用户输入的消息调用 genMsg,如果消息来源 agent 不是当前 agent,就调用 rewriteMessage 标注来源。

源码中多处标注 NOT RECOMMENDED。flow.go:70-77SetSubAgents

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.

"经验证明这个方向不如另一个方向"。具体问题:

  • 完整上下文共享:每 transfer 一次,目标 agent 的上下文就多一段 rewrite 后的历史。多轮 transfer 后上下文膨胀很快。
  • 缺乏隔离:目标 agent 能看到所有前置 agent 的完整历史,包括不该它关心的信息。
  • 消息重写损失信息:rewriteMessage 把 assistant/tool 消息重写为 user 消息,可能丢失角色语义。

(三)AgentWithDeterministicTransferTo:限制 transfer 方向

deterministic_transfer.go:43-54

go 复制代码
func AgentWithDeterministicTransferTo(_ context.Context, config *DeterministicTransferConfig) Agent {
    if ra, ok := config.Agent.(ResumableAgent); ok {
        return &resumableAgentWithDeterministicTransferTo{
            agent:        ra,
            toAgentNames: config.ToAgentNames,
        }
    }
    return &agentWithDeterministicTransferTo{
        agent:        config.Agent,
        toAgentNames: config.ToAgentNames,
    }
}

这个包装器限制被包装的 agent 只能 transfer 到 toAgentNames 列表中的 agent。

deterministic_transfer.go:139-164forwardEventsAndAppendTransfer

go 复制代码
func forwardEventsAndAppendTransfer(iter *AsyncIterator[*AgentEvent],
    generator *AsyncGenerator[*AgentEvent], toAgentNames []string) {

    var lastEvent *AgentEvent
    for {
        event, ok := iter.Next()
        if !ok { break }
        generator.Send(event)
        lastEvent = event
    }

    // 如果最后一个事件没有 Exit 或 Interrupted,自动发出 transfer 事件
    if lastEvent != nil && lastEvent.Action != nil &&
        (lastEvent.Action.Interrupted != nil || lastEvent.Action.Exit) {
        return
    }

    sendTransferEvents(generator, toAgentNames)
}

sendTransferEvents286-301)给每个 toAgentNames 中的 agent 生成一对 assistant+tool 消息,模拟 transfer 调用。

Supervisor 模式用这个机制:每个子 agent 被 AgentWithDeterministicTransferTo 包装,toAgentNames 只有 supervisor 的名字。子 agent 完成后自动 transfer 回 supervisor,不能 transfer 到其他子 agent。

(四)AgentTool:更好的替代方案

agent_tool.go:93-103NewAgentTool 把 agent 包装为 tool:

go 复制代码
func NewAgentTool(_ context.Context, agent Agent, options ...AgentToolOption) tool.BaseTool {
    return &agentTool{
        agent:                  agent,
        fullChatHistoryAsInput: opts.fullChatHistoryAsInput,
        inputSchema:            opts.agentInputSchema,
    }
}

agent_tool.go:154-280InvokableRun 展示了 key 差异:

  1. 独立 session :内部 agent 有独立的 checkpoint store(bridgeStore),不共享父 agent 的 session
  2. Action 隔离Exit, TransferToAgent, BreakLoop 被忽略,不传播到父 agent(agent_tool.go:352-356
  3. 独立 checkpoint:每个 agent tool 有自己的 checkpoint,中断恢复时独立处理
go 复制代码
// agent_tool.go:352-356
//   - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool; these actions only affect
//     the inner agent's execution and do not propagate to the parent agent

对比 transfer 和 AgentTool:

维度 Transfer AgentTool
上下文 共享 session,rewrite 历史 独立 session,工具参数传 request
隔离性 差(完整上下文共享) 好(独立 checkpoint)
子 agent action 传播到父 agent 隔离在工具内部
上下文膨胀 每轮 transfer 追加历史 每次调用独立,不累积
推荐程度 NOT RECOMMENDED 推荐

(五)Host MultiAgent 的 HandOff 回调

E91 和 E92 提到的 Host MultiAgent 也有"交接"概念,但用的是回调机制,不是 ADK 的 transfer。

callback.go:31-33

go 复制代码
type MultiAgentCallback interface {
    OnHandOff(ctx context.Context, info *HandOffInfo) context.Context
}

type HandOffInfo struct {
    ToAgentName string
    Argument    string
}

ConvertCallbackHandlerscallback.go:42-89)监听 ChatModel 的 OnEnd 事件,当 host LLM 产出 tool call 时(每个 tool call 对应一个 specialist),触发 OnHandOff 回调。用户可以在回调中记录日志、修改上下文等。

Host MultiAgent 的 HandOff 和 ADK 的 transfer 是两套不同的机制:

  • Host HandOff:host 把 specialist 当 tool 调,host 的 Graph 负责汇总结果。交接发生在 host 一个 agent 内部。
  • ADK Transfer :agent 之间通过 transfer_to_agent 工具和 TransferToAgentAction 传递控制权。交接发生在 agent 之间。

小结

问题 答案 关键源码
transfer 怎么发生 LLM 调 transfer_to_agent → SendToolGenAction → flowAgent.run 处理 TransferToAgent chatmodel.go:661-677, flow.go:554-581
上下文怎么传 Session 共享 + rewriteMessage 标注来源 + genAgentInput 构建输入 flow.go:188-253, 275-328
为什么 NOT RECOMMENDED 完整上下文共享 → 膨胀快 + 隔离差 + 消息重写损失语义 flow.go:70-77
DeterministicTransfer 怎么限制 包装 agent,子 agent 完成后自动 transfer 回指定列表 deterministic_transfer.go:43-54, 139-164
AgentTool 怎么替代 独立 session + Action 隔离 + 独立 checkpoint agent_tool.go:93-103, 154-280
Host HandOff 和 ADK Transfer 的区别 Host 内部回调 vs Agent 间控制权转移 callback.go:31-89

几个设计判断:

  • Transfer 就是"共享 session + 重写历史"。 目标 agent 能看到前面所有 agent 说了什么,但每条消息被标注了来源。这不是"隔离",而是"透明的上下文传递"------问题恰恰出在这里。

  • AgentTool 是"独立 session + 工具参数"。 子 agent 看不到父 agent 的完整历史,只通过 request 参数接收任务描述。隔离性更好,但也意味着子 agent 无法利用父 agent 的上下文做推理------需要父 agent 在 request 参数中写清楚。

  • NOT RECOMMENDED 不代表不能用。 Supervisor 模式用 transfer 实现简单的路由------router 分析用户意图,transfer 到合适的专家 agent。如果 agent 层级简单(一层 router + 若干专家),上下文膨胀不是大问题。但复杂场景下 AgentTool 更好。

下一篇(E94)讲 Deep Agent 的文件系统工具链------read/write/glob/grep/shell 五个工具怎么设计,以及 filesystem 中间件的实现。

相关推荐
很楠爱上1 小时前
从“AI 看合同”到可举证的合同决策链:CounterClause(对薄) 的架构设计与工程实践
人工智能·经验分享·python·学习·agent
Java后端的Ai之路1 小时前
02、Python普通工厂模式
开发语言·人工智能·python·设计模式·普通工厂模式
爱奥尼欧1 小时前
14.输出解析器-Pydantic与JSON
人工智能·学习·langchain·json
ReleaseU1 小时前
Claude Code 两天三版本、Cursor 把仓库搬进编辑器:AI 编程工具在卷什么?
人工智能·大模型
tachibana21 小时前
初识智能体
人工智能·ai·大模型·llm·agent
找方案1 小时前
Seedance 2.5突破30秒视频生成,AI视频赛道进入工业化时代
大数据·人工智能·字节跳动·可灵·ai视频生成·seedance2.5·工业化时代
李剑一1 小时前
前端转AI要了解的技术,其他人不用看。前端架构基础之:让Js的计算运行在GPU上
前端·aigc·ai编程
IT_陈寒1 小时前
Python的列表拷贝坑得我原地打转
前端·人工智能·后端
deepdata_cn1 小时前
从弱AI到强AI:符号推理是实现通用人工智能AGI的关键钥匙
人工智能·agi·符号推理