一、中间件概述

1.1 是什么

中间件(Middleware)是 LangChain 中一个基于拦截器模式的钩子机制,允许你在 Agent 的整个生命周期中插入自定义逻辑。它是 Agent 架构中最重要的设计之一,提供了强大的扩展点。

1.2 解决的问题

  • 可观测性:记录 Agent 每一步的状态,便于监控和排查;

  • 流程控制:在模型调用、工具调用前后"插入一脚";

  • 动态行为:动态注入提示词、切换模型、强制跳转节点;

  • 复用性:把通用逻辑打包成可复用的中间件组件。

1.3 底层原理

中间件本质是围绕 LangGraph 的 pregel 图模型,在节点周围做包装(wrap)和钩子(hook)。LangChain 提供了两大类接口:

  1. Wrap-style 中间件:wrap_model_call / wrap_tool_call,在核心操作"前后"都做事;

  2. Hook-style 中间件:before_model / after_model,在操作"前"或"后"单独做事。

1.4 两种写法

每个中间件都可以用 装饰器 或 类(继承 AgentMiddleware) 两种方式实现:

  • 装饰器:@wrap_model_call、@wrap_tool_call、@before_model、@after_model

  • 类:继承 AgentMiddleware,实现同名方法

两种写法底层等价——装饰器内部也会创建一个 AgentMiddleware 实例。


二、内置中间件(开箱即用)

LangChain 自带了一些常用中间件,直接导入即可使用。

2.1 TodoListMiddleware(任务清单)

在模型调用时,自动插入一张"待办事项清单"提示词,要求模型在回答前先规划步骤、逐条勾选,让推理过程更结构化、透明。

from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
​
agent = create_agent(
    model=model,
    middleware=[TodoListMiddleware()]
)
​
response = agent.invoke({
    "messages": [{"role": "user", "content": "写一首关于春天的诗"}]
})

模型会先输出一张任务清单(如 "1. 列意象 → 2. 组织语言 → 3. 成诗"),再给出正文。

2.2 其他内置中间件类型

课件中还提到 LangChain 提供了处理错误/重试、上下文管理等场景的内置中间件。使用前建议查阅官方文档确认最新列表:


三、自定义中间件(核心)

自定义中间件分为两大类、四种钩子,覆盖 Agent 生命周期的四大关键节点。

3.1 Hook-style:before_model / after_model

分别在模型调用前/后触发,用于日志、提示词注入、流程控制等。

装饰器实现(审计日志示例):

from langchain.agents.middleware import before_model, after_model, AgentState
from langgraph.runtime import Runtime
from loguru import logger
​
@before_model
def before_log(state: AgentState, runtime: Runtime) -> dict | None:
    logger.info("调用模型前消息数量: {}", len(state["messages"]))
    return None
​
@after_model
def after_log(state: AgentState, runtime: Runtime) -> dict | None:
    logger.info("调用模型后消息数量:{}", len(state["messages"]))
    return None
​
agent = create_agent(model=model, middleware=[before_log, after_log])

类实现:

from langchain.agents.middleware import AgentMiddleware, AgentState
​
class AuditMiddleware(AgentMiddleware):
    def __init__(self, logger):
        super().__init__()
        self.logger = logger
​
    def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
        self.logger.info("before_model: {}", len(state["messages"]))
        return None
​
    def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
        self.logger.info("after_model: {}", len(state["messages"]))
        return None

3.2 流程控制:jump_to 强制跳转

Hook 钩子有个高级能力——通过返回值里的 jump_to 字段,强制把执行流程跳到指定节点,不用经过正常推理链路。常用目标节点:"model"、"tools"、"end"。

典型场景演示(三个中间件 + 一个对照):

中间件

触发条件

动作

force_tool_first

检测到 "direct tool"

before_model 直接 jump_to="tools",省一次模型思考

retry_with_extra_instruction

检测到 "retry model"

after_model 注入 SystemMessage 后 jump_to="model" 二次生成

overflow_context_processor

检测到 "overflow"

before_model 直接 jump_to="end" 熔断流程

from langchain.agents.middleware import before_model, after_model
from langchain.messages import AIMessage, SystemMessage
​
@before_model
def force_tool_first(state, runtime) -> dict | None:
    text = state["messages"][-1].content
    if "direct tool" in text.lower():
        print("[MIDDLEWARE] before_model: jump_to='tools'")
        fake_tool_call = AIMessage(
            content="人工构造的消息",
            tool_calls=[{"name": "get_news", "args": {}, "id": "call_force_001"}],
        )
        return {"messages": [fake_tool_call], "jump_to": "tools"}
    return None
​
@after_model
def retry_with_extra_instruction(state, runtime) -> dict | None:
    # 找到用户输入,判断条件并防止无限重跳
    if "retry model" in user_text.lower() and not already_injected:
        return {
            "messages": [SystemMessage("你必须以【二次回答】开头,并且只用一句话回答。")],
            "jump_to": "model",
        }
    return None
​
@before_model
def overflow_context_processor(state, runtime) -> dict | None:
    if "overflow" in state["messages"][-1].content:
        return {"messages": [AIMessage("上下文窗口溢出,终止")], "jump_to": "end"}
    return None
​
agent = create_agent(
    model=model,
    tools=[get_news],
    middleware=[force_tool_first, retry_with_extra_instruction, overflow_context_processor],
)

基于类实现时,需要用 @hook_config(can_jump_to=[...]) 装饰器为 jump_to 传参:

class MyMiddleware(AgentMiddleware):
    @hook_config(can_jump_to=["tools", "end"])
    def before_model(self, state, runtime) -> dict | None:
        ...

3.3 Wrap-style:wrap_model_call(包裹模型调用)

在模型调用的前后都能做事,是最灵活的一类中间件。wrap(包裹)意味着你同时拿到"请求"和"响应",可以在中间做拦截、修改、重试、缓存。

from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
​
@wrap_model_call
def wrap_model_call_middleware(
    request: ModelRequest,      # 即将发给大模型的所有请求数据
    handler,                    # 下一个中间件或真正的大模型调用
) -> ModelResponse | None:
    # 调用前:动态篡改最后一条消息
    request.messages[-1].content += " -> wrap_model_call_before <- "
    # 真正调用模型(产生真实 Token 消耗)
    response = handler(request)
    # 调用后:篡改返回内容
    response.result[0].content += " -> wrap_model_call_after <- "
    return response

使用场景:拦截、重试、缓存模型调用。

  • 场景 1:重试逻辑(指数退避)

@wrap_model_call
def retry_model(request, handler) -> ModelResponse:
    max_retries = 3
    for attempt in range(max_retries):
        try:
            return handler(request)
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # 指数退避
  • 场景 2:响应缓存(按请求内容生成 md5 键,命中则直接返回)

class ModelCache:
    def __init__(self):
        self.cache = {}
    def create_hook(self):
        @wrap_model_call
        def cache_model(request, handler) -> ModelResponse:
            cache_key = hashlib.md5(
                json.dumps({"messages": [str(m) for m in request.messages],
                            "system": str(request.system_message)}).encode()
            ).hexdigest()
            if cache_key in self.cache:
                return self.cache[cache_key]  # 缓存命中
            response = handler(request)
            self.cache[cache_key] = response
            return response
        return cache_model
  • 场景 3:动态修改系统提示(用 request.override() 优雅地替换)

@wrap_model_call
def add_context(request, handler) -> ModelResponse:
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    new_system_message = SystemMessage(
        content=f"{request.system_message.content or ''}\n当前时间:{current_time}\n语言偏好:中文"
    )
    modified_request = request.override(system_message=new_system_message)
    return handler(modified_request)

3.4 Wrap-style:wrap_tool_call(包裹工具调用)

在工具调用的前后做事,用于监控、重试、修改工具执行/参数。

from langchain.agents.middleware import wrap_tool_call
from langchain.tools.tool_node import ToolCallRequest
​
@wrap_tool_call
def wrap_tool_call_middleware(request: ToolCallRequest, handler) -> ToolMessage | Command:
    result = handler(request)  # 第一次调用(原始参数)
    request.tool_call["args"]["is_forcast"] = True  # 篡改参数
    result = handler(request)  # 第二次调用(修改后参数)
    return result

监控工具执行示例:

@wrap_tool_call
def monitor_tool(request, handler):
    tool_name = request.tool_call["name"]
    start_time = time.time()
    try:
        result = handler(request)
        print(f"✅ {tool_name} 成功,耗时 {time.time()-start_time:.2f}s")
        return result
    except Exception as e:
        print(f"❌ {tool_name} 失败:{e}")
        raise

3.5 参数说明(Wrap-style 通用)

  • request:被封装的请求对象,可能是模型请求(ModelRequest)或工具请求(ToolCallRequest);

  • handler:处理器,用于继续执行请求并返回结果,本质上代表"下一个中间件或最终的服务调用"。


四、装饰器 vs 类:怎么选?

情况

推荐方案

原因

只用一个钩子

装饰器

最简单直接

多个钩子组合

类

把同一中间件的多个 hook 组织成一个整体,结构更集中清晰

需要复杂配置/传参

类

通过 __init__ 传参,运行时自省、调试更友好

装饰器也能用"工厂函数返回多个装饰器"的方式实现多钩子,但会把一个逻辑上属于同一中间件的行为拆成多个独立函数,可维护性不如类写法。


五、中间件执行顺序(重要!)

中间件是乱序定义、顺序生效的——执行顺序只取决于传入 create_agent 的列表顺序。具体规律如"洋葱模型":

  1. before_model:按列表声明的顺序执行(1 → 2 → 3);

  2. after_model:按列表声明顺序倒序执行(3 → 2 → 1);

  3. wrap_model_call:先声明的包在最外层(洋葱结构)。

以三个中间件(1、2、3)为例,最终消息内容的追加顺序是:

原始消息 → before_model-1 → before_model-2 → before_model-3
        → wrap_model-before-1 → wrap_model-before-2 → wrap_model-before-3
        【大模型调用】
        → wrap_model-after-3 → wrap_model-after-2 → wrap_model-after-1
        → after_model-3 → after_model-2 → after_model-1

记忆口诀:before 正序、after 倒序、wrap 先包外。


六、总结

  1. 中间件 = LangChain 1.0 的拦截器钩子,扩展 Agent 的四大扩展点:模型前、模型后、模型包裹、工具包裹。

  2. 两类接口:Wrap-style(wrap_model_call / wrap_tool_call,前后都管)和 Hook-style(before_model / after_model,只前或只后)。

  3. 两种写法:装饰器(单钩子首选)与类(多钩子、复杂配置首选)。

  4. 高阶能力 jump_to:在 hook 中强制跳转到 model / tools / end,实现"省思考调用、二次生成、熔断终止"等流程控制。

  5. 执行顺序:严格遵循传入顺序,before 正序、after 倒序、wrap 洋葱式包裹。

  6. 典型应用:日志审计、重试、缓存、动态注入提示词、工具监控、流程熔断。