
千笔-AIWritePaper · https://www.aiwritepaper.com
多 Agent 系统最容易翻车的不是「少写了一个专家提示」,而是把 handoff(交接) 当成普通 tool 调用:以为只是「请同事帮一下」,实际却把对话所有权 交给了另一个 Agent。官方 Handoffs 与 Orchestrating multiple agents 写得很直:handoffs 对模型呈现为工具(如 transfer_to_refund_agent);一旦触发,专家 Agent 接管本轮后续回复。对照 agents-as-tools (Agent.as_tool()):经理仍拥有最终答复。本文按工程笔记写法,钉死选型、可跑片段、input_type / input_filter / on_handoff 边界与生产禁区。示例模型名写作 gpt-4o,以你账号可用快照与官方文档为准。

图:上方 handoff vs as_tool;中部 triage→专家与 input_type;下方生产禁区对照。
目标说明
读完你应能独立完成五件事:
- 用一句话说清:handoff = 把本轮对话所有权交给专家;as_tool = 经理保留终答并合成专家结果。
- 写出可跑片段:
Agent(handoffs=[...])或handoff(agent, ...),并打印最终由哪个 Agent 收口。 - 会用
input_type+on_handoff:交接时带结构化元数据(如 reason),且授权检查放在 on_handoff 开头。 - 知道何时用
input_filter/nest_handoff_history:改接收方可见历史,而不是靠提示词「请忘记上文」。 - 列出生产禁区:用 handoff 当鉴权、无审计直接写库、把 as_tool 与 handoff 混用却不说明所有权、无限专家扇出、把交接成功写成业务成功。
规格钉死(对照官方 Handoffs / Multi agent):
- 默认 :
handoffs可直接挂Agent,或用handoff()定制。 - 工具名 :默认
transfer_to_<agent_name>;可用tool_name_override。 - input_type :描述 handoff 工具参数 schema,校验后交给
on_handoff;不负责在多个目的地之间派发。 - 历史 :默认接收方看到完整对话史;要裁剪用
input_filter或 RunConfig 级 filter / nest。 - 护栏:单次 run 内交接;input guardrails 仍只作用于链首 Agent,output guardrails 作用于产出终答的 Agent。
适用边界
适合上 handoffs
- 客服/工单:分诊后由账单、退款、FAQ 专家直接对用户说话。
- 政策隔离:不同专家挂不同 tools 与指令,避免一个超大提示词。
- 需要交接瞬间打点:
on_handoff写审计日志或预取数据。 - 希望路由本身成为工作流的一部分,而不是经理复述专家答案。
更适合 as_tool(不要硬上 handoff)
- 经理必须合成多方结果再给用户最终答复。
- 专家只做有界子任务(摘要、分类、草稿),不应接管会话。
- 你要在外层统一 guardrails / 统一口吻。
不该指望它单独搞定
- 业务授权 :能交接 ≠ 用户有权退款;
is_enabled只控制工具是否暴露,不能替代on_handoff内的字段级审批。 - 无限开放域:专家过多会让路由本身幻觉;先合并相近职责。
- 用 input_type 选目的地 :一个
handoff()固定指向一个 agent;多目的地就注册多个 handoff。 - 服务端托管会话 + input_filter :官方注明
conversation_id/previous_response_id等路径不支持 handoff input filters。
风险提示
handoff 成功只说明控制权转移,不说明业务副作用已安全执行。on_handoff 返回成功后 SDK 继续转移------审批失败应抛错而不是静默返回。生产里若把「已 transfer」自动触发付款/删库,缺口在授权与幂等层,不在 SDK。
步骤与机制
1. 交接所有权对照
| 机制 | 谁拥有用户可见终答 | 失败时 | 典型用途 |
|---|---|---|---|
| handoffs | 被交接的专家 | 路由错/专家空转 | 客服分诊、政策隔离 |
| Agent.as_tool() | 经理 Agent | 子任务失败由经理处理 | 合成多方、统一口吻 |
| 代码编排(分类→if) | 你的代码决定 | 确定性更高 | 强合规流水线 |
| 仅提示「请转给退款同事」 | 无硬契约 | 模型口头答应却不转 | 原型演示(勿进生产) |
| input_filter | 改变专家可见史 | 过滤过猛丢上下文 | 去 tool 噪声、脱敏 |
2. 可跑:分诊 + 两个专家
先 pip install openai-agents,并导出 OPENAI_API_KEY。
python
import asyncio
from agents import Agent, Runner, handoff
MODEL = "gpt-4o" # 占位:以账号可用快照为准
billing_agent = Agent(
name="Billing agent",
handoff_description="处理账单、发票、扣款疑问",
instructions="你只回答账单相关问题,简洁给出下一步。",
model=MODEL,
)
refund_agent = Agent(
name="Refund agent",
handoff_description="处理退款申请与进度",
instructions="你只处理退款;先确认订单号,再给状态说明。",
model=MODEL,
)
triage_agent = Agent(
name="Triage agent",
instructions="根据用户意图交接给账单或退款专家,不要自己编造退款结果。",
model=MODEL,
handoffs=[billing_agent, handoff(refund_agent)],
)
async def main():
result = await Runner.run(
triage_agent,
"我要退上周那笔重复扣款,订单 ORD-1024。",
)
print(result.final_output)
# 验收:最终说话的应是 Refund 路径专家,而不是 triage 空话
asyncio.run(main())
验收:出现对退款/订单的实质性回复;tracing 或日志里能看到向 Refund agent 的 transfer,而不是 triage 假装已退款。
3. 可跑:input_type + on_handoff 审计
python
from pydantic import BaseModel
from agents import Agent, Runner, handoff, RunContextWrapper
class EscalationData(BaseModel):
reason: str
priority: str
async def on_escalation(ctx: RunContextWrapper[None], data: EscalationData):
# 断路器:先审再放行;失败请 raise,不要静默 return
if data.priority not in {"low", "mid", "high"}:
raise ValueError("invalid priority")
print("AUDIT", data.reason, data.priority) # 换成你们的审计写入
escalation = Agent(name="Escalation agent", instructions="处理升级工单。", model=MODEL)
triage = Agent(
name="Triage",
model=MODEL,
instructions="需要人工升级时调用交接,并给出 reason 与 priority。",
handoffs=[handoff(escalation, on_handoff=on_escalation, input_type=EscalationData)],
)
要点:input_type 只给本次交接工具参数 ;应用状态放 RunContextWrapper.context。授权依赖解析字段时,检查必须在 on_handoff 开头。
4. 历史过滤(可选)
常见需求:专家不要被此前大量 tool 轨迹干扰。官方提供 agents.extensions.handoff_filters.remove_all_tools 等模式。只在你明确需要时启用;过滤过猛会导致专家丢失关键槽位。推荐提示前缀见 agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX。
生产禁区
| 禁区 | 为什么危险 | 替代做法 |
|---|---|---|
| 把 handoff 当鉴权 | 模型可误路由到高权限专家 | 专家 tools 各自鉴权;on_handoff 再审 |
| 交接成功 = 业务成功 | transfer ≠ 退款已入账 | 业务状态机与幂等键 |
| 无审计直接写库 | 无法追责是谁在何时交接 | on_handoff 写 AUDIT 行 |
| 提示词格式与路由双轨互撕 | 「永远由我回答」vs handoffs 列表 | 指令与 handoffs 同审 |
| 专家扇出无上限 | 成本与错路由爆炸 | 先 2--4 个专家;其余合并 |
| 服务端会话 + input_filter 硬上 | 官方不支持该组合 | 换会话模式或去掉 filter |
| 用 as_tool 却当 handoff 宣传 | 所有权语义相反 | 对外文档写清谁终答 |
可验证清单
- 能口述 handoff 与 as_tool 的所有权差异
- 本地跑通 triage → 至少一条专家路径
-
on_handoff有审计打印或写入;非法 priority 会 raise - 生产配置里没有「裸交接即触发付款/删库」
- 专家数量与
handoff_description短而具体 - 若用 filter,有用例证明关键上下文仍在
踩坑
- 以为 handoffs 只是建议:对 LLM 是工具;不调用就不会交接。
- 一个 handoff 想动态选多人 :应注册多个目的地,让模型选;或自己写自定义
Handoff。 - 在 is_enabled 里做字段级授权:时机太早,参数还没解析。
- 把 manager 的复述当专家终答:若需要专家直接说,用 handoff;若需要合成,用 as_tool。
- 忽略 recommended prompt prefix:模型对 transfer 工具理解不稳时,先补官方前缀再调温度。
与 sessions / output_type 的协作位
多 Agent 工程里 handoffs 很少单独出现。和近期高分笔记对齐时,建议把三块能力看成正交层:
| 层 | 解决什么 | 和 handoff 的交接面 |
|---|---|---|
| sessions / 历史 | 跨轮记忆与去重 | 交接后专家看到的历史是否被 filter 改写 |
| output_type | 终答形状契约 | 专家终答若要入库,在专家 Agent 上挂类型,而不是指望 triage 散文 |
| handoffs | 本轮说话权 | 不替代鉴权,不替代类型校验 |
实践顺序建议:先定「谁终答」,再定「终答什么形状」,最后才加「跨轮怎么记」。反过来先堆专家,再补契约,生产事故率会明显高于演示。
FAQ:交接后工具权限会不会自动继承?
不会按「人类直觉」自动继承。每个 Agent 显式声明自己的 tools 与 instructions。triage 能搜工单,不代表 refund 专家也能搜------除非你给 refund 挂了同类 tool。反之,refund 若挂了「发起退款」副作用工具,即使是被误交接进来,也具备调用面;所以高危工具必须各自鉴权,不能假设「只有对的专家才会被路由到」。
最小审计字段(可直接贴进 on_handoff)
建议至少落盘:ts、from_agent、to_agent、reason、priority、user_id、run_id。没有 run_id 时,用 tracing 导出的链路 id 代替。审计行的验收标准是:出事时能在五分钟内回答「谁在何时把会话交给了谁,当时模型给出的理由是什么」。答不出 = 审计无效。
发布前代码审一眼
- handoffs 列表与提示词中的「可交接对象」一致,没有提示里点名却未注册的专家。
- 每个专家的
handoff_description不超过两句,且含触发条件。 - 无
max_turns=None与无上限副作用 tool 同时出现。 - 文档写明:演示用模型名非生产承诺。
路由提示怎么写才短而硬
handoff_description 与 triage instructions 应写成触发条件,而不是人物小传。可用模板:
- 「当用户明确提到发票、扣款、账单周期时交接 Billing。」
- 「当用户明确要求退款、撤销扣款、退货退款进度时交接 Refund。」
- 「信息不完整时先追问订单号,不要猜测已退款。」
反例:「Billing 是一位经验丰富、善于沟通的金融专家......」------冗长且不含触发条件,模型更易在边界 case 上自作主张。
并发与重入
同一用户连发两条消息时,不要假设两次 run 共享「正在交接」的内存状态,除非你显式用 sessions/自己的状态机。生产上更稳的做法是:每次 run 进入 triage 都重新路由;专家侧用业务键(订单号)做幂等。handoff 解决的是单次 run 内的说话权,不是分布式事务。
观测与回放
出事故时最少要能回答三个问题:当时暴露了哪些 handoff 工具?模型调用了哪一个?on_handoff 是否通过?把 tracing 打开,并在预发环境准备一条「故意错误 priority」用例,确认会 raise 而不是写库。回放材料进 _w/handoffs-smoke-checklist.md。
与代码编排混用
开放域入口可用 LLM triage;一旦进入「已确认退款且金额超过阈值」等强合规分支,应切到代码编排:结构化分类 → 规则选专家 → 人类审批。混用不是妥协,而是把不确定性关在前门,把确定性关在后门。文档里写清切换条件,避免同事以为「全部交给 handoffs 就好」。
端到端验收剧本(十分钟)
- 用户:「查一下 ORD-1024 的发票」。期望:Billing 路径,无退款动作。
- 用户:「这张重复扣款要退款」。期望:Refund 路径;审计行有 reason。
- 伪造
priority=urgent(若你的 schema 不允许):期望on_handoffraise,无写库。 - 关闭 Refund 的副作用 tool,仅保留查询:期望仍能解释状态,不能「假装已退」。
- 打开 tracing:确认 transfer 工具名与专家名一致。
五步全过,才算 smoke 通过。任一步靠人工脑补,记入 _w/handoffs-smoke-checklist.md 的失败栏。
文档口径
对外说明建议写:「triage 负责路由;专家负责回答;业务系统负责落账。」三句都出现,读者才不会以为 Agents SDK 替你做了支付。对内 runbook 再补:误交接回滚步骤、审计查询语句、专家临时 is_enabled=false 的开关位置。
总结
handoffs 的工程价值是清晰的对话所有权转移 ,不是「多挂几个 Agent 显得高级」。选型先问:终答该由谁说?需要元数据与审计就上 input_type/on_handoff;需要裁历史再上 filter。生产禁区的共同主题是:路由成功 ≠ 授权成功 ≠ 业务成功。把这三层拆开,多 Agent 才能从演示走进可运维。