OpenAI Agents SDK 工程笔记:handoffs 多 Agent 交接与生产禁区

千笔-AIWritePaper · https://www.aiwritepaper.com

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

图:上方 handoff vs as_tool;中部 triage→专家与 input_type;下方生产禁区对照。

目标说明

读完你应能独立完成五件事:

  1. 用一句话说清:handoff = 把本轮对话所有权交给专家;as_tool = 经理保留终答并合成专家结果。
  2. 写出可跑片段:Agent(handoffs=[...])handoff(agent, ...),并打印最终由哪个 Agent 收口。
  3. 会用 input_type + on_handoff:交接时带结构化元数据(如 reason),且授权检查放在 on_handoff 开头
  4. 知道何时用 input_filter / nest_handoff_history:改接收方可见历史,而不是靠提示词「请忘记上文」。
  5. 列出生产禁区:用 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,有用例证明关键上下文仍在

踩坑

  1. 以为 handoffs 只是建议:对 LLM 是工具;不调用就不会交接。
  2. 一个 handoff 想动态选多人 :应注册多个目的地,让模型选;或自己写自定义 Handoff
  3. 在 is_enabled 里做字段级授权:时机太早,参数还没解析。
  4. 把 manager 的复述当专家终答:若需要专家直接说,用 handoff;若需要合成,用 as_tool。
  5. 忽略 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)

建议至少落盘:tsfrom_agentto_agentreasonpriorityuser_idrun_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 就好」。

端到端验收剧本(十分钟)

  1. 用户:「查一下 ORD-1024 的发票」。期望:Billing 路径,无退款动作。
  2. 用户:「这张重复扣款要退款」。期望:Refund 路径;审计行有 reason。
  3. 伪造 priority=urgent(若你的 schema 不允许):期望 on_handoff raise,无写库。
  4. 关闭 Refund 的副作用 tool,仅保留查询:期望仍能解释状态,不能「假装已退」。
  5. 打开 tracing:确认 transfer 工具名与专家名一致。

五步全过,才算 smoke 通过。任一步靠人工脑补,记入 _w/handoffs-smoke-checklist.md 的失败栏。

文档口径

对外说明建议写:「triage 负责路由;专家负责回答;业务系统负责落账。」三句都出现,读者才不会以为 Agents SDK 替你做了支付。对内 runbook 再补:误交接回滚步骤、审计查询语句、专家临时 is_enabled=false 的开关位置。

总结

handoffs 的工程价值是清晰的对话所有权转移 ,不是「多挂几个 Agent 显得高级」。选型先问:终答该由谁说?需要元数据与审计就上 input_type/on_handoff;需要裁历史再上 filter。生产禁区的共同主题是:路由成功 ≠ 授权成功 ≠ 业务成功。把这三层拆开,多 Agent 才能从演示走进可运维。

相关推荐
时空未宇2 小时前
Codex 桌面版通过 SSH 访问 Docker 编译环境
语言模型·openai·codex
www.0218 小时前
Codex额度用完怎么办?Windows定时自动继续VS Code Codex任务实战
人工智能·windows·vscode·自动化·openai·codex·autohotkey
9i编程1 天前
20. 对SKILL进行一次全新尝试,改为框架+细节方式的实践及验证:Android 代码调试实录
人工智能·openai·ai编程
全栈弄潮儿1 天前
周复盘:这一周最值得保存的 7 条 AI 编程原则
aigc·openai·ai编程
全栈弄潮儿2 天前
需求不清时,如何让 AI 帮你补全问题,而不是瞎写代码
aigc·openai·ai编程
xn71332 天前
实测 Codex CLI 0.154.0:response.failed 为什么会被 idle timeout waiting for SSE 覆盖
python·openai
AIGC大时代2 天前
OpenAI Agents SDK 工程笔记:tool 调用循环、max_turns 与生产禁区
服务器·数据库·笔记·tool·max_turns·functiontool·生产禁区
VIP_CQCRE2 天前
Visual Studio 也能接入 AI 编程:用 Ace Data Cloud 快速配置 LMLocal
openai·ai编程·visual studio·ace data cloud
Summer-Bright2 天前
深度 | OpenAI 联合三星造芯:从模型层向底层算力延伸,AI 算力供应链开始重新洗牌
人工智能·ai·openai·芯片