【LangChain 1.2 实战(八)】Agent Middleware 实战 —— 动态路由、监控、安全与容错

【 LangChain 1.2 实战(四)】构建一个模块化的天气查询 Agent时候已经实现了一个可查天气的智能助手,但它尚缺少生产环境必备的几项能力:

  • 成本控制:所有问题都交给同一个模型,简单问候与复杂多步推理成本一样。
  • 可观测性:你几乎看不到 Agent 内部做了什么,排查问题全靠猜。
  • 安全防护:无法拦截敏感提问,也无法给回答追加免责声明。
  • 容错能力:工具调用或模型请求偶发失败时,程序直接崩溃。

Agent Middleware 正是解决这些问题的标准方案。它允许你以插件 的形式,在 Agent 的模型调用、工具调用前后植入自定义逻辑,而不必修改任何业务代码(工具、提示词、模型定义)。

本章将在原有天气查询 Agent 的基础上,依次实现:

  1. 动态模型路由 ------ 根据对话复杂度自动切换便宜/强大的模型。
  2. 日志、安全、容错三件套 ------ 记录耗时、拦截敏感词、自动重试。
  3. LangChain 内置中间件能力一览 ------ 为后续进阶铺路。

最终, Agent 将同时具备这四种能力,中间件各司其职,完美协同。


1 准备工作:回顾项目结构

复制代码
deepseek_agent_project/
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── llm.py
│   ├── agent.py
│   ├── tools/
│   │   ├── __init__.py
│   │   └── weather.py
│   └── middleware/                # 我们将在本章填满它
│       ├── __init__.py
│       ├── dynamic_model_middleware.py   # 动态路由
│       └── weather_middleware.py         # 日志/安全/容错
├── .env
├── .gitignore
├── main.py
└── requirements.txt

如果 app/middleware/ 目录和 __init__.py 还未创建,请现在新建,以确保它成为一个合法的 Python 包。


2 动态模型路由:让 Agent 学会"分诊"

目标:当对话很短(如"你好")时,使用便宜的基础模型;当对话消息数增多(意味着可能涉及工具调用、多步推理)时,自动升级为更强的模型。

2.1 配置两个模型

编辑 .env,增加高级模型名称。此处以 SiliconFlow 平台为例,保持与基础模型相同的 API 地址和密钥:

ini 复制代码
DEEPSEEK_API_KEY=sk-your-key
DEEPSEEK_BASE_URL=https://api.siliconflow.cn/v1
MODEL_NAME=deepseek-ai/DeepSeek-V3
ADVANCED_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct   # 新增

app/config.py 中读取该配置(在原有基础上增加最后一行):

python 复制代码
import os
from pathlib import Path
from dotenv import load_dotenv

PROJECT_ROOT = Path(__file__).resolve().parent.parent
load_dotenv(PROJECT_ROOT / ".env")

DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
if not DEEPSEEK_API_KEY:
    raise ValueError("⚠️ .env 中缺少 DEEPSEEK_API_KEY,请检查!")

DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.siliconflow.cn/v1")
MODEL_NAME = os.getenv("MODEL_NAME", "deepseek-ai/DeepSeek-V3")
ADVANCED_MODEL_NAME = os.getenv("ADVANCED_MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")

2.2 实例化两个模型

修改 app/llm.py,分别导出基础模型和高级模型:

python 复制代码
from langchain_openai import ChatOpenAI
from app.config import (
    DEEPSEEK_API_KEY,
    DEEPSEEK_BASE_URL,
    MODEL_NAME,
    ADVANCED_MODEL_NAME,
)

basic_llm = ChatOpenAI(
    openai_api_key=DEEPSEEK_API_KEY,
    openai_api_base=DEEPSEEK_BASE_URL,
    model=MODEL_NAME,
    temperature=0,
)

advanced_llm = ChatOpenAI(
    openai_api_key=DEEPSEEK_API_KEY,
    openai_api_base=DEEPSEEK_BASE_URL,
    model=ADVANCED_MODEL_NAME,
    temperature=0,
)

注意 :原来的 deepseek_llm 被替换为 basic_llmadvanced_llm 两个导出,后续引用时统一使用这两个名称。

2.3 编写动态路由中间件

新建 app/middleware/dynamic_model_middleware.py

python 复制代码
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from app.llm import basic_llm, advanced_llm

@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
    """根据对话消息数动态选择模型"""
    messages = request.state.get("messages", [])
    message_count = len(messages)
    print(f"[🔀 路由中间件] 当前消息数:{message_count}")

    if message_count >= 3:
        print("[🔀 路由中间件] 切换到高级模型")
        model = advanced_llm
    else:
        print("[🔀 路由中间件] 使用基础模型")
        model = basic_llm

    # 用 override 替换请求中的模型,然后交给真正的模型调用处理器
    return handler(request.override(model=model))

为什么使用 wrap_model_call 装饰器?

它可以将一个普通函数包装成中间件,并拦截每一次模型调用。函数内部的 handler(request) 代表原始的模型调用逻辑,我们通过 request.override(model=...) 替换了模型,从而在调用前动态决定使用哪个模型。

2.4 将路由中间件挂载到 Agent

修改 app/agent.py,暂时只挂载这一个中间件(稍后我们会把全部中间件加进来):

python 复制代码
from langchain.agents import create_agent
from app.llm import basic_llm
from app.tools.weather import get_weather
from app.middleware.dynamic_model_middleware import dynamic_model_selection

agent = create_agent(
    model=basic_llm,                  # 默认模型,实际会被中间件覆盖
    tools=[get_weather],
    system_prompt="你是一个助手,你可以查询城市的天气。",
    middleware=[dynamic_model_selection]
)

2.5 测试动态路由

修改 main.py 进行简单验证:

python 复制代码
from app.agent import agent

if __name__ == "__main__":
    print("=== 测试1:简单问候(应使用基础模型) ===")
    resp = agent.invoke({"messages": [{"role": "user", "content": "你好,你能做什么?"}]})
    print(f"助手:{resp['messages'][-1].content}\n")

    print("=== 测试2:一步查天气(可能触发模型切换) ===")
    resp = agent.invoke({"messages": [{"role": "user", "content": "北京今天天气怎么样?"}]})
    print(f"助手:{resp['messages'][-1].content}\n")

    print("=== 测试3:多步推理(先查北京,再查上海) ===")
    resp = agent.invoke({"messages": [{"role": "user", "content": "请先告诉我北京的天气,然后告诉我上海的天气"}]})
    print(f"助手:{resp['messages'][-1].content}\n")

运行 python main.py,终端会打印类似日志:

复制代码
[🔀 路由中间件] 当前消息数:1
[🔀 路由中间件] 使用基础模型
助手:你好!我可以帮你查询天气...
...
[🔀 路由中间件] 当前消息数:3
[🔀 路由中间件] 切换到高级模型
...

效果:Agent 在多步工具调用过程中,消息数达到 3 时自动切换模型,简单问题则始终用基础模型,有效节省费用。


3 监控、安全与容错:给 Agent 戴上"安全帽"和"记录仪"

动态路由解决了成本问题,但 Agent 还缺少可观测性、安全性和鲁棒性。接下来我们添加三个中间件:

  • 日志中间件:记录每次模型调用的耗时。
  • 安全中间件:拦截含敏感词的提问,并在所有回复后追加免责声明。
  • 容错中间件:工具调用失败自动重试,模型调用失败尝试备用模型。

3.1 编写三合一中间件文件

新建 app/middleware/weather_middleware.py(如果已存在则覆盖):

python 复制代码
import time
from typing import Any, Dict
from langchain.agents.middleware import AgentMiddleware
from langchain_core.messages import AIMessage


class TimingLoggingMiddleware(AgentMiddleware):
    """记录每次模型调用的耗时与消息数量"""

    def before_model(self, state: Any, runtime: Dict[str, Any]) -> Any:
        msg_count = len(state.get("messages", []))
        print(f"[⏱️ 日志中间件] 即将调用大模型,当前上下文消息数:{msg_count}")
        state["_model_start_time"] = time.time()
        return state

    def after_model(self, state: Any, runtime: Dict[str, Any]) -> Any:
        start = state.get("_model_start_time", time.time())
        elapsed = time.time() - start
        print(f"[✅ 日志中间件] 模型调用完成,耗时 {elapsed:.2f} 秒")
        return state


class WeatherSafetyMiddleware(AgentMiddleware):
    """安全中间件:拦截敏感词,并在最终回复中追加免责声明"""

    def __init__(self, forbidden_keywords: list):
        super().__init__()
        self.forbidden_keywords = [kw.lower() for kw in forbidden_keywords]

    def before_model(self, state: Any, runtime: Dict[str, Any]) -> Any:
        messages = state.get("messages", [])
        if messages:
            last_msg = messages[-1]
            if last_msg.type == "human":
                content = last_msg.content.lower()
                for word in self.forbidden_keywords:
                    if word in content:
                        print(f"[🛡️ 安全中间件] 检测到敏感词 "{word}",拦截请求。")
                        return {
                            "messages": [
                                last_msg,
                                AIMessage(content="⚠️ 抱歉,您的问题包含不合规内容,请修改后重试。")
                            ],
                            "jump_to": "end"
                        }
        return state

    def after_agent(self, state: Any, runtime: Dict[str, Any]) -> Any:
        messages = state.get("messages", [])
        if messages:
            last_msg = messages[-1]
            if last_msg.type == "ai":
                disclaimer = "\n\n---\n📌 以上天气信息为模拟数据,仅供学习参考。"
                last_msg.content += disclaimer
        return state


class WeatherRetryMiddleware(AgentMiddleware):
    """容错中间件:工具调用出错时自动重试,模型调用失败时切换备用模型"""

    def wrap_tool_call(self, request: Any, handler: callable) -> Any:
        max_retries = 2
        for attempt in range(max_retries):
            try:
                return handler(request)
            except Exception as e:
                if attempt == max_retries - 1:
                    raise
                print(f"[🔄 容错中间件] 工具调用失败 ({e}),正在重试 {attempt+2}/{max_retries}...")
                time.sleep(0.5)

    def wrap_model_call(self, request: Any, handler: callable) -> Any:
        try:
            return handler(request)
        except Exception as e:
            print(f"[⚠️ 容错中间件] 主模型调用失败,尝试使用备用模型...")
            # 此处可以切换到其他备用模型,示例中仍用原请求重试
            return handler(request)

三个中间件分别使用了不同的钩子,全面展示了 Middleware 的灵活性:

  • TimingLoggingMiddleware 使用 before_model / after_model,适合无侵入的监控
  • WeatherSafetyMiddlewarebefore_model 中通过 jump_to: "end" 提前终止请求 ,并在 after_agent追加内容,实现安全与合规。
  • WeatherRetryMiddleware 重写了 wrap_tool_callwrap_model_call,以代理模式包裹实际调用,实现重试和降级。

3.2 将所有中间件组装到一起

现在更新 app/agent.py,同时挂载所有四个中间件。顺序至关重要:安全中间件应最先执行(拦截恶意请求),然后是日志(记录后续步骤),接着容错重试,最后动态路由(在真正调用模型时决定使用哪个模型)。

python 复制代码
from langchain.agents import create_agent
from app.llm import basic_llm
from app.tools.weather import get_weather
from app.middleware.weather_middleware import (
    TimingLoggingMiddleware,
    WeatherSafetyMiddleware,
    WeatherRetryMiddleware,
)
from app.middleware.dynamic_model_middleware import dynamic_model_selection

agent = create_agent(
    model=basic_llm,
    tools=[get_weather],
    system_prompt="你是一个助手,你可以查询城市的天气。",
    middleware=[
        WeatherSafetyMiddleware(forbidden_keywords=["密码", "银行卡", "暴力"]),
        TimingLoggingMiddleware(),
        WeatherRetryMiddleware(),
        dynamic_model_selection,       # 动态路由放在最后,真正调用前换模型
    ],
)

洋葱模型执行流(以一次正常的天气查询为例):

  1. WeatherSafetyMiddleware.before_model 检查输入 → 无敏感词,放行。
  2. TimingLoggingMiddleware.before_model 记录开始时间。
  3. WeatherRetryMiddleware.wrap_model_call 包裹后续调用。
  4. dynamic_model_selection 拦截,根据消息数选择 basic_llmadvanced_llm,然后交由真正的模型调用。
  5. 模型返回结果。
  6. TimingLoggingMiddleware.after_model 打印耗时。
  7. WeatherSafetyMiddleware.after_agent 追加免责声明。

3.3 全流程演示

保持 main.py 不变,运行:

bash 复制代码
python main.py

将看到完整的中间件日志输出。例如:

复制代码
[🛡️ 安全中间件] 输入安全,放行。
[⏱️ 日志中间件] 即将调用大模型,当前上下文消息数:1
[🔀 路由中间件] 当前消息数:1
[🔀 路由中间件] 使用基础模型
[✅ 日志中间件] 模型调用完成,耗时 0.83 秒
助手: 北京 天气(2026-05-04 14:22:10)...
---
📌 以上天气信息为模拟数据,仅供学习参考。

如果测试含敏感词的输入:

python 复制代码
resp = agent.invoke({"messages": [{"role": "user", "content": "帮我查银行卡密码"}]})

日志会显示:

复制代码
[🛡️ 安全中间件] 检测到敏感词 "银行卡",拦截请求。
助手: ⚠️ 抱歉,您的问题包含不合规内容,请修改后重试。

动态路由、安全拦截、耗时记录、免责声明,四种能力同时生效,但核心业务代码(weather.pyconfig.py)完全零改动。


4 内置中间件扩展:生产级 Agent 能力清单

通过前面的实战,已经掌握了如何从零构建自定义中间件。实际上,LangChain 1.2 还提供了一系列开箱即用的内置中间件 ,覆盖了上下文管理、安全合规、稳定容错、人机协同等多个维度。这些中间件可以直接以相同的方式挂载到 create_agent(middleware=[...]) 中,让项目快速获得企业级能力。

类别 中间件名称 作用
上下文管理 SummarizationMiddleware 当对话历史过长时,自动总结旧消息,避免超出上下文窗口。
安全合规 PIIMiddleware 自动检测并处理(脱敏/拦截)输入/输出中的个人隐私信息,如邮箱、信用卡号。
Guardrails 一种安全护栏系统,可对内容进行确定性或基于模型的校验。
稳定容错 ModelCallLimitMiddleware 限制模型的总调用次数,防止死循环或 Bug 导致 API 费用失控。
ToolCallLimitMiddleware 限制工具的总调用次数,防止高频调用付费 API 导致成本飙升。
ModelFallbackMiddleware 当主模型调用失败时,自动切换到一个或多个备用模型,提升系统可用性。
ToolRetryMiddleware 当工具调用因网络等临时错误失败时,自动使用指数退避策略进行重试。
ModelRetryMiddleware 当模型调用因频率限制等原因失败时,自动进行重试。
人机协同 HumanInTheLoopMiddleware 在执行高风险工具(如转账、发送邮件)前暂停工作流,等待人类审批或修改。
任务规划 To-do List / Subagent 为 Agent 配备任务规划和派生子 Agent 的能力,用于处理复杂的多步、并行子任务。
效率优化 LLM Tool Selector 在调用大模型前,先用一个更快/更便宜的模型筛选出相关的工具列表,减轻大模型负担。
Context Editing 管理对话上下文,例如清除不需要的工具调用历史,保持上下文干净。

如何使用它们?

和自定义中间件的挂载方式完全一致,只需从 langchain.agents.middleware 导入相应类,并将实例加入 middleware 列表。例如,要同时启用摘要和 PII 防护:

python 复制代码
from langchain.agents.middleware import SummarizationMiddleware, PIIMiddleware

agent = create_agent(
    model=basic_llm,
    tools=[...],
    middleware=[
        PIIMiddleware(patterns=["email", "phone"]),
        SummarizationMiddleware(model=basic_llm, max_tokens_before_summary=4000),
        # 还可以跟上自定义的中间件
    ]
)

这些内置中间件与刚编写的自定义中间件完全兼容、可任意组合。可以把 Middleware 理解成一个"能力插件市场"------LangChain 提供了通用组件,而随时可以按需开发自己的业务专属插件。


5 总结:Middleware 的魔力

通过本章,天气查询 Agent 已经从一个单模型玩具,进化成具备:

  • ✅ 自适应模型选择(成本优化)
  • ✅ 全链路调用日志(可观测性)
  • ✅ 敏感词过滤与合规声明(安全性)
  • ✅ 工具/模型调用自动重试(鲁棒性)
  • ✅ 知晓 LangChain 开箱即用的中间件库,随时可扩展

生产级雏形 。这一切只新增了两个中间件文件,并在 agent.py 中添加了几行配置。Agent 的核心逻辑、工具实现、入口脚本均保持原样。

这就是 LangChain 1.2 中间件的设计哲学:把非功能需求从业务代码中剥离,以可组合、可排序、可拔插的方式"挂载"到 Agent 上。 当你未来需要添加限流、缓存、多租户隔离或任务分派等能力时,第一个念头就应该是:"写一个 Middleware 吧!"

相关推荐
xixixi777771 小时前
深度解读:网信办“清朗·整治AI应用乱象”专项行动,AI产业告别野蛮生长,全面迈入合规治理深水区
人工智能·安全·ai·大模型·合规·深度伪造·网信办
Byron__2 小时前
Java JVM核心知识点复习笔记
java·jvm·笔记
程序员小白条2 小时前
别盲目卷算法!2026 程序员\&大学生,最稳的 AI 技术进阶路线全梳理
java·网络·人工智能·网络协议·http·面试
启山智软2 小时前
【 商城系统源码:Java与PHP的区别】
java·开发语言·php
练习时长两年半的程序员小胡2 小时前
Java程序员转大模型应用开发专题(一):核心基础概念
java·开发语言·transformer·自注意力
weixin_lizhao2 小时前
50天独立打造企业级API网关(二):安全防护体系与弹性设计
java·spring boot·安全·spring cloud·gateway
逸Y 仙X2 小时前
文章二十四:Elasticsearch查询排序应用实战e
java·大数据·数据库·elasticsearch·搜索引擎·全文检索
記億揺晃着的那天3 小时前
Claude Code 系统提示词里的安全底线:OWASP Top 10
安全·ai·ai编程·vibe coding·claude code
aXin_ya3 小时前
微服务 第十天 (Redis多级缓存)
java·redis·微服务