[Deep Agents:LangChain的Agent Harness-07]利用PatchToolCallsMiddleware修复错乱的消息结构
作为LLM提示词的一个重要组成部分,表示对话历史的消息列表在结构上有一个基本的要求:如果LLM返回的AIMessage包含ToolCall对象,那么Agent会期望每个ToolCall对象都有对应的ToolMessage。但是Agent在执行过程会因为一些异常导致LLM返回的AIMessage中的ToolCall对象没有得到正确的处理,最终导致后续的消息列表中缺失了对应的ToolMessage。PatchToolCallsMiddleware就在面临这种情况是提供了一种补救机制,自动创建并添加缺失的ToolMessage。
1. ReAct工作模式导致的对话历史的交替结构
作为Agent对话历史的消息列表之所以具有确定的结构,是源于Agent基于ReAct的工作模式。ReAct(Reasoning and Acting)是一种让大语言模型(LLM)将推理与行动相结合的交互模式。它解决了模型空谈不干活(只推理不调用工具)或盲目干活(只调工具不分析原因)的问题。ReAct的核心是一个循环过程,通常被称为Thought-Action-Observation循环:
- Thought(推理):模型先写下当前的思考,分析任务目标、目前的进度以及下一步需要做什么;
- Action(行动):模型根据推理结果,决定调用哪个工具(如搜索、计算器、API)并给出参数;
- Observation(观察):系统执行工具后,将结果反馈给模型;
- 循环:模型根据“观察”到的结果,开始新一轮的 Thought,直到得出最终答案;
create_agent函数创建的Agent由model和tools两个核心节点组成,model节点负责生成推理内容和生成工具调用,tools节点则负责执行工具调用并返回结果。消息列表是Agent状态的核心成员,ReAct模式清晰地体现在Agent的执行流程上:
- 当用户指定输入调用Agent时,model节点生成提示词调用LLM,这里的提示词包括封装了用户输入的消息列表(用户可以输入描述任务的文本,此时生成的消息列表中会包含一个
HumanMessage对象,用户也可以输入一个消息列表来模拟一段对话)、工具集描述和系统指令(提示词)。经过LLM的推理,如果它觉得此时能够提供最终的答案,它会将答案封装成一个AIMessage对象返回;如果它觉得还需要调用工具来获取更多信息或进行计算,它会在生成的AIMessage对象中包含一个或多个ToolCall对象来调用工具; - Agent会将接收到的
AIMessage对象添加到消息列表中。如果此AIMessage的ToolCall列表为空,执行流程到此结束;否则Agent会根据工具调用的信息来调用tools节点执行工具。相关的工具以并发形式的执行后,执行的结果会以ToolMessage的形式被添加到消息列表中。即使工具返回的是一个Command对象,也明确要求它必需返回一个ToolMessage对象来描述工具调用的结果; - 包含ToolMessage的消息列表会被再次提交给model节点,进入下一轮的推理循环。
这就决定了在一个推理步骤结束时,作为对话历史的消息应该具有这样的结构:
- 如果某条
AIMessage中包含了一个或多个ToolCall对象,那么在消息列表中必须有对应的一个或多个ToolMessage来描述工具调用的结果; - 每条
ToolMessage必须包含一个tool_call_id字段来与对应的ToolCall进行匹配,以保证消息列表的结构完整性;
2. 对话历史的结构为何会被破坏?
在 ReAct 模式中,消息列表的严格交替结构(Assistant提议 -> Tool执行 -> Assistant总结)是其逻辑闭环的基础,以下是导致结构破坏的常见原因:
- 并发调用与乱序返回:模型一次性发出了三个工具调用(Action A, B, C),但外部执行环境返回结果的顺序变成了 B, A, C。如果直接按接收顺序插入消息列表,而没有与其对应的tool_call_id严格匹配,模型会把B的结果当成A的反馈;
- 强制插入用户干预:在模型发出Action之后、Tool返回结果之前,用户突然又发了一条新消息(如“算了,别查了,换个任务”)。这会导致 Assistant (Action) 后面直接跟了一个 User 消息,中间缺失了必要的 Tool 响应;
- 溢出导致的截断:由于ReAct循环非常多,历史记录太长,开发者简单粗暴地删除了中间的某些消息。如果删除了某个 Tool 消息,但保留了后面基于该工具结果做的 Thought,逻辑链条就会出现断层;
- 模型解析失败:模型由于幻觉或受干扰,没有输出预定的 Action 格式,而是直接输出了乱码。系统无法提取出 Action,导致流程卡死在 Assistant 这一步,无法触发后续的 Tool 消息;
3. 错乱消息结构导致的问题
ReAct模式的循环特性决定了消息列表的具有上述的交替结构,而这种消息结构也反映了LLM过去的推理流程,并用以指导后续的推理。一旦这种链条断裂,模型就会陷入逻辑精神分裂,会产生以下后果:
- 逻辑幻觉 (Hallucination):模型会试图脑补缺失的Observation。如果它提出了查询请求但没收到结果,它可能会根据训练数据编造一个虚假的结果,并以此为基础继续推理;
- 无限递归循环 (Infinite Loop):模型发现上一个Action没有对应的Observation,它会认为我刚才没做成,于是再次发出同样的Action。如果系统逻辑不健壮,会陷入请求-等待-再次请求的死循环,极快地消耗Token;
- 拒绝执行 (Model Refusal):现代对齐后的模型(如GPT-4)对上下文一致性有要求。如果AIMessage消息包含ToolCall但紧接着不是ToolMessage,API 往往会直接报错,导致程序崩溃;
- 因果倒置 (Causal Confusion):如果Observation错位,模型会基于错误的前提得出结论。例如:搜索“A 的股价”,结果返回了“B 的股价”,模型会非常自信地告诉你 A 涨了(实际上是B涨了);
4. 利用PatchToolCallsMiddleware修补错乱的消息结构
我们利用下面这个实例来演示一下PatchToolCallsMiddleware针对对话历史的修补功能。我们创建了一个Agent,并注册了一个工具get_weather用于获取天气信息。我们在调用Agent的时候,构造了一个AIMessage,其中包含了一个针对get_weather工具的ToolCall对象,但是我们没有提供对应的ToolMessage。
fromlangchain.agentsimportcreate_agentfromdeepagents.middleware.patch_tool_callsimportPatchToolCallsMiddlewarefromlangchain.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessage,AIMessage,ToolCallfromdotenvimportload_dotenvimportasyncio load_dotenv()@tooldefget_weather(location:str)->str:"""Get the current weather for a given location."""returnf"The current weather in{location}is sunny with a high of 25°C."agent=create_agent(model=ChatOpenAI(model="gpt-5.2-chat"),tools=[get_weather],middleware=[PatchToolCallsMiddleware()])human_message=HumanMessage(content="What's the weather like in Suzhou today?")ai_message=AIMessage(content="")tool_call:ToolCall={"id":"tool_call_1","name":"get_weather","args":{"location":"Suzhou"}}ai_message.tool_calls.append(tool_call)asyncdefmain():result=awaitagent.ainvoke(input={"messages":[human_message,ai_message]})formessageinresult["messages"]:message.pretty_print()asyncio.run(main())注册的PatchToolCallsMiddleware会自动检测到这个问题,并且为这个ToolCall对象创建一个对应的ToolMessage。虽然这个ToolMessage中的内容会使用预定义的模板来生成,但它至少保证了消息列表的结构完整性,使得Agent可以继续执行后续的推理和行动。在如下所示的对话列表中,出现的第一个ToolMessage就是PatchToolCallsMiddleware自动创建的。
================================ Human Message ================================= What's the weather like in Suzhou today? ================================== Ai Message ================================== Tool Calls: get_weather (tool_call_1) Call ID: tool_call_1 Args: location: Suzhou ================================= Tool Message ================================= Name: get_weather Tool call get_weather with id tool_call_1 was cancelled - another message came in before it could be completed. ================================== Ai Message ================================== Tool Calls: get_weather (call_Exlkyg84lyVTMvOEfGuzgu6N) Call ID: call_Exlkyg84lyVTMvOEfGuzgu6N Args: location: Suzhou, China ================================= Tool Message ================================= Name: get_weather The current weather in Suzhou, China is sunny with a high of 25°C. ================================== Ai Message ================================== Today in **Suzhou, China**, the weather is **sunny** with a **high around 25 °C (77 °F)**. It should be a pleasant day overall—great for being outdoors.PatchToolCallsMiddleware的实现非常简单,它通过重写before_agent/after_agent方法来检测AIMessage中的ToolCall对象,并且在发现缺失对应的ToolMessage时,自动创建一个新的ToolMessage来进行补充。这个新创建的ToolMessage会使用一个预定义的模板来生成内容。
classPatchToolCallsMiddleware(AgentMiddleware):defbefore_agent(self,state:AgentState,runtime:Runtime[Any])->dict[str,Any]|None