Skip to content

交接架构中,行为基于状态动态变化。核心机制:工具 更新一个跨轮次持续存在的状态变量(如 current_stepactive_agent),系统读取该变量来调整行为——要么应用不同的配置(系统提示词、工具),要么路由到不同的 智能体。该模式既支持不同智能体之间的交接,也支持单个智能体内部的动态配置更改。

TIP

交接(handoffs)这一术语由 OpenAI 提出,用于描述使用工具调用(如 transfer_to_sales_agent)在智能体或状态之间转移控制权。

关键特征

  • 状态驱动的行为:行为基于状态变量(如 current_stepactive_agent)变化
  • 基于工具的转换:工具更新状态变量以在状态之间移动
  • 直接与用户交互:每个状态的配置直接处理用户消息
  • 持久状态:状态跨对话轮次存续

何时使用

当你需要强制施加顺序约束(仅在前置条件满足后才解锁能力)、智能体需要在不同状态下直接与用户对话,或者你在构建多阶段对话流程时,请使用交接模式。该模式对客户支持场景尤为有价值,因为此类场景需要按特定顺序收集信息——例如,在处理退款前先收集保修 ID。

基本实现

核心机制是一个返回 Command 来更新状态、从而触发转换到新步骤或新智能体的 工具

python
from langchain.tools import tool
from langchain.messages import ToolMessage
from langgraph.types import Command

@tool
def transfer_to_specialist(runtime) -> Command:
    """Transfer to the specialist agent."""
    return Command(
        update={
            "messages": [
                ToolMessage(  
                    content="Transferred to specialist",
                    tool_call_id=runtime.tool_call_id  
                )
            ],
            "current_step": "specialist"  # 触发行为变更
        }
    )
typescript
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({})
  }
);

INFO

为什么要包含 ToolMessage 当 LLM 调用工具时,它期望得到响应。带有匹配 tool_call_idToolMessage 完成了这个请求-响应循环——没有它,对话历史就会变得格式错误。只要你的交接工具更新了消息,就需要这样做。

完整的实现请参见下面的教程。

实现方法

实现交接有两种方式:带中间件的单个智能体(一个带动态配置的智能体)或 多个智能体子图(作为图节点的不同智能体)。

带中间件的单个智能体

单个智能体根据状态改变其行为。中间件拦截每次模型调用,并动态调整系统提示词和可用工具。工具更新状态变量以触发转换:

python
from langchain.tools import ToolRuntime, tool
from langchain.messages import ToolMessage
from langgraph.types import Command

@tool
def record_warranty_status(
    status: str,
    runtime: ToolRuntime[None, SupportState]
) -> Command:
    """Record warranty status and transition to next step."""
    return Command(
        update={
            "messages": [
                ToolMessage(
                    content=f"Warranty status recorded: {status}",
                    tool_call_id=runtime.tool_call_id
                )
            ],
            "warranty_status": status,
            "current_step": "specialist"  # 更新状态以触发转换
        }
    )
typescript
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(),
    }),
  }
);

完整示例:带中间件的客户支持

python
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.tools import tool, ToolRuntime
from langchain.messages import ToolMessage
from langgraph.types import Command
from typing import Callable

# 1. 使用 current_step 追踪器定义状态
class SupportState(AgentState):  
    """Track which step is currently active."""
    current_step: str = "triage"
    warranty_status: str | None = None

# 2. 工具通过 Command 更新 current_step
@tool
def record_warranty_status(
    status: str,
    runtime: ToolRuntime[None, SupportState]
) -> Command:  
    """Record warranty status and transition to next step."""
    return Command(update={  
        "messages": [  
            ToolMessage(
                content=f"Warranty status recorded: {status}",
                tool_call_id=runtime.tool_call_id
            )
        ],
        "warranty_status": status,
        # 转换到下一步
        "current_step": "specialist"
    })

# 3. 中间件根据 current_step 应用动态配置
@wrap_model_call
def apply_step_config(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
    """Configure agent behavior based on current_step."""
    step = request.state.get("current_step", "triage")  

    # 将步骤映射到其配置
    configs = {
        "triage": {
            "prompt": "Collect warranty information...",
            "tools": [record_warranty_status]
        },
        "specialist": {
            "prompt": "Provide solutions based on warranty: {warranty_status}",
            "tools": [provide_solution, escalate]
        }
    }

    config = configs[step]
    request = request.override(  
        system_prompt=config["prompt"].format(**request.state),  
        tools=config["tools"]  
    )
    return handler(request)

# 4. 使用中间件创建智能体
agent = create_agent(
    model,
    tools=[record_warranty_status, provide_solution, escalate],
    state_schema=SupportState,
    middleware=[apply_step_config],  
    checkpointer=InMemorySaver()  # 跨轮次持久化状态  #
)
typescript
import {
  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

子图交接需要仔细的 上下文工程。与单智能体中间件(其中消息历史自然流动)不同,你必须显式决定哪些消息在智能体之间传递。搞错了,智能体就会收到格式错误的对话历史或臃肿的上下文。请参阅下面的 上下文工程

python
from langchain.messages import AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langgraph.types import Command

@tool
def transfer_to_sales(
    runtime: ToolRuntime,
) -> Command:
    """Transfer to the sales agent."""
    last_ai_message = next(  
        msg for msg in reversed(runtime.state["messages"]) if isinstance(msg, AIMessage)  
    )  
    transfer_message = ToolMessage(  
        content="Transferred to sales agent",  
        tool_call_id=runtime.tool_call_id,  
    )  
    return Command(
        goto="sales_agent",
        update={
            "active_agent": "sales_agent",
            "messages": [last_ai_message, transfer_message],  
        },
        graph=Command.PARENT
    )
typescript
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({}),
  }
);

完整示例:带交接的销售与支持

该示例展示了一个拥有独立销售智能体和支持智能体的多智能体系统。每个智能体是图中的一个独立节点,交接工具允许智能体将对话转移给对方。

python
from typing import Literal

from langchain.agents import AgentState, create_agent
from langchain.messages import AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
from typing_extensions import NotRequired

# 1. 使用 active_agent 追踪器定义状态
class MultiAgentState(AgentState):
    active_agent: NotRequired[str]

# 2. 创建交接工具
@tool
def transfer_to_sales(
    runtime: ToolRuntime,
) -> Command:
    """Transfer to the sales agent."""
    last_ai_message = next(  
        msg for msg in reversed(runtime.state["messages"]) if isinstance(msg, AIMessage)  
    )  
    transfer_message = ToolMessage(  
        content="Transferred to sales agent from support agent",  
        tool_call_id=runtime.tool_call_id,  
    )  
    return Command(
        goto="sales_agent",
        update={
            "active_agent": "sales_agent",
            "messages": [last_ai_message, transfer_message],  
        },
        graph=Command.PARENT,
    )

@tool
def transfer_to_support(
    runtime: ToolRuntime,
) -> Command:
    """Transfer to the support agent."""
    last_ai_message = next(  
        msg for msg in reversed(runtime.state["messages"]) if isinstance(msg, AIMessage)  
    )  
    transfer_message = ToolMessage(  
        content="Transferred to support agent from sales agent",  
        tool_call_id=runtime.tool_call_id,  
    )  
    return Command(
        goto="support_agent",
        update={
            "active_agent": "support_agent",
            "messages": [last_ai_message, transfer_message],  
        },
        graph=Command.PARENT,
    )

# 3. 使用交接工具创建智能体
sales_agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[transfer_to_support],
    system_prompt="You are a sales agent. Help with sales inquiries. If asked about technical issues or support, transfer to the support agent.",
)

support_agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[transfer_to_sales],
    system_prompt="You are a support agent. Help with technical issues. If asked about pricing or purchasing, transfer to the sales agent.",
)

# 4. 创建调用这些智能体的智能体节点
def call_sales_agent(state: MultiAgentState) -> Command:
    """Node that calls the sales agent."""
    response = sales_agent.invoke(state)
    return response

def call_support_agent(state: MultiAgentState) -> Command:
    """Node that calls the support agent."""
    response = support_agent.invoke(state)
    return response

# 5. 创建判断是否应结束或继续的路由器
def route_after_agent(
    state: MultiAgentState,
) -> Literal["sales_agent", "support_agent", "__end__"]:
    """Route based on active_agent, or END if the agent finished without handoff."""
    messages = state.get("messages", [])

    # 检查最后一条消息——如果它是没有工具调用的 AIMessage,我们就完成了
    if messages:
        last_msg = messages[-1]
        if isinstance(last_msg, AIMessage) and not last_msg.tool_calls:  
            return "__end__"

    # 否则路由到当前活跃的智能体
    active = state.get("active_agent", "sales_agent")
    return active if active else "sales_agent"

def route_initial(
    state: MultiAgentState,
) -> Literal["sales_agent", "support_agent"]:
    """Route to the active agent based on state, default to sales agent."""
    return state.get("active_agent") or "sales_agent"

# 6. 构建图
builder = StateGraph(MultiAgentState)
builder.add_node("sales_agent", call_sales_agent)
builder.add_node("support_agent", call_support_agent)

# 根据初始的 active_agent 使用条件路由开始
builder.add_conditional_edges(START, route_initial, ["sales_agent", "support_agent"])

# 在每个智能体之后,检查是否应结束或路由到另一个智能体
builder.add_conditional_edges(
    "sales_agent", route_after_agent, ["sales_agent", "support_agent", END]
)
builder.add_conditional_edges(
    "support_agent", route_after_agent, ["sales_agent", "support_agent", END]
)

graph = builder.compile()
result = graph.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "Hi, I'm having trouble with my account login. Can you help?",
            }
        ]
    }
)

for msg in result["messages"]:
    msg.pretty_print()
typescript
import {
  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_genai: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_genai: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<
  typeof MultiAgentState.State,
  "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<
  typeof MultiAgentState.State,
  "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 交接给另一个智能体时,你必须同时包含:

  1. 包含工具调用的 AIMessage(触发交接的消息)
  2. 确认交接的 ToolMessage(对该工具调用的人工响应)

如果没有这种配对,接收智能体将看到不完整的对话,并可能产生错误或意外行为。

下面的示例假设只调用了交接工具(没有并行工具调用):

python
@tool
def transfer_to_sales(runtime: ToolRuntime) -> Command:
    # 获取触发此次交接的 AI 消息
    last_ai_message = runtime.state["messages"][-1]

    # 创建一条人工工具响应以补齐这一配对
    transfer_message = ToolMessage(
        content="Transferred to sales agent",
        tool_call_id=runtime.tool_call_id,
    )

    return Command(
        goto="sales_agent",
        update={
            "active_agent": "sales_agent",
            # 只传递这两条消息,而不是完整的子智能体历史
            "messages": [last_ai_message, transfer_message],
        },
        graph=Command.PARENT,
    )
typescript
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({}),
  }
);

INFO

为什么不传递所有子智能体消息? 虽然你可以在交接中包含完整的子智能体对话,但这常常会带来问题。接收智能体可能被不相关的内部推理搞糊涂,且 token 成本不必要地增加。通过只传递交接配对,你让父图的上下文保持在高层次的协调上。如果接收智能体需要额外的上下文,请考虑在 ToolMessage 内容中总结子智能体的工作,而不是传递原始消息历史。

将控制权交还给用户

当将控制权交还给用户(结束智能体的回合)时,请确保最后一条消息是 AIMessage。这会维护有效的对话历史,并向用户界面发出智能体已完成其工作的信号。

实现注意事项

在设计多智能体系统时,请考虑:

  • 上下文过滤策略:每个智能体将收到完整的对话历史、过滤后的部分还是摘要?不同的智能体可能因其角色而需要不同的上下文。
  • 工具语义:明确交接工具是只更新路由状态,还是也执行副作用。例如,transfer_to_sales() 是否也应该创建支持工单,还是应该作为一个单独的操作?
  • token 效率:在上下文完整性与 token 成本之间取得平衡。随着对话变长,摘要和有选择的上下文传递变得更加重要。