用 OpenAI Agents SDK 实现同一个 Agent——handoff 与对比
- 理解 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 发生时内部发生了什么:
- 主路由 Agent 判断这是财务问题,决定调用
transfer_to_finance_agent - SDK 把整条消息历史 + 上下文 打包,传给
finance_agent finance_agent接手,在它自己的instructions框架下处理,给出最终答案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 类、Runner、handoff 这些高层抽象。两者可以同时装,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 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务。