ByteNoteByteNote
Agent 之间的交接棒:Handoff 模式
字

字节笔记本

2026年10月24日 · 约 18 分钟读完

Agent 之间的交接棒:Handoff 模式

API中转
¥120

Subagent:并行与任务拆分

本文是《从 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:

go
agent := &agents.Agent{
    Name: "全能客服",
    Instructions: "你是客服。处理退货、退款、物流、产品咨询、投诉...",
    Tools: []agents.Tool{
        // 退货、退款、物流查询、产品库查询、投诉记录...20 个工具
    },
}

问题:

  • 指令臃肿(要讲清楚 5 种业务的处理流程)
  • 工具过多(20 个工具的 schema 全发给模型,token 爆炸)
  • 模型混乱(工具多了,模型不知道该调哪个)

更好的做法,分工:

go
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 指令聚焦、工具精简、模型不混乱。


Agent 循环:一切的基础

原理:Handoff 是个工具

这是理解 handoff 的关键,它不是特殊的运行时机制,它就是一个普通的工具,暴露给模型。

看 Handoff 结构(handoff.go:20),它有 ToolName、ToolDescription,和普通工具一模一样。模型看到的是"有个工具叫 transfer_to_refund,描述是'交接给退款专家'"。

当模型"调用"这个工具时,runner 不执行业务逻辑,而是,切换 active agent。

text
模型(分诊 Agent)说:调用 transfer_to_refund
  → runner 不执行函数,而是:
  → active agent = refundAgent
  → 下一轮循环,refundAgent 接管

这就是 handoff 的全部魔法,它是个"伪工具",调用它不是执行代码,是切换控制权。 这个设计极其优雅,复用了已有的工具调用机制(function calling),不用为 handoff 单独造一套。


源码对照:HandoffTo 便捷构造

HandoffTo(handoff.go:65)是最常用的构造方式:

go
// 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)做命名规范化,空格变下划线、非法字符变下划线、全小写:

go
// 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暴露给模型的名字和描述
InputJSONSchemahandoff 可以带输入(传给目标 Agent 的参数)
OnInvoke返回目标 Agent(可以动态决定)
OnHandoff切权前的副作用回调(日志、数据预取)
InputFilter改写目标 Agent 看到的对话历史
IsEnabled动态门控(条件交接)

OnInvoke 可以动态决定目标

HandoffTo 的 OnInvoke 是固定的(返回 target)。但你可以自定义,根据调用参数动态决定交接给谁:

go
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)在切权前调用,做副作用,

go
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(切权)

text
用户 → 分诊 Agent → [调用 transfer_to_refund]
                              ↓
                        refundAgent 接管 ←─ 看到完整对话历史
                              ↓
                        refundAgent 跑到底,返回给用户
分诊 Agent 退出,不再参与

特点:目标 Agent 接管整个 Run,看到完整历史,跑完直接返回用户。原 Agent 退出。

Agent-as-Tool(嵌套)

text
用户 → 主 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")?

复用工具机制有三个好处:

  1. 模型已经懂 function calling。 不用训练模型理解新的"handoff"概念,它就是调用一个工具。
  2. 代码复用。 handoff 的解析、执行,复用了第 4 章的工具调用流水线(分类→执行→...),只差"执行时切 agent 而非跑函数"。
  3. 统一的心智模型。 开发者不用学两套机制,"工具调用"一套搞定普通工具和 handoff。

代价,handoff 看起来像工具但行为不同(切权而非执行),可能让初学者困惑。但这个困惑是值得的,因为它换来了巨大的简洁性。

这也是第 4 章 processModelResponse 里"handoff 优先"的原因,一个 function_call 先查 handoffMap,命中就当 handoff 处理,不走普通工具逻辑。用查找表的优先级,在同一个机制里区分两种行为。


handoff 在循环里怎么流转

把 handoff 放回 Agent 循环看(第 1 章的循环):

text
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。


小结

读完这一章,你应该理解:

  1. Handoff 的本质?,一个暴露给模型的工具,调用它不是执行代码,是切换 active agent
  2. 为什么复用工具机制?,模型已懂 function calling、代码复用、统一心智模型
  3. transfer_to_ 命名约定?,模型训练时见过这个模式,理解"交接"语义
  4. Handoff vs Agent-as-Tool?,切权(接力棒)vs 嵌套(调用返回)
  5. 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 移植版)。全系列目录见总览。

相关文章

分享: