← 返回教程库

用 OpenAI Agents SDK 实现同一个 Agent——handoff 与对比

最后更新 2026-06-25
你将学到
  • 理解 OpenAI Agents SDK 的核心概念:Agent / tools / handoff 三件套
  • 跑通一个带工具的 Agent,看到它和 Claude Agent SDK 版本产出相同效果
  • 搞清 handoff 机制——一个 Agent 把任务交给另一个专门 Agent 的全过程
  • 建立"两套 SDK 怎么选"的判断标准,不再纠结

学完 上节 Claude Agent SDK 写第一个 Agent 之后,很多人会有一个疑问:OpenAI 那套怎么写?是不是要重新学一遍?

答案是不需要。学会一套 SDK 的核心原理,换另一套只需要适应新的 API 名字——模型、工具、对话历史、循环结构,这四件事无论哪个框架都得有,只是叫法和包装方式不同。这节就带你把上节实现的功能,用 OpenAI Agents SDK 重写一遍,顺带讲清它的一个特色:handoff(代理间交接)。

这篇适合谁:跑通过上节代码,知道 Agent 的基本工作原理,想横向了解另一套主流 SDK,或者项目用 OpenAI 模型需要选型。


OpenAI Agents SDK 核心概念:三件套

OpenAI Agents SDK(Python 包名 openai-agents,以官方为准)围绕三个核心概念构建:

Agent 和 Claude 版本的"LLM + 工具列表 + 循环"一样,Agent 是主角。它接收一段指令(instructions),挂载工具列表,然后自己决定什么时候调工具、什么时候给出最终答案。

from agents import Agent

my_agent = Agent(
    name="助手",
    instructions="你是一个帮用户查询信息的助手,遇到需要查时间的问题请使用工具。",
    tools=[get_current_time],   # 工具后面定义
)

tools 工具是普通 Python 函数,加一个 @function_tool 装饰器就行。装饰器会自动从函数签名和 docstring 里提取名字、描述、参数 schema,不需要手写那一大段 JSON。这是和 Claude SDK 写法感觉差异最大的地方——本质是一样的,只是 OpenAI 这套用装饰器帮你自动生成 schema。

handoff 这是 OpenAI Agents SDK 最有特色的地方。handoff 是"把当前对话的控制权转交给另一个 Agent"——主 Agent 判断这个任务超出自己的专长范围,就把整条消息历史 + 控制权移交给另一个专门的 Agent 去接着干。和工具调用不同:工具调用是"帮我查一下",调完控制权还回来;handoff 是"这件事归你了",控制权转移不回来(除非那个 Agent 再 handoff 出去)。


环境准备

# 安装 SDK(以官方文档为准,截稿 2026-06)
pip install openai-agents

# 验证
python -c "import agents; print('ok')"

配置 API key:

# macOS / Linux
export OPENAI_API_KEY="sk-..."

# Windows PowerShell
$env:OPENAI_API_KEY = "sk-..."

可跑代码:带工具的 Agent

下面这段是完整可复制粘贴直接跑的代码,实现和上节完全相同的功能——查当前时间,再扩展一个查目录文件的工具。

import asyncio
from agents import Agent, Runner, function_tool

# ────────────────────────────────────────────────
# 1. 定义工具——直接用装饰器,docstring 就是描述
# ────────────────────────────────────────────────
@function_tool
def get_current_time(timezone: str = "Asia/Shanghai") -> str:
    """
    获取当前日期和时间,用来回答"现在几点"或"今天几号"一类的问题。
    timezone: 时区,例如 'Asia/Shanghai',默认用上海时间。
    """
    try:
        from datetime import datetime
        import pytz
        tz = pytz.timezone(timezone)
        now = datetime.now(tz)
        return now.strftime(f"%Y-%m-%d %H:%M:%S ({timezone})")
    except Exception:
        from datetime import datetime
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S (本地时间)")


@function_tool
def list_directory(path: str) -> str:
    """
    列出指定目录下的文件和子文件夹,用来了解目录结构。
    path: 要列出的目录路径,例如 '/tmp'。
    """
    import os
    try:
        entries = os.listdir(path)
        if not entries:
            return f"'{path}' 是空目录"
        files = [e for e in entries if os.path.isfile(os.path.join(path, e))]
        dirs  = [e for e in entries if os.path.isdir(os.path.join(path, e))]
        result = f"目录 '{path}' 包含:\n"
        if dirs:
            result += "  文件夹:" + ", ".join(dirs) + "\n"
        if files:
            result += "  文件:" + ", ".join(files)
        return result
    except PermissionError:
        return f"没有权限访问 '{path}'"
    except FileNotFoundError:
        return f"路径 '{path}' 不存在"


# ────────────────────────────────────────────────
# 2. 创建 Agent
# ────────────────────────────────────────────────
assistant = Agent(
    name="通用助手",
    instructions="你是一个帮用户查询信息的助手。需要查时间时用 get_current_time,需要看目录时用 list_directory。",
    model="gpt-4o",   # 以 OpenAI 官方文档模型列表为准(截稿 2026-06)
    tools=[get_current_time, list_directory],
)


# ────────────────────────────────────────────────
# 3. 用 Runner 跑起来(SDK 内部封装了循环)
# ────────────────────────────────────────────────
async def main():
    print("提问:现在上海是几点?今天是几号?")
    result = await Runner.run(
        assistant,
        input="现在上海是几点?今天是几号?",
    )
    print(f"Agent 回答:{result.final_output}")

    print("\n提问:帮我看看 /tmp 目录下有什么文件")
    result2 = await Runner.run(
        assistant,
        input="帮我看看 /tmp 目录下有什么文件",
    )
    print(f"Agent 回答:{result2.final_output}")


if __name__ == "__main__":
    asyncio.run(main())

装依赖: pip install openai-agents pytz

你应该看到什么

跑通后终端输出类似:

提问:现在上海是几点?今天是几号?
Agent 回答:现在上海时间是 2026 年 6 月 25 日 14:32,下午两点半多。

提问:帮我看看 /tmp 目录下有什么文件
Agent 回答:/tmp 目录下包含:文件夹:... 文件:...

和上节 Claude Agent SDK 版本产出相同的效果。差别在于:这里你没有手写 while True 循环,也没有手写 stop_reason 判断——Runner.run() 把这些都封装掉了。内部原理完全一样,只是 SDK 帮你省了样板代码。


handoff 特色讲透

handoff 是 OpenAI Agents SDK 在多 Agent 场景里的核心设计,值得单独讲清楚。

场景:用户发了一段话,可能是问时间(通用助手能处理),也可能是一道专业的财务问题(应该交给财务专员 Agent)。与其让一个 Agent 塞满所有专业知识,不如让主 Agent 做路由,识别类型后把任务"转交"给对应的专员。

from agents import Agent, Runner, handoff

# ────────────────────────────────────────────────
# 专员 Agent:只处理财务问题
# ────────────────────────────────────────────────
finance_agent = Agent(
    name="财务专员",
    instructions=(
        "你是财务专员,专门处理预算分析、报销审核、财务建议。"
        "收到问题后给出专业、简洁的财务建议。"
    ),
    model="gpt-4o",   # 以官方为准(截稿 2026-06)
)

# ────────────────────────────────────────────────
# 主路由 Agent:判断类型,决定自己回答还是 handoff
# ────────────────────────────────────────────────
triage_agent = Agent(
    name="主路由",
    instructions=(
        "你是分发路由。收到用户消息后判断类型:"
        "如果是财务/预算/报销相关问题,使用 transfer_to_finance_agent 移交给财务专员;"
        "其他问题直接回答。"
    ),
    model="gpt-4o",
    handoffs=[handoff(finance_agent)],   # 挂载 handoff 目标
)


async def demo_handoff():
    # 这条应该直接回答
    result = await Runner.run(triage_agent, input="你好,今天天气怎么样?")
    print(f"[通用问题] Agent 回答:{result.final_output}")

    # 这条应该 handoff 给财务专员
    result2 = await Runner.run(
        triage_agent,
        input="我们部门下季度预算超了 15%,有什么快速缩减的建议?",
    )
    print(f"[财务问题] Agent 回答:{result2.final_output}")


if __name__ == "__main__":
    asyncio.run(demo_handoff())

handoff 发生时内部发生了什么:

  1. 主路由 Agent 判断这是财务问题,决定调用 transfer_to_finance_agent
  2. SDK 把整条消息历史 + 上下文 打包,传给 finance_agent
  3. finance_agent 接手,在它自己的 instructions 框架下处理,给出最终答案
  4. Runner.run() 返回的 final_output 是财务专员的回答,不是主路由的

这和工具调用(tool use)的区别要记住:

对比项 tool use(工具调用) handoff(代理交接)
控制权 调完工具,控制权回到原 Agent 控制权转移给新 Agent,不回来
适合场景 原 Agent 能处理,只需要数据或动作 超出原 Agent 专长,需要换专门的"人"
上下文 工具只拿到调用参数 新 Agent 能看到完整对话历史
结果汇报 工具结果回传给原 Agent 继续决策 新 Agent 直接给最终答案

实际项目里,handoff 是构建专员矩阵的核心手段:一个轻量路由 Agent 在最前面做分发,后面挂一排专员(客服专员、财务专员、法务专员……),每个专员只装自己领域的知识和工具。这种模式在编排成本与子代理对话 handoff 里有更深入的讨论。


和 Claude Agent SDK 横向对比

两套 SDK 的底层逻辑完全相同,差异在 API 设计风格和生态侧重上:

维度 Claude Agent SDK (Anthropic) OpenAI Agents SDK
核心对象 anthropic.Anthropic() + 手写循环 Agent 类 + Runner.run()
工具定义 手写 JSON schema 的 tools 列表 @function_tool 装饰器自动生成
循环控制 自己写 while True + 判断 stop_reason Runner 封装,asyncio 异步
多 Agent 自己拼调用链,或用 subagent 模式 handoff 原生支持,声明式配置
模型绑定 默认用 Claude 系列,可换 默认用 GPT 系列,可换
透明度 高,自己写循环,每一步可 print 较低,Runner 封装了细节
TypeScript 支持 @anthropic-ai/sdk 同等完整 openai-agents 主推 Python,TS 版另维护
Tracing / 可观测 需自己接入 内置 tracing,可接 OpenTelemetry

两套 SDK 的原理完全一致:都是模型 + 工具描述 + 消息历史 + 循环。学会一套之后,换另一套主要是适应 API 名字和封装风格,不是重新理解概念。


两套 SDK 怎么选

不要纠结"哪个更好",看你的实际约束:

选 Claude Agent SDK 的场景:

  • 你要用 Claude 系列模型(技术能力、长上下文有优势)
  • 想要更高的透明度,每一步循环都想自己控制、调试
  • 项目要嵌进自己的代码库,不想依赖额外框架
  • 国内部署,Anthropic 的访问路径更可控

选 OpenAI Agents SDK 的场景:

  • 你要用 GPT 系列模型,或者已有 OpenAI 账号和配额
  • 想快速搭多 Agent 系统,handoff 原生支持省不少代码
  • 需要内置 tracing 和可观测能力,调试多 Agent 流程
  • 团队对 async/await 更熟悉,能接受 Runner 的封装方式

两套都不考虑的场景:

  • 需要复杂的图状流程(分支、循环、回退)→ 看 裸 SDK 收缩编排,或考虑 LangGraph 这类专门的编排框架
  • 生产级多 Agent 系统需要精细的成本控制和上下文隔离 → 先读 AI Agent 智能体阶梯 L3 以上的编排篇

一句话:两套 SDK 是互补选项,不是竞争关系。技术层面都能跑,选型主要看你用哪家模型、以及多 Agent 场景的复杂度。


故障排查表

症状 原因 解法
AuthenticationError / Incorrect API key OPENAI_API_KEY 没配或写错 确认 echo $OPENAI_API_KEY 有值;重新 export 或写进 .env
pip install openai-agents 报依赖冲突 和旧版 openai 包版本冲突 pip install --upgrade openai openai-agents;或在虚拟环境里重装
Model not found / The model does not exist 模型名写错,或该账号无权限 OpenAI 官方文档模型列表 确认可用模型名(截稿 2026-06)
Agent 没调工具,直接猜答案 @function_tool 的 docstring 描述不够准确 优化 docstring,把"什么情况该用这个工具"写清楚;原理和 Claude SDK 版的 description 完全一样
handoff 没触发,主路由 Agent 自己回答了 instructions 里的 handoff 触发条件描述模糊 instructions 里明确"遇到 X 类问题必须使用 transfer_to_xxx";条件越具体越好
RuntimeError: no running event loop 在非 async 环境里调用了 await Runner.run() 把入口改成 asyncio.run(main());或者用 Runner.run_sync() 同步版(以官方文档为准)

常见问题

Q:OpenAI Agents SDK 和旧的 openai 包有什么关系?是同一个吗?

不是同一个。openai 包是底层 API 客户端,直接调 Chat Completions / Responses API;openai-agents 是基于 openai 包封装的 Agent 框架,提供了 Agent 类、Runnerhandoff 这些高层抽象。两者可以同时装,openai-agents 内部依赖 openai 包。

Q:handoff 之后,旧 Agent 还能拿回控制权吗?

原生 handoff 是单向的——一旦交出去,控制权归新 Agent,旧 Agent 不会自动拿回来。如果你需要双向交接、或者多轮 handoff 再汇总,那就要手动设计对话流,或者用更复杂的编排模式。这类场景在编排成本与子代理对话 handoff 里有详细讨论。

Q:两套 SDK 能混用吗?比如用 OpenAI Agents SDK 调 Claude 模型?

理论上可以,但需要通过 OpenAI 兼容接口或中间代理层。实际项目里建议不要混——用哪家模型就用哪家的 SDK,维护成本最低,文档也最准确。

Q:这节代码在 Windows 上跑,asyncio 有什么坑吗?

Windows 的 asyncio 事件循环实现(ProactorEventLoop)和 macOS/Linux 有些差异。如果遇到 RuntimeError: Event loop is closed 之类的报错,可以在入口加一行 asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()),或者升级到 Python 3.12+(官方改善了 Windows 兼容性)。以 openai-agents 官方文档为准(截稿 2026-06)。


小结

  • OpenAI Agents SDK 三件套:Agent(指令 + 工具 + 模型)、@function_tool(装饰器自动生成 schema)、handoff(代理间交接)。
  • 和 Claude Agent SDK 的本质完全一样:模型 + 工具 + 历史 + 循环。API 名字不同,原理相通。
  • handoff 的核心区别:工具调用是"帮我做一件事",控制权回来;handoff 是"这件事归你了",控制权转移。
  • 选型不是非此即彼:用哪家模型、需不需要原生 handoff、要不要透明控制循环——这三个问题答完,选型自然清楚。

跑通了两套 SDK,下一步可以去看 裸 SDK 收缩编排,了解什么情况下可以不用框架直接收缩代码;或者翻 AI Agent 智能体阶梯 L3 以上,看真实多 Agent 系统是怎么设计的。

👉 看看 AI 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务

📄 来源 / 自校链接

本文为学习整理,关键步骤与代码请结合下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为学习与落地整理,AI 工具与平台更新较快,关键步骤请结合官方最新资料验证。见免责声明