Handoffs 与动态路由:Agent 之间如何优雅地“交接班“
点击上方 前端Q,关注公众号
回复加群,加入前端Q技术交流群
上一篇我们聊了 Swarm 模式——去掉中央调度者,让 Agent 之间自主交接。文末埋了个钩子:如果说 Swarm 解决了"谁交给谁",那 Handoffs 就是解决"怎么交"。
今天就来拆这个问题。
你可能觉得"交接"不就是 createHandoffTool 一行代码的事吗?实际干过你就知道,坑比想象中多得多:
- ▸交接的时候,上下文怎么传?用户说了一半的需求,新 Agent 能接上吗?
- ▸不是所有交接都该无脑自动化——退款 5000 块,你敢让 Agent 自己拍板?
- ▸同样一个"用户不满意",该交给投诉专员还是高级技术支持?路由逻辑怎么写才不 hardcode?
这篇文章带你从三个维度彻底搞懂 Agent 交接:上下文传递、动态路由、人工介入。
Handoff 的三个层次
先建一个心智模型。Agent 之间的交接不是一刀切的,有三个层次:
层次一:简单交接——只转控制权。 Agent A 说"这事我搞不定,交给 Agent B",系统把控制权转给 B,B 开始处理。上篇的 createHandoffTool 基础用法就是这个层次。
层次二:带上下文交接——传递关键信息。 不只是转控制权,还要把"我做了什么、用户想要什么、当前状态是什么"一起传过去。就像你给同事交接工作,不能只说"这个你来",得说清楚来龙去脉。
层次三:条件交接——根据状态动态决定交给谁。 不是写死"A→B",而是 Agent 根据当前上下文、用户情绪、业务规则来判断"该交给谁"。加上人工介入机制,关键决策让人来拍板。
大部分团队卡在第一层就以为搞定了——结果用户一复杂,Agent 就开始"你不是说过了吗?"的尴尬局面。
createHandoffTool 深度配置
上篇我们用过 createHandoffTool 的基础版,现在来看看它的完整能力。
▎基础回顾
import { createHandoffTool } from "@langchain/langgraph-swarm";
const handoffToHotel = createHandoffTool({
agentName: "hotel_agent",
description: "用户需要酒店相关帮助时,转接给酒店专家",
});
这是最简单的用法——指定目标 Agent 和触发条件描述。但真实场景下,你还需要更多控制。
▎传递上下文数据
Handoff 最大的痛点是上下文断裂。Agent A 处理了 5 轮对话,好不容易搞清楚用户要什么,一交接,Agent B 又从头问。
createSwarm 默认会传递完整的消息历史,但有时候你需要更精细的控制——比如传一份"交接摘要":
import { Command } from "@langchain/langgraph";
import { HumanMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const handoffToHotelWithContext = tool(
async ({ summary, userPreferences }) => {
return new Command({
goto: "hotel_agent",
update: {
activeAgent: "hotel_agent",
messages: [
new HumanMessage({
content: `[交接摘要] ${summary}\n[用户偏好] ${userPreferences}`,
name: "flight_agent",
}),
],
// 还可以更新自定义 State 字段
handoffContext: { summary, userPreferences, from: "flight_agent" },
},
});
},
{
name: "transfer_to_hotel_agent",
description: "用户需要酒店帮助时,带上航班信息和用户偏好一起交接",
schema: z.object({
summary: z.string().describe("当前对话摘要,包含已确定的航班信息"),
userPreferences: z.string().describe("用户的偏好,如预算范围、位置要求等"),
}),
}
);
关键点:让 LLM 在交接时自动生成摘要。通过 tool 的 schema 定义,LLM 调用 handoff 工具时会自动填充 summary 和 userPreferences,把关键信息带给下一个 Agent。
▎自定义 State 传递交接上下文
更结构化的做法——在 State 里加 handoffContext 和 handoffHistory 字段:
const SwarmState = Annotation.Root({
messages: Annotation<BaseMessage[]>({ reducer: (prev, next) => [...prev, ...next], default: () => [] }),
activeAgent: Annotation<string>({ default: () => "triage", reducer: (_, next) => next }),
handoffContext: Annotation<{ from: string; reason: string; summary: string } | null>({
default: () => null, reducer: (_, next) => next,
}),
handoffHistory: Annotation<string[]>({ reducer: (prev, next) => [...prev, ...next], default: () => [] }),
});
每个 Agent 处理前先读 state.handoffContext 了解前因后果,在 Prompt 里加一句"如果有交接摘要,基于摘要继续对话,不要重复询问已知信息"就够了。
动态路由:不只是 if-else
上篇 Swarm 里,Agent 自己决定"交给谁"——本质是 LLM 通过 tool calling 来选择。但有些场景下,你需要更精细的路由逻辑。
▎模式一:基于 State 的条件路由
最直接的方式——根据 State 里的字段做分流:
function triageRouter(state: typeof SwarmState.State) {
const msg = (state.messages.at(-1)?.content as string).toLowerCase();
if (msg.includes("投诉") || msg.includes("退款"))
return new Command({ goto: "complaint_agent", update: { activeAgent: "complaint_agent" } });
if (msg.includes("订单") || msg.includes("物流"))
return new Command({ goto: "order_agent", update: { activeAgent: "order_agent" } });
return new Command({ goto: "general_agent", update: { activeAgent: "general_agent" } });
}
优点是快、可预测、零 LLM 调用。缺点是硬编码关键词,用户换个说法就 miss 了。
▎模式二:LLM 驱动的智能路由
让 LLM 来理解意图,做更智能的分流:
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const routerLLM = new ChatOpenAI({ modelName: "gpt-4o-mini", temperature: 0 });
const routeSchema = z.object({
targetAgent: z.enum(["order_agent", "tech_agent", "complaint_agent", "general_agent"])
.describe("最适合处理当前用户问题的 Agent"),
confidence: z.number().min(0).max(1).describe("路由置信度"),
reasoning: z.string().describe("选择该 Agent 的理由"),
});
async function llmRouter(state: typeof SwarmState.State) {
const lastMsg = state.messages.at(-1)?.content as string;
const structuredLLM = routerLLM.withStructuredOutput(routeSchema);
const result = await structuredLLM.invoke([
{
role: "system",
content: `你是一个意图分类路由器。根据用户消息,判断应该交给哪个 Agent 处理:
- order_agent: 订单查询、退换货、物流追踪
- tech_agent: 产品使用问题、技术故障、操作指导
- complaint_agent: 投诉、不满、差评、情绪激动
- general_agent: 其他通用问题`,
},
{ role: "user", content: lastMsg },
]);
// 置信度低于阈值时,走通用 Agent
const target = result.confidence >= 0.7 ? result.targetAgent : "general_agent";
return new Command({
goto: target,
update: {
activeAgent: target,
handoffContext: {
from: "router",
reason: result.reasoning,
summary: lastMsg,
metadata: { confidence: result.confidence },
},
},
});
}
这里有个巧思:路由用便宜的小模型(gpt-4o-mini),实际处理用强模型(gpt-4o)。路由只需要意图分类,不需要强推理,用小模型既快又省钱。
▎模式三:规则矩阵路由
适合业务规则复杂的场景——把路由逻辑抽成配置:
const routeRules = [
{ condition: (s) => s.handoffContext?.metadata?.userLevel === "vip", target: "senior_agent", priority: 100 },
{ condition: (s) => s.handoffHistory.filter(h => h.includes("complaint")).length >= 2, target: "escalation_agent", priority: 80 },
{ condition: (s) => /退款|退货/.test(s.messages.at(-1)?.content as string), target: "refund_agent", priority: 50 },
];
function ruleMatrixRouter(state: typeof SwarmState.State) {
const matched = routeRules.filter(r => r.condition(state)).sort((a, b) => b.priority - a.priority);
const target = matched[0]?.target || "general_agent";
return new Command({ goto: target, update: { activeAgent: target } });
}
规则矩阵的好处是可配置、可审计、非技术人员也能看懂。实际项目中,这些规则可以放在数据库或配置中心,支持热更新。
▎三种模式怎么选?
| 模式 | 延迟 | 灵活度 | 适合场景 |
|---|---|---|---|
| State 条件路由 | 极低(0ms) | 低 | 规则简单、关键词明确 |
| LLM 驱动路由 | 中等(200-500ms) | 高 | 意图模糊、需要语义理解 |
| 规则矩阵路由 | 极低(0ms) | 中 | 业务规则复杂、需要可配置 |
实战建议:先用 State 条件路由处理确定性高的场景(关键词匹配),兜底用 LLM 路由处理模糊意图。规则矩阵适合中后期业务规则沉淀后使用。
Human-in-the-Loop:关键时刻让人来
Agent 自动交接很爽,但有些场景不能全自动:
- ▸退款超过 500 元——需要人工审批
- ▸Agent 连续交接 3 次还没解决——说明 Agent 搞不定,该人工介入了
- ▸涉及敏感操作(删除数据、发送邮件、金融交易)——必须人工确认
LangGraph.js 提供了 interrupt 机制,让你在任意节点暂停执行、等待人工输入。
▎interrupt 基础
interrupt 的工作原理:
- 节点执行到
interrupt()时,抛出特殊异常暂停整个 Graph - 当前状态通过 checkpointer 持久化保存
- 调用方收到
__interrupt__字段,知道 Graph 在等人 - 人工决策后,通过
Command({ resume: value })恢复执行 - 节点从头重新执行,
interrupt()返回resume传入的值
import { interrupt, Command, MemorySaver } from "@langchain/langgraph";
async function refundApproval(state: typeof State.State) {
const { orderId, amount, reason } = state.refundRequest;
if (amount > 500) {
// 大额退款需要人工审批
const decision = interrupt({
question: "是否批准此退款申请?",
details: { orderId, amount, reason },
});
if (decision.approved) {
return { status: "approved", approvedBy: decision.reviewer };
} else {
return { status: "rejected", reason: decision.rejectReason };
}
}
// 小额退款自动通过
return { status: "approved", approvedBy: "auto" };
}
调用方这样恢复执行:
const config = { configurable: { thread_id: "refund-001" } };
// 第一次调用——命中 interrupt,暂停
const result = await graph.invoke({ refundRequest: { orderId: "ORD-123", amount: 800, reason: "质量问题" } }, config);
console.log(result.__interrupt__);
// [{ value: { question: "是否批准此退款申请?", details: {...} } }]
// 人工审批后恢复
const resumed = await graph.invoke(
new Command({ resume: { approved: true, reviewer: "张经理" } }),
config
);
console.log(resumed.status); // "approved"
▎Handoff + interrupt:交接前审批
把 interrupt 和 handoff 结合——Agent 交接超过 3 次还没解决,自动暂停让人工决定是否接管:
async function escalationNode(state: typeof SwarmState.State) {
if (state.handoffHistory.length >= 3) {
const decision = interrupt({
question: "Agent 已交接多次未解决,是否升级为人工处理?",
context: { history: state.handoffHistory, agent: state.activeAgent },
});
if (decision === "human_takeover") {
return new Command({ goto: "human_agent", update: { activeAgent: "human_agent" } });
}
}
return new Command({ goto: state.activeAgent });
}
▎interrupt 使用注意事项
一、不要把 interrupt 放在 try-catch 里。interrupt 通过抛异常来暂停执行,包在 try-catch 里会吞掉这个异常:
// ❌ 错误写法
async function badNode(state: State) {
try {
const answer = interrupt("请确认");
} catch (err) {
console.error(err); // 会捕获到 interrupt 的异常!
}
}
// ✅ 正确写法:interrupt 放在 try-catch 外面
async function goodNode(state: State) {
const answer = interrupt("请确认");
try {
await riskyOperation();
} catch (err) {
console.error(err);
}
}
二、interrupt 前的副作用要幂等。因为恢复时节点会从头执行,interrupt 前面的代码会再跑一次:
// ❌ 错误写法:每次恢复都会重复创建记录
async function badNode(state: State) {
await db.createLog("开始处理"); // 恢复时会再执行一次!
const answer = interrupt("请确认");
return { result: answer };
}
// ✅ 正确写法:用 upsert 或把副作用放在 interrupt 后面
async function goodNode(state: State) {
const answer = interrupt("请确认");
await db.createLog("处理完成"); // 只在恢复后执行一次
return { result: answer };
}
三、不要动态改变同一节点里 interrupt 的顺序。LangGraph 用索引匹配 resume 值,顺序变了值就对不上:
// ❌ 条件跳过 interrupt 会导致索引错乱
async function badNode(state: State) {
const name = interrupt("你的名字?");
if (state.needsAge) {
const age = interrupt("你的年龄?"); // 可能被跳过!
}
const city = interrupt("你的城市?"); // 索引可能对不上
}
// ✅ 保持 interrupt 调用顺序固定
async function goodNode(state: State) {
const name = interrupt("你的名字?");
const age = interrupt("你的年龄?");
const city = interrupt("你的城市?");
}
实战:带升级机制的智能工单系统
把上面的所有知识点组合起来——做一个真实场景的智能客服系统,包含:
- ▸分诊路由:LLM 驱动的意图识别
- ▸带上下文的 Handoff:交接时传递摘要
- ▸自动升级:处理失败自动交接给高级 Agent
- ▸人工介入:敏感操作前 interrupt 审批
▎核心代码
State 定义——比普通 Swarm 多了 handoffContext 和 handoffCount:
const TicketState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (prev, next) => [...prev, ...next],
default: () => [],
}),
activeAgent: Annotation<string>({
default: () => "triage",
reducer: (_, next) => next,
}),
handoffContext: Annotation<{
from: string;
reason: string;
summary: string;
} | null>({
default: () => null,
reducer: (_, next) => next,
}),
handoffCount: Annotation<number>({
default: () => 0,
reducer: (_, next) => next,
}),
});
分诊路由——用 gpt-4o-mini 做意图分类,再路由到对应 Agent:
async function triageNode(state: typeof TicketState.State) {
const lastMsg = state.messages.at(-1)?.content as string;
const classifier = routerLLM.withStructuredOutput(intentSchema);
const intent = await classifier.invoke([
{ role: "system", content: "分析用户意图:order/tech/complaint/faq" },
{ role: "user", content: lastMsg },
]);
const target = agentMap[intent.target] || "faq_agent";
return new Command({
goto: target,
update: {
activeAgent: target,
handoffContext: { from: "triage", reason: intent.target, summary: intent.summary },
},
});
}
退款工具内嵌 interrupt——大额退款自动暂停等审批:
const applyRefundTool = tool(
async ({ orderId, amount, reason }) => {
if (amount > 500) {
const decision = interrupt({
question: E6DB74">`退款 ¥${amount} 需要审批`,
details: { orderId, amount, reason },
});
if (!decision.approved) return `退款被驳回:${decision.rejectReason}`;
}
return E6DB74">`退款 ¥${amount} 已通过,预计 3-5 个工作日到账。`;
},
{ name: "apply_refund", description: "申请退款(超过500元需人工审批)", schema: z.object({ orderId: z.string(), amount: z.number(), reason: z.string() }) }
);
带上下文的交接工具——交接时 LLM 自动生成摘要:
const escalateToComplaint = tool(
async ({ reason, summary }) => {
return new Command({
goto: "complaint_agent",
update: {
activeAgent: "complaint_agent",
handoffContext: { from: "order_agent", reason, summary },
handoffCount: 1,
},
});
},
{ name: "escalate_to_complaint", description: "用户情绪激动或要投诉时,带上下文交接给投诉专家", schema: z.object({ reason: z.string(), summary: z.string() }) }
);
组装 Graph——入口条件路由,多轮对话直接发给活跃 Agent:
const workflow = new StateGraph(TicketState)
.addNode("triage", triageNode, {
ends: ["order_agent", "tech_agent", "complaint_agent", "faq_agent"],
})
.addNode("order_agent", orderAgent)
.addNode("tech_agent", techAgent)
.addNode("complaint_agent", complaintAgent)
.addNode("faq_agent", faqAgent)
.addConditionalEdges(START, (state) =>
state.activeAgent !== "triage" ? state.activeAgent : "triage"
)
.addEdge("order_agent", END)
.addEdge("tech_agent", END)
.addEdge("complaint_agent", END)
.addEdge("faq_agent", END);
const app = workflow.compile({ checkpointer: new MemorySaver() });
▎运行效果
用户:"我的订单 ORD-999 到哪了?"
→ triage 识别为 order 意图 → 路由到 order_agent → 查询订单回复物流信息
用户:"质量太差了,我要退款,这个订单 3000 块"
→ order_agent 调用 apply_refund → 金额 > 500 → interrupt 等待人工审批
→ 人工:Command({ resume: { approved: true } }) → 退款通过
用户:"虽然退了款但你们产品质量太差了,我要投诉!"
→ order_agent 检测到投诉意图 → escalate_to_complaint 带上下文交接
→ complaint_agent 读取交接摘要 → 表示歉意 → 创建高优先级工单
四个核心能力在一次对话中全部串联:LLM 分诊、带上下文 Handoff、interrupt 审批、Agent 自主升级。
OpenAI Swarm 的 Handoff 实现
聊完 LangGraph.js,看看 OpenAI Swarm 怎么做 Handoff——它是 Swarm 概念的发源地,设计哲学跟 LangGraph 很不一样。
▎OpenAI Swarm 的核心思路
OpenAI Swarm 把 Handoff 做到了极简——Agent 的 tool function 直接返回另一个 Agent 对象:
F92672">from swarm F92672">import Swarm, Agent
complaint_agent = Agent(name="Complaint Agent", instructions="你是投诉处理专家。")
# Handoff = 返回 Agent 对象的函数,注册为工具
F92672">def transfer_to_complaint():
"""用户需要投诉时,转接给投诉专家"""
F92672">return complaint_agent
order_agent = A6E22E">Agent(
name="Order Agent",
instructions="你是订单客服,用户要投诉就转给投诉专家。",
functions=[query_order, transfer_to_complaint],
)
response = A6E22E">Swarm().A6E22E">run(agent=order_agent, messages=[...])
通过 context_variables 传递上下文——一个共享字典:
F92672">def transfer_to_complaint(context_variables):
context_variables["escalation_reason"] = "用户投诉"
F92672">return complaint_agent
response = client.A6E22E">run(agent=order_agent, messages=[...],
context_variables={"user_id": "U-AE81FF">123", "vip_level": "gold"})
▎核心差异对比
| 维度 | OpenAI Swarm | LangGraph.js |
|---|---|---|
| Handoff 方式 | 函数返回 Agent 对象 | Command({ goto }) 或 createHandoffTool |
| 上下文传递 | context_variables 字典 | State + Command({ update }) |
| 状态持久化 | 无(纯内存) | checkpointer 支持持久化 |
| 人工介入 | 不支持 | interrupt + Command({ resume }) |
| 多轮对话 | 需自己管理 | thread_id + checkpointer 自动管理 |
| 路由控制 | 隐式(LLM 选 tool) | 显式(Command/条件边)+ 隐式混合 |
| 定位 | 教学/原型 | 生产级 |
▎我的建议
- ▸学习 Handoff 概念:看 OpenAI Swarm——它把复杂的东西做到了极简,设计文档写得非常好
- ▸快速原型验证:用 OpenAI Swarm——几十行 Python 就能跑通
- ▸生产级系统:用 LangGraph.js——持久化、人工介入、可观测性都是生产必备
- ▸前端项目:必选 LangGraph.js——原生 TypeScript,跟前端工具链无缝衔接
避坑指南
▎坑 1:交接时丢上下文
最常见的坑——Agent A 处理了半天,交给 B,B 又从头问用户。
// ❌ 裸交接,B 不知道 A 做了什么
return new Command({ goto: "agent_b" });
// ✅ 带摘要交接,B 能接上对话
return new Command({
goto: "agent_b",
update: {
handoffContext: {
from: "agent_a",
reason: "需要酒店专家",
summary: "用户要去巴黎,航班已选 CA123(3月15日),预算 2000/晚以内",
},
},
});
▎坑 2:路由死循环
Agent A → B → A → B … 无限循环。解法:State 里记录 handoffCount,超过阈值强制走人工;同时加 recursionLimit: 20 兜底。
▎坑 4:审批节点没有超时机制
interrupt 会无限等待人工输入。如果没人审批,这个 thread 就永远挂着。
解法:在应用层实现超时——定时检查未完成的 interrupt,超过时间自动拒绝或升级。LangGraph 本身不提供超时,但 checkpointer 的持久化能力让你在应用层轻松实现定时扫描 + Command({ resume }) 自动恢复。
总结
这篇文章从三个维度拆解了 Agent 交接的完整知识体系:
上下文传递:
- ▸用
Command({ update })传递交接摘要和结构化数据 - ▸在 State 里定义
handoffContext和handoffHistory追踪交接链 - ▸让 LLM 在调用 handoff 工具时自动生成摘要
动态路由:
- ▸State 条件路由:快、可预测,适合确定性高的场景
- ▸LLM 驱动路由:用小模型做意图分类,智能且省钱
- ▸规则矩阵路由:可配置、可审计,适合复杂业务规则
人工介入:
- ▸
interrupt()暂停执行,等待人工输入 - ▸
Command({ resume })恢复执行,传入决策结果 - ▸interrupt 前的副作用要幂等,不要包在 try-catch 里
- ▸在应用层实现超时机制,防止 thread 永远挂起
至此,多 Agent 协作的三篇已经完整覆盖了主流模式:
13 篇:Supervisor 模式 → 中心调度,项目经理制
14 篇:Swarm 模式 → 去中心化,Agent 自主交接
15 篇:Handoffs + 路由 + 人工介入 → 交接细节全拆透
下一篇我们进入 RAG 与生产优化——给 AI 产品加上知识库检索增强,让 Agent 不只靠"脑补",还能查资料。
推荐资源:
- ▸LangGraph.js Interrupts 概念文档:https://langchain-ai.github.io/langgraphjs/concepts/human_in_the_loop/
- ▸LangGraph.js Multi-Agent 概念:https://langchain-ai.github.io/langgraphjs/concepts/multi_agent/
- ▸OpenAI Swarm(Handoff 灵感来源):https://github.com/openai/swarm
- ▸OpenAI Orchestrating Agents 指南:https://cookbook.openai.com/examples/orchestrating_agents

往期推荐
Multi-Agent Teams:让多个专家 Agent 像团队一样协作

AI Agent 是怎么"想一步做一步"的?拆解 ReAct 模式

从零开始:用 LangChain.js 构建你的第一个 Tool-Calling Agent

最后


点个在看支持我吧

更多推荐




所有评论(0)