这两年做AI应用,Agent框架我算是折腾了不少。从最早手撸ReAct循环,到后来用LangChain搭复杂链路,再到各类编排框架轮番换,踩坑踩得多了,对"框架到底该帮开发者解决什么"这件事特别敏感。直到我把OpenAI官方的Agents SDK搬进一个实际项目里做多智能体协作,才第一次有种"官方终于把该做的脏活都接住了"的感觉。
这篇是OpenAI Agents SDK构建指南的第一篇,目标是带你用一个下午的时间搞清楚三件事:这个SDK到底好在哪、核心概念怎么理解、以及怎么从零跑通一个带工具和智能体交接的完整应用。适合两类人——一类是已经用LangChain或者CrewAI写过东西、想对比一下官方方案的;另一类是刚接触Agent开发、想找个轻量框架直接上手的小白。我可以负责任地说,OpenAI Agents SDK是目前我见过的最"克制"的框架,它没有给你一堆抽象概念,但恰恰是这种克制,让它特别适合直接在生产里落地。
1. 为什么偏偏是它:Agents SDK背后的设计逻辑
1.1 OpenAI的Agent框架进化路线
很多人不知道,在Agents SDK出现之前,OpenAI官方其实先发布过一个叫Swarm的实验项目。Swarm的设计很有意思,它把Agent当成一个可以互相"交接"的实体,用handoff函数在多个Agent之间切换执行权。我当时玩Swarm的时候就觉得这个思路很干净——大量Agent应用本质上就是一个"路由+执行"问题:判断请求要谁来处理,把控制权交过去,处理完再交回来。Swarm的问题在于它确实太实验了,没有官方工具集成,没有可观测性,基本就是一套API的手写封装。
Agents SDK可以理解成Swarm的正式生产版。它保留了Swarm里最精华的handoff机制,同时补齐了普通业务系统真正需要的三块拼图:内置工具全家桶、全链路追踪、输入输出护栏。官方在发布时也明确说了,这套SDK要把Agent开发从"在代码里控制循环"转变为"让SDK接管循环,你只管配置Agent的行为"。这个理念我非常认同。
还有个容易被忽略的细节:Agents SDK默认走的是OpenAI的Responses API,这是专门为Agent设计的新接口。如果你用过Chat Completions,会发现Responses API的响应结构里多了一块output,里面会明确列出每一步的function_call、message、reasoning等条目,也就是说"模型内部走了几步、调了哪些工具"这件事第一次变成了结构化数据,而不是靠人在字符串里硬解析。SDK会自动把这些输出转换成Agent的完整执行上下文,这对复杂多步任务的稳定性帮助非常大。
1.2 轻量优先:能少写就少写
我第一次打开Agents SDK的文档,第一反应是"就这?"——核心概念屈指可数:Agent、Runner、Tool、Handoff、Guardrail、Session。没有Chain,没有Graph,没有Memory模块,没有各种花式抽象。我当时还有点担心这会不会表达能力不够,用下来才发现恰恰相反。
以LangChain为例,早期写一个带工具的Agent,要理解PromptTemplate、LLMChain、Tool、AgentExecutor、Memory这一堆概念,概念之间还有版本差异,升级一次破一次。Agents SDK里就一个Agent对象,你把instructions、tools、handoffs、model、guardrails装进去,再丢给Runner.run_sync,剩下的事情SDK全接管了。这个概念模型极其贴合人类直觉:一个Agent就是"一个角色加它的工具箱和权限范围"。
这不是功能阉割,而是一种明确的设计取舍。Agent框架最该负责的其实是三件事:控制循环、工具调度、上下文管理。这三件事SDK都帮你做了,而且做得比多数手写方案稳。你要做的反而是最不该被框架替代的事情——设计指令、设计工具边界、设计人机交互的流程。某种程度上,Agents SDK像是在逼你把精力花在刀刃上。
1.3 和主流框架的横向对比
我用了很长一段时间LangChain,也短暂试过CrewAI和AutoGen,它们各有特点,但很难说哪一个是无脑最优解。我整理了一张对比表,按我实际使用的体感打分:
| 维度 | OpenAI Agents SDK | LangChain / LangGraph | CrewAI | AutoGen |
|---|---|---|---|---|
| 核心抽象数量 | 极少,5个左右 | 非常多,学习曲线陡 | 中等,角色概念清晰 | 较多,ConversableAgent体系 |
| Agent循环控制 | SDK内置,不可见但稳定 | 可自定义,但需要理解图结构 | 内置,配置为主 | 内置,支持人机混合循环 |
| 多Agent协作 | Handoff原生支持 | 需自行设计图节点 | 角色+任务分配 | 群聊模式,偏研究 |
| 官方工具集成 | 好(WebSearch/FileSearch等) | 弱,依赖第三方 | 一般 | 弱 |
| 可观测性 | 内置Tracing,默认开启 | 需要单独接 | 一般 | 一般 |
| 上手难度 | 低 | 高 | 中 | 中高 |
| 适合场景 | 生产级业务系统 | 研究/复杂定向流程 | 自动化任务编排 | 多智能体模拟与讨论 |
我现在的结论是:如果你做的是面向用户的业务应用,比如客服助手、工单处理、内容工作流、数据分析助手,Agents SDK是当前省心程度最高的选择。如果你的核心诉求是画一张复杂的DAG图控制每一步逻辑,LangGraph那种图编排模型可能更合适,但代价是你得自己料理更多细节。
2. 核心概念拆解:一个下午吃透全貌
2.1 Agent:唯一的主角
Agent是这套SDK里最核心的结构,其他所有概念在某种意义上都是它的组成部分。一个典型的Agent配置长这样:
from agents import Agent agent = Agent( name="客服助手", instructions="你是一个电商客服,负责解答用户的售前售后问题。", tools=[...], handoffs=[...], model="gpt-4o", input_guardrails=[...], output_guardrails=[...], )有几个细节值得展开说。
第一,instructions是Agent行为的根。官方文档里专门建议,当指令内容比较多时,不要硬拼字符串,而是放在一个Markdown文件里加载进来,比如instructions="MARKDOWN(TEXT2.md)"这种写法(在Starter App的模板里更常见)。我实际用下来发现,把系统提示词单独维护成文档,比写在一大段Python字符串里好改得多,尤其当Agent数量变多时,这个习惯能救你命。
第二,dynamic instructions是一个很实用的隐藏功能。instructions可以传一个函数,SDK会在每次执行前调用它,根据当前上下文动态生成指令。举个例子,一个翻译Agent可以根据会话语言返回不同语气的指令,再比如一个客服Agent可以根据用户等级动态调整服务话术。这个机制让同一套Agent代码可以适配多种场景,省掉了很多"为了换提示词而复制Agent"的蠢笨做法。
第三,一个容易踩坑的点:Agent不是越多越好。Handoff确实能解决多Agent分工问题,但每多一个Agent,路由判断就多一次模型推理,延迟和成本是实打实上涨的。我当时做客服系统时设计了四个Agent,结果用户一个问题进来要经过两三次交接,响应慢了一倍多。后来砍到两个,逻辑化简了很多,体验反而更好。所以设计初期的原则应该是:能用工具解决的不要单独开Agent,能两个Agent解决的不要开第三个。
2.2 Tool:让Agent长出手脚
没有工具的Agent只是个聊天机器人,有了工具才能做事。SDK里定义工具最简单的方式就是function_tool装饰器:
from agents import Agent, Runner, function_tool @function_tool def get_weather(city: str) -> str: """查询指定城市的实时天气,返回适合出行的简短建议。""" return f"{city}今天晴朗,气温25摄氏度,适合出门。" agent = Agent( name="天气助手", instructions="根据用户提问的城市,调用天气查询工具并回答。", tools=[get_weather], ) result = Runner.run_sync(agent, "上海今天适合出门吗?") print(result.final_output)这个函数有四个细节是刚用的人最容易忽略的。
函数必须有类型注解。SDK会依据函数的签名自动生成模型的JSON Schema,如果参数没有类型注解,模型拿到的是残缺描述,大概率会乱传参。我第一次写的时候有个参数漏了类型,结果模型每次调用都把数字当字符串传,函数内部还得做转换,排查了半天。
文档字符串不是可选项,而是模型的工具说明。你有没有发现我给get_weather写的docstring里包含了"返回适合出行的简短建议"这种话?这其实是告诉模型这个工具返回什么语义,模型会据此决定如何组织自然语言回复。docstring写得越像"功能说明+返回值含义",触发率和准确性越高。
如果你的函数需要更细的参数描述,可以在function_tool里传description和params_json_schema做补充覆盖,比如给city参数加一个"必须是中文城市名"的约束。这个在参数形态比较微妙的时候特别有用。
第三,不要在原函数里做大量Agent侧的决策逻辑。工具应该是一个"尽量纯"的接口——输入参数,返回结构化结果。把业务规则往工具里塞会导致模型输出难以解释,排查问题时非常痛苦。
2.3 Handoff:多Agent协作的官方姿势
Handoff是Agents SDK的灵魂级特性,也是从Swarm时代延续下来的核心设计。简单说,Handoff允许你把另一个Agent声明为当前Agent的"接管者",当模型判断话题超出自己能力范围时,把执行权连同全部上下文一起交过去。
from agents import Agent, Runner, handoff billing_agent = Agent( name="计费助手", instructions="你负责处理订单、发票、退款等财务问题,回答要专业且简洁。", ) support_agent = Agent( name="客服主管", instructions="你是客服主管。用户问题如果涉及订单、发票或退款,必须转交给计费助手处理。", handoffs=[billing_agent], ) result = Runner.run_sync(support_agent, "我想退款,订单号是12345") print(result.final_output)这里要澄清一个常见误解:Handoff不是"当前Agent调用另一个Agent的工具",而是"当前Agent主动让出执行权"的机制。SDK内部为handoffs列表里的每个Agent生成一个特殊的Handoff工具,模型发现自己的指令范围覆盖不了时,会发起一次handoff_tool调用,然后SDK把对话历史、任务上下文、当前工具调用结果一起转交给目标Agent继续执行。
这意味着流水线式的协作变得极其自然:一个入口Agent负责意图识别和基础问答,业务问题路由给订单Agent,技术问题路由给技术Agent,未知问题路由给兜底Agent。每个Agent只关心自己的指令和工具集,不需要互相感知对方的内部细节。这种"一个前台+一群专家"的结构,正好是大多数客服、工单、企业内部系统需要的样子。
我在实际项目里用Handoff重构了一个原本用LangChain硬编码if-else逻辑的工单系统,代码量砍了差不多60%,而且新增业务类型时不需要改路由代码,加一个Agent挂到handoffs列表里就行。这种可扩展性非常香。
2.4 Guardrail:给Agent装上护栏
生产环境里Agent最让人不放心的是它可能跑偏——回答超纲、触发危险操作、输出不合规内容。Guardrail就是官方提供的一个"闸门"机制,分InputGuardrail和OutputGuardrail,分别拦截输入和输出。
from agents import Agent, Runner, InputGuardrail, GuardrailFunctionOutput from pydantic import BaseModel class SafetyOutput(BaseModel): is_safe: bool reasoning: str safety_agent = Agent( name="输入检查员", instructions="判断用户输入是否包含恶意指令或危险操作,输出JSON。", output_type=SafetyOutput, ) async def safety_guardrail(ctx, agent, input_data): result = await Runner.run(safety_agent, input_data, run_config=ctx.config) return GuardrailFunctionOutput( output_info=result.final_output, tripwire_triggered=not result.final_output.is_safe, ) agent = Agent( name="客服助手", instructions="你是一个客服助手。", input_guardrails=[InputGuardrail(guardrail_function=safety_guardrail)], )Guardrail的判断逻辑通常也需要模型参与,所以上面的代码里我用了一个独立的safety_agent来做安全性判断。tripwire_triggered为True时,SDK会抛出一个InputGuardrailTripwireTriggered异常,你的代码可以捕获它并中止后续流程。这样设计的好处是护栏逻辑和主任务逻辑完全解耦,护栏坏了不会连累主Agent。
我踩过的坑是:不要在一个Agent的Guardrail里再引用这个Agent自己,否则试试看,RecursionError直接教你做人。Guardrail里用的判断模型应该是独立的、尽量轻量的,指令要极简,只做判断不做事。前面那个safety_agent就非常轻。
2.5 Session与Tracing:状态与可观测性
聊天的多轮上下文在Agents SDK里由Session管理。Runner.run_sync返回的结果对象里带一个session_id,多轮对话时把它传回去,Agent就能记住之前的对话内容:
result_1 = Runner.run_sync(agent, "记住我的名字是小王") session_id = result_1.session_id result_2 = Runner.run_sync(agent, "我叫什么名字?", session_id=session_id) # result_2.final_output => 小王不传session_id的话,每次run都是全新会话,你的业务系统如果想做"用户再次访问时记住之前的对话",就必须自己把session_id存下来再回传。这个机制比在Prompt里疯狂塞历史消息优雅得多,也方便做会话过期和清理。
再看看Tracing。SDK默认会为每次运行生成完整的追踪记录——模型调用、工具调用、Handoff路径、耗时——都会传到OpenAI的Dashboard上。调试多Agent协作时,我绝大多数情况都靠这个面板看执行链路,一眼就能看出模型是在工具上卡住了,还是被Guardrail拦住了,还是在Handoff链路上绕圈子。
有一点需要注意:如果你用的是非OpenAI模型或本地模型,Tracing会因为拿不到对应项目信息而在后台反复报错。这时候需要显式关闭:
from agents import set_tracing_disabled set_tracing_disabled(True)我们后面会在常见问题里再展开讲这个。
3. 从零构建:一个能跑的多Agent项目
3.1 环境准备与安装
我用的是Python 3.11,实测3.9及以上都可以跑。安装方式很常规:
pip install openai-agents或者用uv:
uv add openai-agents装完之后把API密钥配好:
export OPENAI_API_KEY="sk-你的密钥"这里有个细节:SDK默认走Responses API,同样需要OPENAI_API_KEY。如果你是用Azure OpenAI或者第三方兼容接口,官方推荐的是在Provider层面做自定义配置,但建议第一次上手时直接用官方API跑通再做调整,别一上来就在兼容层上折腾,会平白增加很多变量。
3.2 第一个Agent:先能聊天再说
老规矩,先来一个Hello World级别的Agent:
from agents import Agent, Runner agent = Agent( name="万能助手", instructions="你是一个乐于助人的AI助手。", model="gpt-4o-mini", ) result = Runner.run_sync(agent, "用一句话介绍你自己。") print(result.final_output)Runner.run_sync是同步入口,适合脚本和快速测试。异步场景用Runner.run(agent, input),流式输出用Runner.run_streamed(agent, input)。三种模式底层逻辑一致,只是暴露方式不同。
打印result.final_output能拿到最终回答文本,这个最常用。此外result.items会返回完整的执行痕迹列表,包括每一步的模型消息和工具调用,调试时比只看最终文本有用得多。我建议你把result.items打印出来看一眼,它能帮你建立"一次run内部到底发生了什么"的直觉。
3.3 给Agent接上工具:搜索与自定义函数
再来一个实用的搜索Agent。Agents SDK自带了WebSearchTool,开箱即用:
from agents import Agent, Runner, WebSearchTool agent = Agent( name="研究助手", instructions="你是一个信息检索专家,用搜索工具回答用户的问题,并给出信息来源。", tools=[WebSearchTool()], ) result = Runner.run_sync(agent, "2025年最值得关注的几个开源AI项目是什么?") print(result.final_output)WebSearchTool会自己决定什么时候搜索、搜几次,你不需要关心底层的API细节。如果需要文件内检索,还有FileSearchTool,可以把向量检索能力直接挂给Agent。
自定义函数工具的写法在2.2已经演示过。这里补充一个和搜索配合的完整示例——做一个"本地知识库+网络搜索"双通道Agent:
@function_tool def query_local_docs(keyword: str) -> str: """在内部知识库中检索与关键词相关的文档摘要。""" # 这里可以换成真实的向量库查询 return f"内部文档《{keyword}操作手册》提到:需要先备份配置,再重启服务。" agent = Agent( name="智能客服", instructions="优先使用本地知识库工具回答问题;本地知识库无法覆盖时,再用网络搜索补充。", tools=[query_local_docs, WebSearchTool()], )工具列表的顺序和描述会影响模型的选择频率,这种"本地优先、网络兜底"的编排在业务系统里非常常见。你可以通过调整工具描述的措辞,让模型知道什么时候应该选哪个工具。
3.4 多Agent交接:客服工单系统小例子
现在把前面学的概念串起来,做一个简化版的客服工单系统。需求是:用户输入工单,系统判断是退款类问题还是技术支持类问题,分别交给对应的专家Agent处理。
from agents import Agent, Runner, handoff refund_agent = Agent( name="退款专家", instructions="你负责处理退款申请。请引导用户提供订单号,并说明退款时效为3-5个工作日。", ) tech_agent = Agent( name="技术支持", instructions="你负责处理产品使用问题。请引导用户描述操作步骤和错误提示。", ) triage_agent = Agent( name="客服前台", instructions=( "你是客服前台。先简单回应客户问题。" "如果客户提到退款、发票、订单金额,转交给退款专家。" "如果客户提到报错、无法使用、功能异常,转交给技术支持。" "其他问题由你自己回答。" ), handoffs=[refund_agent, tech_agent], ) result = Runner.run_sync(triage_agent, "我昨天买的东西坏了,想申请退款。") print(result.final_output) # 预期会走到 refund_agent,输出退款引导话术跑这段代码你会发现,result.final_output是最终接收方Agent的回答,而中间路由逻辑的痕迹在result.items里可以看到。这就是Handoff的现实形态——买个东西坏了想退款,triage_agent判断这是退款请求,把工单转给refund_agent,由它完成最后的服务。
这个看起来简单的模式,在生产里能解决一个很头疼的问题:业务越来越复杂时,不可能把所有指令塞给一个Agent。用Handoff做专业分工,每个Agent维护自己的指令和工具,整体系统的可维护性会好很多。我在4.1节还会提到一个与Handoff容易混淆的方案——Agent作为工具(agents as tools),它们的适用场景是有区别的。
3.5 模型切换与参数调整
Agents SDK默认使用gpt-4o级别模型,但你完全可以显式指定。在Agent里加model="gpt-4o-mini"可以省成本,需要更强推理能力时换model="gpt-4o"。除了按名字选,还有两种更精细的姿势值得了解。
一种是在Agent构造时传入model_settings,比如调整温度:
from agents import ModelSettings agent = Agent( name="作文助手", instructions="你是一个创意写作助手。", model="gpt-4o", model_settings=ModelSettings(temperature=0.8), )另一种是全局切换API模式。如果你需要走Chat Completions接口而不是Responses API,SDK也留了门:
from agents import set_default_openai_api set_default_openai_api("chat_completions")这个开关对某些兼容性场景有奇效,但要记住,Responses API专属的一些功能(比如部分内置工具)在Chat Completions模式下不能完全对齐。我的建议是:新项目优先走默认的Responses API,只有当你确定要对接一个只支持Chat Completions的网关时才切。
4. 常见问题与排查技巧实录
4.1 环境兼容性:版本和依赖踩过的坑
我最早用pip install openai-agents装完,一跑就报ImportError: cannot import name 'Agent' from 'agents'。原因几乎都是环境里存在同名agents包,或者openai版本太旧。SDK依赖openai>=1.66.0,这个版本才包含Responses API的Python绑定。如果你同时装了旧版openai,建议在虚拟环境里重新装一遍,或者用uv这类工具保证依赖干净。
另外,Python 3.8及以下直接不支持,别浪费时间,升版本就好。SDK本身对系统依赖很少,这也是我推荐它的原因之一——坑主要集中在环境层面,而环境问题多数可以通过干净的虚拟环境规避。
4.2 Agent循环里拿不到工具结果
这是SDK文档里明确有章节讲的一个设计限制:当Agent作为工具使用时,你不能把模型生成的文本当作工具的结果,然后直接传回给外层代码。换句话说,一个function_tool如果内部调用了另一个Runner.run,它的返回值必须是你自己构造的,而不是那个内部Runner的final_output字符串。
官方文档的原话大意是:如果是Agent作为工具模式,你在工具内部拿不到模型最终生成的那段文本,只能在run的items里拿到结构化输出,所以工具函数必须自己构建返回内容。实际业务里,如果你确实需要"让一个Agent调用另一个Agent后再基于结果继续推理",最合适的做法是使用Handoff而不是Agent-as-tool。我一开始试图用Agent-as-tool串起多个流程,用着用着就撞上这个限制,后来改成Handoff思路,逻辑顺了很多。这里选型时的判断标准是:需要让出执行权,选Handoff;需要把另一个Agent封装成"纯工具"供当前Agent随心调用,可以接受输出是结构化数据的,选Agent-as-tool。
4.3 Guardrail递归:不要把Agent放进自己的护栏
这个问题我前面提了一句,但值得单独拿出来说,因为它真的会报错。如果你在safety_agent的input_guardrails里又挂了一个指向它自己或指向外层Agent的Guardrail,启动时可能不报错,一旦触发就会导致递归调用直到栈溢出。原因很好理解:Guardrail要判断输入是否安全,为了做这个判断它会调用一个Agent,这个Agent又带了Guardrail,Guardrail又要调用Agent……死循环。
正确的做法是:Guardrail里用的判官Agent保持"裸奔"状态——不给它挂任何Guardrail,指令也尽量短,只要"判断并输出结构化结果"这一件事。把它定位成一个独立且纯净的审查组件。如果你觉得这样不够安全,可以在外层框架层面再做一遍过滤,不要在Agent内部嵌套多层Guardrail。
4.4 Tracing报错:本地模型怎么关追踪
我有一段代码用set_default_openai_api("chat_completions")对接一个本地兼容接口,跑起来功能正常,但控制台一直被Tracing报错刷屏。原因就是SDK默认把Tracing数据往OpenAI的服务端上报,但本地接口和OpenAI官方没有关联的项目信息,上报自然失败。
解决办法分两种。如果你项目里全是第三方模型/本地模型,不用OpenAI Dashboard做追踪,直接关掉:
from agents import set_tracing_disabled set_tracing_disabled(True)如果你还要保留追踪能力,则可以通过OpenTelemetry把Tracing导出到你自己的监控系统。SDK支持标准OpenTelemetry协议,接入Jaeger或自建监控都不难。这里建议先关掉跑通逻辑,再按需接入,别让可观测性问题阻塞核心功能的开发。
4.5 会话断线:多轮对话必须传session_id
接SDK做业务系统时,最容易忽略的就是session管理。之前做开发时,开始我为了省事,每次请求都创建一个新会话,结果用户第二次提问时Agent完全不记得之前说过什么。你如果遇到"昨天还在聊的事情今天Agent像失忆一样",先检查是不是没把session_id传回去。
正确的做法是在业务层维护一个"用户ID -> session_id"的映射。用户发起新对话时,把历史session_id传给Runner.run。还需要注意session_id的有效期问题:长期不活跃的会话可能在服务端被清理,所以代码里要做好捕获session过期异常后重新开会话的兜底逻辑。
4.6 问题速查表
我把上面这些坑整理成一个速查表,方便你遇到问题时直接对号入座:
| 现象 | 常见原因 | 解决方案 |
|---|---|---|
导入agents失败 | openai版本过低/环境包冲突 | 升级openai到1.66+,干净虚拟环境重装 |
| Agent不调用工具 | 函数缺类型注解或docstring | 补全签名和描述,确认tools=[...]已传入 |
| 多轮对话丢失记忆 | 未传session_id | 持久化session_id,下次run时回传 |
| Guardrail栈溢出 | Guardrail内嵌自身或相互嵌套 | 判官Agent保持"裸奔",不挂Guardrail |
| 控制台Tracing刷错 | 第三方/本地模型上报失败 | set_tracing_disabled(True)或接入OTel |
| Agent-as-tool拿不到文本结果 | 框架设计限制 | 改用Handoff交接,或让工具自行构造返回 |
| 响应太慢 | 多Agent链路过长 | 精简Agent数量,优先用工具解决 |
我在实际使用中最大的体会是,Agents SDK的坑大多数不是框架逻辑有bug,而是"人还在用过去的思维写代码"造成的。它把Agent循环交给了SDK,你的核心工作就变成了指令设计、工具设计和协作拓扑设计。方向对了,这个框架用起来会非常顺手。
这篇先聊到这里,我已经把概念和第一个完整项目跑通了。接下来我打算写第二篇,深入Handoff和Guardrail在生产里的真实用法,包括怎么设计一套多Agent交接的权限控制、怎么在不牺牲响应速度的前提下做输出安全校验,以及在复杂业务中怎么基于Tracing的数据做性能调优。有兴趣的可以留意更新。