[LangChain智能体本质论]中间件是如何参与Agent、Model和Tool三者交互的?
LangChain的中间件(Middleware)是围绕Agent执行流程构建的“可插拔钩子系统”。它允许开发者在不修改核心逻辑的情况下,在执行的关键节点(如输入处理、模型调用前后、输出解析等)对数据流进行拦截、修改或验证。中间件类型以AgentMiddleware为基类。
1. AgentMiddleware
AgentMiddleware是一个泛型类型,两个泛型参数分别代表状态和静态上下文的类型,我们可以利用state_schema字段得到状态类型。它的name属性返回中间件的名称,默认返回的是当前的类名。
classAgentMiddleware(Generic[StateT, ContextT]): state_schema:type[StateT]= cast("type[StateT]", _DefaultAgentState) tools: Sequence[BaseTool]@propertydefname(self)->str:return self.__class__.__name__ defbefore_agent(self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passasyncdefabefore_agent( self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passdefbefore_model(self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passasyncdefabefore_model( self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passdefafter_model(self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passasyncdefaafter_model( self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passdefafter_agent(self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passasyncdefaafter_agent( self, state: StateT, runtime: Runtime[ContextT])->dict[str, Any]|None:passdefwrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse],)-> ModelCallResult: msg =("Synchronous implementation of wrap_model_call is not available. ""You are likely encountering this error because you defined only the async version ""(awrap_model_call) and invoked your agent in a synchronous context ""(e.g., using `stream()` or `invoke()`). ""To resolve this, either: ""(1) subclass AgentMiddleware and implement the synchronous wrap_model_call method, ""(2) use the @wrap_model_call decorator on a standalone sync function, or ""(3) invoke your agent asynchronously using `astream()` or `ainvoke()`.")raise NotImplementedError(msg)asyncdefawrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], Awaitable[ModelResponse]],)-> ModelCallResult: msg =("Asynchronous implementation of awrap_model_call is not available. ""You are likely encountering this error because you defined only the sync version ""(wrap_model_call) and invoked your agent in an asynchronous context ""(e.g., using `astream()` or `ainvoke()`). ""To resolve this, either: ""(1) subclass AgentMiddleware and implement the asynchronous awrap_model_call method, ""(2) use the @wrap_model_call decorator on a standalone async function, or ""(3) invoke your agent synchronously using `stream()` or `invoke()`.")raise NotImplementedError(msg)defwrap_tool_call( self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],)-> ToolMessage | Command[Any]: msg =("Synchronous implementation of wrap_tool_call is not available. ""You are likely encountering this error because you defined only the async version ""(awrap_tool_call) and invoked your agent in a synchronous context ""(e.g., using `stream()` or `invoke()`). ""To resolve this, either: ""(1) subclass AgentMiddleware and implement the synchronous wrap_tool_call method, ""(2) use the @wrap_tool_call decorator on a standalone sync function, or ""(3) invoke your agent asynchronously using `astream()` or `ainvoke()`.")raise NotImplementedError(msg)asyncdefawrap_tool_call( self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],)-> ToolMessage | Command[Any]: msg =("Asynchronous implementation of awrap_tool_call is not available. ""You are likely encountering this error because you defined only the sync version ""(wrap_tool_call) and invoked your agent in an asynchronous context ""(e.g., using `astream()` or `ainvoke()`). ""To resolve this, either: ""(1) subclass AgentMiddleware and implement the asynchronous awrap_tool_call method, ""(2) use the @wrap_tool_call decorator on a standalone async function, or ""(3) invoke your agent synchronously using `stream()` or `invoke()`.")raise NotImplementedError(msg)通过前面的介绍我们知道,在调用create_agent函数时可以利用tools参数进行工具注册,其实工具也可以利用tools字段封装到中间件中。中间件被注册时,其封装的工具也会一并予以注册。换句话说,create_agent方法内部会读取所有注册中间件的tools字段存储的工具,连同利用tools参数直接注册的工具一起处理。虽然Agent定义了众多方法,但我们可以将它们划分为如下两类:
- 生命周期拦截器:在Agent和Model执行前后调用,包括
- before_agent/before_model/after_agent/after_model
- abefore_agent/abefore_model/aafter_agent/aafter_model,
- 调用包装器:对Model和Tool的调用进行包装;
- wrap_model_call/wrap_tool_call
- awrap_model_call/awrap_tool_call
2. 生命周期拦截器
对于一个利用create_agent函数创建的Agent,在没有任何中间件注册的情况下,它本质上是由model和tools两个核心节点组成的Pregel对象。注册中间件的生命周期拦截器方法针对Agent和Model调用前后的拦截,是通过为Pregel对象添加额外节点和通道来实现的。我们可以利用如下这个简单的实例来验证:
from langchain.agents import create_agent from dotenv import load_dotenv from langchain.agents.middleware.types import AgentState from langchain_openai import ChatOpenAI from PIL import Image as PILImage from langchain.agents.middleware import AgentMiddleware from typing import Any from langgraph.runtime import Runtime import io classFooMiddleware(AgentMiddleware):defbefore_agent(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().before_agent(state, runtime)defbefore_model(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().before_model(state, runtime)defafter_agent(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().after_agent(state, runtime)defafter_model(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().after_model(state, runtime) load_dotenv()deftest_tool():"""A test tool""" agent = create_agent( model= ChatOpenAI(model="gpt-5.2-chat"), tools=[test_tool], middleware=[FooMiddleware()]) payload = agent.get_graph(xray=True).draw_mermaid_png() PILImage.open(io.BytesIO(payload)).show()print("channels:")for(name, chan)in agent.channels.items():print(f"\t[{chan.__class__.__name__}]{name}")print("trigger_to_nodes")for(name, nodes)in agent.trigger_to_nodes.items():print(f"\t{name}: {nodes}")在如上的演示程序中,我们创建了自定义中间件类型FooMiddleware,并重写它的before_agent、before_model、after_agent和after_model四个方法,我们通过注册此中间件调用create_agent函数创建了一个Agent,并将他的拓扑结构以PNG图片的形式呈现出来(呈现效果如下所示)。
从上图可以看出,注册的中间件为Agent添加了四个节点,这四个节点对应于我们重写的四个方法,节点所在的位置体现四个方法的执行顺序:FooMiddleware.before_agent->FooMiddleware.before_model->FooMiddleware.after_model->FooMiddleware.after_agent。而且FooMiddleware.before_model可以实现针对“tools”节点的跳转,“tools”执行结束后又会被FooMiddleware.before_model拦截。
演示程序还输出了通道列表,以及节点与订阅通道之间的映射关系。从如下的输出结果可以看出,上述四个节点各自具有独立定于的通道。
channels: [BinaryOperatorAggregate]messages [EphemeralValue]jump_to [LastValue]structured_response [EphemeralValue]__start__ [Topic]__pregel_tasks [EphemeralValue]branch:to:model [EphemeralValue]branch:to:tools [EphemeralValue]branch:to:FooMiddleware.before_agent [EphemeralValue]branch:to:FooMiddleware.before_model [EphemeralValue]branch:to:FooMiddleware.after_model [EphemeralValue]branch:to:FooMiddleware.after_agent trigger_to_nodes __start__: ['__start__'] branch:to:model: ['model'] branch:to:tools: ['tools'] branch:to:FooMiddleware.before_agent: ['FooMiddleware.before_agent'] branch:to:FooMiddleware.before_model: ['FooMiddleware.before_model'] branch:to:FooMiddleware.after_model: ['FooMiddleware.after_model'] branch:to:FooMiddleware.after_agent: ['FooMiddleware.after_agent'] 如果我们采用如下的方式再注册一个中间件BarMiddleware:
classBarMiddleware(AgentMiddleware):defbefore_agent(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().before_agent(state, runtime)defbefore_model(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().before_model(state, runtime)defafter_agent(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().after_agent(state, runtime)defafter_model(self, state: AgentState[Any], runtime: Runtime[None])->dict[str, Any]|None:returnsuper().after_model(state, runtime) agent = create_agent( model= ChatOpenAI(model="gpt-5.2-chat"), tools=[test_tool], middleware=[FooMiddleware(),BarMiddleware()])在Agent新的拓扑结构中,优化多出四个针对BarMiddleware的节点。
3. 调用包装器
AgentMiddleware提供了四个方法(wrap_model_call、awrap_model_call、wrap_agent_call和awrap_agent_call),分别用于包装针对Model和Tool的同步和异步调用。对于作为Pregel的Agent来说,针对模型和工具的调用是由“model”和“tools”节点发出的,所以利用中间件对调用的封装也在这两个节点中完成。
classAgentMiddleware(Generic[StateT, ContextT]):defwrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse],)-> ModelCallResult asyncdefawrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], Awaitable[ModelResponse]],)-> ModelCallResult defwrap_tool_call( self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],)-> ToolMessage | Command[Any]asyncdefawrap_tool_call( self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],)-> ToolMessage | Command[Any]3.1 针对模型调用的包装
常规的模型调用会返回一个AIMessage消息。如果采用基于ToolStrategy的结构化输出,除了返回格式化的输出外,还会涉及格式化工具生成的ToolMessage,它们被封装在一个ModelResponse对象里。所以表示模型调用结果的ModelResult类型是ModelResponse和AIMessage这两个类型的联合。
@dataclassclassModelResponse: result:list[BaseMessage] structured_response: Any =None ModelCallResult: TypeAlias = ModelResponse | AIMessage ModelRequest表示模型调用的请求,我们从中可以得到Chat模型组件、请求消息列表、系统指令、注册的工具以及针对工具选择策略、结构化输出Schema、状态、运行时和针对模型的设置。在绝大部情况下,我们通过自定义中间件包装模型调用的目的都是为了更新上述的某一个或者多个请求元素,ModelRequest利用override方法将一切变得简单。
@dataclass(init=False)classModelRequest: model: BaseChatModel messages:list[AnyMessage] system_message: SystemMessage |None tool_choice: Any |None tools:list[BaseTool |dict[str, Any]] response_format: ResponseFormat[Any]|None state: AgentState[Any] runtime: Runtime[ContextT] model_settings:dict[str, Any]= field(default_factory=dict)@propertydefsystem_prompt(self)->str|None:defoverride(self,**overrides: Unpack[_ModelRequestOverrides])-> ModelRequest:class_ModelRequestOverrides(TypedDict, total=False): model: BaseChatModel system_message: SystemMessage |None messages:list[AnyMessage] tool_choice: Any |None tools:list[BaseTool |dict[str, Any]] response_format: ResponseFormat[Any]|None model_settings:dict[str, Any] state: AgentState[Any]由于模型调用的输入和输出类型分别是ModelRequest和ModelResponse,所以被封装的针对模型的同步调用和异步调用可以表示成Callable[[ModelRequest], ModelResponse]和Callable[[ModelRequest], Awaitable[ModelResponse]]对象,wrap_model_call/awrap_model_call方法的handler参数分别返回的就是这两个对象。
3.2 针对工具调用的包装
表示工具调用请求的ToolRequest类型定义如下,请求携带了模型生成的用于调用目标工具的ToolCall对象,代表工具自身的BaseTool对象,以及当前状态和工具运行时。ToolCallRequest也提供了override方法实现针对这些请求元素的更新。
@dataclassclassToolCallRequest: tool_call: ToolCall tool: BaseTool |None state: Any runtime: ToolRuntime defoverride( self,**overrides: Unpack[_ToolCallRequestOverrides])-> ToolCallRequest:class_ToolCallRequestOverrides(TypedDict, total=False): tool_call: ToolCall tool: BaseTool state: Any 调用工具执行的结构可以封装成一个ToolCallRequest反馈给模型,也可以返回一个Command对象实现对状态的更新和跳转,所以wrap_tool_call/awrap_tool_call方法中表示针对工具原始调用的handler参数分别是一个Callable[[ToolCallRequest], ToolMessage|Command[Any]]和Callable[[ToolCallRequest], Awaitable[ToolMessage|Command[Any]]]对象。
在介绍create_agent方法针对工具的注册时,我们曾经说过:除了以可执行对象或者BaseTool对象标识注册的工具外,我们还可以指定一个表示注册工具JSON Schema的字典。但是以这种方式注册的工具并没有绑定一个具体的可执行对象,所以默认是无法被调用的。我们可以采用中间件的方式来解决这个问题。
from langchain.agents import create_agent from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain_core.messages import ToolMessage from langchain.agents.middleware import AgentMiddleware, ToolCallRequest from langgraph.types import Command from typing import Any, Callable load_dotenv() tool ={"name":"get_weather","description":"Get weather information for given city","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}classWeatherMiddleware(AgentMiddleware):defwrap_tool_call( self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],)-> ToolMessage | Command[Any]: tool_call = request.tool_call if tool_call["name"]=="get_weather": city = tool_call["args"]["city"]return ToolMessage( content=f"It's sunny in {city}.", tool_call_id=tool_call["id"])else:return handler(request) agent = create_agent( model= ChatOpenAI(model="gpt-5.2-chat"), tools=[tool], middleware=[WeatherMiddleware()],) result = agent.invoke(input={"messages":[{"role":"user","content":"What is the weather like in Suzhou?"}]})for message in result["messages"]: message.pretty_print()如上面的演示程序所示,我们注册的工具是一个字典,它表示注册工具的JSON Schema。其中提供了工具的名称(“get_weather”)和参数结构(包含一个必需的名为“city”的字符串成员)。注册的WeatherMiddleware通过重写的wrap_tool_call实现了针对工具调用的拦截。如果是针对工具get_weather的调用,我们将天气信息封装成返回的ToolMessage。程序执行后会以如下的方式输出消息历史:
================================ Human Message ================================= What is the weather like in Suzhou? ================================== Ai Message ================================== Tool Calls: get_weather (call_LjastyaYNrovwMhSmvoJMcNz) Call ID: call_LjastyaYNrovwMhSmvoJMcNz Args: city: Suzhou ================================= Tool Message ================================= It's sunny in Suzhou. ================================== Ai Message ================================== The weather in **Suzhou** is **sunny**. ☀️