点击上方 前端Q,关注公众号

回复加群,加入前端Q技术交流群

上一篇我们聊了 Swarm 模式——去掉中央调度者,让 Agent 之间自主交接。文末埋了个钩子:如果说 Swarm 解决了"谁交给谁",那 Handoffs 就是解决"怎么交"。

今天就来拆这个问题。

你可能觉得"交接"不就是 createHandoffTool 一行代码的事吗?实际干过你就知道,坑比想象中多得多:

  • ▸交接的时候,上下文怎么传?用户说了一半的需求,新 Agent 能接上吗?
  • ▸不是所有交接都该无脑自动化——退款 5000 块,你敢让 Agent 自己拍板?
  • ▸同样一个"用户不满意",该交给投诉专员还是高级技术支持?路由逻辑怎么写才不 hardcode?

这篇文章带你从三个维度彻底搞懂 Agent 交接:上下文传递、动态路由、人工介入

Handoff 全景图

Handoff 的三个层次

先建一个心智模型。Agent 之间的交接不是一刀切的,有三个层次:

层次一:简单交接——只转控制权。 Agent A 说"这事我搞不定,交给 Agent B",系统把控制权转给 B,B 开始处理。上篇的 createHandoffTool 基础用法就是这个层次。

层次二:带上下文交接——传递关键信息。 不只是转控制权,还要把"我做了什么、用户想要什么、当前状态是什么"一起传过去。就像你给同事交接工作,不能只说"这个你来",得说清楚来龙去脉。

层次三:条件交接——根据状态动态决定交给谁。 不是写死"A→B",而是 Agent 根据当前上下文、用户情绪、业务规则来判断"该交给谁"。加上人工介入机制,关键决策让人来拍板。

大部分团队卡在第一层就以为搞定了——结果用户一复杂,Agent 就开始"你不是说过了吗?"的尴尬局面。


createHandoffTool 深度配置

上篇我们用过 createHandoffTool 的基础版,现在来看看它的完整能力。

▎基础回顾

typescript

import { createHandoffTool } from "@langchain/langgraph-swarm";

const handoffToHotel = createHandoffTool({
  agentName: "hotel_agent",
  description: "用户需要酒店相关帮助时,转接给酒店专家",
});

这是最简单的用法——指定目标 Agent 和触发条件描述。但真实场景下,你还需要更多控制。

▎传递上下文数据

Handoff 最大的痛点是上下文断裂。Agent A 处理了 5 轮对话,好不容易搞清楚用户要什么,一交接,Agent B 又从头问。

Handoff 上下文传递

createSwarm 默认会传递完整的消息历史,但有时候你需要更精细的控制——比如传一份"交接摘要":

typescript

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 工具时会自动填充 summaryuserPreferences,把关键信息带给下一个 Agent。

▎自定义 State 传递交接上下文

更结构化的做法——在 State 里加 handoffContexthandoffHistory 字段:

typescript

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 里的字段做分流:

typescript

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 来理解意图,做更智能的分流:

typescript

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)。路由只需要意图分类,不需要强推理,用小模型既快又省钱。

▎模式三:规则矩阵路由

适合业务规则复杂的场景——把路由逻辑抽成配置:

typescript

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 机制,让你在任意节点暂停执行、等待人工输入。

Human-in-the-Loop 流程

▎interrupt 基础

interrupt 的工作原理:

  1. 节点执行到 interrupt() 时,抛出特殊异常暂停整个 Graph
  2. 当前状态通过 checkpointer 持久化保存
  3. 调用方收到 __interrupt__ 字段,知道 Graph 在等人
  4. 人工决策后,通过 Command({ resume: value }) 恢复执行
  5. 节点从头重新执行,interrupt() 返回 resume 传入的值
typescript

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" };
}

调用方这样恢复执行:

typescript

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 次还没解决,自动暂停让人工决定是否接管

typescript

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 里会吞掉这个异常:

typescript

// ❌ 错误写法
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 前面的代码会再跑一次:

typescript

// ❌ 错误写法:每次恢复都会重复创建记录
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 值,顺序变了值就对不上:

typescript

// ❌ 条件跳过 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 多了 handoffContexthandoffCount

typescript

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:

typescript

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——大额退款自动暂停等审批:

typescript

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 自动生成摘要:

typescript

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:

typescript

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 vs LangGraph Handoff

▎OpenAI Swarm 的核心思路

OpenAI Swarm 把 Handoff 做到了极简——Agent 的 tool function 直接返回另一个 Agent 对象

python

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 传递上下文——一个共享字典:

python

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 又从头问用户。

typescript

// ❌ 裸交接,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 里定义 handoffContexthandoffHistory 追踪交接链
  • ▸让 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

最后

点个在看支持我吧

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐