
写这篇的时候我手里正好有几个正在用OpenAI Agents SDK打磨的项目。这个系列从第一篇的基础安装讲到这里前四篇已经把所有“跑通Demo”级别的知识点过了一遍怎么初始化服务、怎么调通第一个Agent、怎么写工具装饰器、怎么做基本的调用链。但越往后我越觉得真正让Agent从“玩具”变成“生产力工具”的分水岭不在单个Agent能回答得多聪明而在多个Agent怎么配合、上下文怎么传递、失败之后怎么收场。这一篇我打算把多智能体编排这一整块讲透——包括我在生产环境里踩过的坑以及那些文档里不会明说的经验。先给一个基本结论OpenAI Agents SDK里多智能体协作不是靠“提示词里互相喊话”而是靠指令、交接、上下文和工具边界四件事共同撑起来的。如果这四件事没设计好Agent越多越乱最后会变成一场互相甩锅的灾难。1. 为什么一定要拆分多Agent而不是让一个巨型Agent包打天下1.1 单Agent复杂到一定程度后的必然失控我最早的项目特别天真把客服、订单查询、售后退款、商品推荐全部塞进一个Agent的instructions里。结果就是模型在长上下文里严重“偏科”经常用售后的语气回答商品咨询或者明明应该调用退款工具却跑去查物流。问题的本质不是模型不够聪明而是单个Agent的上下文里混合了太多职责模型对“当前该执行哪个规则”的判别压力越来越大错误率随指令条数指数上升。拆成多Agent之后每个Agent的上下文被有效裁剪只保留自己职责相关的指令和工具。这样每个Agent的提示词可以写得非常专注模型也更容易遵循。用大白话说一个人同时做十件事会精神分裂但十个人各做一件事配合得当就能形成流水线。1.2 分工不是拍脑袋是按“职责边界工具权限”切我在划分Agent时用的标准很简单一组高内聚的指令一组专属工具一个Agent。比如电商项目里Agent名称职责范围可调用工具不允许做的事订单助手查订单、改地址、催发货订单查询工具、地址修改工具退款、改价售后助手处理退款、退货、补偿退款工具、补偿审批工具修改订单状态商品顾问推荐商品、对比参数商品库检索工具、比价工具查订单分诊Agent判断用户意图并交接无业务工具仅负责路由不执行任何业务操作这个表本身就是一个可执行架构图。分诊Agent只负责判断“用户现在需要谁”然后通过SDK的交接机制把会话移交给对应Agent。这样做的好处是工具权限天然收敛任何一个Agent都碰不到职责之外的工具安全边界清晰出问题时也容易定位是谁捅的娄子。1.3 交接机制的正确理解OpenAI Agents SDK里交接handoff不是简单的“调用另一个Agent”而是把整个对话的控制权和历史记录交过去。SDK内部会把交接Agent的历史消息重新包装成新Agent的上下文并且可以用handoff instructions来自定义交接说明。我习惯在每个交接说明里写清楚三件事背景摘要、用户当前诉求、对接收方Agent的明确要求。from agents import Agent, Runner order_agent Agent( name订单助手, instructions负责订单查询、地址修改、催发货禁止处理退款。, tools[query_order_tool, modify_address_tool], ) after_sale_agent Agent( name售后助手, instructions负责退款、退货、补偿处理不回复商品咨询。, tools[refund_tool, compensation_tool], ) triage_agent Agent( name分诊Agent, instructions判断用户意图只负责把话交接给正确的Agent。, handoffs[ Agent( name转订单助手, handoff_description用户想查单、改地址、催发货时交接, input_agentorder_agent, ), Agent( name转售后助手, handoff_description用户要退款、退货、补偿时交接, input_agentafter_sale_agent, ), ], ) result await Runner.run(triage_agent, 我昨天买的手机还没发货帮我催一下) # 这个请求会被分诊到订单助手注意一个细节handoff_description这段描述是给分诊Agent的“信号灯”。分诊Agent本身不执行业务它靠这些描述来决定把会话交给谁。所以描述写得越具体、越贴近真实用户表达分诊准确率越高。我实测下来“用户想查单、改地址、催发货时交接”这种日常化描述远比“负责订单流程”这种抽象描述更管用。2. 上下文传递与状态管理多Agent协作最隐蔽的暗坑2.1 别让每个Agent“失忆”但也要防止上下文变成垃圾桶实际业务里用户不会只说一句话而是连续对话甚至中途会被切换Agent处理。比如用户先问“我订单到哪了”被切换到订单助手然后又问“那退款怎么操作”被切换到售后助手。如果两个Agent之间没有共享状态售后助手就会把前面的订单对话忘得一干二净用户需要重复一遍自己的诉求体验极其糟糕。SDK支持自定义上下文对象可以在一次对话的整个生命周期里贯穿传递。我常用的做法是把会话级的状态放到一个dataclass里在每次Runner.run时通过context参数传入from dataclasses import dataclass, field dataclass class SessionContext: session_id: str user_id: str user_name: str ticket_trace: list field(default_factorylist) current_order_id: str None # 更多业务字段... # 在接手新的用户消息时把同一个会话的context对象继续传进去 result await Runner.run( triage_agent, 那退款怎么操作, contextSessionContext( session_ids-10086, user_idu-520, user_name张三, current_order_idORD-20250101, ticket_trace[用户先催发货, 订单已发货用户改为咨询退款], ), )这个做法的关键在于context不是提示词但它可以被Agent感知。你可以在Agent的instructions里明确写出“根据context中的user_name称呼用户”“处理退款时优先参考current_order_id”。这样即使换了Agent新Agent也能立刻知道用户是谁、之前聊了什么。2.2 我踩过的代价最大的坑过度填充上下文导致Token失控有一段时间我为了让售后Agent“更懂用户”把所有历史交互记录全塞进context结果Prompt Tokens直接暴涨了四倍。更要命的是模型在大量冗余历史中反而抓不住重点回答质量下降。这个坑提醒我上下文是给模型用的“工作记忆”不是日志仓库。我的经验是只放四类东西用户身份标识user_id、name、会员等级当前业务主线current_order_id、问题类型最近三条关键历史操作防止重复提问系统临时标记比如是否需要人工介入其他信息一律走独立的查询工具按需获取。比如售后Agent需要完整的订单物流轨迹时不预先塞在context里而是提供一个query_order_detail(order_id)工具让Agent自己决定要不要查。这样既保证上下文精炼又保留扩展能力。2.3 会话状态同步多轮对话中的“记忆锚点”SDK的Runner.run每次调用都是独立的除非你把历史消息new_input之外的历史传回去否则Agent不会记住上一轮。我在实际项目里维护了一个简单的消息缓存把每次Agent返回的result.to_input_list()追加到会话消息列表里下一次Runner.run时作为历史一起提交。伪代码如下history_messages [] async def chat(user_text): global history_messages result await Runner.run( triage_agent, user_text, contextsession_context, inputhistory_messages [{role: user, content: user_text}], ) history_messages result.to_input_list() return result.final_output这里有一个很容易被忽略的点交接发生时历史消息会自动重组。分诊Agent把订单助手“喊”过来之后订单助手看到的历史并不是分诊Agent的全部原始对话而是经过SDK内部处理的交接摘要。这个设计本身是合理的——但如果你把history_messages又手动加回去可能会造成上下文重复。我的建议是依赖SDK自己的result.to_input_list()来维护历史不要手动拼接否则轻则重复重则导致Agent看到两份互相矛盾的历史。3. 工具注册与动态路由把Agent的“手”管好3.1 工具描述的质量直接决定成败OpenAI Agents SDK会根据函数签名和docstring自动生成工具Schema这一点很方便但也让很多人偷懒docstring写得很随意。我亲眼见过一个项目工具函数写着def get_coupon(user_id):docstring就一句“获取优惠券”结果Agent根本不知道该在什么场景调用导致工具形同虚设。工具docstring的正确写法应该像写给一个刚入职的实习生看的操作手册说明什么时候用、怎么用、有什么坑、返回什么。举例from agents import function_tool function_tool def query_coupon_available(user_id: str, order_amount: float) - str: 查询用户当前可用的优惠券。 适用场景用户询问“有没有优惠”“能便宜多少”“怎么用券”时调用。 如果order_amount小于50返回的列表里不包含满减券不要给用户推荐不可用的券。 返回JSON字符串包含券ID、名称、满减条件、有效期。 ...工具描述写清楚之后Agent的调用准确率会有肉眼可见的提升。这不是玄学——模型是靠描述来匹配用户意图和工具能力的你给它模糊的描述它就给你模糊的调用行为。3.2 动态路由不是所有工具都要注册给所有Agent很多人会把公司所有工具一股脑注册到一个Agent上理由是“万一它要用呢”。这个想法在SDK架构里是毒药。工具数量多模型可选的函数就多选错工具的概率就大而且每个工具定义都会占用Token空间导致有效上下文变短。我在架构里坚持一个原则Agent的工具列表必须和它的职责范围严格一一对应。订单助手只能查订单、改地址绝不能注册退款工具。此外如果你真的要做一个“万能查询Agent”那应该用动态工具路由——让Agent自己通过一个“工具检索器”来决定调用哪个工具而不是在启动时全部注册。3.3 工具调用失败后的自愈工具总会失败这是工程常态。关键的问题是失败之后Agent的行为是什么我遇到最多的情况是工具抛异常后Agent在原地打转反复调用同一个失败工具浪费时间和Token。后来我在每个工具里加了显式的错误反馈把错误原因变成返回值的一部分而不是抛异常让SDK中断function_tool def refund_order(order_id: str, reason: str) - str: 执行退款返回退款结果或失败原因。 try: result refund_api(order_id, reason) if result.status SUCCESS: return f退款成功退款单号{result.refund_no}预计3个工作日到账 return 退款失败 result.fail_reason except TimeoutError: return 退款接口超时建议重新尝试或转人工处理这种设计的目的是给Agent一个“下一步怎么做”的决策依据。如果工具返回了明确的失败原因Agent就能判断“是否重试”“是否换方案”“是否需要转人工”。记住工具不只是执行者还是Agent感知真实世界的传感器。4. 可靠性工程Agent系统真正拉开差距的地方4.1 超时与重试的工程化策略Agent调用模型、工具、外部API的过程都涉及网络波动和响应延迟。SDK层面你可以控制单次Runner.run的超时时间也可以控制工具调用的超时。我建议给工具调用设置一个合理的超时上限比如10秒超时就返回一个明确提示而不是无限等待。原因很简单用户的耐心有限一个查询工具如果超过15秒还没有结果体验已经崩了。重试策略也要分场景查询类操作可以自动重试1-2次写操作退款、改价、发货绝对不自动重试只能提示用户稍后再试或转人工。这是为了防止“事件已处理成功但响应超时导致二次提交”的重复操作事故。4.2 Agent陷入死循环的止损机制多Agent协作场景里最常见的事故是A Agent把活交接给BB觉得这不是自己的职责又交接回A双方来回踢皮球。SDK虽然提供了最大交接轮次控制但默认配置下这个问题仍然可能在复杂业务里爆发。我自己的做法是三层防护第一层在交接说明里写清楚“如果用户诉求不属于职责范围直接回复用户并告知正确渠道不要再次交接”。第二层在Agent指令里加入“禁止反向交接”的明确规则比如售后Agent发现自己不该处理后不允许交回分诊Agent而是直接给用户解释。第三层在应用层做交接次数计数器超过阈值就强制结束会话并转人工客服。这三层防护做下来我在线上基本没有再碰到无限踢皮球的异常。4.3 可观测性谁在什么时间调了什么工具生产环境里Agent的行为具有不确定性没有日志你是没法排查问题的。OpenAI Agents SDK原生支持Tracing可以看到每次运行的详细轨迹——包括模型调用了哪个工具、每个步骤耗时多少、Token消耗多少。我建议从第一天开发起就把trace打开别等到出事故才想起来。我还额外维护了一个结构化日志记录以下字段字段说明session_id会话ID串起多轮对话agent_name当前处理Agent的名字input_content用户输入原文output_contentAgent最终输出tool_calls实际调用的工具序列handoff_path交接链路latency_ms总耗时prompt_tokens / completion_tokensToken消耗error_type异常类型或空有了这些数据即使Agent行为跑偏我也可以回放整个轨迹找出是哪一步的判断出了问题。在AI应用里可观测性不是可选项而是安全的底线。4.4 成本与限流的平衡多Agent架构Token消耗通常是单Agent的2到3倍。比如采用分诊交接模式一个订单查询请求可能要经过分诊Agent判断一次、订单助手执行一次。如果你每天处理10万次请求成本差异会非常明显。我的优化经验是不需要模型智能的环节就别用模型。简单的分诊可以用关键词规则先过滤百分之六七十的流量比如用户消息里出现“退款”就直接路由到售后流程根本不用等模型做意图判断。规则兜底能显著降低延迟和成本还更稳定。对于模型限流SDK本身没有内置全局限流器我是在调用Runner.run的外层封了一个信号量统一控制并发import asyncio semaphore asyncio.Semaphore(50) async def guarded_run(agent, input, context): async with semaphore: return await Runner.run(agent, input, contextcontext)并发限制一定要压在接入层否则流量突增时你会先撞上模型服务端的限流然后得到一整片5xx错误。5. 生产化落地从“能跑”到“能上线”还差什么5.1 输入过滤与输出审核多Agent系统直接面对用户你必须在入口层过滤危险或违规输入在出口层审核Agent输出。否则Agent可能被提示词注入攻击带偏或者输出不合规内容。三层检查是必须的入口关键词过滤、模型输出合规性检测、高危操作人工复核。特别是有退款、改价等敏感操作时一定要设置“高风险操作确认”环节让Agent先输出确认信息用户确认后再执行。5.2 灰度发布与回滚策略Agent系统的改动不像普通代码那样可以精准预测。你改了instructions可能只是一个措辞变化结果某一类问题回答质量大变。所以我在上线流程里强制要求新版本Agent先在灰度流量上跑对比关键指标用户满意度、工具调用准确率、转人工率之后再全量放量。灰度方案很简单按session_id哈希分流把20%的会话切到新版Agent对比老版的成功率。5.3 成本指标纳入监控大盘运行一段时间后你会发现Agent系统的技术指标和业务指标是强联动的。用户咨询量上升Token成本曲线也会陡增。我建议把平均每会话成本、每工具调用成本、分诊失败率这几个指标加进监控大盘并且设置每日预算预警。一旦某个Agent的平均处理成本超过设定阈值立即触发告警避免月底收到天价账单。5.4 与业务系统的异步解耦如果Agent要调用你的订单服务、退款服务注意不能把每一次工具调用都做成同步阻塞。高并发场景下同步调用会把后端系统打垮。我的做法是查询类操作同步调用写操作全部异步化——Agent生成一个操作请求推入消息队列由后端worker执行并回调结果。这样做还有一个额外好处你可以对写操作做审计追踪每一次退款都有完整的操作记录出了纠纷有据可查。6. 这一路实测下来的几点硬心得第一多Agent不是越多越好。我见过有人一个项目拆了十几个Agent每个Agent只有一两句话职责结果交接链路过长错误率反而飙升。我现在的原则是先单Agent能跑的不拆职责确实冲突、工具确实需要隔离的才拆。一般项目三个到五个Agent就足够覆盖绝大多数场景了。第二instructions的写法要像与人沟通一样自然。我发现把指令写成“用户想查单、改地址、催发货时交接”这种描述性语言模型遵循的准确率比“订单助手负责订单流程”这种抽象描述高很多。模型不是在执行程序它是在理解意图所以你给的信号必须贴近真实对话。第三前缀改文档不如前缀改日志。每次调整instructions或工具描述之后不是看看本地跑通就完事而是第一时间跑一组历史回归用例。我留了一批固定的真实用户会话作为测试集每次改动后用这套会话回放比较输出质量。不这么做的话很多回归问题要等上线后被用户骂了才会发现。第四成本优化的空间比你想的大得多。我做过一次全链路审计发现百分之三十以上的Token花在了不必要的长历史上。把历史裁剪、把不必要的工具描述精简、把规则路由前置到模型之前最终整体成本下降了将近一半而回答质量没有明显下降。OpenAI Agents SDK是我目前用过最顺手的多Agent编排框架它的设计思路和工程化接口都踩在正确的方向上。但工具再顺手架构设计这件事没人替你完成。希望这一篇的经验能让你少走几步弯路——每一节提到的坑都是我真实摔过之后才写出来的。