移交 (Handoffs)
在移交 (handoffs) 架构中,行为会根据状态动态改变。其核心机制在于:工具更新跨轮次持久化的状态变量(例如 current_step 或 active_agent),系统读取该变量以调整行为——既可以应用不同的配置(系统提示词、工具),也可以路由到不同的智能体。该模式既支持不同智能体之间的移交,也支持单个智能体内部的动态配置变更。
TIP
移交 (handoffs) 一词由 OpenAI 提出,指使用工具调用(例如 transfer_to_sales_agent)在不同智能体或状态之间转移控制权。
核心特性
- 状态驱动的行为:行为根据状态变量(例如
current_step或active_agent)发生变化 - 基于工具的转换:工具更新状态变量以在不同状态之间流转
- 直接用户交互:每个状态的配置直接处理用户消息
- 持久化状态:状态在对话轮次之间保持持久
何时使用
当你需要强制执行顺序约束(仅在满足前置条件后才解锁能力)、智能体需要在不同状态下直接与用户交谈,或者你正在构建多阶段对话流时,请使用移交模式。该模式在需要按特定顺序收集信息的客户支持场景中尤为实用——例如,在处理退款之前收集保修 ID。
基础实现
其核心机制是一个返回 Command 来更新状态的工具,从而触发向新步骤或智能体的转换:
import { tool, ToolMessage, type ToolRuntime } from "langchain";
import { Command } from "@langchain/langgraph";
import { z } from "zod";
const transferToSpecialist = tool(
async (_, config: ToolRuntime<typeof StateSchema>) => {
return new Command({
update: {
messages: [
new ToolMessage({
content: "Transferred to specialist",
tool_call_id: config.toolCallId
})
],
currentStep: "specialist" // 触发行为变更
}
});
},
{
name: "transfer_to_specialist",
description: "Transfer to the specialist agent.",
schema: z.object({})
}
);NOTE
为什么包含 ToolMessage? 当 LLM 调用工具时,它期望获得一个响应。带有匹配 tool_call_id 的 ToolMessage 完成了这一“请求-响应”循环——如果没有它,对话历史将会格式异常。只要你的移交工具更新了消息,就必须这样做。
完整实现请参阅以下教程。
了解如何使用移交模式构建客户支持智能体,其中单个智能体在不同配置之间流转切换。
实现方式
实现移交有两种方式:带有中间件的单智能体(具有动态配置的单个智能体)或多智能体子图(作为图节点的不同智能体)。
带有中间件的单智能体
单个智能体会根据状态改变其行为。中间件会拦截每次模型调用,并动态调整系统提示词与可用工具。工具则更新状态变量以触发流转转换:
import { tool, ToolMessage, type ToolRuntime } from "langchain";
import { Command } from "@langchain/langgraph";
import { z } from "zod";
const recordWarrantyStatus = tool(
async ({ status }, config: ToolRuntime<typeof StateSchema>) => {
return new Command({
update: {
messages: [
new ToolMessage({
content: `Warranty status recorded: ${status}`,
tool_call_id: config.toolCallId,
}),
],
warrantyStatus: status,
currentStep: "specialist", // 更新状态以触发流转
},
});
},
{
name: "record_warranty_status",
description: "Record warranty status and transition to next step.",
schema: z.object({
status: z.string(),
}),
}
);完整示例:带有中间件的客户支持
typescriptimport { createAgent, createMiddleware, tool, ToolMessage, type ToolRuntime, } from "langchain"; import { Command, MemorySaver, StateSchema } from "@langchain/langgraph"; import { z } from "zod"; // 1. 定义包含 current_step 跟踪器的状态 const SupportState = new StateSchema({ currentStep: z.string().default("triage"), warrantyStatus: z.string().optional(), }); // 2. 工具通过 Command 更新 currentStep const recordWarrantyStatus = tool( async ({ status }, config: ToolRuntime<typeof SupportState.State>) => { return new Command({ update: { messages: [ new ToolMessage({ content: `Warranty status recorded: ${status}`, tool_call_id: config.toolCallId, }), ], warrantyStatus: status, // 流转到下一步 currentStep: "specialist", }, }); }, { name: "record_warranty_status", description: "Record warranty status and transition", schema: z.object({ status: z.string() }), } ); // 3. 中间件根据 currentStep 应用动态配置 const applyStepConfig = createMiddleware({ name: "applyStepConfig", stateSchema: SupportState, wrapModelCall: async (request, handler) => { const step = request.state.currentStep || "triage"; // 将步骤映射到对应的配置 const configs = { triage: { prompt: "Collect warranty information...", tools: [recordWarrantyStatus], }, specialist: { prompt: `Provide solutions based on warranty: ${request.state.warrantyStatus}`, tools: [provideSolution, escalate], }, }; const config = configs[step as keyof typeof configs]; return handler({ ...request, systemPrompt: config.prompt, tools: config.tools, }); }, }); // 4. 使用中间件创建智能体 const agent = createAgent({ model, tools: [recordWarrantyStatus, provideSolution, escalate], middleware: [applyStepConfig], checkpointer: new MemorySaver(), // 跨轮次持久化状态 });
多智能体子图
多个不同的智能体作为独立的节点存在于图中。移交工具使用 Command.PARENT 在智能体节点之间进行导航,以指定下一步执行哪个节点。
WARNING
子图移交需要细致的上下文工程。与单智能体中间件(消息历史自然流转)不同,你必须显式决定在智能体之间传递哪些消息。如果处理不当,智能体将会接收到格式异常的对话历史或臃肿的上下文。详情请参阅下文的上下文工程。
import {
tool,
ToolMessage,
AIMessage,
type ToolRuntime,
} from "langchain";
import { Command, StateSchema, MessagesValue } from "@langchain/langgraph";
const CustomState = new StateSchema({
messages: MessagesValue,
});
const transferToSales = tool(
async (_, runtime: ToolRuntime<typeof CustomState.State>) => {
const lastAiMessage = runtime.state.messages
.reverse()
.find(AIMessage.isInstance);
const transferMessage = new ToolMessage({
content: "Transferred to sales agent",
tool_call_id: runtime.toolCallId,
});
return new Command({
goto: "sales_agent",
update: {
activeAgent: "sales_agent",
messages: [lastAiMessage, transferMessage].filter(Boolean),
},
graph: Command.PARENT,
});
},
{
name: "transfer_to_sales",
description: "Transfer to the sales agent.",
schema: z.object({}),
}
);完整示例:通过移交实现销售与支持
此示例展示了一个包含独立销售和支持智能体的多智能体系统。每个智能体都是一个独立的图节点,移交工具允许智能体之间互相转移对话。
typescriptimport { StateGraph, START, END, StateSchema, MessagesValue, Command, ConditionalEdgeRouter, GraphNode, } from "@langchain/langgraph"; import { createAgent, AIMessage, ToolMessage } from "langchain"; import { tool, ToolRuntime } from "@langchain/core/tools"; import { z } from "zod/v4"; // 1. 定义包含 active_agent 跟踪器的状态 const MultiAgentState = new StateSchema({ messages: MessagesValue, activeAgent: z.string().optional(), }); // 2. 创建移交工具 const transferToSales = tool( async (_, runtime: ToolRuntime<typeof MultiAgentState.State>) => { const lastAiMessage = [...runtime.state.messages] .reverse() .find(AIMessage.isInstance); const transferMessage = new ToolMessage({ content: "Transferred to sales agent from support agent", tool_call_id: runtime.toolCallId, }); return new Command({ goto: "sales_agent", update: { activeAgent: "sales_agent", messages: [lastAiMessage, transferMessage].filter(Boolean), }, graph: Command.PARENT, }); }, { name: "transfer_to_sales", description: "Transfer to the sales agent.", schema: z.object({}), } ); const transferToSupport = tool( async (_, runtime: ToolRuntime<typeof MultiAgentState.State>) => { const lastAiMessage = [...runtime.state.messages] .reverse() .find(AIMessage.isInstance); const transferMessage = new ToolMessage({ content: "Transferred to support agent from sales agent", tool_call_id: runtime.toolCallId, }); return new Command({ goto: "support_agent", update: { activeAgent: "support_agent", messages: [lastAiMessage, transferMessage].filter(Boolean), }, graph: Command.PARENT, }); }, { name: "transfer_to_support", description: "Transfer to the support agent.", schema: z.object({}), } ); // 3. 创建带有移交工具的智能体 const salesAgent = createAgent({ model: "google:gemini-3.6-flash", tools: [transferToSupport], systemPrompt: "You are a sales agent. Help with sales inquiries. If asked about technical issues or support, transfer to the support agent.", }); const supportAgent = createAgent({ model: "google:gemini-3.6-flash", tools: [transferToSales], systemPrompt: "You are a support agent. Help with technical issues. If asked about pricing or purchasing, transfer to the sales agent.", }); // 4. 创建调用智能体的智能体节点 const callSalesAgent: GraphNode<typeof MultiAgentState.State> = async (state) => { const response = await salesAgent.invoke(state); return response; }; const callSupportAgent: GraphNode<typeof MultiAgentState.State> = async (state) => { const response = await supportAgent.invoke(state); return response; }; // 5. 创建检查是否应结束或继续的路由器 const routeAfterAgent: ConditionalEdgeRouter<{ InputSchema: typeof MultiAgentState.State; Nodes: "sales_agent" | "support_agent" }> = (state) => { const messages = state.messages ?? []; // 检查最后一条消息——如果是没有工具调用的 AIMessage,则执行完毕 if (messages.length > 0) { const lastMsg = messages[messages.length - 1]; if (lastMsg instanceof AIMessage && !lastMsg.tool_calls?.length) { return END; } } // 否则路由到当前活跃的智能体 const active = state.activeAgent ?? "sales_agent"; return active as "sales_agent" | "support_agent"; }; const routeInitial: ConditionalEdgeRouter<{ InputSchema: typeof MultiAgentState.State; Nodes: "sales_agent" | "support_agent" }> = (state) => { // 根据状态路由到当前活跃的智能体,默认为销售智能体 return (state.activeAgent ?? "sales_agent") as | "sales_agent" | "support_agent"; }; // 6. 构建图 const builder = new StateGraph(MultiAgentState) .addNode("sales_agent", callSalesAgent) .addNode("support_agent", callSupportAgent); // 根据初始 activeAgent 从条件路由开始 .addConditionalEdges(START, routeInitial, [ "sales_agent", "support_agent", ]) // 在每个智能体执行后,检查是否应结束或路由到另一个智能体 .addConditionalEdges("sales_agent", routeAfterAgent, [ "sales_agent", "support_agent", END, ]); builder.addConditionalEdges("support_agent", routeAfterAgent, [ "sales_agent", "support_agent", END, ]); const graph = builder.compile(); const result = await graph.invoke({ messages: [ { role: "user", content: "Hi, I'm having trouble with my account login. Can you help?", }, ], }); for (const msg of result.messages) { console.log(msg.content); }
TIP
对于大多数移交场景,推荐使用带有中间件的单智能体——它更简单。仅在需要定制化的智能体实现(例如某个节点本身就是一个包含反思或检索步骤的复杂图)时,才使用多智能体子图。
上下文工程
通过子图移交,你可以精确控制智能体之间传递的消息内容。这种精准度对于维护有效的对话历史以及避免由于上下文膨胀而导致下游智能体产生混淆至关重要。有关此主题的更多信息,请参阅上下文工程。
移交过程中的上下文处理
在智能体之间进行移交时,你需要确保对话历史保持有效。LLM 要求工具调用必须与其响应成对出现,因此当使用 Command.PARENT 移交给另一个智能体时,必须同时包含这两者:
- 包含工具调用的
AIMessage(触发移交的消息) - 确认移交的
ToolMessage(对该工具调用的人工构造响应)
如果没有这种成对配对,接收方智能体将看到不完整的对话,并可能产生错误或意外行为。
以下示例假设仅调用了移交工具(没有并行工具调用):
const transferToSales = tool(
async (_, runtime: ToolRuntime<typeof MultiAgentState.State>) => {
// 获取触发本次移交的 AI 消息
const lastAiMessage = runtime.state.messages.at(-1);
// 创建人工工具响应以补全配对
const transferMessage = new ToolMessage({
content: "Transferred to sales agent",
tool_call_id: runtime.toolCallId,
});
return new Command({
goto: "sales_agent",
update: {
activeAgent: "sales_agent",
// 仅传递这两条消息,而不是子智能体的完整历史
messages: [lastAiMessage, transferMessage],
},
graph: Command.PARENT,
});
},
{
name: "transfer_to_sales",
description: "Transfer to the sales agent.",
schema: z.object({}),
}
);NOTE
为什么不传递所有子智能体消息? 虽然你可以在移交时包含完整的子智能体对话,但这通常会带来问题。接收方智能体可能会被无关的内部推理所干扰,而且 Token 成本也会不必要地增加。通过仅传递移交配对消息,你可以让父图的上下文聚焦于高层级的协调。如果接收方智能体需要额外的上下文,请考虑在 ToolMessage 内容中对子智能体的工作进行概括总结,而不是直接传递原始消息历史。
将控制权归还给用户
当将控制权归还给用户时(结束智能体的轮次),请确保最后一条消息是 AIMessage。这既能维护有效的对话历史,也能向用户界面发出智能体已完成工作的信号。
实现注意事项
在设计多智能体系统时,请考虑以下方面:
- 上下文过滤策略:每个智能体是接收完整的对话历史、过滤后的部分内容还是摘要?不同角色的智能体可能需要不同的上下文。
- 工具语义:明确移交工具是仅更新路由状态,还是同时执行副作用。例如,
transfer_to_sales()是否还应创建一张支持工单,或者那应该是一个独立的操作? - Token 效率:在上下文完整性与 Token 成本之间取得平衡。随着对话越来越长,摘要和选择性上下文传递会变得愈发重要。
