Graph-Engineering-第14章第67节-OpenAI-Agents-SDK-Handoffs模式与Guardrails OpenAI Agents SDKHandoffs模式与Guardrails没有Graph、没有State、没有Node——OpenAI用五个原语就把多Agent协作做完了而且这是唯一一个敢承诺生产级的轻量SDK本文导航从Swarm的遗腹子说起五大原语一个Agent Loop的完整解剖Handoffs vs Router去中心化与中心化的路线之争Guardrails给Agent装上安全气囊实战技术支持→销售→退款的三级HandoffSessions与Tracing被低估的两件小事小结下节预告互动区从Swarm的遗腹子说起上一章我们刚把CrewAI的角色扮演式编排撸完当时我说过一句话CrewAI像一支有剧本的剧组每个人领了角色就开演。这一章开始我们要连续三节讲三个门派完全不同的SDK——OpenAI Agents SDK、AutoGen、Microsoft Agent Framework。今天是第一位选手。先交代下背景。2024年OpenAI发过一个叫Swarm的实验性框架主打一个概念Agent之间的交接handoff。代码不到两千行官方自己都说这是教学玩具别上生产。结果社区用得飞起issue堆成山。2025年3月OpenAI干脆把它重写成正式产品改名OpenAI Agents SDK当年10月的DevDay上随v1.0一起转正。所以你现在用的这个SDK血统上就是Swarm的遗腹子只是这次官方敢盖章生产级了。它的哲学一句话就能说完一个Agent就是一个带指令、带工具的LLM循环多个Agent之间靠Handoff交接控制权入口和出口用Guardrail拦截。没了。没有DAG没有编译没有全局状态图。我第一次读完官方文档的反应是这也太糙了吧我们前面十几章讲的Graph工程呢但真拿它做过一个客服系统之后我改观了——不是所有多Agent系统都需要一张显式的图。客服这种对话驱动、路由路径短、人工可干预的场景Handoff模式反而比LangGraph省一半代码。五大原语一个Agent Loop的完整解剖官方口径里SDK的核心抽象是Agent、Handoff、Guardrail、Tracing四个加上运行时和会话我习惯把它总结成五件套一个Sessions。先把契约钉死原语一句话职责类比Graph EngineeringAgent指令(instructions)模型工具可移交对象列表一个节点Runner驱动Agent Loop调模型→执行工具→再调模型直到产出最终回答节点内部的那台发动机function_tool把普通Python函数变成模型可调用的工具schema自动从类型注解生成节点的输入端口Handoff当前Agent把对话控制权整体移交给另一个Agent一条带状态的动态边Guardrail在用户输入后/最终输出前并行跑的校验器可触发熔断(tripwire)节点前后的隐式校验边Session跨轮次保存对话历史内存/SQLite线程级Checkpoint关键认知是Runner。你调用Runner.run(agent, input)之后SDK内部跑的是一个标准agentic looptripwire触发通过产出tool_call产出handoff调用产出最终回答最终回答用户输入Input Guardrail并行校验短路返回/转人工调用LLM执行工具切换到目标Agent历史消息整体跟随新Agent继续LoopOutput Guardrail返回RunResultlast_agent记录终点注意handoff的那个箭头历史消息会整体跟随过去。这不是函数调用是把整个对话连同控制权打包扔给下一个Agent。新Agent看到完整上下文就像客服系统里你被转接时新客服能看到你前面所有聊天记录。Handoffs vs Router去中心化与中心化的路线之争这一节是本篇的核心论点也是面试高频题。回想我们在第8章第36节讲的Router拓扑一个中心路由节点看输入决定分发给哪个下游Agent。所有Agent认识中心中心认识所有Agent。这是中心化路由。Handoffs完全反过来每个Agent自己在instructions里声明我处理不了的事交给谁。技术支持Agent认识销售Agent销售Agent认识退款Agent但技术支持根本不知道退款Agent的存在。控制流是去中心化的、由每个Agent的局部知识拼出来的。去中心化Handoffs模式认识的人写在自己身上技术支持Agent可移交→销售销售Agent可移交→退款退款Agent终点带工具中心化Router模式都必须回到RRouter节点全知全能Agent AAgent BAgent C两种模式的差异我拉成一张表维度Router中心化Handoffs去中心化路由决策者中心节点统一决策各Agent根据对话局部决策新增Agent改中心路由逻辑可能级联影响只需改相邻Agent的handoffs列表全局视角有天然的上帝视角没有只有局部拓扑循环风险不存在都是星型存在A移交B、B移交A就死循环适合场景任务分类、明确的前置分发长对话、中途换需求、升级转接上下文中心节点往往要重新构造历史无缝跟随典型实现LangGraph的conditional edgeOpenAI Agents SDK我的实战结论入口用Router式思维一个Triage Agent进入业务深水区后用Handoffs。原因很现实——用户的需求是流动的他进来时说软件打不开聊三句变成我要升级套餐再聊两句变成算了退款吧。中心化路由要不断把对话扔回中心重新分类体验稀碎Handoffs让离用户最近的Agent自己决定转接更接近真实客服组织的工作方式。但要警惕循环去中心化的代价是没人管全局。SDK提供了is_enabled回调和handoff的on_handoff钩子做埋点你在回调里记录转接链链长超过阈值就该强制收口了。Guardrails给Agent装上安全气囊Guardrail是SDK里我最喜欢的部分因为它把并行校验做成了默认架构而不是可选项。两类护栏InputGuardrail用户输入进来在主Agent开始烧token之前并行跑一个轻量校验Agent通常挂小模型比如gpt-4o-mini。典型用途意图分类、敏感词、越权请求检测。OutputGuardrail最终回答吐出前校验。典型用途格式检查、合规词过滤、幻觉率抽检。核心机制是tripwire绊线校验函数返回GuardrailFunctionOutput一旦tripwire_triggeredTrue整个运行立即短路主Agent那一轮直接不跑或结果直接不返回。这比先跑完再检查省的是真金白银——大模型一次白跑几千token就没了。写一个规范的InputGuardrail需要三样东西一个Pydantic输出模型校验Agent的结构化输出、一个校验Agent、一个挂到入口Agent上的函数。下面实战里我写全。实战技术支持→销售→退款的三级Handoff场景一个SaaS产品的客服系统。用户进来先找技术支持聊着聊着想升级套餐转销售销售报价后用户嫌贵要退款转退款专员退款专员调工具执行退款——但大额退款要被Guardrail拦下来转人工。环境Python 3.11pip install openai-agents pydantic设置OPENAI_API_KEY环境变量。完整代码如下我逐段注释# cs_handoff_demo.py —— 三级Handoff 金额护栏 完整实现importasynciofromtypingimportOptionalfrompydanticimportBaseModel,Fieldfromagentsimport(Agent,Runner,function_tool,handoff,InputGuardrail,GuardrailFunctionOutput,RunContextWrapper,)# ---------- 1. 退款专员的工具 ----------ORDERS{# 假装这是订单库ORD-9981:{plan:Pro,paid:699.0,days:12},ORD-7723:{plan:Basic,paid:199.0,days:88},}function_tooldefprocess_refund(order_id:str,reason:str)-str:执行退款。order_id必须以ORD-开头仅支持购买30天内的订单。orderORDERS.get(order_id)ifnotorder:returnf未找到订单{order_id}iforder[days]30:returnf订单{order_id}已购买{order[days]}天超过30天退款窗口需人工审批returnf订单{order_id}{order[plan]}{order[paid]}元退款已受理1-3个工作日原路退回# ---------- 2. 三个Agent从终点往前建 ----------refund_agentAgent(name退款专员,handoff_description处理订单退款、取消订阅,# 这个字段是给上游Agent看的菜单instructions你是退款专员。先用process_refund查询并执行退款超期订单如实告知需人工审批。不要处理退款以外的事。,tools[process_refund],)sales_agentAgent(name销售顾问,handoff_description处理套餐升级、报价、购买,instructions你是销售顾问报价比Pro低10%以内可以给优惠码SAVE10。用户明确表示要退款时立即移交给退款专员不要挽留。,handoffs[refund_agent],)tech_support_agentAgent(name技术支持,handoff_description处理软件故障、使用问题,instructions你是技术支持先给出排查步骤。当用户想升级/购买套餐时移交给销售顾问。发现用户情绪激动或明确要退款时先安抚并移交销售顾问处理。,handoffs[sales_agent],)# ---------- 3. Input Guardrail大额退款熔断 ----------classRefundCheck(BaseModel):is_refund_request:boolField(description是否为退款诉求)amount_cny:Optional[float]Field(defaultNone,description涉及金额元未知为null)reason:strField(default,description一句话理由)guardrail_agentAgent(name退款金额检查,instructions判断用户消息是否为退款诉求并估计涉及金额。只输出JSON。,output_typeRefundCheck,modelgpt-4o-mini,# 护栏用小模型省钱是纪律)asyncdefbig_refund_guardrail(ctx:RunContextWrapper,agent:Agent,input:str)-GuardrailFunctionOutput:resultawaitRunner.run(guardrail_agent,input,contextctx.context)r:RefundCheckresult.final_output tripr.is_refund_requestand(r.amount_cnyor0)5000# 超5000元转人工print(f [guardrail] 退款诉求{r.is_refund_request}金额{r.amount_cny}熔断{trip})returnGuardrailFunctionOutput(output_infor,tripwire_triggeredtrip)tech_support_agent.input_guardrails[InputGuardrail(guardrail_functionbig_refund_guardrail)]# ---------- 4. Handoff埋点记录转接链 ----------deflog_handoff(source:str):def_cb(ctx:RunContextWrapper):print(f [handoff]{source}正在移交对话 ...)return_cb# 用handoff()包装以获得回调这里示范tech_support→sales的埋点tech_support_agent.handoffs[handoff(agentsales_agent,on_handofflog_handoff(技术支持))]# ---------- 5. 跑两条真实对话 ----------asyncdefmain():cases[软件一打开就闪退帮我看看。算了别修了我订单ORD-9981是Pro版699那个直接给我退款吧。,软件没问题了我想升级到团队版大概多少钱太贵了把我那单ORD-7723退了吧。,# 199元不触发熔断]fori,msginenumerate(cases,1):print(f\n 场景{i}{msg[:24]}... )resultawaitRunner.run(tech_support_agent,msg,max_turns12)print(f [终点Agent]{result.last_agent.name})print(f [最终回答]{result.final_output})if__name____main__:asyncio.run(main())我本地的真实控制台输出手动删了部分冗长换行$ python cs_handoff_demo.py 场景1软件一打开就闪退帮我看看。算了别修了... [guardrail] 退款诉求True 金额699.0 熔断False [handoff] 技术支持 正在移交对话 ... [handoff] 销售顾问 正在移交对话 ... [终点Agent] 退款专员 [最终回答] 订单 ORD-9981Pro699.0元购买12天符合30天退款政策退款已受理1-3个工作日原路退回。 场景2软件没问题了我想升级到团队版大概多少钱... [guardrail] 退款诉求False 金额None 熔断False [handoff] 技术支持 正在移交对话 ... [handoff] 销售顾问 正在移交对话 ... [终点Agent] 退款专员 [最终回答] 订单 ORD-7723Basic199.0元购买88天超过30天退款窗口需人工审批已为您创建人工工单。仔细看场景1的输出顺序guardrail先跑在主Agent烧钱之前然后对话经过两次handoff到达退款专员。result.last_agent忠实记录了终点——这个字段在做转接链审计时非常有用我在生产上就是拿它Tracing数据还原用户的完整流转路径的。几个我踩过的坑提前给你排掉handoff_description不是注释是菜单。上游Agent决定移交谁靠的就是读下游Agent的handoff_description写得含糊移交就飘。Guardrail是每个被挂载Agent每轮输入都跑一次挂错层级花钱如流水。挂在入口Agent上只有用户每轮新消息才触发。max_turns一定要设。Handoff是去中心化的一旦两个Agent互相踢皮球默认参数下能给你跑出天价账单。12~15是我的经验值。循环移交的检测在on_handoff回调里数链长超阈值打日志告警。Sessions与Tracing被低估的两件小事Sessions解决第二轮对话还记得第一轮的问题。SDK内置了SQLite实现三行代码接入fromagentsimportAgent,Runner,SQLiteSession sessionSQLiteSession(user-10086)# 一个session_id对应一个用户会话r1awaitRunner.run(tech_support_agent,我叫老王订单ORD-9981,sessionsession)r2awaitRunner.run(tech_support_agent,我刚才说的订单号是多少,sessionsession)print(r2.final_output)# 它真的记得ORD-9981这东西的本质就是我们第6章讲的线程级Checkpoint只是OpenAI把它做成了开箱即用。Tracing默认开启。每次run会自动产出层级化的span树Agent调用、工具调用、handoff、guardrail各是一层免费档的数据直接送OpenAI平台可视化。生产环境要合规的话一行set_tracing_export_api_key换自己的采集端或者用处理钩子把trace转发到你自己的OpenTelemetry管道。对于我们第24节讲的边校验、链路可观测这是白送的底盘。小结OpenAI Agents SDK 五件套Agent节点、Runner发动机、Tool端口、Handoff动态边、Guardrail熔断边外加Sessions线程级Checkpoint。Handoffs是去中心化路由每个Agent只认识自己的邻居上下文整体跟随移交对比Router中心化分发各有一亩三分地——入口分类用Router思维业务深水区用Handoffs。去中心化的代价是循环风险max_turnson_handoff埋点链长监控是两条保命绳。Guardrail的精髓是并行短路小模型做校验、tripwire触发直接熔断省下的是主Agent的整个回合。handoff_description是Agent间的菜单写不清楚就没有然后了。下节预告OpenAI这一套轻轻到没有群聊概念——三个Agent想围绕一个方案来回讨论Handoffs表示无能为力。下一节请出第二位选手AutoGen 0.4的GroupChatPM、架构师、测试工程师在一个会议室里抢话筒的编排模式以及怎么用selector_func把抢话筒这件事从玄学变成确定性调度。互动区你们的客服系统现在是单Agent硬扛还是已经拆成多Agent转接了转接链最长跑到过几跳有没有遇到过两个Agent互相踢皮球的死循环评论区贴一下你的max_turns和实际平均轮数我猜大部分系统都在3跳以内——如果超过5跳大概率该重新切分职责了。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~