ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenAI Agents SDK 实战:从对话到工具调用与多Agent协作

OpenAI Agents SDK 实战:从对话到工具调用与多Agent协作 1. 从能聊天到能干活Agents SDK 到底解决了什么大多数人第一次接触大模型应用开发都是从写一个对话循环开始的用户输入问题模型返回回答把历史消息拼进上下文再发一次。这个模式跑个问答 Demo 没问题但一旦你想让它帮我查一下库存然后发一封邮件立刻就卡住了——模型只会输出文字它没法真的去调用你的库存接口也没法真的发邮件。OpenAI Agents SDK 要解决的就是这个断层。它把模型输出文字升级成模型输出行动指令再由框架负责执行这些指令、把执行结果喂回模型、继续下一轮推理直到任务完成。这个循环在行业里通常叫Agent Loop智能体循环而 Agents SDK 就是把这个循环标准化、工程化的那层封装。我把它理解成一个带调度能力的对话引擎对话引擎负责和模型交互调度能力负责决定什么时候该调用哪个工具、调用完怎么把结果接回去。你写业务逻辑它管流程编排。这篇文章适合三类人看一是有一定 Python 基础、想从调 API进阶到做 Agent的开发者二是手里已经有一堆内部接口数据库、CRM、工单系统想让模型自动去调用的工程师三是想搞清楚 Agent 框架底层到底在干什么、不想被各种概念忽悠的技术负责人。我会从核心概念讲到可运行代码再讲我实际踩过的坑尽量让你看完就能动手。需要先说明一点Agents SDK 的版本迭代比较快API 细节可能随版本变化但核心抽象是稳定的——Agent、Tool、Runner、Handoff 这几个概念理解了换版本也就是改改参数的事。下面所有代码基于常见的 Python 用法具体以你安装的版本为准。2. 拆开 Agents SDK 的四个核心零件在写第一行代码之前得先把 SDK 的几个核心概念理清楚。很多人上来就抄示例跑通了也不知道每行在干嘛一旦要改需求就懵了。我习惯先把零件认全再组装。2.1 Agent不是模型而是带人设和工具箱的执行者很多人误以为 Agent 就是那个大模型。其实在 SDK 里Agent 是一个配置对象它至少包含三样东西用哪个模型、系统指令instructions是什么、能用哪些工具tools。from agents import Agent weather_agent Agent( nameWeatherAssistant, instructions你是一个天气助手用户问天气时调用工具查询不要凭记忆编造。, modelgpt-4o-mini, tools[get_weather], )这里的关键认知是Agent 本身不执行任何东西它只是一份说明书。真正让它跑起来的是 Runner。这个分工很重要就像菜谱和厨师的关系——菜谱写得再好也得有人照着做。instructions这个字段值得多说一句。它不是普通的提示词而是这个 Agent 的行为准则。我建议在这里明确写清楚三件事角色是什么、什么情况下必须调用工具、什么情况下不能瞎编。实测下来把不要编造写进 instructions能显著降低模型胡说的概率。2.2 Tool把普通函数变成模型能调用的能力Tool 是 Agent 和外部世界交互的唯一通道。SDK 最舒服的一点是你可以直接把一个 Python 函数用装饰器变成工具函数签名和文档字符串会自动转成模型能理解的工具描述。from agents import function_tool function_tool def get_weather(city: str) - str: 查询指定城市的当前天气。 Args: city: 城市名称例如北京。 # 实际项目里这里调用真实天气 API return f{city}今天晴气温 22 摄氏度。注意那个 docstring——它不是写给人看的是写给模型看的。模型靠它判断这个工具是干嘛的、参数怎么填。我见过太多人工具写得没问题但 docstring 一句话带过结果模型要么不调用要么参数乱填。把 docstring 当成给模型的 API 文档来写这个习惯能省掉后面大量调试时间。参数类型标注也很关键。city: str告诉模型这个参数是字符串如果你写成count: int模型就知道要传数字。类型标注错了模型传参就会出错。2.3 Runner真正驱动循环的引擎Runner 负责把 Agent 跑起来把用户输入发给模型模型决定调用工具就执行工具把结果回传再让模型继续直到模型给出最终回答。from agents import Runner import asyncio async def main(): result await Runner.run(weather_agent, input北京今天天气怎么样) print(result.final_output) asyncio.run(main())Runner.run返回的结果对象里final_output是最终回答但还有别的字段值得关注比如中间的工具调用记录。调试的时候把这些中间步骤打出来你就能看到模型到底调了哪个工具、传了什么参数、工具返回了什么——这是排查问题最有效的手段。2.4 Handoff让一个 Agent 把活交给另一个 Agent当任务复杂到单个 Agent 搞不定时就需要 Handoff移交。比如一个客服系统前台 Agent 负责接待识别到是退款问题就移交给退款专员 Agent。refund_agent Agent( nameRefundSpecialist, instructions你专门处理退款问题先核实订单号再操作。, ) triage_agent Agent( nameTriage, instructions你是前台退款相关问题移交给退款专员。, handoffs[refund_agent], )Handoff 的本质是当前 Agent 决定这活我不干了交给别人SDK 会把对话上下文一起传过去。这个机制让复杂系统可以拆成多个专职 Agent每个只管自己那一摊比塞一个巨型 instructions 靠谱得多。把这四个零件记住Agent 是说明书Tool 是手脚Runner 是引擎Handoff 是接力棒。后面所有内容都是围绕它们展开的。3. 环境搭建与第一个能跑通的 Agent概念清楚了动手。这一节我按实际搭建顺序走一遍把容易忽略的细节都标出来。3.1 依赖安装与密钥配置先装包。建议用虚拟环境别污染全局。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install openai-agents密钥配置是最容易出问题的地方。SDK 默认从环境变量OPENAI_API_KEY读取密钥。我强烈建议用.env文件管理而不是硬编码在代码里。# .env 文件内容 OPENAI_API_KEY你的密钥from dotenv import load_dotenv load_dotenv() # 必须在导入 agents 之前调用注意load_dotenv()一定要放在导入 agents 相关模块之前。我踩过一次坑先 import 了 agents 再 load_dotenv结果 SDK 初始化时读不到密钥报了个很迷惑的认证错误排查了半小时才发现是顺序问题。3.2 一个最小可运行示例下面这个例子包含一个工具和一个 Agent能完整跑通提问—调用工具—返回结果的循环。import asyncio from dotenv import load_dotenv load_dotenv() from agents import Agent, Runner, function_tool function_tool def get_order_status(order_id: str) - str: 根据订单号查询订单状态。 Args: order_id: 订单编号格式如 ORD12345。 # 模拟数据库查询 fake_db { ORD12345: 已发货预计明天送达, ORD67890: 待付款, } return fake_db.get(order_id, 未找到该订单请确认订单号是否正确。) order_agent Agent( nameOrderAssistant, instructions( 你是订单查询助手。用户提供订单号时调用 get_order_status 查询。 如果用户没给订单号先礼貌地询问订单号不要自己编造。 ), tools[get_order_status], ) async def main(): result await Runner.run(order_agent, input帮我查一下 ORD12345 的状态) print(result.final_output) if __name__ __main__: asyncio.run(main())跑起来你会看到类似您的订单 ORD12345 已发货预计明天送达的输出。看起来简单但这背后发生了好几件事模型读懂了用户意图、决定调用工具、提取出订单号ORD12345、工具执行返回结果、模型把结果组织成自然语言。这一整套就是 Agent 的价值。3.3 为什么用异步一个容易被忽略的性能点你可能注意到所有示例都用async/await。这不是为了显得高级。Agent 执行过程中有大量等待——等模型响应、等工具执行。如果用同步写法一个请求就得阻塞一个线程。异步写法下一个进程能同时处理很多请求。如果你的业务是 Web 服务异步几乎是必须的。FastAPI 这类框架本身就是异步的直接await Runner.run(...)就能无缝集成。如果你只是写个脚本跑批处理同步也不是不行但既然 SDK 主推异步跟着用能少踩坑。提示Runner.run是异步方法必须用await。如果你在同步函数里调用它会得到一个 coroutine 对象而不是结果这是新手最常见的错误之一。4. 工具设计的门道让模型会用而不是乱用工具是 Agent 能力的边界。工具设计得好Agent 聪明设计得烂Agent 就是个智障。这一节讲我总结的几个原则。4.1 工具粒度别太粗也别太细一个工具干一件事这是基本原则。但一件事的粒度怎么把握我见过两种极端。一种是工具太粗比如一个handle_user_request(action, params)工具什么都能干结果模型根本不知道该传什么参数。另一种是太细把查订单拆成连接数据库执行 SQL格式化结果三个工具模型得连续调三次才能完成一件事出错概率翻倍。合理的粒度是一个工具对应一个业务动作查订单状态、创建工单、发送通知。每个工具的参数控制在 1 到 4 个参数名要能自解释。4.2 参数校验别指望模型永远传对模型会传错参数这是必然的。所以工具内部一定要做校验不能假设输入合法。function_tool def transfer_points(user_id: str, amount: int) - str: 给用户转移积分。 Args: user_id: 用户 ID纯数字字符串。 amount: 转移积分数必须为正整数。 if not user_id.isdigit(): return 错误用户 ID 必须是纯数字请重新确认。 if amount 0: return 错误转移积分必须大于 0。 # 执行转移逻辑 return f已为用户 {user_id} 转移 {amount} 积分。注意这里返回的是错误信息字符串而不是抛异常。工具返回的错误信息会回传给模型模型看到用户 ID 必须是纯数字后往往能自己纠正或者向用户追问。如果直接抛异常整个循环就断了。这个设计很关键——把错误变成模型能理解的反馈而不是让程序崩溃。4.3 工具描述里的潜规则docstring 除了说明功能还能引导模型行为。几个实用技巧在描述里写明什么时候该用比如当用户询问订单物流时调用此工具。写明什么时候不该用比如不要用此工具查询历史订单历史订单请用 query_history。参数描述里给示例值模型看到示例传参准确率明显提升。我做过对比同样一个工具docstring 写得详细和写得敷衍模型调用成功率能差出一大截。这不是玄学是因为模型就是靠这些文字来判断的。4.4 工具返回结果的长度控制工具返回的内容会进入模型的上下文。如果你返回一大坨 JSON不仅浪费 token还可能把关键信息淹没。我的做法是工具返回给模型的内容要精简只保留模型做决策需要的信息。比如查订单数据库返回 30 个字段但模型只需要订单状态和预计送达时间那就只返回这两个。需要详细信息时再设计一个专门的工具去查。这样既省 token又让模型判断更准。5. 多 Agent 协作Handoff 的正确打开方式单 Agent 能搞定的事有限。真实业务往往是一个问题涉及多个领域这时候 Handoff 就派上用场了。5.1 什么时候该拆 Agent我的判断标准很简单当 instructions 开始互相打架时就该拆了。比如一个客服 Agent既要处理退款需要谨慎、要核实身份又要推销新品需要热情、要主动推荐。这两种行为模式塞在一个 instructions 里模型会精神分裂。拆成退款专员和销售顾问两个 Agent各自 instructions 纯粹表现立刻不一样。另一个信号是工具数量。当一个 Agent 挂了十几个工具模型选择工具的准确率会下降。拆成几个 Agent每个挂三五个工具准确率回升。5.2 Handoff 的上下文传递Handoff 时对话历史会一起传过去。这意味着接手 Agent 能看到之前聊了什么不用用户重复。但这也带来一个问题上下文可能很长接手 Agent 被无关信息干扰。triage Agent( nameTriage, instructions你是前台。判断用户意图退款问题交给退款专员其他问题自己处理。, handoffs[refund_agent], )实际用下来我建议在接手 Agent 的 instructions 里明确写忽略与你的职责无关的历史信息能减少干扰。5.3 避免 Handoff 死循环一个坑A 移交给 BB 觉得不该自己管又移交回 A来回踢皮球。SDK 通常有最大轮次限制但更稳妥的做法是在 instructions 里写清楚职责边界让每个 Agent 明确什么该我管什么不该我管。注意Handoff 不是越多越好。我见过有人设计了七八个 Agent 互相移交结果调试极其困难一个请求在几个 Agent 之间跳来跳去根本不知道哪一步出了问题。能用两个 Agent 解决就别用五个。6. 调试与可观测性Agent 出问题时怎么查Agent 最让人头疼的地方是黑盒感——它不按预期走你也不知道为什么。这一节讲我的排查方法。6.1 打开详细日志SDK 支持追踪tracing能把每一步都记录下来。开发阶段强烈建议打开。from agents import set_tracing_disabled set_tracing_disabled(False) # 确保追踪开启打开后你能看到每次模型调用的输入输出、工具调用的参数和结果。这是排查问题的第一手资料。6.2 常见问题与排查路径我把踩过的坑整理成一张表方便对照。现象可能原因排查方向模型不调用工具直接编答案instructions 没强调用工具或工具描述不清检查 instructions 是否写明必须调用工具检查 docstring工具调用参数错误参数类型标注错或 docstring 没给示例检查类型标注补充参数示例报认证错误密钥没加载或 load_dotenv 顺序错确认 load_dotenv 在 import 之前循环停不下来工具一直返回让模型继续调用的结果检查工具返回值设置最大轮次Handoff 后答非所问上下文干扰接手 Agent 被无关信息带偏在接手 Agent instructions 里限定职责6.3 用最小复现定位问题当 Agent 行为异常时我的习惯是把它简化到最小只留一个工具、一句 instructions、一个输入。如果最小版本正常再逐步加回复杂度直到问题复现。这个方法能快速定位是哪个工具或哪句 instructions 导致的。很多时候问题出在 instructions 的某句话上。模型对措辞很敏感一句话的差别可能导致行为完全不同。所以 instructions 要像写代码一样反复测试和打磨。7. 从 Demo 到生产我踩过的几个真实的坑前面讲的偏怎么用这一节讲怎么用好。都是我在实际项目里踩出来的经验。7.1 成本控制Agent 比你想的费钱Agent 的一次任务可能触发多轮模型调用每轮都消耗 token。一个看似简单的请求背后可能是三五次模型调用。如果不加控制成本会超预期。我的做法一是给工具返回结果瘦身减少上下文长度二是能用小模型的地方就用小模型比如意图识别用gpt-4o-mini就够没必要上大模型三是设置最大轮次防止失控循环。7.2 超时与重试模型调用和工具调用都可能超时。生产环境必须处理。工具内部调用外部 API 时要设超时模型调用失败时要有重试逻辑。但重试要小心——如果工具是有副作用的比如已经发了邮件重试可能导致重复操作。这类工具要做幂等设计。7.3 安全边界别让 Agent 为所欲为这是最重要的一点。Agent 能调用工具就意味着它能对真实系统产生真实影响。一个能删除用户的工具如果被模型误调用后果严重。我的原则危险操作必须二次确认。比如删除类工具不要直接执行而是返回请确认是否删除等用户确认后再调用真正执行的工具。另外工具内部要做权限校验不能假设调用方有权限。注意永远不要给 Agent 一个万能执行工具比如能执行任意 SQL 或任意 shell 命令的。这是安全灾难。工具的能力边界要收得尽可能窄。7.4 版本升级的应对Agents SDK 还在快速迭代API 可能变。我的建议是锁定依赖版本在 requirements 里写死版本号升级前先在测试环境验证。别在生产环境直接pip install -U容易出事。8. 下一步可以往哪走把上面这些跑通你已经能做出一个可用的 Agent 应用了。接下来可以探索几个方向。一是接入真实业务系统。把示例里的假数据库换成你的真实接口这是最有价值的落地。二是加入记忆能力让 Agent 记住用户的历史偏好这通常需要配合外部存储。三是多 Agent 编排用 Handoff 构建更复杂的协作流程。四是评估与测试给 Agent 建立一套自动化测试确保改动不会让行为退化——这块很多人忽略但生产环境必不可少。我个人在实际操作中的体会是Agent 开发最难的不是写代码而是把模糊的业务需求翻译成清晰的 instructions 和工具设计。代码半小时能写完instructions 可能要改二十遍。所以别急着堆功能先把一个场景打磨到稳定再扩展下一个。一个稳定好用的单 Agent价值远大于一堆半成品的多 Agent 系统。最后分享一个小技巧每次改完 instructions 或工具都拿一组固定的测试问题跑一遍对比改动前后的表现。这个习惯能帮你快速判断改动是变好了还是变差了避免感觉好像好点了这种模糊判断。
返回列表