ARTICLE DETAIL

资讯详情

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

OpenAI Agents SDK 实战:从工具调用到多智能体协作

OpenAI Agents SDK 实战:从工具调用到多智能体协作 1. 从能聊天到能干活Agents SDK 到底解决了什么大多数人第一次接触大模型开发都是从写一个对话循环开始的用户输入问题模型返回答案程序把答案打印出来。这个模式跑通很快但一旦你想让模型真正做点事——查数据库、调接口、按条件分支、多步骤协作——就会发现原来的代码结构根本撑不住。你需要手动解析模型返回的意图手动决定下一步调哪个函数手动把函数结果拼回对话历史再手动判断任务是否完成。写到最后业务逻辑和调度逻辑搅在一起改一处崩三处。OpenAI Agents SDK 就是冲着这个痛点来的。它把让模型自主决策并调用工具完成任务这件事抽象成了一套清晰的编程原语Agent智能体、Tool工具、Handoff交接、Guardrail护栏、Session会话。你不再需要自己写那个又臭又长的 while 循环SDK 内置的 Runner 会替你完成调用模型 → 解析工具调用 → 执行工具 → 回填结果 → 再次调用模型的完整闭环直到模型给出最终答案或者触发终止条件。这套 SDK 的定位很明确它不是给终端用户用的聊天产品而是给开发者用的编排框架。适合谁学我认为有三类人最该认真看第一类是已经用 API 裸写过工具调用、被状态管理折磨过的后端工程师第二类是想把内部知识库、业务系统接入大模型的产品技术负责人第三类是做自动化流程、想让 AI 替代部分人工操作环节的独立开发者。如果你只是想让模型陪你聊天那用现成的对话产品就够了没必要碰 SDK。我自己的体会是Agents SDK 最大的价值不在于它提供了多少炫酷功能而在于它把多智能体协作这件事从玄学变成了工程。以前你说让一个 Agent 负责检索、一个负责总结、一个负责审核听起来很美好但真写起来交接时的上下文怎么传、失败怎么回滚、循环怎么终止全是坑。SDK 用 Handoff 和 Guardrail 把这些边界条件显式化了你写出来的代码是可读、可测、可维护的。这一点比任何性能优化都重要。接下来的内容我会从环境搭建一路讲到多智能体协作把每个原语背后的设计意图、实际写代码时容易踩的坑、以及我实测下来比较稳的配置方式都摊开讲清楚。文章偏实战代码可以直接抄但更重要的是理解每一步为什么这么做。2. 环境准备别急着写 Agent先把这几个地基打牢2.1 依赖安装与版本选择的门道安装本身只有一行命令但版本选择有讲究。Agents SDK 迭代很快不同小版本之间 API 偶有调整我建议在项目里锁定一个明确版本而不是用默认的最新版pip install openai-agents0.0.7为什么强调锁版本因为 SDK 目前还在快速演进期我遇到过升级一个小版本后Runner.run的返回结构变化、导致下游解析代码报错的情况。生产项目里requirements.txt或pyproject.toml里写死版本号是省心的做法。另外SDK 依赖openai官方库安装时会自动带上但如果你项目里已经有旧版openai可能出现版本冲突建议先pip list | grep openai看一眼现状。环境变量方面API Key 的配置是必须的export OPENAI_API_KEY你的密钥注意不要把密钥硬编码在代码里也不要在调试时打印完整密钥。用环境变量或密钥管理服务这是底线。2.2 一个最小可运行示例先跑通再谈架构在深入任何概念之前我强烈建议先跑通这个最小示例建立直观感受from agents import Agent, Runner agent Agent( name助手, instructions你是一个简洁的助手回答控制在两句话以内。 ) result Runner.run_sync(agent, 用一句话解释什么是智能体。) print(result.final_output)这段代码里Agent定义了是谁、什么性格、什么职责Runner负责怎么跑。run_sync是同步版本适合脚本和调试异步场景用Runner.run。跑通之后你会看到SDK 帮你处理了消息格式、模型调用、结果提取的全过程你只需要关心 Agent 的定义。这里有个新手常犯的错把instructions当成随便写写的提示词。实际上instructions是 Agent 的岗位说明书它决定了模型的行为边界。写得越具体Agent 越稳定。比如你是一个助手和你是一个只回答 Python 编程问题、遇到其他领域问题就礼貌拒绝的助手后者的可控性明显更强。2.3 项目目录结构一开始就分好后面少重构我见过太多项目所有 Agent 定义、工具函数、配置全塞在一个main.py里跑到第三个 Agent 就乱成一团。建议一开始就按职责分目录project/ ├── agents/ # 各个 Agent 的定义 │ ├── __init__.py │ ├── researcher.py │ └── writer.py ├── tools/ # 工具函数 │ ├── __init__.py │ └── search.py ├── config/ # 配置与常量 │ └── settings.py └── main.py # 入口负责编排这样分的好处是Agent 之间通过 import 互相引用Handoff 的配置一目了然工具函数独立出来方便单独测试配置集中管理换模型、改超时只动一个地方。这个结构不是强制的但实测下来项目规模超过三个 Agent 之后这种分法的维护成本明显更低。3. Agent 与 Tool让模型真正动手的核心机制3.1 Agent 定义里那些容易被忽略的参数一个完整的 Agent 定义远不止name和instructions。我把常用参数和它们的作用整理成表方便对照参数作用我的建议nameAgent 标识Handoff 和日志里会用到用英文、语义清晰别用中文instructions行为指令决定 Agent 的职责边界具体、可执行避免空泛model指定使用的模型简单任务用小模型复杂推理用大模型tools该 Agent 可调用的工具列表按需给别一股脑全塞handoffs可交接的目标 Agent只配真正需要的下游output_type结构化输出类型需要 JSON 结果时必配重点说model和tools这两个。model的选择直接影响成本和延迟我的经验是检索、分类、格式转换这类任务用小模型完全够用需要多步推理、复杂决策的才上大模型。一个系统里混用不同模型是控制成本的有效手段。tools的配置则关系到 Agent 的能力边界。给太多工具模型容易选错给太少任务完不成。我的做法是每个 Agent 只给它完成本职工作必需的工具。比如一个检索 Agent只给搜索工具一个计算 Agent只给计算器职责单一出错概率就低。3.2 用 function_tool 把普通函数变成工具工具的定义是 Agents SDK 最实用的部分。你不需要写复杂的 JSON Schema只要用装饰器标注一个普通 Python 函数SDK 会自动从类型注解和文档字符串生成工具描述from agents import function_tool function_tool def get_weather(city: str) - str: 查询指定城市的天气。 Args: city: 城市名称例如北京。 # 实际项目中这里调用真实天气 API return f{city}今天晴气温 22 度。这里有几个关键点都是踩过坑才明白的第一文档字符串不是可选项是必选项。模型靠它理解这个工具是干什么的、参数怎么填。文档写得含糊模型就会乱调。我建议文档字符串里明确写出参数的含义和示例值。第二类型注解必须准确。city: str告诉模型这个参数是字符串。如果你写city不带注解SDK 生成 schema 时可能出错模型也拿不到足够信息。第三返回值尽量是字符串或可序列化对象。复杂对象要么转成 JSON 字符串要么定义 Pydantic 模型。直接返回一个自定义类实例模型看不懂。3.3 工具调用失败时模型会怎么反应这是很多人没想过的问题工具执行抛异常了怎么办实测下来SDK 会把异常信息回传给模型模型通常会尝试换一种方式调用或者向用户说明情况。但这个行为不是百分百可靠所以工具函数内部要做好异常处理function_tool def query_database(sql: str) - str: 执行只读查询并返回结果。 try: # 执行查询 return format_result(rows) except Exception as e: return f查询失败{str(e)}请检查 SQL 语法或换一种查询方式。把异常转成友好的文字返回比让异常直接抛出更稳。因为模型看到查询失败请换一种方式这样的提示会主动调整策略而一个未捕获的异常可能导致整个 Runner 中断。提示工具函数里不要做耗时过长的操作。模型调用工具是有超时的一个跑三分钟的工具会让整个流程卡死。长任务应该拆成提交任务 轮询结果两步。4. Handoff 与 Guardrail多智能体协作的两根支柱4.1 Handoff 的本质是控制权转移不是函数调用很多人第一次看到 Handoff会把它理解成Agent A 调用 Agent B。这个理解不准确。Handoff 的本质是控制权的彻底转移A 把当前对话的上下文交给 B之后由 B 继续和用户交互A 不再参与。这跟函数调用调完返回的模型完全不同。理解这一点很重要因为它决定了你什么时候该用 Handoff什么时候该用工具。判断标准很简单如果子任务完成后需要回到原 Agent 继续用工具如果子任务完成后由新 Agent 接管用 Handoff。配置 Handoff 的代码很直观from agents import Agent billing_agent Agent( nameBillingAgent, instructions你负责处理账单、退款、发票相关问题。 ) triage_agent Agent( nameTriageAgent, instructions你是客服入口判断用户问题类型并转交给对应专员。, handoffs[billing_agent] )当triage_agent判断用户问的是账单问题它会触发 Handoff把控制权交给billing_agent。之后用户的所有消息都由billing_agent处理。4.2 Handoff 的上下文传递哪些信息会跟着走这是实操中最容易出问题的地方。Handoff 发生时完整的对话历史会传递给目标 Agent但目标 Agent 的instructions会替换掉原来的。这意味着如果目标 Agent 需要某些背景信息要么这些信息已经在对话历史里要么你得通过其他机制显式传递。我踩过的一个坑设计了一个检索 Agent和一个总结 Agent检索 Agent 把找到的资料放在自己的内部状态里然后 Handoff 给总结 Agent。结果总结 Agent 拿不到那些资料因为它只能看到对话历史看不到前一个 Agent 的内部变量。解决办法是让检索 Agent 把资料作为消息输出到对话里这样 Handoff 时资料就跟着上下文一起过去了。4.3 Guardrail给 Agent 装上刹车和安检Guardrail 分两类输入护栏和输出护栏。输入护栏在用户消息进入 Agent 之前检查输出护栏在 Agent 给出答案之前检查。它们的作用是拦截不合规、不安全、或者不符合业务规则的内容。from agents import Agent, GuardrailFunctionOutput, input_guardrail input_guardrail async def check_topic(ctx, agent, input_text): 拦截与业务无关的问题。 is_off_topic 股票 in input_text or 彩票 in input_text return GuardrailFunctionOutput( output_info{reason: 话题超出服务范围}, tripwire_triggeredis_off_topic )当tripwire_triggered为True时整个流程会被中断不会继续调用模型。这个机制的价值在于它把不该回答的问题挡在了模型调用之前既省了 token又避免了模型被诱导输出不当内容。我的经验是Guardrail 不要写得太复杂。它应该是快速、确定的规则判断而不是另一个需要模型推理的任务。复杂的判断逻辑交给 Agent 本身去做更合适。Guardrail 的定位是硬性红线不是智能审核。4.4 多智能体协作的典型拓扑结构把 Agent、Tool、Handoff 组合起来常见的协作结构有这么几种结构适用场景特点链式流水线任务如检索→分析→写作简单直接但中间环节失败影响全局分诊式客服、工单分类入口 Agent 负责路由各专员独立监督式复杂决策需要审核一个主 Agent 协调多个子 Agent循环式需要反复迭代的任务配合终止条件使用注意防死循环选哪种结构取决于你的任务是否可拆解、拆解后是否需要回退、以及失败时的容错要求。我个人的偏好是分诊式 工具的组合入口 Agent 做路由具体任务用工具完成只有确实需要独立上下文和指令的场景才用 Handoff。这样结构清晰调试也容易。5. 会话状态与上下文管理别让 Agent 失忆5.1 Session 机制解决了什么默认情况下每次调用Runner.run都是独立的Agent 不记得上一轮说了什么。这在单次任务里没问题但多轮对话就崩了。Session 机制就是用来保存对话历史的from agents import Agent, Runner, SQLiteSession session SQLiteSession(user_123, conversations.db) agent Agent(name助手, instructions你是一个有记忆的助手。) result1 Runner.run_sync(agent, 我叫小明。, sessionsession) result2 Runner.run_sync(agent, 我叫什么, sessionsession) print(result2.final_output) # 应该能答出小明Session 把对话历史持久化下次调用时自动带上。SDK 提供了内存版和 SQLite 版生产环境建议用数据库版避免进程重启后记忆丢失。5.2 上下文窗口的取舍历史不是越多越好对话历史越长token 消耗越大而且模型对早期信息的注意力会下降。我实测下来超过二十轮的对话早期内容的召回率明显降低。所以需要主动管理上下文定期摘要把早期对话压缩成一段摘要替换掉原始消息。滑动窗口只保留最近 N 轮更早的丢弃。关键信息提取把用户的关键偏好、已确认的事实单独存起来每轮注入。这几种策略可以组合使用。我的做法是用户画像和已确认事实用结构化存储对话历史用滑动窗口。这样既控制了 token又不会丢掉重要信息。5.3 跨 Agent 的上下文一致性多 Agent 协作时上下文一致性是个隐形难题。A Agent 和用户确认了某个事实Handoff 给 B Agent 后B 是否知道这个事实答案是如果这个事实在对话历史里B 就知道如果在 A 的内部状态里B 就不知道。所以设计多 Agent 系统时我有一条原则所有需要跨 Agent 共享的信息都必须显式地出现在对话历史或共享的 Session 里。不要依赖某个 Agent 的内部变量那是个陷阱。这条原则听起来简单但能避免大量为什么 B 不知道 A 说过的话的诡异 bug。6. 结构化输出与错误处理让结果可被程序消费6.1 用 Pydantic 模型约束输出格式Agent 返回自然语言人看着舒服但程序没法直接用。如果你要把结果存数据库、传给下游系统就需要结构化输出from pydantic import BaseModel from agents import Agent class TaskResult(BaseModel): title: str priority: int tags: list[str] agent Agent( name任务解析器, instructions从用户描述中提取任务信息。, output_typeTaskResult ) result Runner.run_sync(agent, 明天下午三点前把报告发给张总很急。) print(result.final_output.priority) # 直接访问字段配置output_type后SDK 会强制模型按这个 schema 输出返回的对象可以直接当 Pydantic 模型用。这个功能在构建自动化流程时特别有用因为下游代码不用再解析文本。注意结构化输出会略微增加延迟和 token 消耗因为模型需要生成符合 schema 的内容。简单任务不必强上需要程序消费结果时才用。6.2 常见错误类型与应对策略跑 Agent 的过程中我遇到过几类典型错误整理出来供参考错误类型表现应对工具调用死循环Agent 反复调同一个工具设置最大轮次限制工具内加去重输出格式不符结构化输出解析失败简化 schemainstructions 里强调格式上下文超限token 超出模型上限启用滑动窗口或摘要Handoff 循环A 转 BB 又转回 A明确职责边界加交接次数上限工具超时长时间无响应工具内设超时长任务异步化其中工具调用死循环是最常见的。模型有时候会陷入调用工具→结果不满意→再调用的循环。解决办法是在 Runner 层面设置max_turns超过就强制终止并返回当前状态。这个参数一定要设否则一个 bug 可能烧掉大量 token。6.3 日志与可观测性出问题时你能看到什么Agent 系统出问题时最难的是不知道它内部发生了什么。SDK 提供了 tracing 机制可以记录每一步的调用from agents import set_tracing_disabled # 开发阶段开启追踪生产环境按需关闭 set_tracing_disabled(False)开启后每次运行会生成 trace包含模型调用、工具执行、Handoff 的完整链路。调试多 Agent 系统时这个功能是救命的。我建议开发阶段始终开启生产环境根据成本和合规要求决定。除了 SDK 自带的 tracing我还会在关键节点手动打日志Agent 启动、工具调用前后、Handoff 发生时。这些日志在排查为什么这个请求走了错误的 Agent这类问题时比 trace 更直接。7. 一个完整案例把知识库问答拆成三个 Agent7.1 需求拆解与 Agent 职责划分假设要做一个内部知识库问答系统用户提问系统从文档里找答案并回复。直接用一个 Agent 加一个检索工具也能做但为了演示多 Agent 协作我把它拆成三个Router Agent判断问题类型决定走哪条路。Retriever Agent负责检索相关文档片段。Answer Agent基于检索结果组织答案。这个拆法的好处是每个 Agent 职责单一检索和回答可以分别优化。坏处是链路变长延迟增加。所以是否拆分取决于你的任务复杂度和对延迟的容忍度。简单问答一个 Agent 足够需要多源检索、多轮验证的才值得拆。7.2 检索工具的实现要点检索工具是知识库系统的核心。这里不展开向量数据库的细节只讲和 Agent 集成时的关键点function_tool def search_knowledge(query: str, top_k: int 3) - str: 在知识库中检索相关文档片段。 Args: query: 检索关键词或问题。 top_k: 返回的片段数量默认 3。 results vector_store.search(query, top_ktop_k) if not results: return 未找到相关文档。 formatted \n\n.join( f[片段{i1}] {r.content} for i, r in enumerate(results) ) return formatted要点有三个返回结果要带编号方便 Answer Agent 引用没找到时要明确说明避免模型编造top_k 作为参数暴露给模型让它自己决定检索多少条。第三点很多人忽略但对复杂问题很有用——模型可以主动扩大检索范围。7.3 从检索到回答的 Handoff 配置把三个 Agent 串起来retriever Agent( nameRetriever, instructions你负责检索知识库把找到的片段原样返回不要自己总结。, tools[search_knowledge] ) answerer Agent( nameAnswerer, instructions基于检索到的片段回答问题只使用片段中的信息找不到就说不知道。, ) router Agent( nameRouter, instructions判断用户问题是否需要查知识库。需要则交接给 Retriever。, handoffs[retriever] ) retriever.handoffs [answerer]注意retriever的 instructions 里强调不要自己总结这是为了防止它在检索阶段就动手改写内容导致 Answer Agent 拿到的不是原文。职责分离的关键就是每个 Agent 只做自己那一段不越界。7.4 实测中的延迟与成本观察这套三 Agent 结构跑下来我观察到的数据仅供参考具体因模型和文档量而异单次问答的延迟大约是单 Agent 方案的 1.8 到 2.5 倍token 消耗大约是 2 倍。多出来的开销主要在两次额外的模型调用路由判断和检索决策。所以我的建议是如果延迟敏感把 Router 的判断逻辑用规则或小模型实现不要用大模型。路由判断通常很简单关键词匹配加少量规则就能覆盖大部分场景没必要动用大模型。把大模型留给真正需要推理的环节这是控制成本的核心思路。8. 几个我踩过的坑和对应的解法8.1 instructions 写得太聪明反而坏事我一开始喜欢把 instructions 写得很详细恨不得把所有边界情况都列进去。结果发现指令太长时模型反而会忽略其中一部分。后来我改成分层写法核心职责用一两句话讲清楚边界情况用简短的列表补充整体控制在合理长度内。指令不是越长越好清晰比全面更重要。8.2 工具描述里的隐藏陷阱工具函数的文档字符串里如果写了这个工具很慢请谨慎调用之类的话模型有时会过度谨慎该调的时候不调。工具描述应该客观陈述功能不要加入主观建议。要不要调用、调用几次让模型根据任务自己判断你只需要把工具的能力说清楚。8.3 Handoff 后原 Agent 的残留影响Handoff 发生后原 Agent 的 instructions 不再生效但对话历史里可能还留着原 Agent 说过的话。如果原 Agent 说过我马上帮你转接而目标 Agent 不知道这个上下文回复可能显得突兀。解决办法是在 Handoff 的指令里让原 Agent 明确说明交接原因这样目标 Agent 能从历史里读到背景。8.4 别在 Guardrail 里做重活前面提过这里再强调一次。Guardrail 应该是轻量的规则判断。我见过有人在 Guardrail 里调用另一个模型做内容审核结果每次请求都要多跑一次模型延迟翻倍。重活交给专门的审核服务Guardrail 只做快速拦截。9. 下一步可以往哪些方向扩展跑通基础的多 Agent 协作之后有几个方向值得继续深入。第一是持久化与恢复把 Session 存到数据库支持断点续跑这对长任务很重要。第二是并行执行多个独立的检索或分析任务可以用异步并发跑缩短总延迟。第三是评估体系给 Agent 建立测试集量化它的准确率和稳定性没有评估就没法优化。第四是成本监控记录每次调用的 token 消耗找出成本大头针对性优化。这些方向我在后续的实践里会陆续展开。就目前这一篇而言把 Agent、Tool、Handoff、Guardrail、Session 这五个原语理解透能搭出一个结构清晰、可维护的多 Agent 系统就已经跨过了从玩模型到做工程的那道坎。剩下的都是在真实业务里磨出来的经验。
返回列表