☰
OpenAI Agents SDK多智能体编排:从上下文传送到生产级落地
2026/9/29 7:31:45 网站建设 项目流程

写这篇的时候,我手里正好有几个正在用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_agent=order_agent, ), Agent( name="转售后助手", handoff_description="用户要退款、退货、补偿时交接", input_agent=after_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_factory=list) current_order_id: str = None # 更多业务字段... # 在接手新的用户消息时,把同一个会话的context对象继续传进去 result = await Runner.run( triage_agent, "那退款怎么操作?", context=SessionContext( session_id="s-10086", user_id="u-520", user_name="张三", current_order_id="ORD-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, context=session_context, input=history_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把活交接给B,B觉得这不是自己的职责又交接回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, context=context)

并发限制一定要压在接入层,否则流量突增时你会先撞上模型服务端的限流,然后得到一整片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编排框架,它的设计思路和工程化接口都踩在正确的方向上。但工具再顺手,架构设计这件事没人替你完成。希望这一篇的经验能让你少走几步弯路——每一节提到的坑,都是我真实摔过之后才写出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询