
字节笔记本
2026年10月24日 · 约 18 分钟读完
Agent 之间的交接棒:Handoff 模式

本文是《从 0 写 Agent》系列第 15 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Handoff:Agent 之间的"交接棒"」。
一个 Agent 干所有事,指令会臃肿、工具会过多、模型容易混乱。复杂任务应该分工,客服 Agent 接待、账单 Agent 处理付款、技术 Agent 解答问题,它们之间交接。
Handoff(交接)就是 Agent 之间的"交接棒"机制。这一章拆它的本质,handoff 不是特殊的运行时机制,它就是一个暴露给模型的工具。模型"调用"这个工具,runner 就切换 active agent。
引子:为什么需要多个 Agent
想象一个电商客服系统,用单个 Agent:
agent := &agents.Agent{
Name: "全能客服",
Instructions: "你是客服。处理退货、退款、物流、产品咨询、投诉...",
Tools: []agents.Tool{
// 退货、退款、物流查询、产品库查询、投诉记录...20 个工具
},
}问题:
- 指令臃肿(要讲清楚 5 种业务的处理流程)
- 工具过多(20 个工具的 schema 全发给模型,token 爆炸)
- 模型混乱(工具多了,模型不知道该调哪个)
更好的做法,分工:
triageAgent := &agents.Agent{
Name: "分诊",
Instructions: "判断用户问题类型,交接给对应专家",
Handoffs: []agents.Handoff{
agents.HandoffTo(refundAgent),
agents.HandoffTo(shippingAgent),
agents.HandoffTo(productAgent),
},
}
refundAgent := &agents.Agent{
Name: "退款专家",
Instructions: "你只处理退款...",
Tools: []agents.Tool{ refundTool, policyTool },
}分诊 Agent 接待用户,判断问题类型,handoff 给对应专家。每个 Agent 指令聚焦、工具精简、模型不混乱。

原理:Handoff 是个工具
这是理解 handoff 的关键,它不是特殊的运行时机制,它就是一个普通的工具,暴露给模型。
看 Handoff 结构(handoff.go:20),它有 ToolName、ToolDescription,和普通工具一模一样。模型看到的是"有个工具叫 transfer_to_refund,描述是'交接给退款专家'"。
当模型"调用"这个工具时,runner 不执行业务逻辑,而是,切换 active agent。
模型(分诊 Agent)说:调用 transfer_to_refund
→ runner 不执行函数,而是:
→ active agent = refundAgent
→ 下一轮循环,refundAgent 接管这就是 handoff 的全部魔法,它是个"伪工具",调用它不是执行代码,是切换控制权。 这个设计极其优雅,复用了已有的工具调用机制(function calling),不用为 handoff 单独造一套。
源码对照:HandoffTo 便捷构造
HandoffTo(handoff.go:65)是最常用的构造方式:
// agents/handoff.go:65
func HandoffTo(target *Agent) Handoff {
desc := "Handoff to the " + target.Name + " agent to handle the request."
return Handoff{
ToolName: transformToolName("transfer_to_" + target.Name), // transfer_to_refund
ToolDescription: desc,
AgentName: target.Name,
OnInvoke: func(context.Context, *RunContext, string) (*Agent, error) {
return target, nil // 直接返回目标 agent
},
}
}自动生成 transfer_to_<name> 的工具名。transformToolName(handoff.go:55)做命名规范化,空格变下划线、非法字符变下划线、全小写:
// agents/handoff.go:55
func transformToolName(name string) string {
return strings.ToLower(invalidToolNameChars.ReplaceAllString(
strings.ReplaceAll(name, " ", "_"), "_"))
}比如 target.Name 是 "退款专家",工具名变成 transfer_to_退款专家(中文会保留,因为正则只匹配非字母数字下划线)。一般建议用英文名,得到 transfer_to_refund。
命名约定为什么重要
transfer_to_ 前缀不是随便定的,它是一个约定,让模型理解"这是交接工具"。模型在训练时见过大量 transfer_to_xxx 的模式,它知道调用这种工具意味着"转交"。
好命名是 prompt 工程的一部分,模型对 transfer_to_refund 的理解,比对 tool_1 或 switch_agent 准确得多。agents-go 沿用 Python SDK 的命名约定,保证模型行为一致。
Handoff 的完整字段
Handoff 不只是工具名+目标,它还有几个高级字段(handoff.go:20):
| 字段 | 干什么 |
|---|---|
ToolName / ToolDescription | 暴露给模型的名字和描述 |
InputJSONSchema | handoff 可以带输入(传给目标 Agent 的参数) |
OnInvoke | 返回目标 Agent(可以动态决定) |
OnHandoff | 切权前的副作用回调(日志、数据预取) |
InputFilter | 改写目标 Agent 看到的对话历史 |
IsEnabled | 动态门控(条件交接) |
OnInvoke 可以动态决定目标
HandoffTo 的 OnInvoke 是固定的(返回 target)。但你可以自定义,根据调用参数动态决定交接给谁:
dynamicHandoff := agents.Handoff{
ToolName: "route_request",
OnInvoke: func(ctx, rc, argsJSON string) (*agents.Agent, error) {
var args struct{ Urgency string }
json.Unmarshal([]byte(argsJSON), &args)
if args.Urgency == "high" {
return seniorAgent, nil // 紧急 → 资深
}
return juniorAgent, nil // 普通 → 初级
},
}动态路由,同一个 handoff 工具,根据参数交接给不同 Agent。 适合"智能分诊"场景。
OnHandoff 的副作用钩子
OnHandoff(handoff.go:39)在切权前调用,做副作用,
handoff := agents.Handoff{
OnHandoff: func(ctx, rc, argsJSON string) error {
// 记录交接日志
log.Printf("handoff to %s", target.Name)
// 预取数据给目标 agent 用
rc.SetValue("customer_history", fetchHistory(rc.UserID))
return nil
},
}注意它和 OnInvoke 的区别,OnInvoke 返回目标 Agent(决定交接给谁),OnHandoff 做副作用(不影响交接决策,但为目标 Agent 准备数据)。
切权 vs 嵌套:Handoff vs Agent-as-Tool
这是 Part III 最核心的概念区分。Handoff 是"切权",还有另一种叫"Agent-as-Tool"(第 18 章详讲)的"嵌套"。先看区别:
Handoff(切权)
用户 → 分诊 Agent → [调用 transfer_to_refund]
↓
refundAgent 接管 ←─ 看到完整对话历史
↓
refundAgent 跑到底,返回给用户
分诊 Agent 退出,不再参与特点:目标 Agent 接管整个 Run,看到完整历史,跑完直接返回用户。原 Agent 退出。
Agent-as-Tool(嵌套)
用户 → 主 Agent → [调用 research_agent_as_tool("查 X")]
↓
researchAgent 跑一轮(只看到"查 X")
↓
返回结果给主 Agent ←─ 控制权归还
↓
主 Agent 继续处理特点:子 Agent 只收到传入的 input,跑完把结果返回给主 Agent,控制权归还。主 Agent 继续主导。
怎么选
| 场景 | 用哪个 |
|---|---|
| 不同领域专家接力(客服→退款→物流) | Handoff(接力棒传递) |
| 主 Agent 需要子任务结果继续决策 | Agent-as-Tool(调用返回) |
| 一次性委托,不需要回来 | Handoff |
| 反复调用不同子能力 | Agent-as-Tool |
类比,Handoff 像把客户转接给另一个客服(原客服挂线);Agent-as-Tool 像主客服问一下技术同事再回复客户(主客服不挂线)。
设计权衡:为什么 Handoff 复用工具机制
有人会想,为什么不为 handoff 单独设计一套运行时机制(比如特殊的"handoff event")?
复用工具机制有三个好处:
- 模型已经懂 function calling。 不用训练模型理解新的"handoff"概念,它就是调用一个工具。
- 代码复用。 handoff 的解析、执行,复用了第 4 章的工具调用流水线(分类→执行→...),只差"执行时切 agent 而非跑函数"。
- 统一的心智模型。 开发者不用学两套机制,"工具调用"一套搞定普通工具和 handoff。
代价,handoff 看起来像工具但行为不同(切权而非执行),可能让初学者困惑。但这个困惑是值得的,因为它换来了巨大的简洁性。
这也是第 4 章 processModelResponse 里"handoff 优先"的原因,一个 function_call 先查 handoffMap,命中就当 handoff 处理,不走普通工具逻辑。用查找表的优先级,在同一个机制里区分两种行为。
handoff 在循环里怎么流转
把 handoff 放回 Agent 循环看(第 1 章的循环):
for turn {
resp = model.GetResponse(history, tools+handoffs)
if resp 有 handoff 调用 {
target = handoff.OnInvoke() // 决定交接给谁
handoff.OnHandoff() // 副作用
active_agent = target // 切换!
// 可选:handoff.InputFilter 改写历史
continue // 下一轮用新 agent
}
if resp 有普通工具调用 { 执行工具 }
if resp 是最终答案 { return }
}关键,切换 agent 后,循环继续,但用的是新 agent 的指令、工具、模型配置。 这就是"交接棒",同一个循环,换了跑者。
而且,新 agent 也能有自己的 handoffs,它可以再交接给下一个 agent。形成交接链:分诊 → 退款 → 主管审批。每一步都是一次 handoff。
小结
读完这一章,你应该理解:
- Handoff 的本质?,一个暴露给模型的工具,调用它不是执行代码,是切换 active agent
- 为什么复用工具机制?,模型已懂 function calling、代码复用、统一心智模型
transfer_to_命名约定?,模型训练时见过这个模式,理解"交接"语义- Handoff vs Agent-as-Tool?,切权(接力棒)vs 嵌套(调用返回)
- OnInvoke vs OnHandoff?,前者决定交接给谁(可动态),后者做副作用
Handoff 让 Agent 从"单打独斗"变成"团队协作"。 但交接有个问题,目标 Agent 要看到对话历史,历史怎么处理?原样全传?还是过滤?这是第 16 章的内容。
下一篇
第 16 章,Handoff 的历史处理。目标 Agent 接管时,看到的是完整历史还是过滤后的?InputFilter 怎么改写目标 Agent 的视角?为什么 handoff 后要重置 previousResponseID?还有 agents-go 一个精妙的设计,双 item 列表(generatedItems vs sessionItems)。
本文是《从 0 写 Agent》系列第 15 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。
