LangChain_Agent中间件实战

把 Agent 从"能跑"变成"可控":LangChain v1 中间件实战

很多 Agent 项目在演示阶段表现不错:模型能够理解问题、选择工具并返回答案。但一旦进入真实业务环境,问题很快就会暴露出来:对话越聊越长、工具偶尔报错、不同用户需要不同权限、模型输出必须经过审核,而且每一次调用都需要记录和监控。

如果把这些逻辑全部塞进 Agent 主流程,代码会迅速变得难以维护。LangChain v1 的中间件机制提供了更清晰的解决方案:在不改动核心推理循环的情况下,拦截 Agent 生命周期中的关键节点,对上下文、模型调用、工具执行和最终结果进行统一处理。

本文将系统介绍 LangChain Agent 中间件的工作方式,并结合对话摘要、内容审核、自定义钩子、状态更新和流程跳转等场景,说明如何让 Agent 具备真正面向生产环境的可控性。

一、中间件解决了什么问题

一个典型的 Agent 执行过程大致如下:

text 复制代码
用户输入
   ↓
调用模型进行推理
   ↓
模型判断是否调用工具
   ├─ 否 → 生成最终回答
   └─ 是 → 执行工具 → 将结果交给模型继续推理

中间件可以介入这条链路的多个位置:

text 复制代码
用户输入
   ↓
[模型调用前]
   ↓
模型推理
   ↓
[模型调用后]
   ↓
[工具执行前] → 工具执行 → [工具执行后]
   ↓
[Agent 结束前]
   ↓
最终回答

这些拦截点可以处理大量与业务流程无关、但对系统稳定性至关重要的横切需求。

介入位置 常见用途
Agent 启动前 初始化状态、身份校验、记录请求
模型调用前 动态提示词、模型切换、工具过滤、上下文注入
模型调用后 输出校验、调用计数、日志记录、提前结束
工具执行前 权限检查、参数校验、审计记录
工具执行后 异常转换、结果审核、状态写入
Agent 结束后 指标统计、资源清理、最终结果处理

中间件的价值可以概括为四点:

  1. 非侵入式控制:无需修改 Agent 的核心循环,就能干预模型和工具调用。
  2. 可观测性:通过钩子收集运行日志、调用次数、Token 用量和耗时数据。
  3. 可靠性增强:集中实现重试、降级、限流、异常处理和内容安全策略。
  4. 能力复用:把通用逻辑封装成独立组件,在多个 Agent 中组合使用。

对于复杂应用来说,中间件并不是锦上添花的功能,而是将原型系统推进到生产环境的重要基础设施。

二、动态控制模型与工具

Agent 的失败不一定来自模型能力不足。很多时候,真正的问题是模型没有获得正确的上下文,或者看到了不应该使用的工具。

根据对话复杂度切换模型

短对话可以使用成本较低、响应更快的模型;当消息数量增加、任务变复杂时,再切换到能力更强的模型。

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


@wrap_model_call
def dynamic_model_selection(
    request: ModelRequest,
    handler,
) -> ModelResponse:
    message_count = len(request.state["messages"])

    model = advanced_model if message_count > 10 else basic_model
    return handler(request.override(model=model))

request.override() 会生成一份修改后的请求。Agent 主流程不需要知道模型何时发生了切换,只负责继续执行。

根据状态过滤工具

如果某些工具只能由已认证用户调用,可以在模型看到工具列表之前完成过滤。

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


@wrap_model_call
def filter_tools_by_auth(
    request: ModelRequest,
    handler,
) -> ModelResponse:
    authenticated = request.state.get("authenticated", False)

    if not authenticated:
        public_tools = [
            tool for tool in request.tools
            if tool.name.startswith("public_")
        ]
        request = request.override(tools=public_tools)

    return handler(request)

这种方式有两个好处:一是减少无关工具对模型决策的干扰,二是避免仅靠提示词声明权限。真正的权限控制应该落实在程序逻辑中,而不是寄希望于模型"自觉遵守"。

三、用对话摘要控制上下文长度

长对话会持续占用上下文窗口。直接删除早期消息虽然节省 Token,却可能让 Agent 忘记用户偏好、关键决策和未完成任务。

SummarizationMiddleware 的思路是:当对话达到指定阈值时,调用一个摘要模型压缩较早的消息,再把"历史摘要 + 最近消息"交给主模型。

text 复制代码
完整历史消息
   ↓
检查 Token 或消息数量
   ↓ 达到阈值
摘要早期消息
   ↓
历史摘要 + 最近若干条完整消息
   ↓
交给主模型继续推理

两个核心参数

摘要行为主要由 triggerkeep 控制。

trigger 决定何时触发摘要:

配置方式 含义 示例
("tokens", n) Token 数超过阈值时触发 ("tokens", 4000)
("messages", n) 消息数量超过阈值时触发 ("messages", 20)
("fraction", x) 上下文使用率超过指定比例时触发 ("fraction", 0.8)

keep 决定摘要后保留多少近期内容:

配置方式 含义 示例
("messages", n) 保留最近 n 条完整消息 ("messages", 20)
("tokens", n) 将保留内容控制在指定 Token 范围 ("tokens", 2000)
("fraction", x) 将上下文压缩到窗口的指定比例 ("fraction", 0.3)

基础配置

python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.memory import InMemorySaver


agent = create_agent(
    model="gpt-5-mini",
    tools=[weather_tool, calculator_tool],
    checkpointer=InMemorySaver(),
    middleware=[
        SummarizationMiddleware(
            model="gpt-4o-mini",
            trigger=("tokens", 4000),
            keep=("messages", 20),
        )
    ],
)

这里使用较轻量的模型负责摘要,可以降低额外成本。检查点则负责保存同一会话的状态,调用时需要传入稳定的 thread_id

python 复制代码
config = {"configurable": {"thread_id": "user-session-001"}}

result = agent.invoke(
    {"messages": [{"role": "user", "content": "继续刚才的话题"}]},
    config=config,
)

多条件触发

trigger 可以接收多个条件,只要其中一个满足就会执行摘要。

python 复制代码
SummarizationMiddleware(
    model="gpt-4o-mini",
    trigger=[
        ("tokens", 3000),
        ("messages", 20),
    ],
    keep=("messages", 10),
)

如果应用会在不同上下文窗口的模型之间切换,可以使用比例配置:

python 复制代码
SummarizationMiddleware(
    model="gpt-4o-mini",
    trigger=("fraction", 0.8),
    keep=("fraction", 0.3),
)

生产环境中还应关注以下问题:

  • 摘要模型不必与主模型相同,通常可以选择更快、更便宜的模型。
  • 摘要提示词应明确要求保留用户偏好、关键事实、决策和待办事项。
  • 阈值过低会频繁触发摘要,增加延迟和成本;阈值过高则可能逼近上下文上限。
  • 摘要是一种有损压缩,关键业务数据不应只存在于自然语言历史中,还应写入结构化状态或数据库。

四、用内容审核建立安全边界

面向用户开放的 Agent 不仅要检查输入,还要考虑模型输出和工具返回结果。工具可能读取外部网页、用户文档或第三方接口,这些内容同样可能包含不安全信息。

OpenAIModerationMiddleware 可以在多个位置执行内容审核:

python 复制代码
from langchain_openai.middleware import OpenAIModerationMiddleware


moderation = OpenAIModerationMiddleware(
    model="omni-moderation-latest",
    check_input=True,
    check_output=True,
    check_tool_results=True,
    exit_behavior="end",
    violation_message="请求或响应触发了内容安全策略。",
)

主要配置项如下:

参数 作用
check_input 在调用主模型前审核用户输入
check_output 在返回结果前审核模型输出
check_tool_results 审核工具执行结果
exit_behavior 定义检测到违规内容后的处理方式
violation_message 自定义拦截或替换消息

三种违规处理策略

end:立即结束

检测到违规后停止后续执行,直接返回提示消息。它适合大多数对外服务,行为明确,安全边界也最容易理解。

python 复制代码
OpenAIModerationMiddleware(
    check_input=True,
    check_output=True,
    exit_behavior="end",
)
error:抛出异常

中间件抛出 OpenAIModerationError,由上层应用决定如何记录日志、触发告警或转换为统一的 API 响应。

python 复制代码
from langchain_openai.middleware import (
    OpenAIModerationError,
    OpenAIModerationMiddleware,
)


agent = create_agent(
    model="gpt-5-mini",
    tools=tools,
    middleware=[
        OpenAIModerationMiddleware(
            check_input=True,
            check_output=True,
            check_tool_results=True,
            exit_behavior="error",
            violation_message="内容违反安全策略:{categories}",
        )
    ],
)

try:
    result = agent.invoke({"messages": messages})
except OpenAIModerationError as exc:
    logger.warning("Moderation blocked request: %s", exc)

这种方式更适合已经建立统一异常处理和监控体系的后端服务。

replace:替换后继续

违规内容会被替换为指定文本,然后 Agent 继续运行。

python 复制代码
OpenAIModerationMiddleware(
    check_input=True,
    exit_behavior="replace",
    violation_message="[部分内容已根据安全策略移除]",
)

替换模式能保持对话连续,但需要谨慎使用。原始内容被替换后,语义可能不完整,模型也可能无法准确理解用户意图。高风险场景通常更适合直接结束或抛出异常。

五、自定义中间件的两种钩子风格

LangChain 提供节点风格和包装风格两类钩子。二者的差异不只是写法不同,更重要的是控制能力不同。

节点风格:在固定时机执行逻辑

节点风格钩子按生命周期顺序执行,适合日志、计数、校验和简单状态更新。

钩子 触发时机
before_agent Agent 启动前,只执行一次
before_model 每次调用模型前
after_model 每次模型返回后
after_agent Agent 结束后,只执行一次
python 复制代码
from typing import Any

from langchain.agents import AgentState
from langchain.agents.middleware import before_agent, after_model
from langgraph.runtime import Runtime


@before_agent
def log_request(
    state: AgentState,
    runtime: Runtime,
) -> dict[str, Any] | None:
    print("Agent started")
    return None


@after_model
def count_model_calls(
    state: AgentState,
    runtime: Runtime,
) -> dict[str, Any]:
    return {
        "model_call_count": state.get("model_call_count", 0) + 1
    }

节点钩子返回字典时,LangChain 会将其合并到 Agent 状态中。

包装风格:完全控制一次调用

包装风格钩子会包裹模型或工具调用,可以决定是否调用、调用几次、使用哪些参数,以及如何处理返回值。

钩子 包裹对象 典型用途
wrap_model_call 模型调用 重试、缓存、模型切换、请求改写
wrap_tool_call 工具调用 异常转换、权限控制、审计、结果处理

下面的中间件为模型调用增加最多三次尝试:

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import (
    ModelRequest,
    ModelResponse,
    wrap_model_call,
)


@wrap_model_call
def retry_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    for attempt in range(3):
        try:
            return handler(request)
        except Exception:
            if attempt == 2:
                raise

    raise RuntimeError("unreachable")

工具异常也可以被转换为模型能够理解的 ToolMessage,避免单次工具故障让整个 Agent 崩溃:

python 复制代码
from collections.abc import Callable

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


@wrap_tool_call
def handle_tool_error(
    request: ToolCallRequest,
    handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
    try:
        return handler(request)
    except Exception as exc:
        return ToolMessage(
            content=f"工具执行失败,请检查参数后重试:{exc}",
            tool_call_id=request.tool_call["id"],
        )

需要注意的是,不应把内部堆栈、数据库信息或密钥直接暴露给模型和最终用户。生产系统通常要先记录完整异常,再返回经过脱敏的简短说明。

六、装饰器与类式中间件如何选择

当一段逻辑只有一个明确职责时,装饰器写法最直接。例如单独记录日志、统计模型调用次数或包装工具异常。

如果一个中间件需要同时实现多个钩子,类式写法会更容易组织:

python 复制代码
from collections.abc import Callable
from typing import Any

from langchain.agents import AgentState
from langchain.agents.middleware import (
    AgentMiddleware,
    ModelRequest,
    ModelResponse,
)
from langgraph.runtime import Runtime


class ObservabilityMiddleware(AgentMiddleware):
    def before_agent(
        self,
        state: AgentState,
        runtime: Runtime,
    ) -> dict[str, Any] | None:
        print("Agent started")
        return None

    def after_agent(
        self,
        state: AgentState,
        runtime: Runtime,
    ) -> dict[str, Any] | None:
        print("Agent finished")
        return None

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        print("Calling model")
        response = handler(request)
        print("Model completed")
        return response

注册时传入实例即可:

python 复制代码
agent = create_agent(
    model="gpt-5-mini",
    tools=tools,
    middleware=[ObservabilityMiddleware()],
)

简单来说:单一钩子优先使用装饰器;需要共享配置、内部成员或多个钩子协作时,使用类式中间件。

七、多个中间件的执行顺序

中间件顺序会直接影响系统行为。假设注册顺序如下:

python 复制代码
middleware=[middleware_a, middleware_b]

节点风格的前置钩子按注册顺序执行,后置钩子按相反顺序返回:

text 复制代码
A.before → B.before → 模型调用 → B.after → A.after

包装风格类似嵌套函数,前面的中间件位于外层:

text 复制代码
A 进入
  B 进入
    执行模型或工具
  B 返回
A 返回

这意味着列表顺序不是无关紧要的配置细节。例如,安全审核、认证和审计通常应尽早介入;缓存放在审核之前还是之后,也会影响未经审核的内容是否可能进入缓存。

一个实用原则是:先明确每个中间件保护或改变的对象,再决定它位于调用链的内层还是外层。

八、扩展状态并记录运行数据

AgentState 是默认状态结构。自定义中间件可以扩展专属字段,让多个钩子共享数据。

python 复制代码
from typing import NotRequired

from langchain.agents import AgentState


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

节点风格钩子适合返回简单的状态更新:

python 复制代码
from typing import Any

from langchain.agents.middleware import after_model
from langgraph.runtime import Runtime


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

包装风格钩子需要同时返回模型响应和状态更新时,可以使用 ExtendedModelResponseCommand

python 复制代码
from collections.abc import Callable

from langchain.agents.middleware import (
    ExtendedModelResponse,
    ModelRequest,
    ModelResponse,
    wrap_model_call,
)
from langgraph.types import Command


@wrap_model_call(state_schema=TrackingState)
def track_token_usage(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ExtendedModelResponse:
    response = handler(request)
    tokens = response.result[-1].response_metadata[
        "token_usage"
    ]["completion_tokens"]

    return ExtendedModelResponse(
        model_response=response,
        command=Command(
            update={"last_model_call_tokens": tokens}
        ),
    )

对于带 reducer 的字段,例如消息列表,多次更新会按照 reducer 规则合并;普通自定义字段发生冲突时,则需要关注中间件嵌套顺序带来的覆盖结果。

九、在满足条件时提前结束或跳转

节点风格钩子不仅能更新状态,还可以通过 jump_to 改变执行方向。常见目标包括:

  • "end":结束 Agent,并继续触发结束钩子。
  • "tools":跳转到工具节点。
  • "model":重新进入模型节点。

钩子必须通过 hook_config 显式声明允许跳转到哪些目标,否则跳转请求会被忽略。

python 复制代码
from typing import Any

from langchain.agents import AgentState
from langchain.agents.middleware import after_model, hook_config
from langchain_core.messages import AIMessage
from langgraph.runtime import Runtime


@after_model
@hook_config(can_jump_to=["end"])
def stop_when_policy_blocks(
    state: AgentState,
    runtime: Runtime,
) -> dict[str, Any] | None:
    last_message = state["messages"][-1]

    if "POLICY_BLOCKED" in last_message.content:
        return {
            "messages": [AIMessage("当前请求无法继续处理。")],
            "jump_to": "end",
        }

    return None

流程跳转只适用于节点风格钩子。包装风格钩子的控制方式是决定是否调用 handler,不能使用 jump_to

十、面向生产环境的设计原则

中间件能力很强,但过度堆叠也会让调用链难以理解。实践中可以遵循以下原则。

保持单一职责

日志、审核、重试、缓存和权限控制尽量拆成不同中间件。每个组件只解决一类问题,更容易测试和复用。

优先优雅降级

监控或日志中间件自身发生异常时,通常不应让业务 Agent 一起崩溃。对于非关键功能,可以捕获异常并降级;对于认证和安全审核等关键功能,则应采用失败即关闭的策略。

根据控制需求选择钩子

  • 日志、计数、状态标记适合节点风格。
  • 重试、缓存、动态模型和动态工具适合包装风格。
  • 需要同时处理多个阶段并共享配置时,适合类式中间件。

单独测试后再组合

先验证每个中间件的输入、输出、异常和状态更新,再测试多个中间件组合后的顺序。尤其要覆盖模型失败、工具超时、审核拦截和摘要触发等分支。

关注可观测性成本

记录所有原始提示词和模型输出可能带来隐私风险,也会增加日志存储成本。日志中间件应具备脱敏、采样和分级能力。

优先使用成熟实现

对于摘要、内容审核等通用能力,优先使用框架提供的预构建中间件。只有当默认行为不能满足业务要求时,再实现自定义版本。

结语

Agent 的核心循环解决的是"如何推理和调用工具",中间件解决的则是"如何让这套能力长期、稳定、安全地运行"。

通过中间件,我们可以动态选择模型和工具、压缩长对话、审核输入输出、统一处理异常、记录运行指标、扩展状态,甚至改变执行方向。更重要的是,这些能力能够与业务逻辑保持解耦,以独立组件的形式组合和复用。

当 Agent 从实验代码走向真实系统时,真正决定质量的往往不是又增加了多少工具,而是是否建立了清晰的控制边界。中间件正是这条边界的主要承载方式。

相关推荐
幸福在路上wellbeing3 小时前
AI 智能体开发 · Day 1 详细学习手册
人工智能·学习
xian_wwq3 小时前
【学习笔记】框架层坍缩——LangChain 们正在被重新定义-14/15
笔记·学习·langchain
axinawang3 小时前
第14课:查找、统计、分割字符串
python
倒流时光三十年3 小时前
第一阶段 02 · Mapping 与数据类型(text vs keyword 是重点)
后端·python·django
2601_961593423 小时前
视频分辨率太低?Topaz Video AI v1.6.0 让画质跃升
人工智能·macos·音视频
联盟分享专家3 小时前
联盟营销自动化指南:如何扩展您的联盟营销规模与提升转化率?
大数据·人工智能
小大宇3 小时前
python蓝图、拦截器、异常处理、redis队列、线程池、控制台及日志文件输出
python
向夏威夷 梦断明暄3 小时前
从 Bun 的 Rust 重写,看 C# 如何重建 AI 基础设施层
人工智能·rust·c#
谁看我谁 狼家二丫3 小时前
机器学习漫游(1) 基本设定
人工智能·机器学习