LangChain / Middleware / Custom middleware

原文链接:https://docs.langchain.com/oss/python/langchain/middleware/custom

中间件

通过实现在代理执行流程中特定节点运行的钩子来构建自定义中间件。

钩子

中间件提供了两种风格的钩子来拦截代理执行:

节点风格钩子

在特定执行节点顺序运行。用于日志记录、验证和状态更新。

包装风格钩子

围绕每次模型或工具调用运行。

节点风格钩子

在特定执行节点顺序运行。用于日志记录、验证和状态更新。

选择你的中间件需要的钩子。你可以在节点风格钩子和包装风格钩子之间选择。

节点风格钩子在特定的执行节点运行:

钩子 运行时机
before_agent 代理启动之前(每次调用一次)
before_model 每次模型调用之前
after_model 每次模型响应之后
after_agent 代理完成之后(每次调用一次)

包装风格钩子围绕每次调用运行,让你控制执行过程:

钩子 运行时机
wrap_model_call 围绕每次模型调用
wrap_tool_call 围绕每次工具调用

示例:

装饰器

python 复制代码
from langchain.agents.middleware import before_model, after_model, AgentState
from langchain.messages import AIMessage
from langgraph.runtime import Runtime
from typing import Any


@before_model(can_jump_to=["end"])
def check_message_limit(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    if len(state["messages"]) >= 50:
        return {
            "messages": [AIMessage("对话已达到限制。")],
            "jump_to": "end"
        }
    return None

@after_model
def log_response(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    print(f"模型返回:{state['messages'][-1].content}")
    return None

包装风格钩子

拦截执行并控制处理函数的调用时机。用于重试、缓存和转换。

你可以决定处理函数被调用零次(短路)、一次(正常流程)或多次(重试逻辑)。

可用钩子:

  • wrap_model_call - 围绕每次模型调用
  • wrap_tool_call - 围绕每次工具调用

示例:

装饰器

python 复制代码
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable


@wrap_model_call
def retry_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    for attempt in range(3):
        try:
            return handler(request)
        except Exception as e:
            if attempt == 2:
                raise
            print(f"重试 {attempt + 1}/3,错误:{e}")

状态更新

节点风格钩子和包装风格钩子都可以更新代理状态。机制不同:

节点风格钩子(before_agent、before_model、after_model、after_agent):直接返回字典。该字典使用图的归约器(reducers)应用到代理状态。

包装风格钩子(wrap_model_call、wrap_tool_call):对于模型调用,返回带有Command的ExtendedModelResponse,在模型响应之外注入状态更新。对于工具调用,直接返回Command。当你需要基于模型或工具调用期间运行的逻辑来跟踪或更新状态时使用这些钩子,例如摘要触发点、使用元数据或从请求或响应计算的自定义字段。

节点风格钩子

从节点风格钩子返回字典,将更新合并到代理状态中。字典的键映射到状态字段。

python 复制代码
from langchain.agents.middleware import after_model, AgentState
from langgraph.runtime import Runtime
from typing import Any
from typing_extensions import NotRequired


class TrackingState(AgentState):
    model_call_count: NotRequired[int]


@after_model(state_schema=TrackingState)
def increment_after_model(state: TrackingState, runtime: Runtime) -> dict[str, Any] | None:
    return {"model_call_count": state.get("model_call_count", 0) + 1}

包装风格钩子

从wrap_model_call返回带有Command的ExtendedModelResponse,从模型调用层注入状态更新:

python 复制代码
from typing import Callable
from langchain.agents.middleware import (
    wrap_model_call,
    ModelRequest,
    ModelResponse,
    AgentState,
    ExtendedModelResponse
)
from langgraph.types import Command
from typing_extensions import NotRequired

class UsageTrackingState(AgentState):
    """带有令牌使用跟踪的代理状态。"""

    last_model_call_tokens: NotRequired[int]


@wrap_model_call(state_schema=UsageTrackingState)
def track_usage(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ExtendedModelResponse:
    response = handler(request)
    return ExtendedModelResponse(
        model_response=response,
        command=Command(update={"last_model_call_tokens": 150}),
    )

Command通过图的归约器流动,因此更新被正确应用,消息是追加式的而非替换现有状态。

多个中间件的组合

当多个中间件层返回ExtendedModelResponse时,它们的命令会组合:

  • 命令通过归约器应用:每个Command成为一个独立的状态更新。对于消息,这意味着它们是追加式的。
  • 外层在冲突时优先:对于非归约器状态字段,命令先内层后外层应用。最外层中间件的值在冲突键上优先。
  • 重试安全:如果外层中间件实现了可能导致多次调用handler()的逻辑(例如重试逻辑),先前调用的命令将被丢弃。
python 复制代码
from typing import Annotated, Callable

from langchain.agents.middleware import (
    AgentMiddleware,
    AgentState,
    ExtendedModelResponse,
    ModelRequest,
    ModelResponse,
)
from langchain.messages import SystemMessage
from langgraph.types import Command
from typing_extensions import NotRequired


def _last_wins(_a: str, b: str) -> str:
    """归约器:最后写入者优先(外层覆盖内层)。"""
    return b


class CustomMiddlewareState(AgentState):
    """代理状态:trace_layer使用最后写入优先(外层胜出),消息使用追加式归约器。"""

    # 最后写入优先的非归约器字段:两个中间件都写入;最外层的值胜出
    trace_layer: NotRequired[Annotated[str, _last_wins]]


class OuterMiddleware(AgentMiddleware):
    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ExtendedModelResponse:
        response = handler(request)
        return ExtendedModelResponse(
            model_response=response,
            command=Command(update={
                "trace_layer": "outer",
                "messages": [SystemMessage(content="[外层已运行]")],
            }),
        )


class InnerMiddleware(AgentMiddleware):
    """添加trace_layer和消息。外层添加到相同的键;trace_layer:外层胜出,消息:追加式。"""

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ):
        response = handler(request)
        return ExtendedModelResponse(
            model_response=response,
            command=Command(update={
                "trace_layer": "inner",
                "messages": [SystemMessage(content="[内层已运行]")],
            }),
        )

创建中间件

你可以通过两种方式创建中间件:

基于装饰器的中间件

对于单钩子中间件快速简便。使用装饰器包装单个函数。

基于类的中间件

对于具有多个钩子或配置的复杂中间件更强大。

基于装饰器的中间件

对于单钩子中间件快速简便。使用装饰器包装单个函数。

可用装饰器:

节点风格:

  • @before_agent - 代理启动之前运行(每次调用一次)
  • @before_model - 每次模型调用之前运行
  • @after_model - 每次模型响应之后运行
  • @after_agent - 代理完成之后运行(每次调用一次)

包装风格:

  • @wrap_model_call - 使用自定义逻辑包装每次模型调用
  • @wrap_tool_call - 使用自定义逻辑包装每次工具调用

便捷装饰器:

  • @dynamic_prompt - 生成动态系统提示

示例:

python 复制代码
from langchain.agents.middleware import (
    before_model,
    wrap_model_call,
    AgentState,
    ModelRequest,
    ModelResponse,
)
from langchain.agents import create_agent
from langgraph.runtime import Runtime
from typing import Any, Callable


@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    print(f"即将使用 {len(state['messages'])} 条消息调用模型")
    return None

@wrap_model_call
def retry_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    for attempt in range(3):
        try:
            return handler(request)
        except Exception as e:
            if attempt == 2:
                raise
            print(f"重试 {attempt + 1}/3,错误:{e}")

agent = create_agent(
    model="gpt-5.5",
    middleware=[log_before_model, retry_model],
    tools=[...],
)

何时使用装饰器:

  • 只需要单个钩子
  • 没有复杂配置
  • 快速原型开发

基于类的中间件

对于具有多个钩子或配置的复杂中间件更强大。当你需要为同一个钩子定义同步和异步实现,或者想要在单个中间件中组合多个钩子时,使用类。

注意:AgentMiddleware子类可以声明三个类属性,代理工厂在编译时接收:

  • state_schema --- 使用自定义字段扩展代理状态。参见自定义状态模式。
  • tools --- 注册随中间件提供的额外工具(例如待办列表中间件上的write_todos)。
  • transformers --- 注册范围感知的流转换器工厂。参见自定义流转换器。

示例:

python 复制代码
from langchain.agents.middleware import (
    AgentMiddleware,
    AgentState,
    ModelRequest,
    ModelResponse,
)
from langgraph.runtime import Runtime
from typing import Any, Callable

class LoggingMiddleware(AgentMiddleware):
    def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print(f"即将使用 {len(state['messages'])} 条消息调用模型")
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print(f"模型返回:{state['messages'][-1].content}")
        return None

    async def abefore_model(
        self, state: AgentState, runtime: Runtime
    ) -> dict[str, Any] | None:
        # before_model的异步版本
        return None

    async def aafter_model(
        self, state: AgentState, runtime: Runtime
    ) -> dict[str, Any] | None:
        # after_model的异步版本
        print(f"模型返回:{state['messages'][-1].content}")
        return None


agent = create_agent(
    model="gpt-5.5",
    middleware=[LoggingMiddleware()],
    tools=[...],
)

何时使用类:

  • 为同一个钩子定义同步和异步实现
  • 单个中间件中需要多个钩子
  • 需要复杂配置(如可配置阈值、自定义模型)
  • 需要带初始化时配置的可重用跨项目中间件

自定义状态模式

如果你的中间件需要跨钩子跟踪状态,中间件可以使用自定义属性扩展代理状态。这使得中间件能够:

  • 跨执行跟踪状态:在代理执行生命周期中维护计数器、标志或其他值
  • 在钩子之间共享数据:将信息从before_model传递到after_model,或在不同的中间件实例之间传递
  • 实现横切关注点:添加如速率限制、使用跟踪、用户上下文或审计日志等功能,而无需修改核心代理逻辑
  • 做出条件决策:使用累积状态决定是否继续执行、跳转到不同节点或动态修改行为

装饰器

python 复制代码
from langchain.agents import create_agent
from langchain.messages import HumanMessage
from langchain.agents.middleware import AgentState, before_model, after_model
from typing_extensions import NotRequired
from typing import Any
from langgraph.runtime import Runtime


class CustomState(AgentState):
    model_call_count: NotRequired[int]
    user_id: NotRequired[str]


@before_model(state_schema=CustomState, can_jump_to=["end"])
def check_call_limit(state: CustomState, runtime: Runtime) -> dict[str, Any] | None:
    count = state.get("model_call_count", 0)
    if count > 10:
        return {"jump_to": "end"}
    return None


@after_model(state_schema=CustomState)
def increment_counter(state: CustomState, runtime: Runtime) -> dict[str, Any] | None:
    return {"model_call_count": state.get("model_call_count", 0) + 1}


agent = create_agent(
    model="gpt-5.5",
    middleware=[check_call_limit, increment_counter],
    tools=[],
)

# 使用自定义状态调用
result = agent.invoke({
    "messages": [HumanMessage("你好")],
    "model_call_count": 0,
    "user_id": "user-123",
})

自定义流转换器

要求 :中间件注册的转换器需要 langchain>=1.3.2

中间件可以注册流转换器工厂,将实时代理流中的事件投影到类型化的扩展通道上。这对于展示计数器、侧通道工件、部分输出或线级脱敏非常有用,而无需耦合到框架的内置投影。

在编译时,中间件注册的工厂与调用者直接传递给代理工厂的任何内容合并。最终排序规则将内置的ToolCallTransformer放在前面,让调用者提供的条目排在最后。

设置transformers类属性为工厂可调用对象的元组。每个工厂的形状为Callable[[tuple[str, ...]], StreamTransformer],作为factory(scope)调用,其中scope是迷你复用作用域元组(根为(),子图为非空);每次调用返回一个新的转换器以保持每个子图隔离。

python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware


class ToolActivityMiddleware(AgentMiddleware):
    transformers = (ToolActivityTransformer,)


agent = create_agent(
    model="gpt-5-nano",
    tools=[...],
    middleware=[ToolActivityMiddleware()],
)

参见中间件上的注册转换器了解完整排序规则和PII脱敏示例。

执行顺序

当使用多个中间件时,了解它们如何执行:

python 复制代码
agent = create_agent(
    model="gpt-5.5",
    middleware=[middleware1, middleware2, middleware3],
    tools=[...],
)

执行流程

图示文本描述:

  1. before_agent: middleware1 → middleware2 → middleware3
  2. before_model: middleware1 → middleware2 → middleware3
  3. wrap_model_call: middleware1 包裹 middleware2 包裹 middleware3
  4. 模型调用
  5. after_model: middleware3 → middleware2 → middleware1
  6. after_agent: middleware3 → middleware2 → middleware1

关键规则:

  • before_*钩子:从第一个到最后一个
  • after_*钩子:从最后一个到第一个(反向)
  • wrap_*钩子:嵌套(第一个中间件包裹所有其他中间件)

代理跳转

要从中间件提前退出,返回带有jump_to的字典:

可用跳转目标:

  • 'end':跳转到代理执行结束(或第一个after_agent钩子)
  • 'tools':跳转到工具节点
  • 'model':跳转到模型节点(或第一个before_model钩子)

装饰器

python 复制代码
from langchain.agents.middleware import after_model, hook_config, AgentState
from langchain.messages import AIMessage
from langgraph.runtime import Runtime
from typing import Any


@after_model
@hook_config(can_jump_to=["end"])
def check_for_blocked(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    last_message = state["messages"][-1]
    if "BLOCKED" in last_message.content:
        return {
            "messages": [AIMessage("我无法回应那个请求。")],
            "jump_to": "end"
        }
    return None

最佳实践

  • 保持中间件聚焦------每个中间件应该做好一件事
  • 优雅地处理错误------不要让中间件错误导致代理崩溃
  • 使用适当的钩子类型:
    • 节点风格用于顺序逻辑(日志记录、验证)
    • 包装风格用于控制流(重试、后备、缓存)
  • 清楚地记录任何自定义状态属性
  • 在集成之前独立地对中间件进行单元测试
  • 考虑执行顺序------将关键中间件放在列表前面
  • 尽可能使用内置中间件

示例

动态提示

在运行时动态修改系统提示,在每次模型调用之前注入上下文、用户特定指令或其他信息。这是最常见的中间件用例之一。

使用ModelRequest上的system_message字段来读取和修改系统提示。它包含一个SystemMessage对象(即使代理是用字符串system_prompt创建的)。

装饰器

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import ModelRequest, ModelResponse, wrap_model_call
from langchain.messages import SystemMessage


@wrap_model_call
def add_context(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    new_content = list(request.system_message.content_blocks) + [
        {"type": "text", "text": "额外上下文。"}
    ]
    new_system_message = SystemMessage(content=new_content)
    return handler(request.override(system_message=new_system_message))

注意

  • ModelRequest.system_message始终是一个SystemMessage对象,即使代理是用system_prompt="string"创建的
  • 使用SystemMessage.content_blocks以块列表形式访问内容,无论原始内容是字符串还是列表
  • 修改系统消息时,使用content_blocks并追加新块以保留现有结构
  • 你可以直接将SystemMessage对象传递给create_agentsystem_prompt参数,用于缓存控制等高级用例

动态模型选择

装饰器

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import ModelRequest, ModelResponse, wrap_model_call
from langchain.chat_models import init_chat_model

complex_model = init_chat_model("claude-sonnet-4-6")
simple_model = init_chat_model("claude-haiku-4-5-20251001")


@wrap_model_call
def dynamic_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    if len(request.messages) > 10:
        model = complex_model
    else:
        model = simple_model
    return handler(request.override(model=model))

动态选择工具

在运行时选择相关工具以提高性能和准确性。本节介绍过滤预注册工具。对于在运行时发现的工具注册(例如来自MCP服务器),请参见运行时工具注册。

优点:

  • 更短的提示 - 仅暴露相关工具以降低复杂度
  • 更好的准确性 - 模型从更少选项中选择正确工具
  • 权限控制 - 基于用户访问权限动态过滤工具

装饰器

python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable


@wrap_model_call
def select_tools(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    """基于状态/上下文选择相关工具的中间件。"""
    # 基于状态/上下文选择少量相关工具子集
    relevant_tools = select_relevant_tools(request.state, request.runtime)
    return handler(request.override(tools=relevant_tools))

agent = create_agent(
    model="gpt-5.5",
    tools=all_tools,  # 所有可用工具需要预先注册
    middleware=[select_tools],
)

工具调用监控

装饰器

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools.tool_node import ToolCallRequest
from langgraph.types import Command


@wrap_tool_call
def monitor_tool(
    request: ToolCallRequest,
    handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
    print(f"执行工具:{request.tool_call['name']}")
    print(f"参数:{request.tool_call['args']}")
    try:
        result = handler(request)
        print("工具执行成功")
        return result
    except Exception as e:
        print(f"工具执行失败:{e}")
        raise

提示缓存(Anthropic)

在使用Anthropic模型时,使用带有缓存控制指令的结构化内容块来缓存大型系统提示:

装饰器

python 复制代码
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.messages import SystemMessage
from typing import Callable


@wrap_model_call
def add_cached_context(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    # 始终使用内容块
    new_content = list(request.system_message.content_blocks) + [
        {
            "type": "text",
            "text": "这是一个要分析的大型文档:\n\n<document>...</document>",
            # 此点之前的内容被缓存
            "cache_control": {"type": "ephemeral"}
        }
    ]

    new_system_message = SystemMessage(content=new_content)
    return handler(request.override(system_message=new_system_message))

注意

  • ModelRequest.system_message始终是一个SystemMessage对象,即使代理是用system_prompt="string"创建的
  • 使用SystemMessage.content_blocks以块列表形式访问内容,无论原始内容是字符串还是列表
  • 修改系统消息时,使用content_blocks并追加新块以保留现有结构
  • 你可以直接将SystemMessage对象传递给create_agentsystem_prompt参数,用于缓存控制等高级用例

其他资源

  • 中间件API参考
  • 内置中间件
  • 测试代理
相关推荐
孙启超20 小时前
【AI应用开发】LangChain 中 Chain 和 Agent 核心区别?
java·人工智能·langchain·llm·rag·ai应用开发·agent loop
Wang's Blog1 天前
AI Agent白手起家28: LangChain 五种提示词模板实战解析
大数据·人工智能·langchain
GuWenyue1 天前
大模型幻觉无解?7步搭建Milvus+LangChain电子书RAG,私有小说精准问答零编造
人工智能·langchain·ai编程
'pi%'1 天前
多 Agent 协同方案实践:基于 LangGraph 搭建能源领域智能调度工作流
人工智能·爬虫·microsoft·langchain·ocr·能源
春水碧于天,画船听雨眠1 天前
LangChain学习笔记(一)
笔记·学习·langchain
扯蛋4382 天前
1.x 时代的记忆系统 (一)
langchain·llm·aigc
梦想三三2 天前
LangChain RAG PDF 智能问答实战:用 Streamlit 构建本地知识库(完整代码)
人工智能·python·langchain·大模型·rag
ganjiee00072 天前
langchain.messages引用报错
langchain
大模型真好玩2 天前
LangChain DeepAgents 速通指南(十二)——一文详解生产级智能体的命令体系和工程设计
人工智能·langchain·agent