Agent 间 Transfer 交接:用户在不同 Agent 间无缝切换(第93篇-E79)
2026/8/25 5:58:26 网站建设 项目流程

上一篇 拆了三种 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 启动时运行:

// chatmodel.go:881-891transferToAgents:=a.subAgentsifa.parentAgent!=nil&&!a.disallowTransferToParent{transferToAgents=append(transferToAgents,a.parentAgent)}iflen(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(中英双语):

可用的其他 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

func(tta transferToAgent)InvokableRun(ctx context.Context,argumentsInJSONstring,_...tool.Option)(string,error){params:=&transferParams{}sonic.UnmarshalString(argumentsInJSON,params)// 关键:通过 SendToolGenAction 把 TransferToAgentAction 附加到工具事件err=SendToolGenAction(ctx,TransferToAgentToolName,NewTransferToAgentAction(params.AgentName))returntransferToAgentToolOutput(params.AgentName),nil}

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

第四步:flowAgent 处理 transfer

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

// flow.go:554-581vardestNamestringiflastAction!=nil{iflastAction.TransferToAgent!=nil{destName=lastAction.TransferToAgent.DestAgentName}}ifdestName!=""{agentToRun:=a.getAgent(ctxForSubAgents,destName)ifagentToRun==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

func(a*flowAgent)getAgent(ctx context.Context,namestring)*flowAgent{for_,subAgent:=rangea.subAgents{ifsubAgent.Name(ctx)==name{returnsubAgent}}ifa.parentAgent!=nil&&a.parentAgent.Name(ctx)==name{returna.parentAgent}returnnil}

先在 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是关键:

funcrewriteMessage(msg Message,agentNamestring)Message{varsb strings.Builder sb.WriteString("For context:")ifmsg.Role==schema.Assistant{ifmsg.Content!=""{sb.WriteString(fmt.Sprintf(" [%s] said: %s.",agentName,msg.Content))}// tool call 也标注fori:=rangemsg.ToolCalls{f:=msg.ToolCalls[i].Function sb.WriteString(fmt.Sprintf(" [%s] called tool: `%s` with arguments: %s.",agentName,f.Name,f.Arguments))}}elseifmsg.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())returnrewritten}

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

genAgentInput:构建目标 agent 的输入

flow.go:275-328

func(a*flowAgent)genAgentInput(ctx context.Context,runCtx*runContext,skipTransferMessagesbool)(*AgentInput,error){input:=deepCopyAgentInput(runCtx.RootInput)events:=runCtx.Session.getEvents()historyEntries:=make([]*HistoryEntry,0)// 先放用户原始输入for_,m:=rangeinput.Messages{historyEntries=append(historyEntries,&HistoryEntry{IsUserInput:true,Message:m})}// 再放 session 中的所有事件for_,event:=rangeevents{// 跳过 transfer 消息(如果 skipTransferMessages=true)ifskipTransferMessages&&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=messagesreturninput,nil}

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

这就是 NOT RECOMMENDED 的原因

源码中多处标注 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

funcAgentWithDeterministicTransferTo(_context.Context,config*DeterministicTransferConfig)Agent{ifra,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

funcforwardEventsAndAppendTransfer(iter*AsyncIterator[*AgentEvent],generator*AsyncGenerator[*AgentEvent],toAgentNames[]string){varlastEvent*AgentEventfor{event,ok:=iter.Next()if!ok{break}generator.Send(event)lastEvent=event}// 如果最后一个事件没有 Exit 或 Interrupted,自动发出 transfer 事件iflastEvent!=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:

funcNewAgentTool(_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,中断恢复时独立处理
// 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:

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

(五)Host MultiAgent 的 HandOff 回调

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

callback.go:31-33

typeMultiAgentCallbackinterface{OnHandOff(ctx context.Context,info*HandOffInfo)context.Context}typeHandOffInfostruct{ToAgentNamestringArgumentstring}

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 处理 TransferToAgentchatmodel.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 隔离 + 独立 checkpointagent_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 中间件的实现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询