ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenAI Agents SDK 实战:从工具调用到多 Agent 协作与 Session 记忆

OpenAI Agents SDK 实战:从工具调用到多 Agent 协作与 Session 记忆 上个月我在给内部一个售后场景搭智能客服一开始偷懒直接调 Chat Completions 接口自己维护 messages 列表、解析 tool_calls、执行完函数再把结果塞回去、还要处理上下文超限……主循环逻辑写了快两百行结果一个工具返回格式略不统一整条链路就崩。后来把代码推倒重写换上 OpenAI Agents SDK从环境准备到多 Agent 协作的主干流程一个下午就跑通了。这篇就把这段真实经历整理成一份 OpenAI Agents SDK 构建指南的第一篇覆盖装包、最小 Agent、工具调用、Handoff 多 Agent 协作和会话持久化适合已经懂大模型 API 基本用法、正准备动手搭 Agent 的 Python 开发者直接照着抄。1. Agents SDK 到底解决什么问题我踩过的裸调用深坑1.1 看着简单实际很难的“Agent 主循环”如果你只调过 Chat Completions可能会觉得让模型“用工具”就是多传一个 tools 参数而已。真自己写一遍才知道坑在哪模型返回的 tool_calls 是个结构化对象你要自己解析 function name 和 arguments执行完本地函数后要把输出拼成一条 roletool 的消息回传循环继续如果两个工具都有输出消息顺序错了模型就乱上下文长度不够了要自己决定丢哪条历史、保留哪条摘要等你想拆成“售前 Agent”“售后 Agent”“退款 Agent”三个角色还要自己设计路由逻辑。这些不是不能做只是每个人都会做一遍而且做出来的版本大概率不一样。我当时的代码里光是把函数返回值统一成字符串就花了不少精力因为模型对复杂 JSON 的理解经常不一致。1.2 SDK 给的四件套Agent、Runner、Handoff、SessionOpenAI Agents SDK 的核心抽象其实非常少少到你一张便利贴就能写完抽象作用对应我原来自己写的部分Agent定义模型行为指令、模型、工具、可转交对象System Prompt tools 参数拼接Runner执行 Agent管理整个对话循环我那个两百行的 while 循环Handoff一个 Agent 把控制权转给另一个 Agent我那个手写路由逻辑Session保存并恢复一段连续对话的状态我自己维护的 messages 数组这四件套基本把 Agent 应用的骨架搭好了。你只需要往里面填业务逻辑不用再重复造轮子。1.3 和 LangChain 这类框架相比它的定位差异在哪我也用过 LangChain给我的感觉是抽象层太厚光是“Chain”“Memory”“Tool Spec”这套概念就要学半天出了问题排查链路还长。Agents SDK 则更接近“OpenAI 原生能力的直接映射”它的 Agent 基本等同于你脑子里的“带工具的 Chat Model”Runner 就是那个帮你跑完循环的执行器。如果你的团队已经在直接用 OpenAI API想少学一套抽象、尽快出活这个 SDK 会比 LangChain 更顺手。当然它目前不是万能的复杂的编排、图状态流转还是得上专门的工作流引擎但那不是“一”要讨论的。2. 第一次上手装包、配 Key、跑通最小 Agent2.1 安装pip install openai-agents 就够了环境要求很简单Python 3.9 以上用虚拟环境隔离一下就行python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install openai-agents这个包会自动拉取它依赖的 openai 官方 Python SDK所以你不需要再单独装 openai装一个 openai-agents 就都齐了。装完后验证一下版本pip show openai-agents2.2 API Key 配置运行前把环境变量设置好export OPENAI_API_KEYsk-你的KeyWindows 的命令行下改成set OPENAI_API_KEYsk-你的Key注意不要把 Key 硬编码在 Python 文件里。项目要提交仓库的话建议用 python-dotenv 把 Key 放进 .env 文件并加入 .gitignore这是基本卫生避免哪天不小心把 Key 提交到公开仓库。2.3 最小可运行代码创建一个 agent_demo.pyfrom agents import Agent, Runner agent Agent( nameAssistant, instructionsYou are a helpful assistant., ) result Runner.run_sync(agent, Write a haiku about recursion in programming) print(result.final_output)运行python agent_demo.py正常情况下你会看到一首俳句。这一小段代码虽然短但已经完整跑通了一个 Agent 闭环系统指令、用户消息、模型推理、结果返回。Runner.run_sync 是同步方法底层其实是把异步 Runner.run 包了一层。所以你的代码里如果是 async 上下文直接用异步版本更合理import asyncio from agents import Agent, Runner async def main(): agent Agent(nameAssistant, instructionsYou are a helpful assistant.) result await Runner.run(agent, Tell me a short joke.) print(result.final_output) asyncio.run(main())从这一步开始你就在用官方推荐的异步方式了。后面凡是涉及并发、流式输出的场景都是基于这个异步接口展开的。3. 让 Agent“会干活”工具调用机制3.1 function_tool 装饰器普通函数秒变工具Agent 光会聊天没有生产力给它配工具才是关键。Agents SDK 里把一个 Python 函数变成模型可调用的工具只需要加一个装饰器from agents import Agent, Runner from agents.tool import function_tool function_tool def get_weather(city: str) - str: 获取指定城市的当前天气。 return f{city}今天晴22℃。 agent Agent( nameWeatherBot, instructions你是天气助手用户问天气时调用 get_weather。, tools[get_weather], ) result Runner.run_sync(agent, 北京今天冷吗) print(result.final_output)这里最巧妙的点是模型自己判断该不该调工具、该传什么参数然后 SDK 帮你把函数跑完再把返回值喂回模型最后模型基于返回值组织语言回答。你完全不需要关心 tool_calls 的解析和回填Runner 全包了。3.2 类型注解和 docstring 为什么缺一不可这个坑我必须单独强调函数的类型注解和 docstring 不是给人看的是给模型看的。SDK 在把函数暴露给模型时会根据你的函数签名和 docstring 生成一个 JSON Schema模型在这个 Schema 的引导下决定调用哪个工具、填什么参数。我实测下来的经验是参数必须写类型注解比如 city: str否则模型不知道该传字符串还是对象返回值最好也标注类型比如 - str让模型知道拿到的是字符串docstring 里要写清楚参数含义、返回格式、典型调用场景参数默认值会影响 Schema 的 optional 标记业务上最好显式声明。举个反例如果你写def get_weather(city):而没有类型注解模型大概率猜不透这个 city 应该怎么填或者干脆不调用这个工具。这类问题非常隐蔽不报错就是行为不对。3.3 从返回结果里看 Agent 到底做了什么排错时很需要知道工具到底有没有被调用、参数传对没有。RunnerResult 里有个 new_items 字段会记录这一轮产生的消息和工具调用result Runner.run_sync(agent, 帮我查一下上海的天气) for item in result.new_items: print(item)你会看到类似 function_call 和 function_call_output 的记录能直接确认模型是否按预期调用了工具。更直观的方式是看 Tracing 面板这个我在第 7 章会详细说。4. 构建第一个完整 Agent带工具调用的服务台助手4.1 先写清 Instructions再写代码工具只是能力Instructions 决定这个 Agent 怎么用能力。写 Agent 指令可以直接把“人话 prompt”写进去但建议至少包含这几层信息角色定位你是谁负责什么行为边界哪些事不能做哪些事必须转人工处理流程先做什么、再做什么回答风格简洁还是详细要不要带单号、步骤编号兜底策略工具查不到数据时怎么说绝不允许编造。我现在的习惯是先把这段指令写成一个独立字符串再用变量传进 Agent而不是内联在 Agent 构造参数里。这样后续调整提示词不用动代码结构。4.2 服务台助手完整代码下面这个示例同时演示了多工具组合和异步流式输出模拟一个能查订单、能办理退货的服务台 Agentimport asyncio from agents import Agent, Runner from agents.tool import function_tool ORDER_DB { A1001: {status: 已发货, item: 无线耳机, price: 399}, A1002: {status: 已签收, item: 机械键盘, price: 699}, } function_tool def query_order(order_id: str) - str: 查询订单状态。入参 order_id 是订单号返回订单的 JSON 字符串。 order ORDER_DB.get(order_id) if not order: return f没有找到订单 {order_id} return f{order_id} 当前状态{order[status]}商品{order[item]}价格{order[price]} function_tool def request_refund(order_id: str, reason: str) - str: 为已签收的订单提交退货申请。入参 order_id 是订单号reason 是退货原因。 return f订单 {order_id} 的退货申请已提交原因{reason}预计 1-3 个工作日处理。 instructions 你是售前售后客服助手职责包括 1. 用户查询订单状态时使用 query_order 工具。 2. 用户要求退货时先查订单状态已签收的订单才能调用 request_refund。 3. 查不到订单时明确告诉用户“订单号不存在”不要编造结果。 4. 退货申请成功后回复中必须包含申请单号格式REF-订单号。 回答要简洁不超过三句话。 agent Agent( nameSupportAssistant, instructionsinstructions, tools[query_order, request_refund], ) async def main(): result await Runner.run(agent, 订单 A1002 到了但是键盘有个键是坏的我要退货) print(result.final_output) asyncio.run(main())跑一次你会发现Agent 会先调用 query_order 确认订单状态再调用 request_refund 提交申请最后给你一个符合格式的答复。这就是一个非常典型的“多工具协作”业务闭环。4.3 流式输出别让用户干等服务台场景里用户等着回复一次性输出其实体验一般。用 Runner.run_streamed 可以让回复像打字机一样逐字出现from agents import Agent, Runner agent Agent(nameSupportAssistant, instructionsinstructions, tools[query_order, request_refund]) result Runner.run_streamed(agent, 订单 A1001 到哪了) async for event in result.stream_events(): if event.type raw_response_event and hasattr(event.data, delta): print(event.data.delta, end, flushTrue)这段代码里raw_response_event 是最原始的输出事件delta 就是每个增量片段。流式输出不会改变 Agent 的逻辑只是把返回形式从“一次性”变成“逐段推送”对用户观感提升相当明显。5. 多 Agent 协作Handoff 机制实现任务分派5.1 为什么要拆成多个 Agent而不是一个大 Prompt等业务变复杂你会发现一个万能 Agent 很难维护指令里塞了售前、售后、退款、物流四种职责工具列表几十个模型在工具选择上会变迟钝上下文也很容易互相干扰。拆成多个 Agent 后每个 Agent 只需要关心自己的指令和少量工具职责清晰工具调用准确率也会更好。这个思路和“一个函数只做一件事”完全一致。你只是把“函数”换成了“Agent”。5.2 三 Agent 服务台分诊、售前、退款直接看代码这是 official 示例的一个变体import asyncio from agents import Agent, Runner sales_agent Agent( nameSalesAgent, instructions你是售前顾问负责回答价格、优惠、库存问题。回答要给出明确推荐。, ) refund_agent Agent( nameRefundAgent, instructions你是退款专员负责处理退货退款问题。需要询问订单号和原因。, ) triage_agent Agent( nameTriageAgent, instructions你是客服分诊员。根据用户问题判断类型涉及价格、优惠、商品咨询转给 SalesAgent涉及退货、退款、售后转给 RefundAgent。, handoffs[sales_agent, refund_agent], ) async def main(): result await Runner.run(triage_agent, 你们最近耳机有优惠吗) print(最终回答, result.final_output) print(实际处理的Agent, result.last_agent.name) asyncio.run(main())第一次看到这种代码时不少人会困惑怎么没有显式写“如果包含优惠关键词就转给 sales_agent”答案是不需要。模型会根据 triage_agent 的 instructions 自行判断然后调用 SDK 自动注册的转交工具。这就是声明式 Agent 的好处——你描述职责模型负责路由。5.3 Handoff 背后发生了什么Handoff 不是简单的“把两个模型的回答拼在一起”。当一个 Agent 决定转交时实际上是把当前会话的控制权、消息历史、可能的状态信息一起移交给目标 Agent目标 Agent 带着自己的指令和工具开始处理后续对话。从用户视角看他感知不到切换只会觉得“对面换了个人在认真回答”。这也是多 Agent 场景里最丝滑的地方多个 Agent 共享同一段对话历史但每个片段里的“人设”和可用能力是独立的。调试多 Agent 时记得多看一眼 result.last_agent.name它能告诉你这一轮到底是谁在处理。如果回答风格不对第一件事就是确认最终处理者是不是你预期的那个。6. 会话状态Sessions 让 Agent 记住上下文6.1 为什么需要 Session现在代码有个问题每次 Runner.run 都是一次独立对话Agent 根本不记得上一轮说了什么。真实业务里用户会说“还是刚才那个订单”“我刚才退到一半”。要让 Agent 记住这些就需要 Session。更准确地说一段连续对话对应一个 SessionSession 内部保存的是这条对话链路上所有消息记录。你需要保存哪个 Session、恢复哪个 Session都由你来管理。默认行为其实有点反直觉如果你不传 session 给 Runner它会每次自动创建新 session并把你上一个默认 session 标记为“不再需要”。这在小 demo 里无所谓但真实服务里必须自己做 Session 管理。6.2 用 InMemorySessionManager 实现多轮记忆Agents SDK 内置了一个 InMemorySessionManager可以先用它跑通import asyncio from agents import Agent, Runner from agents.session import Session from agents.sessions import InMemorySessionManager agent Agent(nameAssistant, instructions你是一个有帮助的助手。) memory InMemorySessionManager() session Session(session_iduser_123_001, agent_nameAssistant) async def main(): result1 await Runner.run(agent, 我的订单号是 A1002, sessionsession) print(第一轮, result1.final_output) result2 await Runner.run(agent, 我刚才说的订单号是什么, sessionsession) print(第二轮, result2.final_output) asyncio.run(main())第二轮输出时Agent 会正确回答“A1002”。因为在第二次 Runner.run 时你传了同一个 sessionSDK 会把该 Session 的历史消息拼回 messages 里Agent 自然就“记得”。这里我额外说一句InMemorySessionManager 适合开发和单机小场景数据存在进程内存里服务重启就没了。生产环境建议自己实现 SessionManager 的持久化把 Session 存到 Redis 或数据库核心接口有 create_session、get_session、update_session、list_sessions、delete_session 这几个按需实现即可。这个细节我准备放到下一篇展开先在这里留个观念Session 是 Agent 应用的地基越早把存储逻辑想清楚越好。6.3 多 Agent 场景下的 Session 同样适用Session 和 Handoff 可以共存triagen 把活转给 refund_agent 后对话历史仍然写在同一个 Session 里。用户后续说“刚才提交的退款怎么还没处理”分诊 Agent 依然能通过 Session 恢复整段上下文。这也是多 Agent 应用用起来不“失忆”的关键。6.4 换用户、换 Agent 时注意 Session 隔离Session 是按业务场景隔离的不同用户要不同 session_id不同业务线也建议建立不同的 Session 前缀。如果后端给两个人共用了同一个 session你会看到用户 A 说的事情莫名其妙出现在用户 B 的对话里排查起来相当费劲。我自己就踩过这种共享 Session 的坑最后还是老老实实按“用户ID业务线时间戳”生成 Session ID。7. 实战里最容易被卡住的几个坑与调试经验7.1 工具函数签名不规范模型就是不调用这一条我在第 3 章提过但实际项目里最常遇到必须再强调一次。标准写法是function_tool def query_order(order_id: str) - str: 查询订单信息。入参 order_id 是订单号。错误写法包括function_tool def query_order(order_id): # 没有类型注解 ... function_tool def query_order(**kwargs): # 参数语义不明确 ...没有类型注解时SDK 生成的 JSON Schema 就不完整模型可能不知道参数该传什么甚至直接跳掉这个工具。没有 docstring 时模型只能从函数名字猜意图猜中算运气猜不中是常态。提示凡是给模型用的工具函数都要保证“有完整类型注解 有 docstring 参数含义写得明明白白”。这不是代码规范问题是模型能不能正确理解工具的问题。7.2 run_sync 和 run 别混着用我见过有人在一个异步服务里调 Runner.run_sync导致事件循环被阻塞整个服务的响应全部变慢。如果你已经在用 FastAPI 或类似异步框架一律用 await Runner.run(...)不要图省事用同步方法。 如果确实需要在同步代码里调用再用 run_sync但要注意它内部会自己创建事件循环嵌套调用时会报“event loop is already running”之类的问题。7.3 Agent 会陷入“自我怀疑循环”执行复杂任务时Agent 偶尔会反复调同一个工具、自言自语好几轮最后却没有输出结论。比如让它查库存它查了一次觉得不够又查一次参数还一样。解决办法有两个方向在 Instructions 里明确“工具返回结果后直接回答不要重复调用同一个工具”在业务层对工具调用次数做上限控制比如同一轮对话里同一个工具最多调用三次。这两种方案可以并用前者靠模型自觉后者是硬性保护。我现在的项目里两种都加上效果比较稳。7.4 调试要找 Tracing 面板不要只靠 printAgents SDK 默认集成了 Tracing 能力只要你是通过官方客户端跑并且设置了 OPENAI_API_KEY整个对话链路——消息、工具调用、成本、耗时——都会自动上报。去 platform.openai.com 打开 Tracing 面板能看到每一次 Agent 运行的全过程比 print 大法直观太多。截个我查得最多的字段agent 名称、模型、每次工具调用的参数和输出、整轮耗时长在哪一步。碰到“同一段 Prompt 为什么这次结果不对”这种玄学问题先看 Tracing 里模型实际收到的 Messages 顺序大多数问题都能一眼定位。提示如果你是私有化部署、想彻底关掉 Tracing 上报可以用 set_tracing_disabled(True)。但开发阶段强烈建议开着它是排错利器。7.5 生产环境优先接自定义 Session 存储InMemorySessionManager 跑 demo 很爽但进程一重启用户对话全丢。我在项目里把它换成了 Redis 实现实现那几个接口并不复杂。核心注意点是Session 里存的消息结构要稳定升级代码时做好兼容别让旧 Session 因为字段缺失直接恢复失败。这套骨架跑通之后你会发现从“裸调 API 写循环”到“基于 SDK 搭 Agent”差的不是一个包而是一套对 Agent 运行的抽象理解。现在这套四件套已经支撑起我这边的好几个内部机器人了后续我准备接着写第二篇重点聊聊 Guardrails 安全防护、自定义 Session 存储的完整实现以及更复杂的 Agent 拓扑编排。这篇先到这里有疑问欢迎评论区交流我看到都会回。
返回列表