[LangChain智能体本质论]中间件是如何参与Agent、Model和Tool三者交互的?

[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_agentbefore_modelafter_agentafter_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_callawrap_model_callwrap_agent_callawrap_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类型是ModelResponseAIMessage这两个类型的联合。

@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]

由于模型调用的输入和输出类型分别是ModelRequestModelResponse,所以被封装的针对模型的同步调用和异步调用可以表示成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**. ☀️ 

Read more

2025年第十六届蓝桥杯省赛JavaB组真题回顾

2025年第十六届蓝桥杯省赛JavaB组真题回顾

第16届蓝桥杯省赛已经结束了,第一次参加也是坐牢了4个小时,现在还是来总结一下吧(先声明以下的解法,大家可以当作一种思路来看,解法不一定是正解,只是给大家提供一种能够正常想到的思路吧) 试题A:逃离高塔 本题其实没有什么难度,就是一个循环遍历即可,那么唯一需要注意的就是循环遍历的过程中,int是会爆的,这里需要用long来进行存储 public class Main{ public static void main(String[] args){ int ans=0;//记录最终答案 for(long i=1;i<=2025;i++){ long x=i*i*i; if(n%10==3){ ans++; } } System.out.println(ans); } } ​  最后进行的答案就是:

By Ne0inhk
如何解决打包报错:Failed to load module script: Expected a JavaScript module script but the server responded

如何解决打包报错:Failed to load module script: Expected a JavaScript module script but the server responded

如何解决打包报错:Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of “text/html” 💡作者: AI前端Web万粉变现经纪人 📅发布日期:2025-11-04 📚标签:前端报错、打包错误、Vite、Webpack、部署问题 如何解决打包报错:Failed to load module script: Expected a JavaScript module script but the server responded with a MIME

By Ne0inhk
如何设计一套 Java 项目的 Skill 体系

如何设计一套 Java 项目的 Skill 体系

关键词:Skill 体系 | 架构设计 | 模块化 | 可组合 | 工程化思维 一、从单个 Skill 到 Skill 体系:质变的开始 前面两篇文章,我们分别讲了: * Service Skill:自动生成业务层代码 * Controller Skill:自动生成接口层代码 但如果只是零散的 Skill,那还只是"工具集"。 真正的威力在于: 把多个 Skill 组织成体系,形成协同作战的能力矩阵 这就像: * 单个技能 = 单兵作战 * Skill 体系 = 特种部队 从"点"到"面",从"工具&

By Ne0inhk
手把手带你吃透Java中的WebSocket,纯干货不废话!

手把手带你吃透Java中的WebSocket,纯干货不废话!

手把手带你吃透Java中的WebSocket,纯干货不废话! 一、从 “小麻烦” 引出 WebSocket 在互联网的世界里,HTTP 协议就像是一个勤劳的 “快递员”,一直勤勤恳恳地为客户端和服务器传递着信息。多年来,HTTP 协议凭借着简单、灵活的特性,成为了 Web 通信的基石,像我们日常上网浏览网页、提交表单等操作,背后都离不开 HTTP 协议的支持。它采用请求 - 响应的模式,客户端发起请求,服务器返回响应,这种模式就好比你在网上购物,下单(发送请求)后等待商家发货(返回响应),简单直接,在大多数情况下都能很好地满足我们的需求。 不过,时代在发展,互联网应用也越来越丰富多样。就像你现在不满足于只是逛逛静态网页,还想和朋友来一场畅快淋漓的在线聊天,或者实时查看股票行情的变化。这时候,HTTP 协议这个 “老快递员” 就有点力不从心了。因为 HTTP 协议是单向通信的,

By Ne0inhk