一、交接模式(Handoffs)定义
在交接模式中,系统行为会基于状态动态变化。其核心机制为:工具会更新一个可跨轮次持久化的状态变量(例如current_step(当前步骤)或active_agent(活跃智能体)),系统通过读取该变量来调整行为 ------ 要么应用不同的配置(系统提示词、工具),要么将任务路由至另一个智能体。这种模式既支持不同智能体之间的任务交接,也可实现单个智能体内部的动态配置变更。
"交接"(handoffs)这一术语由 OpenAI 提出,用于描述借助工具调用(例如transfer_to_x_agent)在多个智能体或不同状态之间转移控制权的机制。
二、核心特征
- 状态驱动行为:行为基于状态变量(如 current_step(当前步骤)或 active_agent(活跃智能体))动态调整
- 基于工具的状态转换:通过工具更新状态变量,实现不同状态间的交接
- 用户直接交互:每个状态的配置均可直接处理用户消息
- 状态持久化:状态可在多轮对话中保持有效
三、适用场景
当你需要强制执行顺序约束(仅在前置条件满足后开放对应功能)、智能体需要跨不同状态与用户直接对话,或需要构建多阶段对话流程时,可采用该交接模式。此模式在客服场景中尤为实用,例如需要按特定顺序收集信息的业务 ------ 比如处理退款前,需先收集用户的保修编号。
四、基础实现
核心机制是借助一个工具返回指令(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" # Triggers behavior change
}
)
五、实现方案
实现交接机制有两种方式:带中间件的单智能体方案(一个智能体搭配动态配置)与多智能体子图方案(将独立智能体作为图节点)。
5.1 带中间件的单智能体方案
单个智能体基于状态调整自身行为。中间件会拦截每次模型调用,并动态调整系统提示词与可用工具。工具通过更新状态变量来触发状态交接。
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. Define state with current_step tracker
class SupportState(AgentState):
"""Track which step is currently active."""
current_step: str = "triage"
warranty_status: str | None = None
# 2. Tools update current_step via 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,
# Transition to next step
"current_step": "specialist"
})
# 3. Middleware applies dynamic configuration based on 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")
# Map steps to their configurations
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. Create agent with middleware
agent = create_agent(
model,
tools=[record_warranty_status, provide_solution, escalate],
state_schema=SupportState,
middleware=[apply_step_config],
checkpointer=InMemorySaver() # Persist state across turns #
)
5.2 多智能体子图
多个独立的智能体作为图中的不同节点存在。交接工具通过Command.PARENT指定下一个待执行的节点,以此在智能体节点之间进行调度。 子图交接需要精心设计上下文。与单智能体中间件(其消息历史可自然流转)不同,你必须明确决定在智能体之间传递哪些消息。如果这一步处理不当,智能体将会接收到格式错误的对话历史,或冗余臃肿的上下文。详情参见下文的上下文设计部分。
5.2.1 实现示例
本示例展示了一个包含独立销售智能体与支持智能体的多智能体系统。每个智能体均为独立的图节点,交接工具可支持智能体之间的对话转接。
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. Define state with active_agent tracker
class MultiAgentState(AgentState):
active_agent: NotRequired[str]
# 2. Create handoff tools
@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. Create agents with handoff tools
sales_agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
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="anthropic:claude-sonnet-4-20250514",
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. Create agent nodes that invoke the agents
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. Create router that checks if we should end or continue
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", [])
# Check the last message - if it's an AIMessage without tool calls, we're done
if messages:
last_msg = messages[-1]
if isinstance(last_msg, AIMessage) and not last_msg.tool_calls:
return "__end__"
# Otherwise route to the active agent
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. Build the graph
builder = StateGraph(MultiAgentState)
builder.add_node("sales_agent", call_sales_agent)
builder.add_node("support_agent", call_support_agent)
# Start with conditional routing based on initial active_agent
builder.add_conditional_edges(START, route_initial, ["sales_agent", "support_agent"])
# After each agent, check if we should end or route to another 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()
对于大多数任务交接场景,建议使用搭配中间件的单智能体方案------ 这种方式更为简洁。仅当你需要实现定制化智能体时(例如,某个节点本身是包含反思或检索流程的复杂子图),才考虑使用多智能体子图。
5.2.2 上下文工程
在子图交接过程中,你可以精准控制智能体之间流转的消息内容。这种精准性对于维持有效的对话历史、避免因上下文冗余而干扰下游智能体至关重要。 交接过程中的上下文处理 在智能体间进行任务交接时,你需要确保对话历史的有效性。大语言模型要求工具调用与其响应内容一一对应,因此在使用Command.PARENT交接至其他智能体时,必须同时传入以下两类消息: 包含工具调用的AI 消息(即触发任务交接的那条消息) 确认任务交接的工具消息(即针对该工具调用生成的人工响应) 若缺少这种一一对应的消息配对,接收方智能体将会获取不完整的对话内容,进而可能产生错误或出现不符合预期的行为。 以下示例基于仅调用了交接工具的场景(无并行工具调用):
python
@tool
def transfer_to_sales(runtime: ToolRuntime) -> Command:
# Get the AI message that triggered this handoff
last_ai_message = runtime.state["messages"][-1]
# Create an artificial tool response to complete the pair
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",
# Pass only these two messages, not the full subagent history
"messages": [last_ai_message, transfer_message],
},
graph=Command.PARENT,
)
为什么不传递子智能体的全部消息?虽然你可以在任务交接时附带子智能体的完整对话内容,但这种做法往往会引发诸多问题:接收方智能体可能会被不相关的内部推理内容干扰而陷入混乱,同时还会造成不必要的令牌成本增加。 仅传递任务交接消息对,可以让主图的上下文始终聚焦于高层级的协同调度。如果接收方智能体需要更多上下文信息,建议在工具消息的内容中总结子智能体的工作内容,而非直接传递原始的消息历史。
将控制权交还给用户 当把控制权交还给用户(结束智能体的本轮任务)时,需确保最终发送的消息为一条AI 消息。此举既能维持有效的对话历史,也可向用户界面传递 "智能体已完成当前任务" 的信号。
六、实现注意事项
在设计多智能体系统时,需考量以下几点:
- 上下文过滤策略:每个智能体应接收完整的对话历史、经过筛选的内容片段,还是对话摘要?不同智能体因其职能不同,所需的上下文信息也可能存在差异。
- 工具语义定义:明确交接工具仅用于更新路由状态,还是同时会产生附带影响。例如,transfer_to_sales() 函数是否需要同步创建售后工单,还是应将创建工单作为一项独立操作?
- 令牌成本优化:平衡上下文完整性与令牌消耗成本。随着对话篇幅不断增加,对话摘要生成与选择性上下文传递的重要性会愈发凸显。