news 2026/11/3 13:12:57

OpenAI Agents SDK 多Agent交接机制:用Handoffs解决客服复杂业务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenAI Agents SDK 多Agent交接机制:用Handoffs解决客服复杂业务

做了大半年的单 Agent 客服,我越来越觉得“一个 Agent 包打天下”是个陷阱。指令越堆越长,售前的促销话术和售后的安抚语气互相打架,模型经常拿错工具,改一处提示词整个链路的输出都跟着抖。这篇《OpenAI Agents SDK 构建构建指南(四)》就把我最关注的问题讲透:如何用 Handoffs 把一条业务拆成多个专职 Agent,让它们像真实团队一样互相交接、各管一段,同时解决上下文膨胀、工具权限过宽和提示词互相污染的问题。

这篇内容适合两种人:一是已经在用 OpenAI Agents SDK 搭单 Agent,但被复杂业务逼到瓶颈的开发者;二是正准备把客服、工单、售前售后这类意图分明的流程做成多 Agent 架构的架构师。我会从 Handoffs 的底层执行机制讲起,给一个完整可跑的客服案例,然后重点讨论交接时的上下文处理、安全护栏、Trace 排查和从单 Agent 平滑迁移的实操经验。下面的代码我都基于openai-agentsPython SDK 当前正式版接口写过一遍,如果你的版本比较新,个别回调签名有细微差异,以你本地环境的函数注解为准。

1. 单Agent被业务拆碎:为什么系统要引入任务交接

先说一个我在社区里反复看到的场景。有人把客服需求写成一个 Agent,instructions里又讲订单查询、又讲退款政策、还讲优惠活动,工具列表里挂上七八个函数。刚开始 demo 没问题,上线之后就发现:用户问“能便宜点吗”,模型可能把退款工具调出来;用户只想查物流,它却把整个售前话术搬出来。原因不是模型变笨了,而是单个 Agent 要同时维护太多相互冲突的判断。

1.1 单 Agent 的四个真实瓶颈

我把单 Agent 被拆碎的契机归纳成四类,实际项目里遇到任何一个都该考虑拆分了。

提示词互相污染。售后 Agent 需要语气温和、先道歉再讲方案;售前 Agent 需要主动推品、适度促销。放在同一个instructions里,模型经常学到的是“既要又要则均不彻底”,输出的文案既不够专业也不够亲切。

上下文窗口压力。一个会话如果从售前咨询一直聊到退货,和订单相关的历史、和退款相关的历史全堆在同一个上下文里。还没聊几轮就到窗口上限,老用户会话尤其明显。

工具权限过宽。单 Agent 挂载所有工具,等于把订单查询、退款申请、物流同步的权限全部交给同一个决策链路。模型对“边界”的理解取决于提示词,提示词一旦含糊,误调用就发生了。

灰度困难。想单独优化退款话术,改的却是整个 Agent 的提示词,影响面覆盖所有其他功能。做一次 A/B 测试,整个业务线的输出都跟着变。

1.2 Handoffs 解决问题的思路

OpenAI Agents SDK 给出的答案不是把多个 Agent 拼在一起跑,而是把“交接”做成 SDK 内置的机制。顶层的 Agent 可以声明自己能把任务交给谁,运行时模型收到用户输入后,如果判断当前 Agent 不擅长处理,就会生成一个 Handoff 指令,Runner 自动把控制权移交给目标 Agent,继续在同一个调用链里运行。

这里的关键认知是:多 Agent 不等于多次调用 Runner。你仍然只需要一次Runner.run(agent, input),系统内部可以经历“前台接待 -> 订单查询 -> 售后退款”这样的多次交接,对外表现得像一次完整对话。

我见过不少朋友把多 Agent 做成“一个 Agent 调用另一个 Agent”,也就是子代理模式。子代理不是不能用,但它的语义和 Handoff 完全不同,后面我会专门讲清楚两者的选择标准。

2. Handoffs的执行机制:控制权如何从一个Agent交给下一个

Handoffs 英文直译是“交接”,你可以把它理解成公司的转接电话:前台接到用户需求,判断应该由哪个部门处理,然后直接把电话转过去,原前台不再参与后续通话。这套机制要生效,需要理解三个层次:Runner 的执行流程、交接列表的声明方式、以及 Handoff 和子代理的本质区别。

2.1 Runner 执行交接的完整链路

一次典型的交接运行过程如下:

  1. 调用Runner.run(top_agent, "用户输入"),顶层 Agent 开始运行。
  2. Agent 调用模型,模型返回正常文本内容,则直接结束运行。
  3. 如果模型判断当前不擅长处理该请求,它会返回一个 Handoff 输出,指明目标 Agent。
  4. Runner 检测到 Handoff,把当前 Agent 的执行状态和目标 Agent 的信息记录下来。
  5. 目标 Agent 获取同样的执行上下文,继续调用模型。
  6. 重复步骤 2 到 5,直到某个 Agent 返回普通文本,最终输出返回给调用方。

这个流程意味着,Handoff 不是程序里硬编码的if-else,而是让模型根据当前对话决定“是否交接、交给谁”。所以目标 Agent 的“自我介绍”写得清不清楚,直接决定整个链路跑得对不对。

我用一个生活化的类比:你把用户提问交给模型,模型不是万能接线员,它更像一个熟练的前台,知道每个问题的归属,然后把电话转到对应负责人。如果前台的“通讯录”写得含糊,转错电话是必然的。

2.2 直接传 Agent 和 handoff():两种声明方式

在 SDK 里给 Agent 配置交接对象有两种写法。

第一种,直接传 Agent 实例:

triage_agent = Agent( name="前台接待", instructions="你是客服前台,判断用户意图后将请求交接给对应专员。", handoffs=[order_tracking_agent, refund_agent], )

这种写法最简单,SDK 会为每个 Agent 自动生成一个 Handoff 对象,交接描述默认取Agent.name。

第二种,用handoff()函数生成更精确的交接配置:

from agents import Agent, Runner, handoff triage_agent = Agent( name="前台接待", instructions="你是客服前台,判断用户意图后将请求交接给对应专员。", handoffs=[ handoff( agent=order_tracking_agent, description="当用户查询订单状态、物流进度时,交给订单查询专员", ), handoff( agent=refund_agent, description="当用户提出退货、退款、换货诉求时,交给售后退款专员", ), ], )

这两种声明方式在运行结果上没有本质区别,但handoff()的description参数价值巨大。我强烈建议所有交接都显式写 description,原因后面讲。

关于handoff()的on_handoff回调,很多教程只是提一嘴,我实际用下来它非常有用。你可以在交接发生时执行一些业务逻辑,比如记录状态、初始化上下文、给下游系统发通知。我一般这样写:

async def prepare_refund(ctx, agent): # ctx.context 是整个调用链共享的上下文 ctx.context["stage"] = "refund_pending" ctx.context["handoff_count"] = ctx.context.get("handoff_count", 0) + 1 print(f"[handoff] 进入 {agent.name}") refund_handoff = handoff( agent=refund_agent, description="当用户提出退货、退款、换货诉求时,交给售后退款专员", on_handoff=prepare_refund, )

要注意,on_handoff的参数签名在不同小版本里有调整,我这里按最常用的写法展示。它接收的执行上下文包装对象,和工具函数里的RunContextWrapper是一致的,这点很关键,因为你可以在回调里读写跨 Agent 共享的业务数据。

2.3 子代理和 Handoff:控制权归谁才是核心差异

很多初学者搞不清handoffs和“Agent 里挂一个工具,工具内部再调用另一个 Agent”的区别。我在项目里总结的口诀:看控制权回不回原 Agent。

子代理模式下,当前 Agent 把任务外包出去,拿到结果后继续自己做判断。比如售前 Agent 需要查库存,它调用库存查询 Agent,得到“该型号有货”后,售前 Agent 仍然负责后续回答。控制权始终在父 Agent 手中。

Handoff 模式下,当前 Agent 把整个会话移交给目标 Agent,原 Agent 不再参与后续决策。比如用户说要退货,前台 Agent 交接给售后退款 Agent,退货怎么处理、怎么回答,全由售后说了算,前台不会再插嘴。

这个差异决定了它们的适用场景:

需求推荐方式原因
查询数据后继续由原 Agent 汇总回答子代理消息需要汇总到原 Agent 输出
用户意图发生了根本转移Handoff原 Agent 无法继续处理该意图
需要几个 Agent 分段完成同一目标Handoff职责边界清晰,避免上下文混合
需要拆分提示词和工具权限Handoff每个 Agent 独立优化,互不干扰

我经常看到一个错误设计:把所有业务 Agent 做成工具,让一个主 Agent 去调用它们,最后主 Agent 还得处理自己并不擅长的售后问题。正确的做法往往是:让主 Agent 充当路由,识别到售后意图后直接 Handoff 给售后 Agent,后续不再介入。

3. 客服实战:前台分流、订单查询、售后退款到底怎么交接

理论讲完,直接上一个能跑的例子。我以一个典型的电商客服场景为例:用户进来可能是查物流、可能是问退款,也可能是问优惠。我希望前台负责分流,订单查询专员负责查物流,售后退款专员负责处理售后,并且售后专员如果发现用户还关心物流,也能把会话转给订单查询专员。

3.1 三个 Agent 的业务边界

先想清楚每个 Agent 的职责,再写代码,不要反过来。

  • 前台接待:负责听用户诉求,不做具体业务,判定意图后交接。它的instructions只需要写清楚怎么判断意图,不需要夹杂政策细节。
  • 订单查询专员:只负责查询订单状态和物流信息,挂载订单查询相关的工具。
  • 售后退款专员:只负责退货退款换货,挂载退款申请工具,但也可以把手头的会话交接给订单查询专员。

代码长这样:

import asyncio from agents import Agent, Runner, handoff # 工具函数按前几篇的方式定义,这里示意 async def get_order_status_tool(order_id: str) -> str: """查询订单状态与物流信息""" return f"订单 {order_id} 已出库,预计两天后送达" async def apply_refund_tool(order_id: str, reason: str) -> str: """提交退款申请,金额与审核由财务系统处理""" return f"订单 {order_id} 的退款申请已受理,退款原因:{reason}" order_tracking_agent = Agent( name="订单查询专员", instructions=( "你是电商客服的订单查询专员,只负责查询订单状态、物流进度。" "回答要简洁,直接给出状态和预计送达时间。" "如果用户提到退货退款,请交接给售后退款专员。" ), tools=[get_order_status_tool], ) refund_agent = Agent( name="售后退款专员", instructions=( "你是电商客服的售后退款专员,负责处理退货、退款、换货问题。" "遇到退款申请,先了解原因,再调用退款的函数。" "如果用户同时询问物流进度,请交接给订单查询专员。" ), tools=[apply_refund_tool], handoffs=[order_tracking_agent], ) def make_triage_agent(): return Agent( name="前台接待", instructions=( "你是电商客服前台,你的任务是识别用户意图并交接给对应专员。" "涉及订单状态、物流进度时,交接给订单查询专员。" "涉及退货、退款、换货时,交接给售后退款专员。" "如果无法判断,追问用户具体需求,不要自行处理业务。" ), handoffs=[ handoff( agent=order_tracking_agent, description="当用户查询订单状态、物流进度时,交给订单查询专员", ), handoff( agent=refund_agent, description="当用户提出退货、退款、换货诉求时,交给售后退款专员", ), ], ) async def main(): triage_agent = make_triage_agent() result = await Runner.run( triage_agent, "你好,我前天买的手机还没发货,帮我看看什么情况。", ) print("最终回复:", result.final_output) if __name__ == "__main__": asyncio.run(main())

这段代码假设你已经按前两篇的方式定义好了@function_tool装饰的工具函数,我没在这里重复粘贴注册工具的过程。整个链路是:用户输入 -> 前台接待 -> 判断物流意图 -> 交接给订单查询专员 -> 查询后直接回复。整个过程中,我不用自己去判断该调用哪个 Agent,模型会根据handoffs描述自动完成分流。

3.2 双层交接:退款专员也能再次交接

我在代码里让refund_agent持有handoffs=[order_tracking_agent],这是很多真实场景里需要的设计。用户先投诉“这个商品我申请退款”,客服开始处理后,用户又追问“顺便帮我看看另一个订单的物流”。这时候售后专员不应该假装自己会查物流,它应该把会话再次交接给订单查询专员。

交接链会长成这样:

前台接待 -> 售后退款专员 -> 订单查询专员

你可能会担心:售后退款专员把订单号已经记在自己的上下文里了,交接给订单查询专员时,订单查询专员能看到之前的上下文吗?答案是能,因为整个会话历史在交接时会默认往下传递。不过这种“默认传递”也有副作用,后面第 4 节会重点讲。

3.3 一次运行观察:模型如何决定不交接

我实际跑这个案例时,有一个很有意思的输入:“东西质量太差了,我要投诉。要是我下单的地址能改就好了。”这句话里既有售后意图(投诉),又有修改地址意图(订单变更)。模型最终把会话交给了售后退款专员,但并没有马上调用退款工具,而是先追问用户是退款还是换货。这说明 Handoff 并不代表目标 Agent 必须立刻执行某个动作,它仍然按照自己的instructions来判断下一步。这个观察对排查问题很有用:交接目标选对了,但后续动作不到位,问题往往出在目标 Agent 的instructions而不是路由逻辑。

4. 上下文与历史消息:交接时最容易丢信息的地方

Handoff 跑通很简单,跑通之后最容易踩的坑全在上下文处理。我把这部分单独拿出来,因为我在生产环境里遇到的交接故障,十有八九和上下文有关。

4.1 context 是跨 Agent 共享的业务背包

Runner.run支持传入一个context参数,它是一个普通 dict(实际封装成RunContextWrapper),所有 Agent、所有工具、所有on_handoff回调共享同一个引用。这意味着跨 Agent 传业务数据,正确的通道是 context,而不是让模型在自然语言里转述。

async def main(): user_context = { "user_id": "u_10086", "order_id": "ORDER20250201", "ticket_id": None, } result = await Runner.run( make_triage_agent(), "我要退款", context=user_context, )

我在第 2 节的on_handoff回调里写ctx.context["stage"] = "refund_pending",就是利用这个共享机制。目标 Agent 的名字可能不一样,但是它们读到的ctx.context是同一个对象。业务数据只要放进 context,就不会因为 Handoff 而丢失。

我在早期版本里犯过一个错误:让前台 Agent 对用户说“请稍等,我会把你的订单号转给售后专员”,然后用模型提取订单号再拼进新的提示词里。这种方案极不稳定,用户一句话里可能包含多个订单号,模型可能会提取错,还可能被 prompt injection 干扰。正确做法是,能通过上下文传递的信息,绝不放进自然语言。

4.2 历史消息全量移交:既是资产也是坑

默认情况下,当 Agent A 交接给 Agent B 时,B 会看到 A 之前收到的所有历史消息,包括触发交接的那条 Handoff 输出。这有好处:B 不需要用户复述问题,信息连续。

但也有坏处。最典型的坑,是我在一个工单场景里遇到的:前台 Agent 为了安抚用户,错误承诺“我们 3 小时内一定解决,不用再问”;交接给售后专员后,售后专员也顺着这个错误承诺继续回答。问题根源在传递下去的历史消息里包含了一个错误结论,售后专员把它当成了事实。

解决方式是用 SDK 的input_filter,在交接时对历史消息做处理。你可以选择把前面 Agent 的消息全部清掉,只保留最新一条用户输入;也可以把历史消息改写成一个标准化的摘要,再传给下一个 Agent。

大致的处理模式是这样:

async def clean_history(ctx, agent, history): # 返回新的上下文、目标 Agent、清洗后的输入消息列表 filtered = [msg for msg in history if msg["role"] == "user"] return ctx, agent, filtered

具体签名以你当前 SDK 版本的文档为准,我这里的重点是思路:不要让上一个 Agent 的口误变成下一个 Agent 的前提。清洗策略需要结合业务决定,比如售后场景更关注“用户原始诉求”,前台的话术反而需要被过滤。

4.3 多语言场景:交接后语言突变

还有一个很常见但很少被提及的问题:多语言客服场景下,前台 Agent 用英文整理诉求,售后 Agent 的instructions用中文写的,结果售后 Agent 开始用中文回复英文用户。

这不是 Bug,而是模型把“Agent 的指令语言”当成了“回复语言”。我的解决办法有两种。

一种是在每个 Agent 的instructions里加一句固定的全局约定:“始终使用用户输入使用的语言回复用户。”另一种是更稳的做法,在交接时通过input_filter或on_handoff回调,把用户的原始语言偏好写进 context,下游 Agent 在每次回复前都检查 context 中的语言字段。

我实际推荐第二种,因为提示词约定仍然依赖模型自觉,而 context 里写语言字段后,可以用代码逻辑强制校验输出。如果你还没有处理多语言诉求,至少在第一类方法里加上全局约定,能省掉很多“为什么切换语言”的排查时间。

4.4 别把业务状态放在提示词摘要里

交接链路越长,越有人想把“当前业务进展”用自然语言写进提示词,传给下一个 Agent。比如在instructions里写“用户已经完成了实名认证,退款原因写的是七天无理由”。短会话没问题,长会话很快就变成一团乱麻,模型既无法区分哪些是事实、哪些是上一个 Agent 的推测,也无法处理遗漏字段。

正确的做法是第 4.1 节的 context 思路:所有业务状态都写到结构化的 context 字段里,比如ctx.context["verification_passed"] = True,下游工具或 Agent 直接读取。自然语言只负责“表达”,结构化字段负责“事实”,两者不要混。

5. 输入输出护栏:多Agent场景下的安全闸门

多 Agent 链路越长,单个环节的误判被放大的概率越高。我在生产环境中不会让任何一个 Agent 直接裸奔,至少要在入口和出口设置护栏。SDK 里把这类机制叫 Guardrail,它分成输入护栏和输出护栏两类,都是通过回调函数返回一个“是否触发拦截”的结果。

5.1 护栏拦截的是模型越界,不是业务逻辑

先纠正一个理解误区:护栏不是为了替代instructions,而是为了把“模型自觉”变成“代码强制”。

以单人客服为例,你可以在instructions里写“不要承诺退款时限”,模型大部分时候能遵守,但总有几个边界 case 会翻车。输出护栏的思路是:在 Agent 输出内容前,让一个校验函数判断这次输出是否违反规则,一旦触发,就把输出替换成预设的兜底文案,或者取消这次输出。

这就像公司的合规部门,业务人员可以提出方案,但发出去的每一句话都要过一道合规检查。多 Agent 场景下尤甚,因为交接链中的任何一个 Agent 都可能被 prompt injection 诱导输出违规内容,护栏是最后一道硬性防线。

5.2 输入护栏:拦截异常用户输入

输入护栏一般放在最高层 Agent 的路由之前,判断用户的输入是否值得进入客服链路。比如广告、刷屏、重复提交、无关闲聊,可以用一个轻量校验函数直接拦截,避免浪费后面的模型调用。

一个简单的关键词级输入护栏大概长这样:

from agents import GuardrailFunctionOutput, InputGuardrail async def block_spam(ctx, agent, user_input): blocked = any(token in user_input.lower() for token in ["广告词", "重复内容"]) return GuardrailFunctionOutput( output_info={"blocked": blocked}, tripwire_triggered=blocked, ) triage_agent = Agent( name="前台接待", ..., input_guardrails=[block_spam], )

这里的关键点在于,tripwire_triggered是护栏触发的标志位。触发后 SDK 会按照运行时配置决定是中止执行还是用预设文案替代,从而避免后续 Agent 继续处理垃圾输入。

如果你的场景需要更复杂的语义判断,可以用一个小的分类模型或者另一个 LLM 调用,但要注意控制延迟。我的经验是,入口护栏的优先级是“快”和“省”,它挡掉的是明显异常输入,不需要做到极致准确。

5.3 输出护栏:防止售后Agent给出危险承诺

输出护栏更值得花心思。我还是用退款场景举例。

假设售后退款专员接到退款申请,模型可能直接输出“您的退款将在 24 小时内原路退回”。这句话本身没问题,但如果财务系统实际要 3 到 7 天,这就是一个潜在的投诉炸弹。

输出护栏的处理思路是:让 Agent 先输出一个结构化的结果,比如RefundResult(approved: bool, amount: float, eta_days: int),校验函数检查这个结构化的数据,如果发现eta_days < 3,就触发 tripwire,把输出替换成合规文案“退款申请已受理,到账时间以财务审核结果为准”。

from pydantic import BaseModel from agents import GuardrailFunctionOutput class RefundResult(BaseModel): approved: bool amount: float eta_days: int async def check_refund_output(ctx, agent, output): result = RefundResult.model_validate_json(output) bad_commitment = result.approved and result.eta_days < 3 return GuardrailFunctionOutput( output_info=result, tripwire_triggered=bad_commitment, )

这段逻辑里,output是 Agent 返回的文本,你把它解析成结构化数据,再执行自己的业务校验。触发拦截后,正常流程会用预设内容替换输出,用户看到的是兜底文案,而不是错误承诺。相比在提示词里反复强调“不要承诺时限”,这种方式是硬性的、可测试的。

5.4 护栏与 Handoff 的配合顺序

在多 Agent 场景中,护栏应该放在哪些位置?我的建议是三层:第一层在入口,对所有用户输入做一次输入护栏;第二层在每个 Agent 的instructions里用提示词约束边界;第三层在关键输出点(尤其是涉及退款、转账、承诺时效等操作)设置输出护栏。

注意,我在第二层用的是“提示词约束”而不是“护栏”,因为代码级护栏成本更高、有延迟,不必要的环节不要叠。你要先识别最严重、最容易触发安全风险的输出点,把护栏放在最关键的位置,而不是给每个 Agent 都装一堆 Guardrail。

有一个容易忽略的问题:输出护栏在 Handoff 场景下会怎样?如果护栏拦截的是 Agent 的普通文本回复,它不影响 Handoff 本身;但如果你在护栏校验时解析output时出了异常,可能会阻塞整个调用链。所以我建议护栏函数里做充分的 try-except,并把异常降级为“不触发拦截”,让链路继续跑,同时把异常打到日志里。

6. 交接出问题怎么排查:Trace追踪与数据对比

Handoff 链路一旦跑起来,最大的难点不是写代码,而是出问题后定位问题。交接失败、误交接、接错 Agent,这些错误不像普通函数报错那样直接,它们发生在模型决策层,没有堆栈可看。排查手段主要靠 Trace 追踪和回归数据集。

6.1 用 Trace 看清一次请求的完整链路

OpenAI Agents SDK 默认开启 Tracing。你在本地跑一次Runner.run,它会记录整个调用链的关键事件,包括 Agent 切换、每轮模型调用、工具调用、Handoff 事件。如果配置了 API Key,你可以在官方 Dashboard 上看到这些 trace。

排查交接问题时,我先看的不是最终输出,而是这几个事件:

  • 顶层 Agent 是哪一次模型调用生成了 Handoff;
  • Handoff 的目标 Agent 是谁;
  • 目标 Agent 后续调用了哪些工具;
  • 最后是哪个 Agent 生成了最终回复。

有一次用户说“我要退货但先查下物流”,系统一直把会话交给了订单查询专员,但用户下一个问题还是退货。我打开 trace,发现前台的系统提示词里,“退货”和“物流”两个关键词都被写进了订单查询专员的描述里,描述过于含糊,模型自然无法准确判断归属。

这类问题只看最终输出是看不出来的,你必须看到“模型在哪个节点选择了哪条路径”,Trace 是唯一的客观证据。

6.2 定位误交接的四步排查路径

我总结了一套排查误交接的固定流程,现在每次遇到问题都先按这个顺序走一遍。

第一步,复现。拿同一段用户输入重新跑一次Runner.run,固定模型参数,把 trace ID 记下来。不要一边看输出一边猜原因。

第二步,看 trace 里最上层的 Agent 决策。确认模型是否真的生成了 Handoff,以及它输出 Handoff 前看到的最后一条 assistant 消息长什么样。很多时候你会发现,模型其实已经判断得差不多了,只是handoffs列表里两个 Agent 的描述太像,它选错了对象。

第三步,对比相邻案例。比如“我要退货”被交接给订单查询专员,而“我要换货”被正确交接给售后退款专员。把两个输入并排放在一起,看两个 Agent 的 description 中是否有区分度足够的信息。如果描述只有“售后专员”几个字,模型当然区分不了。

第四步,小步修改。调整 description 之后,只跑这次失败的样例和两个相邻样例,确认修复没有引入新的误判。不要一次性重写所有 handoff 描述。

这套排查路径的核心原则是:模型层面的问题,不要用拍脑袋的提示词修改来解决,先找到那个具体决策点。

6.3 建一个交接回归集:20条话术跑一遍

排查一个问题容易,难的是防止问题回归。我在项目里维护了一个交接回归集,大概 20 条典型用户输入,每条都标记了期望的交接目标 Agent。任何一次instructions或 handoff 描述改动,我都会跑一遍回归集来确认整体行为没有退化。

回归集不一定需要写复杂测试框架,一开始一个脚本加一个字典就够了:

cases = [ ("我要查询订单状态", "订单查询专员"), ("东西还没到,物流显示已签收但我没收到", "订单查询专员"), ("我想退货", "售后退款专员"), ("这个商品质量有问题,要求换货", "售后退款专员"), ("随便看看有什么优惠", "前台接待"), ] for user_input, expect_agent in cases: result = await Runner.run(triage_agent, user_input) current = ... # 从 result 中获取最终处理 Agent 名,不同版本获取方式不同 print(user_input, "->", current, "期望:", expect_agent)

运行结果出来后,重点看两类差异:一是交接目标不匹配,二是本该交接却由前台自己输出了内容。这两类都是边界问题,会在长尾流量里慢慢暴露。养成每次改完 prompt 都跑一遍回归集的习惯,能大幅减少上线后的意外。

7. 从单Agent平滑迁移到多Agent的个人经验

前面几节讲的是“怎么用”,最后我说说“怎么迁”。很多团队不是不知道多 Agent 好,而是担心一拆把现有功能拆崩了。从我带过的项目里看,只要按步骤来,风险完全可控。

7.1 先画边界,再动代码

迁移的第一步不是写 Agent,而是把当前业务的人机流程画出来。我用最简单的“意图-动作”表格,把所有用户诉求列出来,标出每个意图涉及的工具和回复策略。

以客服为例:

用户意图涉及动作建议归属
查询订单/物流调用订单查询工具订单查询专员
退款/退货/换货调用退款申请工具售后退款专员
询问优惠/推荐商品无写操作,读取活动配置售前导购
无法识别意图追问澄清前台接待

画完这张表后,你自然知道哪些 Agent 需要被拆出来。边界的核心原则是:写操作和只读操作分离,涉及财务、售后、对外承诺的意图要单独隔离,因为这类的安全风险最高。

7.2 迁移顺序:保留原Agent作为前台

我建议的迁移路径是:原有单 Agent 先改造成“前台接待”,增加handoffs列表,但instructions里仍保留一部分回答能力,作为兜底。然后拆出第一个只读查询 Agent,比如订单查询专员。验证稳定之后,再把写操作相关意图拆成第二个 Agent,并加上输出护栏。

这样的好处是,即使在迁移过程中 Handoff 偶尔判断失误,原 Agent 仍然能接住,不会直接影响用户。等新 Agent 的准确率稳定后,再逐渐收紧原 Agent 的instructions,让兜底越来越少。

千万不要一开始就把所有功能拆成五个 Agent,然后一次性切换。每拆一个 Agent,都要经过回归集验证,确认业务边界没有被改坏。

7.3 生产环境必做的三件事

多 Agent 上线前,我建议至少做完三件事。

第一,给所有 Agent 的最终输出开启 Trace,并把 trace 导出到自己的日志系统,方便后续审计。如果完全依赖官方 Dashboard,你无法在出问题时快速回溯历史链路。

第二,保留result.raw_responses字段。Runner.run返回的结果对象里包含了完整的分步运行记录,这些数据是排查问题的一手证据。我在生产环境里会把 raw_responses 序列化后存进日志,至少保留 30 天。

第三,给 Handoff 的on_handoff回调加上日志埋点。每次交接发生的时间、来源 Agent、目标 Agent、context 中的关键业务字段,都打出来。交接链路出问题时,这些日志就是“事故现场”。

关于兜底方案,我再分享一个细节:顶层前台 Agent 的instructions里,我会写一句话——“如果你无法判断用户意图,请礼貌地请用户详细描述问题,不要自行处理,也不要强行交接。”这句话看起来平淡无奇,但能挡住大量模型在边界状态下的胡乱交接。它比写一堆“禁止什么”更有效,因为给了模型一个明确的出口。

从我自己的实战体会来说,多 Agent 交接最大的收益不是单点能力变强,而是每个环节都可以独立改动、独立灰度、独立回滚。以前改一句退款话术要重新验证整个客服链,现在只要聚焦售后退款专员一个 Agent。这种工程上的控制感,才是把业务拆碎带来的真正价值。

最后再提醒一句:Handoff 是模型决策机制,不是事务机制。它可能误判、可能在复杂输入下选择错误路径,所以必须和护栏、回归集配套使用。先把投诉率最高的两个意图拆出来跑稳定,再逐步扩展,比一次性画一个大而全的多 Agent 架构图要靠谱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 13:12:05

从零搭建buzz热度系统:核心机制、实现坑与实战经验

1. 从“buzz”这个词本身说起&#xff1a;它到底在指什么“buzz”这个词在英文里的原始含义是蜜蜂飞行时那种嗡嗡声&#xff0c;后来被大量借用到了传播学、营销和互联网产品领域。你如果经常看海外科技媒体的报道&#xff0c;会发现“buzz”几乎成了一个万能标签&#xff1a;它…

作者头像 李华
网站建设 2026/11/3 13:11:24

Maximo入门:EAM系统结构、配置与MboSet开发实战

简介&#xff1a;《maximo入门培训》面向刚接触企业资产管理系统&#xff08;EAM&#xff09;的开发者、实施顾问与运维人员&#xff0c;围绕 Maximo 的 J2EE 技术架构、程序结构、页面开发、工作流、后台任务和数据库配置展开&#xff0c;帮助读者建立从基础概念到二次开发的整…

作者头像 李华
网站建设 2026/11/3 13:10:30

AXI4总线协议核心机制与工程实践:五通道、握手与突发传输

简介&#xff1a;这份AXI4总线协议学习文档面向SOC设计、FPGA开发及AMBA总线入门者&#xff0c;围绕AXI4的五个独立通道、READY/VALID握手机制、通道信号定义、突发传输、outstanding地址与乱序收发等核心知识点展开&#xff0c;并结合ZYNQ中ARM与FPGA交互场景说明其应用价值。…

作者头像 李华
网站建设 2026/11/3 13:10:30

AI协同平台在工业改造中的效率提升实践

1. 项目背景与核心挑战 良策金宝作为一家专注于工业技术改造的服务商&#xff0c;在传统作业模式下长期面临效率瓶颈。我们团队在服务某汽车零部件制造商时&#xff0c;曾遇到典型困境&#xff1a;需要同时协调12名工程师对7条产线进行设备升级&#xff0c;平均每个改造方案需要…

作者头像 李华
网站建设 2026/11/3 13:09:56

钓鱼攻击防御新策略与谷歌安全浏览挑战

1. 钓鱼攻击的现状与谷歌安全浏览的挑战 在当前的互联网环境中&#xff0c;钓鱼攻击已经成为网络安全领域最为普遍且危害巨大的威胁之一。根据最新的网络安全报告显示&#xff0c;钓鱼攻击在所有网络攻击中的占比已经超过40%&#xff0c;而且这个数字还在持续增长。攻击者不断改…

作者头像 李华
网站建设 2026/11/3 13:08:58

企业在线学习系统设计与关键技术解析

1. 企业在线学习系统的核心价值与现状 &#xff08;开头部分&#xff0c;约300字&#xff09; 最近三年接触过47家企业培训负责人&#xff0c;发现一个共性痛点&#xff1a;传统线下培训的出勤率从2019年的78%跌至2023年的32%&#xff0c;而同期线上学习完成率却逆势增长到65%。…

作者头像 李华