资讯动态

Handoffs与动态路由:多Agent系统不丢上下文的交接机制

发布时间:2026/10/14 22:42:04 来源:尧图企业网站定制
先说个我踩过的坑。团队早期做内部服务Agent时什么都交给一个“全能Agent”处理查订单、改密码、退款、报表查询、知识库问答全塞进一个系统提示词里。一开始还能跑单子一多就原形毕露——上下文越来越长模型开始“人格分裂”一会儿用客服语气一会儿又像工程师说话甚至出现改密码的对话里突然蹦出退款政策的幻觉内容。后来我们把职能拆开做成多个专业Agent结果新的麻烦又来了请求进来不知道该找谁每个Agent都推理一遍交接上下文时漏参数、丢前提用户问一句“我之前那个申请处理到哪了”新Agent完全接不上话。折腾了大半个月才算把Handoffs和动态路由这套机制理顺。这篇文章想把“交接班”这件事讲透什么是Handoffs什么是动态路由怎么设计才能不丢上下文、不绕圈子、不让Agent踢皮球。适合正在做Agent编排、多角色协作或意图路由的朋友不管你是用现成框架还是自己实现底层思路都是通用的。1. 为什么Agent之间需要“交接班”单体Agent的瓶颈与拆分的阵痛1.1 单体Agent的三大痛点很多团队一开始都会走“一个Agent干所有事”的路线因为它最简单逻辑都在一个上下文窗口里。但业务复杂后三大痛点会接踵而至。第一是上下文膨胀。假设每个子任务平均消耗2000 token十个不同场景叠加在一个会话里就是2万token起步而且历史对话不会自动遗忘。送到模型里的内容越多注意力越分散关键信息反而容易被淹没。我用过一个具体例子用户在对话里先确认了一笔退款金额后来又问另一笔订单的物流由于旧的退款上下文还在模型把两个订单号混在一起产生了幻觉。第二是角色冲突。把客服话术、工程师排障思路、财务审核规则塞进同一个系统提示词本质上是让模型在同一段上下文里扮演多个人设互相干扰。测试时你会发现模型会在售后安抚的话里突然冒出技术术语或者在需要严格执行政策时反而开始变通。这不是模型能力问题是系统设计给了它太多互相矛盾的指令空间。第三是错误传染。一个环节出错会顺着上下文蔓延到所有后续任务。比如有一次模型误判了用户所在地区后续所有涉及配送范围、售后政策的回答全部跟着错纠正起来发现之前的推理已经污染了一整段对话记忆。单体架构下这种污染很难隔离。这时候自然想到拆分把不同职能做成独立Agent各自维护独立系统提示词和上下文。但拆分之后新的问题马上暴露出来。1.2 拆开之后的新问题谁来接、怎么接拆成多个Agent后第一个问题是没人负责“分诊”。用户说“我要退货”系统里有订单Agent、售后Agent、物流Agent究竟是哪个接手如果让所有Agent轮流尝试既浪费token又容易出错——好几个Agent都认为自己能处理最后结果都是不可靠的。第二个问题是缺乏明确的交接协议。假设售后Agent处理到一半发现需要订单信息它怎么把当前对话状态安全地转给订单Agent简单地把整个对话历史丢过去新Agent要重新理解一遍而且可能理解错。更麻烦的是如果两个Agent同时以为自己在负责用户会收到两套互相冲突的回答。第三个问题是边界模糊。例如“改收货地址”既可能属于订单Agent也可能属于账号Agent。业务上这需要路由决策而不是靠每个Agent自行判断。没有统一的路由层只能随机选一个或者让用户重新描述体验极差。1.3 交接的本质责任与上下文的双重转移要解决这些问题先想清楚“交接”的意思。现实中交接班至少包含三件事上一班的人把未完成的事说清楚、正在做的事情出到哪一步了、下一班的人确认接手并承担后续责任。Agent之间的Handoffs也是同样不只是一次函数调用而是责任转移加上下文转移的组合。责任转移意味着任何时刻系统里有且只有一个Agent对当前用户请求负责其他Agent都处于旁观状态。如果两个Agent同时活跃就会出现回答打架或重复处理的问题。上下文转移也不是把全部历史对话原封不动搬过去而是提炼出对方真正需要的信息用户是谁、目标是什么、已经完成哪些步骤、有哪些限制条件、还需要回答什么。把这些整理成一份结构化的“交接单”交给下一个Agent比甩一坨原始聊天记录可靠得多。理解了这一点后面的设计才会有清晰方向。2. Handoffs与动态路由的核心概念先弄清这两个词的关系2.1 Handoffs不只是一个函数调用不少框架把Handoffs实现成一个工具调用嵌入到Agent的思考过程中比如某个Agent在工具列表里看到一个“transfer_to_order_agent”模型判断当前不归自己管时就会调用它。这只是表现形式真正重要的是背后的协议设计。一个完整的Handoffs至少包含三个环节。首先是触发条件也就是什么情况下允许Agent发起交接是主动判断自己能力不足还是被外部路由层强制指定。其次是内容打包就是交接信息需要按约定格式组装。最后是确认与回执接收方需要确认自己能否处理如果不能要么拒绝并退回要么再交给下一个。我见过很多实现把Handoffs简化成“往函数里传一句reply”新Agent拿着这句话继续对话结果老Agent辛苦推理出来的中间结论全部丢失。这就是为什么Handoffs不只是一个函数调用的原因——你传的不是一句话而是一个结构化的状态转移包。需要明确一点Handoffs通常是Agent内部的自主行为由模型根据上下文判断决定要不要交接。动态路由则是在Agent系统入口或某个中间层做决策决定请求分给哪个Agent。两者处在系统的不同层面但合作非常紧密。2.2 动态路由是决策层Handoffs是执行层可以把动态路由理解为分诊台护士请求进来先判断应该挂哪个科Handoffs则是科室医生发现自己处理不了或发现更合适的人选直接把病人资料移交过去。前者是一次性的路径选择后者可以在处理过程中多次发生。没有动态路由每个Agent都要自己去判断是不是自己的活效率低、重复推理多。没有Handoffs动态路由只能做最开始的分配但现实业务往往是跨域的——用户先问订单再问发票先投诉物流再要求退款。这类场景必须靠Handoffs在多个Agent之间接力。所以它们的关系不是非此即彼而是搭配使用。入口路由做粗粒度分流Agent内部的Handoffs做细粒度的动态调整。我在实际项目里把路由设计成可选但建议开启的层简单系统可以只靠Handoffs让Agent之间互相转但复杂系统必须有明确的入口路由否则每个请求都要所有Agent用一次模型成本根本扛不住。2.3 三种主流的路由判定方式动态路由具体怎么做我的经验是分三档按业务复杂度逐步升级。第一档是纯规则匹配。直接把用户请求的关键字、正则、参数映射到对应的Agent比如包含“退款/退货”就路由到售后Agent。优点是零成本、可解释、不会误判缺点是没法处理同义表达和新场景。适合内部系统、指令固定的场景。第二档是嵌入向量匹配。把历史转人工记录和Agent说明先向量化用户请求进来后做余弦相似度检索取最接近的Agent。这比纯规则泛化能力强但需要对每个Agent准备足够有代表性的样例语句。我一般会让Agent本身生成50到100条它所擅长的意图描述再人工微调后建索引。缺点是边界区域容易模糊两个Agent的向量距离接近时无法细粒度区分。第三档是让LLM做路由决策。把Agent列表和能力说明拼进系统提示词让模型输出一个JSON决定交给谁。这是目前效果最好的做法能理解复杂上下文和隐式意图。成本比前两者高而且需要额外防止模型瞎猜。我在生产系统里通常采用混合模式先走规则无法命中就做带候选Agent列表的LLM判定兜底再落到人工客服。用一张表对比判定方式优点缺点适用场景规则匹配零成本、快速、可解释泛化差、维护成本随场景上升指令固定的系统、初版验证向量检索能处理同义表达、泛化强边界模糊、需要样例数据意图相对稳定、Agent数量多LLM判定理解上下文、最灵活成本高、有误判风险复杂意图、对话历史相关路由实践中没必要一上来就上LLM判定。先跑规则等碰壁了再叠加上下文匹配最后才加LLM。每一层解决前面一层解决不了的问题。3. 设计一套优雅的交接机制核心数据结构与协议细节3.1 交接消息的字段设计建议所有交接都通过一个标准结构体传递而不是裸传字符串。我常用这样的数据结构各个语言大同小异dataclass class HandoffMessage: target_agent: str # 目标Agent标识 source_agent: str # 发起交接的Agent reasoning: str # 为何交接供追踪和调试 user_id: str # 用户身份 session_id: str # 会话标识 goal: str # 用户当前的核心目标 context: dict # 结构化上下文如订单号、状态、前置条件 completed_steps: list # 已经完成的排查或处理步骤 constraints: list # 限制如用户是VIP、必须使用某某政策 priority: int # 优先级高级会员或紧急工单可插队 timestamp: str # 时间戳 reply_to_user: str # 上一Agent最后给用户的回复避免重复追问 handoff_history: list # 交接路径防止A转B转A其中最重要的是context和goal。goal让新Agent不需要重新逼问用户想干什么context则保存了和当前任务有关的变量。handoff_history记录了Agent跳转路径这能用来拦截循环交接——如果某个Agent已经在交接链里重复出现两次直接终止交给兜底。为什么我把reply_to_user单独拿出来因为很多系统交接后新Agent会直接问“请问您需要什么帮助”把上一轮的结果给说没了。这个字段让新Agent知道用户已经听到过什么保留体验的连续性。3.2 上下文传递的边界不是什么都传新手最容易犯的错是“全量传”。把整个 session 的对话历史一锅端给下一个Agent美其名曰保留完全上下文。后果是token暴涨而且新Agent被大量无关信息干扰。我的习惯是三层结构第一层只传结构化信息就是上一节的context、goal、已完成步骤第二层传最近N轮关键对话通常最多三轮用来理解用户语气第三层是原始完整历史但不直传而是落库保存只给一个历史记录ID新Agent需要时可以按需检索。判断该传什么有个简单的标准你是否希望新Agent在回答时“想起来”这些信息如果是放进context如果只是防止用户重复提问把原文放进最近对话摘要。其余全都可以丢弃或放历史库。另外要注意上下文里不要包含敏感键值。比如用户的支付密钥、内部审批流、直觉推断的中间结论不该进入Agent上下文。我在项目里对所有传给Agent的字段做过白名单校验只允许context里定义的键其他的一律过滤。3.3 动态路由的分层实现路由器的设计我通常做成可插拔管道每一层命中就返回没有冲突。class DynamicRouter: def __init__(self, agents: dict): self.agents agents self.rules [] self.embeddings None def add_rule(self, pattern, target): self.rules.append((pattern, target)) def load_embedding_index(self, index): self.embeddings index def route(self, user_query: str, history: str, user_context: dict) - str: # 1. 规则快速命中 for pattern, target in self.rules: if pattern.search(user_query): return target # 2. 向量相似度忽略分诊能力不足的请求 if self.embeddings is not None: best, score self.embeddings.search(user_query, topk1) if score 0.75: return best # 3. LLM判定 return self.llm_route(user_query, history, user_context)这里有几个细节值得说。规则的优先级一定高于向量和LLM因为规则是确定性逻辑命中就靠谱。向量阈值我设0.75不是拍脑袋而是当时用验证集做ROC曲线找到的平衡点低于这个值会导致很多误判而不是召回不足。LLM判定是兜底必须给候选Agent列表和对应能力说明不要让它从所有Agent里自己翻。LLM判定的提示词我总结过一个比较稳定的模板包括这样几部分Agent清单及能力、用户查询、最近的对话历史、以及“如果都都不匹配返回 unknown”。关键是让模型有说“不知道”的权利不要逼它硬选。def llm_route(self, user_query, history, user_context): agent_desc \n.join( f{aid}: {self.agents[aid].description} for aid in self.agents ) prompt f你是一个意图路由器判断当前用户请求应当由哪个Agent处理。 Agent列表 {agent_desc} 用户请求{user_query} 最近对话{history[:2000]} 请输出一个JSON格式为 {{target: agent_id}}。 如果无法确定输出 {{target: unknown}}。 resp llm_call(prompt) target parse_json(resp).get(target, unknown) return target if target in self.agents else unknown输出里设置“unknown”非常重要因为一旦路由Layer没法判定后面的Handoffs和兜底策略就有机会接管而不是在入口直接卡死。4. 实战演练搭一个带Handoffs的多Agent客服系统4.1 场景与Agent划分拿一个真实做过的客服系统案例来说明。用户会问四类事查订单、申请退款、投诉物流、询问知识库政策。我最初设计了四个Agent路由Agent接所有请求入口并分流、订单Agent、售后Agent、知识库Agent。但很快发现实际问题没这么简单。用户可能问“我的订单为什么这么久没发货我要投诉”这里面既有订单查询又有物流投诉还有可能转向退款。所以四个Agent不够需要增加一个投诉升级Agent作为兜底处理那些规则覆盖不了的复杂情绪或多个意图叠加的请求。每个Agent的能力描述我都会写清楚“这个Agent负责什么”和“这个Agent不负责什么”。后者更重要它能让Agent在碰到不属于自己的请求时主动发起Handoffs而不是强行硬答。例如订单Agent的不负责列表包含退货、投诉、账号类问题。4.2 核心代码实现Agent、Handoffs和主循环下面是一个简化但完整可读的实现基于Python的类设计。核心是把Handoffs作为Agent工具暴露给模型让模型在对话过程中自主决定是否移交。from dataclasses import dataclass, field import json class Agent: def __init__(self, name, system_prompt, toolsNone): self.name name self.system_prompt system_prompt self.tools tools or [] def invoke(self, context, message): # 这里接入模型调用把system_prompt、context和message传进去 # 工具调用由框架解析模型输出tool_call就执行对应函数 response llm_with_tools( messages[ {role: system, content: self.system_prompt}, *context, {role: user, content: message} ], toolsself.tools ) return response def make_handoff_tool(target_agent_name): 生成一个Handoffs工具Agent可以调用它把自己手里的对话交给别人 def handoff_to_agent(goal: str, context: dict, reply_to_user: str): return HandoffMessage( target_agenttarget_agent_name, source_agentNone, # 运行时会填充 reasoningagent主动转交, goalgoal, contextcontext, reply_to_userreply_to_user, handoff_history[] ) return { type: function, function: { name: fhandoff_to_{target_agent_name}, description: f将当前对话交接给{target_agent_name}处理, parameters: { type: object, properties: { goal: {type: string, description: 用户的核心目标和当前诉求}, context: {type: object, description: 结构化上下文}, reply_to_user: {type: string, description: 上一Agent对用户的最后回复} }, required: [goal, context, reply_to_user] } } }关键点是make_handoff_tool返回的是一个工具定义。每个Agent初始化时都会把其他所有Agent的名字拼成Handoffs工具列表这样模型在对话中看到不归自己管的内容时就会去调用这些工具。再看主循环如何把路由、Agent响应、Handoffs串起来def main_loop(user_message, session, router): initial_agent router.route(user_message, session.history, session.context) if initial_agent unknown: initial_agent support_agent # 兜底Agent current_agent get_agent(initial_agent) handoff_history [] for _ in range(MAX_STEPS): # 防止无限循环 response current_agent.invoke(session.context, user_message) if response.has_tool_call(handoff): handoff parse_handoff(response) handoff.source_agent current_agent.name handoff.handoff_history handoff_history [current_agent.name] # 检测循环当前Agent已经在交接历史里再次出现 if handoff.handoff_history.count(handoff.target_agent) 2: return escalate_to_human(handoff) session.context.append(情况说明) current_agent get_agent(handoff.target_agent) continue # 没有Handoffs正常回复 return response.message.to_user() # 超出步数上限交给人工兜底 return escalate_to_human(session)主循环的设计要点是MAX_STEPS我设的是5。如果5次交接还没解决大概率是路由逻辑或Agent边界设计有问题直接转人工不要在自动流程里钻牛角尖。escalate_to_human这一步值得多说一句。自动链路不可能永远100%正确一定要设计人工转接路径。我在转接时会把HandoffMessage自动整理成给运营看的工作台摘要运营一眼就能知道用户经历了哪些Agent、哪个步骤出了问题而不是甩给人家上万字的原始对话。4.3 让“交接班”真正优雅的四个细节第一交接必须有确认回执。接手Agent收到Handoff后要先输出一句确认消息比如“好的我来帮您处理退货问题”然后再进入具体回答。如果接手Agent的模型连续两次输出都明显偏向“不属于我”就需要触发再次路由而不是硬答。第二处理过程中的“临时状态”需要固化。比如用户刚输入了一个订单号但还没得到结果订单Agent发起Handoffs前必须把这个订单号写进context否则售后Agent接手后会问“请提供您的订单号”让用户重复劳动。我在测试中就遇到过这种情况被用户吐槽过。第三控制交接后新Agent的“失忆范围”。每个接手Agent最好只基于context 最近一轮消息开始不要把自己系统和提示词外的初始化信息带入。我使用一个session归一化函数每次Handoffs时根据白名单重新生成上下文避免脏数据累积。第四交接日志一定要可观测。我在每个交接点都打结构化日志谁发起了交接、原因是什么、传了哪些字段、每个字段的字节数、接收方是否成功确认。排障时这些日志比任何调试器都好用。5. 排查实录交接中最容易翻车的五种现场这部分都是我在实践和给同事代码评审时反复见的坑整理成速查表。现象根本原因解决方案A转B再转A死循环两个Agent都判断问题不在自己在HandoffMessage里维护handoff_history同一Agent出现两次直接转人工新Agent追问用户已经回答过的信息context中没有沉淀关键实体强制要求Agent在转交前把结构化字段抽出来写单元测试覆盖这个行为路由误判到不相关Agent用户回答被截断向量路由阈值过低或LLM判定缺少候选范围提高阈值或加返回候选列表限制LLM判定时增加“可以提供不超过3个候选”的指令交接后token突然暴涨把完整历史全量传给了新Agent改为结构化信息加最近3轮摘要需要完整历史时按需检索多个Agent同时响应消息乱序缺少唯一接收者约束主循环里保证同一时间只有一个current_agent其他Agent要么等待要么被移除我为每一个问题配一个具体的排查步骤方便大家在实际中快速定位。死循环是最高频的问题。第一次遇到时两个Agent互相说“这不是我需要处理的”来回三次后模型输出开始崩溃。我当时的修复比较粗暴判断同一个Agent在handoff_history出现的次数达到两次就强制转人工后来效果稳定后就一直保留了。追teaser信息重复是第二个高频问题。根源在于Agent判断“信息不够”时往往直接转交而不是提取现有信息。我在框架层加了一个“实体提取守卫”如果一个Agent要发起Handoffs系统会先检查当前对话中是否已经出现订单号、用户名、地址等关键实体若存在但没填进context就用一个固定提示词要求Agent先把实体提取完毕再转交。这个给QA效果显著用户重复提问率下降了大概四成。LLM判定的误判问题我觉得最值得分享。一开始我用“让LLM根据所有Agent描述自行选择”结果它兴致勃勃地猜错误率不低。后来我把Agent描述改成标准模板职责范围 不职责范围 2个例子。加上“不知道就 unknown”这台词后误判率降了很多。不要高估模型对模糊边界的理解力你要尽可能清晰地把边界画出来。还有一个容易忽略的提示词细节在路由层的系统提示词中不能为了追求效率强迫模型“一定要选一个出来”这会让它把用户无关的话也硬分给某个Agent。给unknown出口你会发现整体准确率反而提升。日志是所有这些问题的共同解药。我发现每次深挖一个交接事故都是靠一条条的HandoffMessage日志还原出全貌。开始我不打日志遇到问题只能靠猜。后来我把每一次HandoffMessage的reasoning字段必填代码里强制校验让模型把自己的判断理由写下来。这真的是最好的排障方式强推。6. 做了这么多我觉得“优雅”其实就三点第一点是“可选是否还记得”。新Agent通过结构化context知道自己该知道什么不需要去读所有废话。这是体验连续性维度的优雅。第二点是“永远有兜底”。路由层给unknown交接层给MAX_STEPSAgent层给handoff_history。只要你准备好了退路整个系统才敢大胆做自动决策。这是稳定性维度的优雅。第三点是“一切皆可观测”。每条Handoff都有一个带理由的记录每个路由决策都有输入输出。没有日志所谓优雅只是看起来正常而已。最后分享一个个人操作习惯每次做Agent交接机制时我都会先在纸上画一条“责任转移链”从用户发起请求到最终回复把每一步的负责Agent和传递信息写在旁边。画完后能立刻发现多手交接、信息丢失点以及角色职责重叠的位置。这套工作方法比任何框架内置的能力都管用它逼你把“交接”当成系统设计的一等公民而不是后期补上的补丁。多Agent系统的复杂度不会因为用了哪个框架而降低真正降低复杂度的永远是你把责任边界和上下文传递设计得有多清楚。希望这篇文章能让你下次处理Agent交接时少几次手忙脚乱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑