[LangChain中的Multi-Agent模式-03]Handoffs:状态驱动的多阶段流程编排与状态机管理
在交接(Handoffs)模式中整个Agent被构建为一个状态机当前的状态决定了可执行的操作操作的执行又会导致状态的改变。该模式的一个典型实现方案为利用工具更新一个持久存储的状态变量例如current_step或active_agent系统读取该变量来调整行为包括应用不同的配置系统提示、工具或将请求路由到不同的Agent。这种模式既支持不同Agent之间的交接也支持单个Agent内部的动态配置更改。该模式的典型特征包括状态驱动行为行为会根据状态变量而改变基于工具的转换工具更新状态变量以实现状态间的转换直接用户交互每个状态的配置直接处理用户消息持久状态状态在对话回合中保持不变。交接模式特别适合使用在需要强制执行顺序约束的应用场景比如客服人员需要在不同状态下与用户直接对话或者构建多阶段对话流程时。对于我们构建的差旅助手前面已经通过Sub-Agent和Router模式提供了实现现在我们将它改成Handoffs交接模式重新实现一遍。上图反映了采用Handoffs模式的执行流程先进行意图分析根据得到意图执行相应的状态没有机票购买和酒店预订需求直接退出只有机票购买需求完成机票购买任务后退出只有酒店预订需求完成酒店预订任务后退出同时具有两种需求先后完成机票购买和酒店预订后退出不采用并行执行1. 意图分析虽然这个系列的主题是Multi-Agent但是如果考虑到我们将Sub-Agent封装在工具之中交接模式与是否采用Multi-Agent没有关系所以接下来我们采用Single-Agent方法来演示我们直接调用MCP服务器提供的buy_airplane_ticket和book_hotel工具。由于交接模式采用基于状态的顺序执行流程所以状态类型State中除了定义表示意图的intent字段还定义了current_step字段表示当前所在步骤。交接模式没有Sub-Agent架构中的Supervisor也没有路由架构的汇总节点所以我们定义额外的字段is_last_step表示当前是否是流程的最后一环因为它需求对前面完成的工作做一个总结。classState(AgentState):intent:Literal[book_hotel,buy_airplane_ticket,both]|Nonecurrent_step:Annotated[Literal[analyze_intent,buy_airplane_ticket,book_hotel]|None,AnyValue]is_last_step:Annotated[bool,AnyValue]分析意图的工具函数analyze_intent与之前的实现基本一致我们将用于请求原封不动发送给LLM并且利用结构化输出返回明确的出行安排意图。唯一的区别在于每一步任务完成之后需要将下一步以及是否是最后一步写入状态。按照我们的分析如果请求包含机票购买的意图下一步任务就是购买机票如果只包含酒店预订需求下一步就应该预订酒店这一点体现在返回的Command针对current_step和is_last_step通道的写入上;如果两种需求皆无整个流程结束此时analyze_intent方法利用Command对goto字段的设置结束整个处理流程。classIntent(TypedDict):出行安排意图need_book_hotel:bool是否涉及预订酒店need_buy_airplane_ticket:bool是否涉及购买机票intent_analysis_modelChatOpenAI(modelgpt-5.2-chat).with_structured_output(schemaIntent)toolasyncdefanalyze_intent(request:str,runtime:ToolRuntime)-Command:分析用户的出行安排意图判断是购买机票、预订酒店还是两者都有result:Intentcast(Intent,awaitintent_analysis_model.ainvoke(f根据如下请求分析用户的出行安排意图判断是住宿安排、交通安排还是两者都有\n\n{request}))need_book_hotelresult[need_book_hotel]need_buy_airplane_ticketresult[need_buy_airplane_ticket]ifnotneed_book_hotelandnotneed_buy_airplane_ticket:returnCommand(update{messages:[ToolMessage(content用户请求无住宿安排和交通安排意图!)]},goto__end__,)intentbothifneed_book_hotelandneed_buy_airplane_ticketelse(book_hotelifneed_book_hotelelsebuy_airplane_ticket)intent_description预订酒店和购买机票ifintentbothelse(预订酒店ifintentbook_hotelelse购买机票)next_stepbuy_airplane_ticketifneed_buy_airplane_ticketelsebook_hotelreturnCommand(update{intent:intent,current_step:next_step,is_last_step:intent!both,messages:[ToolMessage(contentf根据分析用户的出行安排意图是{intent_description}。,tool_call_idruntime.tool_call_id,)]})2. 购买机票后如何预订酒店由于意图分析是我们自定义的工具我们可以利用返回的Command设置下一步。如果同时具有机票购买和酒店预订的需求在购买机票后需要将下一步设置为酒店预订但是购买机票是MCP服务器提供的工具该如何实现的。这可以借助于langchain_mcp_adapters提供的ToolCallInterceptor来拦截调用MCP工具的结果并将其封装成返回的Command来完成对应通道的写入。这个ToolCallInterceptor是通过如下这个transfor_to_next_step函数实现的。asyncdeftransfor_to_next_step(request:MCPToolCallRequest,handler):runtime:ToolRuntime[Any,State]cast(ToolRuntime[Any,State],request.runtime)resultcast(CallToolResult,awaithandler(request))tool_namerequest.name intentruntime.state[intent]ifintentbothandtool_namebuy_airplane_ticket:contents,aircraft_convert_call_tool_result(result)contentBlockscast(list[ContentBlock],contents)tool_messageToolMessage(content_blockscontentBlocks,aircraftaircraft,tool_call_idruntime.tool_call_id)returnCommand(update{messages:[tool_message],current_step:book_hotel,is_last_step:True,})returnresult clientMultiServerMCPClient(connections{server:{transport:stdio,command:python,args:[server.py]}},tool_interceptors[transfor_to_next_step])3. 利用系统提示词和工具提供控制流程和前面介绍的主管模式和路由模式相比你会发现交接模式最为简洁它不需要一个协调者Supervisor或者路由器完全由参与者自行完成交接任务。由于工具永远是被动的执行者所以流程的流转还得由LLM来完成所以我们必需在某一步都为它提供精准的系统提示词指导它工作并且提供给它所需的工具集去执行它的决策所以这里依然会使用到LangChain的中间件。这个为每一步设置系统提示词和工具集的中间件体现在如下所示的标注了wrap_model_call包装器的configure_step函数上。如代码片段所示我们为每一步设置可准确的系统提示词和工具集。中间件在拦截针对模型的调用后从状态中提取当前的步骤后对当前使用的提示词和工具集进行设置。如果当前为最后异步还需要借助于系统提示词让它汇总出行信息。asyncdefmain():tools{tool.name:toolfortoolinawaitclient.get_tools(server_nameserver)}tools[analyze_intent]analyze_intent step_based_tooks{analyze_intent:[analyze_intent],buy_airplane_ticket:[tools[buy_airplane_ticket]],book_hotel:[tools[book_hotel]],}step_based_system_messages{analyze_intent:SystemMessage(content(你是一个善于语义分析的差旅助理你唯一的工作是调用analyze_intent工具根据请求分析出行安排意图判断是需要预订酒店、购买机票还是两者都需要。如果请求中没有出行安排相关的意图请直接回复意图不明无法执行。)),buy_airplane_ticket:SystemMessage(content(你是一个专注于交通安排的差旅助理你唯一的工作是在无需授权情况下调用注册的buy_airplane_ticket工具购买机票。完成购买是首要任务无需考虑其他任何信息。你可以自行决定具体的航司、舱位等级和航班。)),book_hotel:SystemMessage(content(你是一个专注于住宿安排的差旅助理你唯一的工作是在无需授权情况下调用注册的book_hotel工具预订酒店。完成预订是首要任务无需考虑其他任何信息。你可以自行决定具体的酒店、价位和房型等。)),}wrap_model_call# type: ignore[misc]asyncdefconfigure_step(request:ModelRequest,handler,):current_steprequest.state.get(current_step,analyze_intent)is_last_steprequest.state.get(is_last_step,False)system_messagestep_based_system_messages[current_step]ifis_last_step:system_messageSystemMessage(f{system_message.content}汇总预订的酒店如果没有请忽略和购买机票信息如果没有请忽略。如果没有任何预订信息请回复意图不明无法执行。)requestrequest.override(toolsstep_based_tooks[current_step],# type: ignoresystem_messagesystem_message,)returnawaithandler(request)# type: ignore4. 构建和测试Agent我们利用MultiServerMCPClient连接“SubAgent集中编排视角下的上下文隔离与并行化实现”中构建的MCP服务器进而获取可用的工具。我们通过指定状态类型State、工具包括自定义的用于意图分析的analyze_intent工具和MCP服务器提供的buy_airplane_ticket和book_hotel工具和为每一步设置系统提示词和工具集的中间件调用create_agent函数将Agent创建出来然后采用与前面一样的测试用例供了四种输入来模拟四种情况同时包含酒店预订和机票购买需求、只包含酒店预订或者机票购买需求以及不涉及这两种需求对构建的Agent进行测试asyncdefmain():clientMultiServerMCPClient(connections{server:{transport:stdio,command:python,args:[server.py]}},tool_interceptors[transfor_to_next_step])tools{tool.name:toolfortoolinawaitclient.get_tools(server_nameserver)}...agentcreate_agent(modelChatOpenAI(modelgpt-5.2-chat),state_schemaState,tools[toolfortoolintools.values()],middleware[configure_step],# type: ignore)inputs[我有明后两天(从上海)去成都的出差请帮我预订酒店明天入住后天离开和机票往返明天出发后天返回。,我有明后两天(从上海)去成都的出差请帮我预订酒店明天入住后天离开。,我有明后两天(从上海)去成都的出差请帮我预订机票往返明天出发后天返回。,随便聊聊]formessageininputs:reusltawaitagent.ainvoke(input{messages:[HumanMessage(contentmessage)]})print(f用户输入{message})print(f系统回复{reuslt[messages][-1].text}\n\n)以下是对应的四段输出用户输入我有明后两天(从上海)去成都的出差请帮我预订酒店明天入住后天离开和机票往返明天出发后天返回。 系统回复✅ 已识别您的出行安排意图**需要同时预订机票和酒店**。 根据您的需求已为您完成以下安排 ### ✈️ 机票预订往返 - **去程**上海 → 成都 出发时间明天2026-04-1812:00 ✈️ 航班号MU5401 - **返程**成都 → 上海 出发时间后天2026-04-1912:00 ✈️ 航班号MU5402 ### 酒店预订 - **城市**成都 - **酒店**如家酒店 - **入住时间**2026-04-18 - **退房时间**2026-04-19 如需调整航班时间、酒店档次或添加其他出行服务如接送机随时告诉我 用户输入我有明后两天(从上海)去成都的出差请帮我预订酒店明天入住后天离开。 系统回复✅ **酒店预订已完成** 已为您成功预订以下酒店 - **城市**成都 - **酒店**如家酒店 - **入住时间**2026年4月18日 - **退房时间**2026年4月19日 祝您成都出差顺利如需调整或再次预订住宿随时告诉我。用户输入我有明后两天(从上海)去成都的出差请帮我预订机票往返明天出发后天返回。 系统回复✅ **出行安排已识别并处理** 我已识别到您的出行意图为**购买机票往返**。 ✈️ **机票预订信息如下** - **去程**上海 → 成都 出发时间明天4月18日12:00 航班号MU5401 - **返程**成都 → 上海 出发时间后天4月19日12:00 航班号MU5402 机票已成功为您预订。如您还需要 **酒店预订、座位选择或改签提醒**欢迎随时告诉我。祝您出差顺利用户输入随便聊聊 系统回复意图不明无法执行再来看针对第一个请求反映Agent内部的调用链流转方式是不是比前面两种模式简单。但是由于交接模式一般采用顺序执行流程如是涉及大量的并行计算比如本例中购买机票和预订酒店没有实现为并行就需要考虑一下是否适合了。如果一定要采用这种模式将涉及大量并行推理的逻辑实现在一个Sub-Agent中然后封装成一个工具作为顺序流程中的一个环节也是很容易实现的。5. 完整程序如下给出了完整的代码fromtypingimportAny,Literal,Annotated,TypedDict,castfromclickimportCommandfromlangchain_mcp_adapters.clientimportMultiServerMCPClientfromlanggraph.channelsimportAnyValuefromlangchain_mcp_adapters.interceptorsimportMCPToolCallRequestfromlangchain.agentsimportAgentState,create_agentfromlangchain.agents.middlewareimportwrap_model_call,ModelRequestfromlangchain_openaiimportChatOpenAIfromlangchain.toolsimporttool,ToolRuntimefromlangchain_core.messagesimportSystemMessage,ToolMessage,HumanMessagefromlangchain_core.messages.contentimportContentBlockfromlanggraph.typesimportCommandfrommcp.typesimportCallToolResultfromlangchain_mcp_adapters.toolsimport_convert_call_tool_resultimportasynciofromdotenvimportload_dotenv load_dotenv()classIntent(TypedDict):出行安排意图need_book_hotel:bool是否涉及预订酒店need_buy_airplane_ticket:bool是否涉及以购买机票intent_analysis_modelChatOpenAI(modelgpt-5.2-chat).with_structured_output(schemaIntent)toolasyncdefanalyze_intent(request:str,runtime:ToolRuntime)-Command:分析用户的出行安排意图判断是购买机票、预订酒店还是两者都有result:Intentcast(Intent,awaitintent_analysis_model.ainvoke(f根据如下请求分析用户的出行安排意图判断是住宿安排、交通安排还是两者都有\n\n{request}))need_book_hotelresult[need_book_hotel]need_buy_airplane_ticketresult[need_buy_airplane_ticket]ifnotneed_book_hotelandnotneed_buy_airplane_ticket:returnCommand(update{messages:[ToolMessage(content用户请求无住宿安排和交通安排意图!)]},goto__end__,)intentbothifneed_book_hotelandneed_buy_airplane_ticketelse(book_hotelifneed_book_hotelelsebuy_airplane_ticket)intent_description预订酒店和购买机票ifintentbothelse(预订酒店ifintentbook_hotelelse购买机票)next_stepbuy_airplane_ticketifneed_buy_airplane_ticketelsebook_hotelreturnCommand(update{intent:intent,current_step:next_step,is_last_step:intent!both,messages:[ToolMessage(contentf根据分析用户的出行安排意图是{intent_description}。,tool_call_idruntime.tool_call_id,)]})classState(AgentState):intent:Literal[book_hotel,buy_airplane_ticket,both]|Nonecurrent_step:Annotated[Literal[analyze_intent,buy_airplane_ticket,book_hotel]|None,AnyValue]is_last_step:Annotated[bool,AnyValue]asyncdeftransfor_to_next_step(request:MCPToolCallRequest,handler):runtime:ToolRuntime[Any,State]cast(ToolRuntime[Any,State],request.runtime)resultcast(CallToolResult,awaithandler(request))tool_namerequest.name intentruntime.state[intent]ifintentbothandtool_namebuy_airplane_ticket:contents,aircraft_convert_call_tool_result(result)contentBlockscast(list[ContentBlock],contents)tool_messageToolMessage(content_blockscontentBlocks,aircraftaircraft,tool_call_idruntime.tool_call_id)returnCommand(update{messages:[tool_message],current_step:book_hotel,is_last_step:True,})returnresult clientMultiServerMCPClient(connections{server:{transport:stdio,command:python,args:[server.py]}},tool_interceptors[transfor_to_next_step])asyncdefmain():tools{tool.name:toolfortoolinawaitclient.get_tools(server_nameserver)}tools[analyze_intent]analyze_intent step_based_tooks{analyze_intent:[analyze_intent],buy_airplane_ticket:[tools[buy_airplane_ticket]],book_hotel:[tools[book_hotel]],}step_based_system_messages{analyze_intent:SystemMessage(content(你是一个善于语义分析的差旅助理你唯一的工作是调用analyze_intent工具根据请求分析出行安排意图判断是需要预订酒店、购买机票还是两者都需要。如果请求中没有出行安排相关的意图请直接回复意图不明无法执行。)),buy_airplane_ticket:SystemMessage(content(你是一个专注于交通安排的差旅助理你唯一的工作是在无需授权情况下调用注册的buy_airplane_ticket工具购买机票。完成购买是首要任务无需考虑其他任何信息。你可以自行决定具体的航司、舱位等级和航班。)),book_hotel:SystemMessage(content(你是一个专注于住宿安排的差旅助理你唯一的工作是在无需授权情况下调用注册的book_hotel工具预订酒店。完成预订是首要任务无需考虑其他任何信息。你可以自行决定具体的酒店、价位和房型等。)),}wrap_model_call# type: ignore[misc]asyncdefconfigure_step(request:ModelRequest,handler,):current_steprequest.state.get(current_step,analyze_intent)is_last_steprequest.state.get(is_last_step,False)system_messagestep_based_system_messages[current_step]ifis_last_step:system_messageSystemMessage(f{system_message.content}汇总预订的酒店如果没有请忽略和购买机票信息如果没有请忽略。如果没有任何预订信息请回复意图不明无法执行。)requestrequest.override(toolsstep_based_tooks[current_step],# type: ignoresystem_messagesystem_message,)returnawaithandler(request)# type: ignoreagentcreate_agent(modelChatOpenAI(modelgpt-5.2-chat),state_schemaState,tools[toolfortoolintools.values()],middleware[configure_step],# type: ignore)inputs[我有明后两天(从上海)去成都的出差请帮我预订酒店明天入住后天离开和机票往返明天出发后天返回。,我有明后两天(从上海)去成都的出差请帮我预订酒店明天入住后天离开。,我有明后两天(从上海)去成都的出差请帮我预订机票往返明天出发后天返回。,随便聊聊]formessageininputs:reusltawaitagent.ainvoke(input{messages:[HumanMessage(contentmessage)]})print(f用户输入{message})print(f系统回复{reuslt[messages][-1].text}\n\n)asyncio.run(main())