9.中间件 (Middleware)

1. 什么是中间件 (Middleware)?

Middleware(中间件),简单说就是Agent 执行过程中的钩子函数,是 LangChain 1.x 的"王牌"工程化能力。

钩子(Hooks) 是框架或系统在某些关键执行点暴露的扩展接口。开发者可以"挂上"自己的逻辑,在那些点上插入、修改或替换行为,而完全无需改变主流程代码。就像在流水线上某个环节设置了一个"检查点"或"插入器"。

借助中间件,开发者可以高度定制和控制 Agent 运行的每一个环节,这是处理 Agent 生命周期的标准方式


2. 为什么需要中间件?

如果没有中间件,Agent 的执行流程通常比较直接: 1 用户输入 -> 拼接提示词/消息 -> 调用模型 -> 如有需要调用工具 -> 返回结果

这种方式对于简单场景已经足够,但一旦进入真实项目,往往会遇到很多额外需求,例如:

  • 想根据问题复杂度动态 切换模型
  • 限制 某些用户只能调用部分工具;
  • 想在工具报错时 自动重试 或返回兜底结果;
  • 想在模型调用前 插入额外的系统提示
  • 想记录每一步的 执行日志 ,方便排查问题;
  • 想在敏感信息出现时 阻断执行
  • 想在正式执行工具前增加 人工审批

这些需求有一个共同特点:**它们不是 Agent 的核心业务逻辑,但又会影响 Agent 的执行过程。**如果把这些逻辑全部直接写进主流程,会带来几个问题:

  1. 主流程会迅速变乱 Agent 本身只需要关心"理解用户需求、决定是否调用工具、生成结果",但一旦把日志、鉴权、重试、风控、审计都塞进去,主逻辑就会变得臃肿。
  2. 很多逻辑是横切需求,难以复用 例如日志、重试、风控、权限控制,通常不是某一个 Agent 独有的,而是多个 Agent 都需要。如果直接写死在每个 Agent 里,会产生大量重复代码。
  3. 流程控制粒度不够细 有些逻辑必须发生在"模型调用前",有些要发生在"工具调用后",如果没有统一的执行拦截点,开发者只能手动改主流程,既麻烦又容易出错。
  4. 后期维护成本高 当你需要增加一个新规则,例如"所有外部工具调用前都先做审计",如果系统没有中间件机制,往往需要修改很多处代码。

总结: 中间件的价值就在于把这些与业务无关、但与执行过程强相关的横切逻辑,从 Agent 主流程中分离出来 。让 Agent 主体代码 聚焦业务 ,而借助中间件,实现" 拦截流程、修改流程、增强流程 "。

3. 内置中间件的分类

LangChain 提供了丰富的与模型供应商无关的内置中间件,主要分为以下六个类别:

  1. 成本与资源控制类
    • 核心目标:控成本、控配额、避免无限调用(解决 Agent 太贵、太能跑的问题)。
    • 包含Model call limit(限制模型调用次数)、Tool call limit(限制工具调用次数)、Summarization(上下文摘要)、Context editing(裁剪上下文)。
  2. 稳定性与容错保障类
    • 核心目标:保证服务不中断、失败后尽量自动恢复(做高可用、容灾)。
    • 包含Model fallback(主模型失败切换备用)、Model retry(模型失败自动重试)、Tool retry(工具调用失败自动重试)。
  3. 安全与合规风控类
    • 核心目标:让 Agent 可控、可审、合规(防乱执行、泄露敏感信息)。
    • 包含Human-in-the-loop(人工审批)、PII detection(检测和处理个人敏感信息)。
  4. 决策增强与智能编排类
    • 核心目标:提升 Agent 的决策质量和任务拆解能力。
    • 包含To-do list(任务规划与步骤跟踪)、LLM tool selector(子模型预筛选工具)、Subagent(生成子 Agent 拆解任务)。
  5. 执行能力扩展类
    • 核心目标:给 Agent 更多"手脚",从纯推理扩展成执行体。
    • 包含Shell tool(持久化 Shell)、File search(文件搜索)、Filesystem(文件系统读写)。
  6. 开发调试与测试辅助类
    • 核心目标:方便开发、测试、验证 Agent 行为(服务于研发调试阶段)。
    • 包含LLM tool emulator(模拟工具执行)、SummarizationContext editingHuman-in-the-loop

4. 常用内置中间件详解与实战

下面我们将挑选最常用的几个中间件,逐一拆解它们的核心作用关键配置 ,并给出具体的代码示例


4.1 PIIMiddleware (Personally Identifiable Information / 个人身份信息脱敏)

  • PII 是什么 :PII 全称是 Personally Identifiable Information,即个人身份信息。
  • 主要作用:在用户的输入发给大模型之前,或者大模型的输出发给用户之前,自动检测并处理(打码/替换/报错)敏感信息(如邮箱、银行卡号、MAC地址等),防止数据泄露。
  • 核心配置项
    • pii_type:要检测的数据类型(内置支持 email, credit_card, url, ip, mac_address,也支持通过 detector 自定义正则)。
      • 💡 高阶用法提示 :虽然它叫 PII 中间件,但因为支持自定义正则(detector),你完全可以把它当成一个"万能文本拦截器"。比如拦截竞品名称、拦截脏话、或者拦截特定的业务指令,只要写个对应的正则表达式,它都能帮你完美拦截或打码!
    • strategy:脱敏策略。
      • redact:完全替换,如 [REDACTED_EMAIL]
      • mask:打星号掩码,如 ****-****-****-5100
      • hash:替换为哈希值。
      • block:一旦检测到直接抛出异常拦截。
    • 拦截环节控制 (这三个开关决定了中间件在数据的哪个流转环节生效):
      • apply_to_input(默认 True):拦截用户的输入 。在"用户输入"发往"大模型"之前进行拦截。把用户的真实隐私数据脱敏后再发给云端,是最常用且最核心的保护手段。如果把它设为 False,并且另外两个也不开,那中间件就等同于失效了。
      • apply_to_output(默认 False):拦截大模型的输出。在"大模型的回答"展示给"用户"之前进行拦截。用于防范大模型在回答时不小心泄露记忆中的隐私数据,在展示给前端前强制打码。
      • apply_to_tool_results(默认 False):拦截工具的返回结果。当 Agent 调用工具从数据库里查到了真实的机密档案后,在把档案交还给大模型分析之前,先将敏感数据脱敏。

实战示例

python 复制代码
import re
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
from langchain.messages import HumanMessage

agent = create_agent(
    model=model,
    tools=[],
    middleware=[
        # 1. 常规用法:把邮箱完全替换为标签
        PIIMiddleware("email", strategy="redact"),
        
        # 2. 常规用法:把信用卡号打上星号掩码
        PIIMiddleware("credit_card", strategy="mask"),
        
        # 3. 高阶用法:防竞品!自定义正则,一旦输入包含友商名字,全部打码!
        PIIMiddleware(
            pii_type="competitor_brand",
            strategy="mask",
            detector=re.compile(r"文心一言|通义千问|豆包|Kimi", re.IGNORECASE)
        ),
        
        # 4. 高阶用法:防脏话!自定义正则,检测到脏话直接抛异常拦截!
        PIIMiddleware(
            pii_type="profanity",
            strategy="block", # block 策略会直接引发拦截报错
            detector=re.compile(r"笨蛋|傻X|脑残", re.IGNORECASE)
        )
    ]
)

# 【测试场景 1】:正常提问但包含竞品和隐私
response = agent.invoke({
    "messages": [HumanMessage("你们比文心一言和Kimi强吗?我的邮箱是 test@qq.com")]
})
# 大模型实际收到的将会是:"你们比****和****强吗?我的邮箱是 [REDACTED_EMAIL]"

# 【测试场景 2】:恶意提问包含脏话
# response = agent.invoke({"messages": [HumanMessage("你这个笨蛋AI!")]}) 
# 结果:中间件直接抛出异常,大模型完全不会收到这条消息!

4.2 HumanInTheLoopMiddleware (人在环中/人工审批)

  • 主要作用 :在 Agent 决定调用某个工具,但尚未真正执行该工具前,强行挂起并中断运行。等待外部人类管理员确认(同意/拒绝/修改参数)后,再恢复执行。
  • 💡 本质解析 :它的底层原理仅仅是一个普通的程序中断 (Interrupt) 。之所以能顺理成章地实现"同意/拒绝/修改参数",完全是因为这个中断被极其精准地卡在了"大模型已准备好参数,但工具尚未执行"这个特殊的时间点
  • 实际开发中的典型场景
    1. 高危/破坏性操作拦截:如打款、发邮件、操作生产数据库(删库/改库)、重启服务器等。
    2. 内容发布前审核 :Agent 撰写了一篇推文、公众号文章或法律合同,准备调用 发布 工具。此时拦截,由人类审核、修改错别字后再放行发布。
    3. 大额计费/高耗时任务卡点:Agent 准备调用一个极度昂贵的第三方 API,或者准备启动一个需要跑 5 个小时的云端数据分析任务。人工确认 Agent 的前置数据收集无误后,再允许扣费执行。
    4. 业务参数的人工纠偏 (Edit):大模型从一张模糊的发票中提取了报销金额准备入账,人工不仅可以"同意/拒绝",还可以选择"修改参数(Edit)",把金额从 800 改为 80 之后再让工具继续执行。
  • 核心配置项
    • interrupt_on:字典配置,指定哪些工具需要中断。如 {"send_email_tool": True}
    • 权限收口(防篡改设计) :通过传入配置字典,精细限制人类介入时可以做出的决策 {"allowed_decisions": ["approve", "reject"]}
      • 💡 实战案例(发工资防篡改) :假设你有一个 pay_salary(employee, amount) 工具。当 Agent 准备给员工发 5000 元并挂起等待审批时,如果不加限制,管理员是可以通过传回 edit 决策,强行把金额改成 50000 元然后再放行的。这极易引发严重的安全和贪腐漏洞。通过配置 ["approve", "reject"] 白名单,就可以在底层彻底锁死管理员的"修改权"。一旦前端界面有人伪造 edit 请求发给后台,中间件会立刻抛出异常拦截,确保这类高危死板操作"只能批,不能改"。
    • description_prefix:中断时抛出的自定义提示前缀。

实战示例

python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
# 【关键配置:状态保存 (Checkpointer)】
# 既然程序要"中断"并等待人类,系统就必须把中断那一刻的"案发现场(所有变量、消息)"给保存下来。
# InMemorySaver 是存到内存里(仅供测试)。在生产环境中,你通常会换成持久化存储存到数据库里,这样哪怕服务器重启也能恢复。
checkpointer = InMemorySaver()

# --- 💡 持久化数据库保存(长期记忆)备查示例 ---
# 1. Sqlite 示例 (适用于轻量级/单机部署):
# from langgraph.checkpoint.sqlite import SqliteSaver
# import sqlite3
# conn = sqlite3.connect("checkpoints.sqlite", check_same_thread=False)
# checkpointer = SqliteSaver(conn)
# 
# 2. Postgres 示例 (适用于真实企业级高并发部署):
# from langgraph.checkpoint.postgres import PostgresSaver
# from psycopg_pool import ConnectionPool
# pool = ConnectionPool(conninfo="postgresql://user:pass@localhost:5432/dbname")
# checkpointer = PostgresSaver(pool)
# checkpointer.setup() # 首次运行建表
# ---------------------------------------------

# thread_id 就像是单机游戏的"存档槽位"。程序挂起时保存在槽位 "1";人类审批完后,系统要知道去槽位 "1" 读档并恢复执行。
config = {"configurable": {"thread_id": "1"}}

agent = create_agent(
    model=model,
    tools=[send_email_tool, make_phone_call_tool], # 假设有两个高危业务工具
    checkpointer=checkpointer,
    middleware=[
        # 不需要写两个中间件,只需在 interrupt_on 字典里配置多个工具即可
        HumanInTheLoopMiddleware(
            interrupt_on={
                # 精细配置:发邮件属于高危操作,只允许管理员"同意"或"拒绝",禁止私自"修改"收件人或内容
                "send_email_tool": {"allowed_decisions": ["approve", "reject"]},
                
                # 简单配置:打电话相对灵活,传 True 表示默认允许所有决策(包含修改 edit 电话号码)
                "make_phone_call_tool": True
            },
            description_prefix="【高危操作警告:请审批后再放行】" # 顺便配一个自定义的拦截前缀
        )
    ]
)

# 1. 触发任务,Agent 运行到任意一个高危工具前都会中断
response = agent.invoke({"messages": [HumanMessage("帮我给张三打电话,并发送邮件")]}, config=config)

# 2. 检查是否被挂起,并提取挂起信息(用于发给前端展示)
if response.get("__interrupt__"):
    # 取出中断的具体数据(里面包含将要执行的工具名、参数、以及我们刚才配的 description_prefix 警告语)
    interrupt_data = response["__interrupt__"][0].value
    
    # --- 💡 提取具体工具名称和参数的示例代码(备查) ---
    # interrupt_data 通常是一个字典,你可以这样精准提取信息发给前端:
    # pending_action = interrupt_data.get("action", "") # 获取警告前缀文本
    # tool_call = interrupt_data.get("tool_call", {})   # 获取完整的工具调用对象
    # tool_name = tool_call.get("name") # 拿到具体触发拦截的工具名,如 "send_email_tool"
    # tool_args = tool_call.get("args") # 拿到大模型准备的参数,如 {"to": "张三", "content": "..."}
    # ---------------------------------------------------
    
    print("任务已暂停,等待人工审批...")
    print(f"【发给前端的拦截数据】: {interrupt_data}")
    
    # 在真实的前后端分离项目中,你此时会 return 一个 JSON 给前端,比如:
    # return {"status": "paused", "alert_message": interrupt_data}
    # 此时后端的这一次 API 请求就结束了。
    
    # 3. 前端弹出弹窗,用户点击了"同意",前端发起新的请求给后端,后端带着决策恢复执行
    # 这里模拟收到前端传回的同意指令
    decisions = {"decisions": [{"type": "approve"}]}
    final_response = agent.invoke(Command(resume=decisions), config=config)

4.3 ToolRetryMiddleware (工具调用自动重试)

  • 主要作用 :当调用的工具抛出异常(如网络超时、API 限流)时,不让 Agent 直接崩溃或放弃,而是采用指数退避算法(等待时间成倍增加)进行重试,并可以加入随机抖动(Jitter)防止高并发时雪崩。
  • 核心配置项
    • max_retries:最大重试次数(如 3 次)。
    • retry_on_exceptions:一个元组,指定遇到哪些错误才触发重试。
      • 💡 实战中最常配置的错误类型有:
        1. 内置网络异常TimeoutError (请求超时), ConnectionError (连接被重置/断网)。
        2. HTTP 库异常 :比如 requests.exceptions.RequestExceptionhttpx.RequestError
        3. API 限流与服务崩盘 :通常只针对特定的 HTTP 状态码重试,比如 429 Too Many Requests (你调得太快被限流了)、502 Bad Gateway503 Service Unavailable504 Gateway Timeout
        4. (注意:千万别对 400 Bad Request 重试,因为那代表你传的参数是错的,原地重试毫无意义!)
    • backoff_factor:退避乘数(如 2.0,意味着每次等待时间翻倍)。
    • jitter:布尔值,是否加入随机抖动错开重试时间。
      • 💡 为什么要开 Jitter(防惊群/雪崩效应)? 假设某个天气 API 突然断线了 1 秒,而你的系统里恰好有 100 个并发的 Agent 正在调它。如果不加抖动,这 100 个请求会在同一毫秒 失败,然后按照公式精确等待 2 秒,最后又在同一毫秒 集体发起重试。这种瞬间集中的爆发流量(Thundering Herd)会把刚刚喘过气的 API 再次打挂。开启 jitter=True 后,会在每次等待时间上加一个随机偏差(比如有的等 2.1 秒,有的等 2.9 秒),把 100 个请求的时间错开打散,极大提升了重试的成功率。生产环境下高并发时强烈建议永远设为 True
  • 💡 核心避坑与概念辨析:Agent 开发中的"三种重试机制"
    • 在 Agent 的执行循环中,会遇到三种不同的报错,它们由完全不同的机制负责重试,千万不要搞混:
    • 1. 大模型 API 挂了(连大模型时网络不通)
      • 谁负责 :大模型初始化参数,如 ChatOpenAI(max_retries=3)
      • 解释:这属于还没走到工具调用阶段,纯粹是连不上大模型厂商的服务器。这跟中间件毫无关系。
    • 2. 外部工具 API 挂了(瞬时的物理网络故障)
      • 谁负责ToolRetryMiddleware(本节主角)。
      • 解释 :大模型正常工作并生成了完全正确的参数 ,但你的代码去调外部服务(如天气API、数据库)时网络超时或被限流了。此时中间件会带着原参数原地盲目重试,大模型对这个重试过程完全不知情。
    • 3. 逻辑/格式出错(大模型参数生成错误)
      • 谁负责 :定义 Tool 时的 handle_tool_error=True 配置。
      • 解释 :大模型把该传 int 的地方传了 string,导致程序校验报错。此时如果你用 ToolRetryMiddleware,拿着同样的错参数重试 100 次依然是错的。开启 handle_tool_error 的作用是:把报错文本扔回给大模型 ,让大模型产生认知并重新生成正确的新参数(认知纠错重试)。

实战示例

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

# 💡 技巧:如何精准针对 502/503 重试,而避开 400?
# 中间件只认"异常类(Class)",不认状态码。所以最佳实践是在 Tool 内部做转换:
"""
@tool
def flaky_network_tool():
    resp = requests.get("...")
    if resp.status_code in [502, 503, 504, 429]:
        # 遇到服务端瞬时故障,抛出 ConnectionError (在重试白名单里,会触发物理重试)
        raise ConnectionError(f"服务端崩溃: {resp.status_code}") 
    elif resp.status_code == 400:
        # 遇到大模型传错参数,抛出 ValueError (不在白名单里,直接报错并交给 handle_tool_error 纠错)
        raise ValueError("参数传错了!") 
    return resp.json()
"""

agent = create_agent(
    model=model,
    tools=[flaky_network_tool],
    middleware=[
        ToolRetryMiddleware(
            max_retries=3,
            backoff_factor=2.0,
            jitter=True, 
            # 必须传入 Python 的"异常类 (Class)"元组
            retry_on_exceptions=(TimeoutError, ConnectionError, requests.exceptions.ReadTimeout)
        )
    ]
)

4.4 TodoListMiddleware (任务规划与追踪)

  • 主要作用 :对付大模型处理复杂长链路任务时的"健忘症"(上下文注意力偏移)。
    • 梳理过程 :它会强制 Agent 在正式干活前,先调用系统内置的 write_todos 工具,把大任务拆解成 1、2、3 步待办清单。
    • 时刻提醒 :在后续的每一步执行循环中,中间件都会偷偷把这份清单的当前进度(未开始 / 进行中 / 已完成强行注入到大模型的上下文提示词里。这就好比在屏幕上贴了一张永远撕不掉的便签,大模型每次思考前都会看到:"哦,我已经做完了第一步,现在该做第二步了",从而有效防止烂尾。
  • 核心配置项
    • 通常不需要特殊传参,初始化实例 TodoListMiddleware() 传入即可。
    • 关键点 :必须配合明确的 system_prompt 使用,在 Prompt 里大吼一声"你必须先给我写计划!",以此来激活它的行为。
  • 💡 深度理解:Agent(监工)与大模型(大脑)的协作全流程
    • 第一步 (下达指令):System Prompt 告诉大模型"你必须先写计划"。如果不写这句,大模型过于自信往往会直接跳过写计划,中间件也就成了摆设。
    • 第二步 (大模型拟定计划并"上报") :大模型收到复杂任务后,开始思考计划。但它不是把计划写在普通的聊天文本里(如果是普通文本,程序很难精准提取)。它会去调用一个特定的工具(比如 write_todos),把计划作为参数传进去。------ 这个"工具调用 (Tool Call)",就是联系大模型和底层代码的"特殊标记"!
    • 第三步 (Agent截获并建档) :中间件其实一直在后台盯着大模型调用的每一个工具。一旦发现大模型调用了 write_todos 这个工具,中间件就会立刻截获这些参数,并在系统的后台数据库(State)里建一个档(真正的 Todo 列表)。
    • 第四步 (开始全程监视):从这一刻起,中间件(监工)手里就拿到了大模型亲自写的"军令状"。在接下来的每一轮对话中,中间件都会在把用户的消息发给大模型之前,偷偷在开头拼上一段当前进度(比如:"监工提醒:你的计划共3步,目前第1步已完成,第2步待办")。
    • 第五步 (画勾与推进):大模型做完一步,可能还会调一个"打勾"的工具,中间件再次截获,更新后台的状态。然后再进入下一轮监视。
    • 总结归纳
      • 计划是谁写的?------ 大模型写的(用它的脑力)。
      • 计划是谁在存、谁在管?------ 中间件 / Agent 框架存的(用代码的确定性)。
      • 怎么让大模型不忘?------ 中间件每一轮都拿这个计划去"糊"大模型的脸(强制注入 Prompt)。

实战示例

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

agent = create_agent(
    model=model,
    tools=[list_files, read_file, write_file, run_tests],
    middleware=[TodoListMiddleware()],
    system_prompt="你是一个代码修复助手。遇到复杂任务时,请先使用 write_todos 工具制定详细的排查和修复计划,然后再按计划执行。"
)

# Agent 会先生成一个包含"1.读取 2.修复 3.测试"的 TODO 列表,然后在全局 State 中逐步更新它们的状态 (pending -> in_progress -> completed)。

4.5 SummarizationMiddleware (上下文摘要)

  • 主要作用:当长对话不断累积,快要撑爆大模型的上下文窗口(Token 限制)时,它会在后台默默调用大模型,把几十条历史消息压缩成一段精简的摘要,从而极大地节省 Token 费用并避免报错。
  • 核心配置项
    • model:用于专门做摘要提炼的模型(可以选一个小号便宜的模型)。
    • trigger:触发条件。它可以接收一个列表,里面配置多个条件,它们之间是"或 (OR)"的关系 (只要满足任意一个就会立刻触发摘要)。
      • "messages":按对话消息条数触发(如 10 条)。
      • "tokens":按 Token 绝对数量触发(如 4000 个 Token)。
      • "fraction":按模型最大上下文窗口的百分比触发(如 0.8 表示达到模型允许的最大 Token 数的 80% 时触发)。
      • 💡 实战最佳实践:强烈建议主用 tokensfraction 触发 。正如你所想,如果你仅按 ("messages", 10) 触发,万一用户连发了 10 句"嗯"、"好的"这种短消息,系统也会傻傻地跑去调大模型做摘要,纯属浪费钱。只有按 Token/百分比 触发,才是真正的"防爆上限"。
    • keep:做完摘要后,要在尾部保留多少最近的原始对话(防止大模型做完摘要后,丢失眼前的语境)。
      • 可以不传吗? 可以。如果不传,默认会把几乎所有的历史消息全部拿去做摘要。但这极易导致大模型"接不上话"(比如你刚说"把上一句翻译一下",它因为只看到浓缩摘要,已经忘了"上一句"的原文了)。
      • 💡 实战最佳实践:triggerkeep 必须"单位统一"
        • 如果你按 ("tokens", 4000) 触发,那么保留时也务必按 keep=("tokens", 1000) 来保留。
        • 千万不要混着用(比如触发用 tokens,保留用 messages),这会导致截断逻辑极其不可控,容易引发难以排查的 Bug。
        • 生产环境终极公式 :触发和保留全部统一按 tokens 计算
    • summary_prompt:自定义发给大模型做摘要时的系统提示词。
      • 主要作用:强制规定大模型写摘要的格式和侧重点,确保不遗漏关键业务上下文。
      • 💡 中文开发必填项 :如果不传这个参数,框架底层默认会发一段英文的系统提示词(如 Summarize the conversation )。这极易导致大模型把你几千字的纯中文对话硬生生总结成一段英文,甚至导致下一轮聊天时大模型"神经错乱"用英文回复用户。因此,只要你开发的是中文 Agent,这个参数必须填上中文提示词! (例如:"请务必用中文提炼以下对话的核心要点:\n{messages}")。

实战示例

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

agent = create_agent(
    model=model, # 主模型
    tools=[],
    middleware=[
        SummarizationMiddleware(
            model="gpt-4o-mini", # 用便宜模型做摘要
            
            # --- 💡 实战推荐写法(单位统一,最稳妥防爆) ---
            trigger=[("tokens", 4000)],
            keep=("tokens", 1000),
            summary_prompt="请用精简的中文总结以下历史对话的核心业务信息:\n{messages}" 
            
            # --- 📚 课程演示写法(多重防线 OR 逻辑) ---
            # trigger=[
            #     ("tokens", 100),     # 任意满足其中一个条件,就会立刻触发摘要
            #     ("messages", 6),
            #     ("fraction", 0.001)
            # ],
            # keep=("messages", 2)     # (注:实战中强烈不建议 trigger 和 keep 混用不同单位)
        )
    ]
)

4.6 ModelCallLimitMiddleware (模型调用限流)

  • 主要作用 :防止 Agent 陷入死循环(比如"工具报错 -> 大模型道歉并瞎猜参数重试 -> 继续报错"的无限循环),强行限制单次 invoke 调用中大模型能被触发的最大次数。
  • 核心配置项
    • run_limit:单次 invoke 允许的最大调用次数。
      • 💡 常见误区:它能限制特定工具的调用次数吗(比如 A 工具最多调 3 次,B 工具 5 次)?
      • 不能!它是一个全局设置 。一旦设置之后,所有的工具都必须在这个全局总额度的约束下执行。它限制的是**"大模型大脑被激活的整体总次数"**,不管大模型是在调 A 工具、B 工具,还是纯聊天,只要总循环次数达到阈值,就会被无情掐断。
      • 如果你想实现"针对特定工具的个性化限流(如 A 工具只准调 3 次)",请看下一节专门的解决手段:4.7 节 ToolCallLimitMiddleware
    • exit_behavior:到达思考上限后的退出策略。主要有以下两种行为模式:
      • "error"(硬性崩溃):一旦超限,程序直接抛出异常(通常是类似 AgentIterationLimitError 等)。这要求你在外层调用时必须写 try...except 来捕获它,否则整个后端服务(如 FastAPI)会直接 500 报错崩溃。适合对结果正确性要求极高、宁愿报错也不给半成品的场景。
      • "end"(平滑截断):一旦超限,不报错,而是强行给 Agent 踩一脚急刹车。它会把大模型最后一次产生的输出(哪怕是个半成品,或者工具报错信息),直接当做"最终答案"返回给用户。这种方式能保证系统不崩溃,但用户可能会看到一句没头没尾的话。适合做 ToC 的对话产品,保证"总有回复"。

实战示例

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

agent = create_agent(
    model=model,
    tools=[buggy_tool],
    middleware=[
        ModelCallLimitMiddleware(
            run_limit=5, # 思考超过 5 次,坚决掐断
            exit_behavior="error"
        )
    ]
)

4.7 ToolCallLimitMiddleware (特定工具调用限流)

  • 主要作用 :专门针对某一个具体的工具调用次数进行精细化限制。完美解决在复杂业务中"某些极度消耗资源的工具(如大数据库查询),每次对话只能允许调 3 次"的需求。
  • 核心配置项
    • tool_name:你要限制的具体工具名称。如果不传,则限制所有的工具总计调用次数。
    • run_limit:单次 invoke 运行中,该工具被允许调用的最大次数(与 4.6 节一样,下次 invoke 时计数器会清零重新计算)。
    • exit_behavior:超额后的策略。除了常规的 "error""end",它多了一个极度聪明的专有策略:"continue"
      • 💡 实战强烈推荐 "continue" :当大模型第 4 次尝试调用已被限制 3 次的 A 工具时,系统不会崩溃,也不会直接结束,而是伪造一条包含错误信息的虚拟回复(告诉大模型:"该工具调用额度已耗尽")。大模型收到这个假报错后,会放弃死磕这个工具,转而通过聊天或其他工具继续推进任务。

实战示例

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

agent = create_agent(
    model=model,
    tools=[search_tool, calculator_tool],
    middleware=[
        # 针对特定工具做个性化限制
        ToolCallLimitMiddleware(
            tool_name="search_tool", # 💡 重点:只死死盯住 search_tool,不管 calculator_tool
            run_limit=3,             # 限制单次对话最多只能查 3 次
            exit_behavior="continue" # 💡 重点:查满了就骗大模型说"额度耗尽",让它靠自己回答
        )
    ]
)

4.8 ModelFallbackMiddleware (模型故障转移)

  • 主要作用:当主模型(因网络、限流或宕机等原因)无法访问时,自动无缝切换到备用模型,保障系统的高可用性。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import ModelFallbackMiddleware
from langchain.chat_models import init_chat_model

# 1. 定义主模型
primary_model = init_chat_model("openai:gpt-5.4-mini")

# 2. 定义包含备用模型的中间件
fallback = ModelFallbackMiddleware(
    fallback_models=[
        init_chat_model("openai:gpt-4o-mini"),
        init_chat_model("anthropic:claude-3-haiku")
    ]
)

# 3. 挂载中间件
agent = create_agent(
    model=primary_model,
    tools=[],
    middleware=[fallback],
)

4.9 LLMToolSelectorMiddleware (智能工具筛选)

  • 主要作用 :当系统接入的工具过多(比如100个)时,如果每次把所有工具都传给大模型,不仅极大地消耗 Token,还容易导致大模型眼花缭乱选错工具。该中间件会使用一个便宜的子模型,根据用户的提问,先从所有工具中初筛出最相关的几个工具,然后再交给主模型处理。
  • 核心配置项
    • model:用于专门做工具初筛的子模型。
    • max_tools:限定最终能被选出并交给主模型的工具总数。
    • always_include:白名单,指定的工具无论如何都会被包含,不占用 max_tools 额度。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolSelectorMiddleware

tool_selector = LLMToolSelectorMiddleware(
    model=model_in, # 用于初筛的子模型
    max_tools=5,    # 核心限制:最多只给主模型看 5 个工具
    always_include=["get_weather"] # get_weather 工具必须始终可用
)

agent = create_agent(
    model="deepseek-v4-flash", # 干重活的主模型
    tools=[...100个工具...], # 原始的庞大工具库
    middleware=[tool_selector]
)

4.10 ModelRetryMiddleware (模型调用自动重试)

  • 主要作用 :当大模型自身调用失败时(如 API 限流、网络波动),进行指数退避重试。它的策略算法与 ToolRetryMiddleware 基本一致,但专门针对模型调用环节。
  • 核心配置项
    • max_retries / backoff_factor / initial_delay / max_delay:控制重试次数与等待时间。
    • jitter:布尔值,是否加入随机抖动,强烈建议在高并发生产环境中开启(True),防止所有失败请求在同一刻同时重试引发"惊群效应/雪崩"。
    • on_failure:当达到最大重试次数依然失败时的退出策略:
      • "error":直接抛出异常(通常适用)。
      • "continue":将错误信息包装后塞回对话历史,让大模型知道失败了并继续决策。
  • 💡 核心辨析:它与 ToolRetryMiddleware 的区别
    • 失败的对象不同ToolRetryMiddleware 重试的是外部工具 API (比如调用的天气 API 挂了,但大模型还在正常思考);ModelRetryMiddleware 重试的是大模型本身的 API(比如连不上 OpenAI 或被限流了,大模型都没法思考了)。
    • 所处的阶段不同:模型重试发生在 Agent 试图让大脑(LLM)运转出结果的时刻;工具重试发生在 Agent 大脑已经成功运转完毕,决定去调用外部手脚(Tools)的时刻。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRetryMiddleware

agent = create_agent(
    model="deepseek-cat",
    middleware=[
        ModelRetryMiddleware(
            max_retries=6,        # 最大重试 6 次
            backoff_factor=2.0,   # 每次等待时间翻倍
            initial_delay=1.0,    # 首次等待 1 秒
            max_delay=10.0,       # 最大等待上限 10 秒
            on_failure="continue", 
            jitter=True,          # 开启抖动,削峰填谷
        ),
    ],
)

4.11 LLMToolEmulator (模拟工具执行)

  • 主要作用 :用于开发调试与测试辅助 。它相当于前端开发中的 Mock 数据服务器。当业务流程还在开发阶段,真实的外部工具接口(如查数据库、调用第三方收费 API)尚未写好或不方便调用时,我们可以用这个中间件让另一个大模型来"假装"执行工具并生成逼真的假数据,从而低成本跑通整个 Agent 测试链路。
  • 💡 核心进阶:硬编码 Mock vs LLM 模拟器 Mock
    • 硬编码 Mock :在代码里写死 return {"temp": 25}。适合逻辑简单、参数固定的轻量级工具(如发邮件,永远返回"发送成功"即可)。
    • LLM 模拟器 Mock :解决硬编码无法应对的真实高级场景:
      1. 动态入参响应:查北京返回北京数据,查伦敦返回伦敦数据,保证 Agent 多轮推理逻辑不割裂。
      2. 重型非结构化入参:如大模型传了一个 SQL 查询数据库,硬编码无法解析 SQL,但陪练模型能看懂并精准脑补出几行合理的假表格数据。
      3. 零代码测试极端场景:只需给陪练模型加一句系统提示词"请假装返回破坏性的雷暴台风天气",无需改代码即可测通 Agent 的灾难兜底和取消行程的逻辑。
      4. 海量工具一键 Mock:如果有 100 个工具,无需手写 100 个假函数,只需写好标准注释,LLM 模拟器即可自动完成所有造假。
  • 💡 底层执行流程解密
    1. 拦截 :主模型(大脑)正常思考,决定调用某个工具(比如 get_weather(city="北京")),中间件会瞬间拦截这个请求,不让它去执行真实的工具代码。
    2. 后台发 Prompt :中间件在后台悄悄拼接一段提示词发给陪练的 mock_model。这段提示词大致是:"你现在是一个工具模拟器。Agent 正在试图调用名为 get_weather 的工具,参数是 {"city": "北京"}。这个工具的介绍是:'查询指定城市的天气'。请你假装你是这个工具,为我生成一份合理的返回结果。"
    3. 模型幻觉造假mock_model 收到提示词后,不会去发真实的网络请求,而是直接利用自己的推理能力"脑补"出一份假数据。
    4. 瞒天过海:中间件拿到这份假数据,原路返回给主模型,主模型毫无察觉,以为真实调用成功了,继续执行后续逻辑。
  • 如何控制 Mock 的结构化输出 (JSON Schema)? : 如果你的后续代码要求工具必须返回特定的 JSON 格式,你只需要把格式要求写在工具的注释(Docstring)里 !中间件会把你的注释原封不动地打包进后台 Prompt 中发给 mock_model。只要注释写得清晰,mock_model 就会乖乖按照你要求的 JSON Schema 生成假数据。
  • 完整实战示例(Mock 查天气)
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolEmulator
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.messages import HumanMessage

# 1. 定义一个"空壳"工具(假装后端同事还没开发完天气 API)
@tool
def get_weather(city: str) -> str:
    """
    查询指定城市的天气。
    
    【重要:返回值格式要求】
    请务必返回一个严格的 JSON 字符串,包含以下字段:
    - "city": 城市名
    - "temperature": 气温(数字)
    - "condition": 天气状况(如:晴、雨)
    - "wind": 风力(如:微风)
    
    示例:{"city": "北京", "temperature": 25, "condition": "晴", "wind": "微风"}
    """
    # 里面啥真实的逻辑也没有,如果你真的去执行它,它会直接报错崩溃
    raise NotImplementedError("后端接口还没写完呢!")

# 2. 准备两个大模型
main_model = init_chat_model("deepseek-chat") # 测试的主模型("大老板")
mock_model = init_chat_model("gpt-4o-mini")   # 充当 Mock Server 的陪练模型

# 3. 组装 Agent,挂上模拟器
agent = create_agent(
    model=main_model,
    tools=[get_weather], # 把这个半成品的空壳工具放进去
    middleware=[
        # 挂上模拟器,把 mock_model 派过去当陪练
        LLMToolEmulator(model=mock_model)
    ]
)

# 4. 运行测试
response = agent.invoke({
    "messages": [HumanMessage("今天北京天气怎么样?")]
})
# 主模型拿到 mock_model 编造的 JSON 假数据后,会根据假数据完美回答用户。
print(response["messages"][-1].content)

4.12 ContextEditingMiddleware (上下文裁剪编辑)

  • 主要作用 :在多轮对话中,工具调用的结果(如网页爬取的长文本、代码执行输出)会迅速撑爆上下文。该中间件像"上下文抽脂手术",在不影响纯文字聊天的前提下,自动在后台物理删除过于冗长的工具调用废料,极大节省 Token 成本。
  • 💡 核心原理辨析:为什么删掉信息,Agent 却不会变笨?
    • 精准打击目标 :它默认只针对 ToolMessage(外部工具返回的原始数据)进行清理,绝不会误删你和 AI 之间聊天的 HumanMessage 或常规 AIMessage
    • "中间产物"理论 :大模型调用工具拿到几万字的原始数据后,通常会在接下来的回合中把有用的精华提取出来 汇报给你。一旦精华被提取,那几万字的长篇大论就变成了毫无价值的"中间产物(废料)"ContextEditingMiddleware 就是在提取完成后,果断把这些"中间产物"扔进垃圾桶。丢失的只是废料,真正的核心信息依然安全地保存在正常的聊天记录里!
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import ContextEditingMiddleware, ClearToolUsesEdit

agent = create_agent(
    model="deepseek-chat",
    tools=[get_weather],
    middleware=[
        ContextEditingMiddleware(
            edits=[
                # 举例:配置某些规则来清理早期的工具调用记录
                ClearToolUsesEdit(
                    trigger=50,
                    keep=0,
                ),
            ],
        ),
    ]
)
  • 💡 黄金组合:与 SummarizationMiddleware 双管齐下 : 在真实的工业级 Agent 里,这两个中间件通常是一起用的,各司其职:
    1. 先用 ContextEditingMiddleware(上下文裁剪编辑):Agent 查完数据库、读完长文件,得到结论后,那些好几万字的原始文件结果就没用了。赶紧用它把这些"工具废话"物理删除,极大地省出空间。
    2. 再用 SummarizationMiddleware(上下文摘要):即便删了工具附件,如果用户跟 Agent 连续聊了三天三夜,纯文字聊天记录也会撑爆。这时候再触发摘要机制,把前两天的聊天记录浓缩成一段总结。 这两把"手术刀"双管齐下,你的 Agent 就能跟用户永远聊下去,永远不会报 Token 超限的错误了。

4.13 FilesystemFileSearchMiddleware (本地文件搜索)

  • 主要作用 :给 Agent 直接装上本地机器的文件搜索能力。挂载后会自动为 Agent 添加基于底层操作系统的两大底层搜索工具:
    • Glob (Global) :通过通配符匹配文件名和路径 (找书的外壳)。常用:*.txt (找当前目录下所有 txt);**/*.py (递归找所有子目录下的 py 文件)。
    • Grep (Global Regular Expression Print) :通过正则精准搜索文件内部的具体文本 (找书里的句子)。常用:grep -i "error" app.log (忽略大小写找 error);grep -r "TODO" ./src (遍历目录搜所有 TODO)。
  • 核心配置项
    • root_path:允许 Agent 搜索的根目录(沙箱目录)。
    • allowed_extensions:限制可搜索的文件后缀(防乱读)。
    • use_ripgrep:开启以获得极致搜索性能(需系统安装 ripgrep)。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import FilesystemFileSearchMiddleware

agent = create_agent(
    model=model,
    tools=[], # 这里留空,中间件会自动帮你把 Glob 和 Grep 工具加进去
    middleware=[
        FilesystemFileSearchMiddleware(
            root_path="../todo_workspace", 
            use_ripgrep=True,
            max_file_size_mb=10 # 防止读取超大型文件 OOM
        ),
    ],
)
  • 💡 核心进阶:底层设计与实战工作流剖析
    1. 为什么要设计成中间件,而不是普通工具?
      • 全局安全沙箱 :中间件能在底层强行锁定 root_path。只要大模型敢越界搜索,中间件直接拦截,保证系统绝对安全。
      • 一键批量注入:无需手动实例化,挂载一个中间件就能在后台自动把配套的搜索武器一次性全打包塞给大模型。
    2. 有了 Grep 找内容,为什么还需要"读文件(Read File)"工具?
      • Grep 拿到的是只有一两行的"代码碎片",而大模型修 Bug 需要"全局上下文"。
      • 王牌工作流 :Agent 用 Glob 找文件位置 -> 用 Grep 搜出报错词在哪一行 -> 最后调用 读文件 把这块代码拉过来,结合上下文才能真正看懂并修复 Bug。
    3. use_ripgrep=True 到底强在哪?
      • 极度残暴的速度:底层是 Rust,多线程狂飙,秒杀传统 grep。
      • 聪明懂事 :默认自动读取项目里的 .gitignore,并自动过滤隐藏和二进制文件,结果极度纯净。
    4. max_file_size_mb 的防崩溃机制
      • 一般设置 10MB~20MB 足矣。防止巨型日志或表数据瞬间撑爆大模型的 Token 上限(导致大模型 OOM)。中间件会在打开文件前,调用操作系统底层接口查大小,超限则直接拒读。

4.14 ShellMiddleware (持久化终端执行)

  • 主要作用:给 Agent 提供一个真实的、持久化的操作系统 Shell 环境(如 Bash/Zsh),让大模型能像真正的程序员一样在机器上"敲命令行"。适用于运维 Agent、自动化脚本 Agent。
  • 💡 核心原理:为什么要做成中间件,而不做成普通 Tool?
    1. 状态保持与生命周期管理(最核心原因) :普通的工具(如 ShellTool)是"用完即毁"的,如果大模型第一步执行 cd /app,第二步执行 ls,由于两次开了不同的进程,cd 操作完全无效。而做成中间件后,它会在 Agent 启动时在后台唤醒一个长生不死的终端进程,后续的所有命令全在这个存活的进程中执行,完美保留了上下文状态(如路径跳转、环境变量配置)。
    2. 底层安全拦截 :中间件可以在命令送给系统执行前进行危险指令过滤,防止执行 rm -rf / 等自毁操作。
  • 💡 它是怎么做到"隐身执行"不弹黑窗口的? AI 编程工具不会去调起系统带 UI 的终端软件(如 Mac 的 Terminal App),而是通过底层代码(如子进程 Subprocess)直接唤醒一个纯净的无头进程 (Headless Process) 。它利用操作系统的数据管道 (Pipes) ,将大模型的指令顺着输入管 (stdin) 塞进去,然后顺着输出管 (stdout) 抽取结果。整个过程在内存后台完成,毫无弹窗痕迹。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import ShellMiddleware

agent = create_agent(
    model="deepseek-coder",
    tools=[], 
    middleware=[
        # 挂载后,大模型即可连续不断地执行带有上下文状态的 shell 命令
        ShellMiddleware(
            shell="zsh",       # 指定底层使用的 shell 程序
            timeout_seconds=30 # 防止某些交互式命令卡死整个 Agent
        )
    ]
)

4.15 FilesystemMiddleware (文件系统读写管理)

  • 主要作用 :它是文件系统的**"资源管理器 + 文本编辑器"**。挂载后会为 Agent 注入四大王牌工具:查看目录 (list_dir)读文件 (read_file)写新文件 (write_file)修改文件 (edit_file)。直接赋予 Agent 真正长臂管辖的读写改能力。
  • 💡 核心辨析:它与 FilesystemFileSearchMiddleware 的区别
    • FilesystemFileSearchMiddleware (4.13) :是"搜索引擎 "。只包含 GlobGrep,能力是只读且只能搜,用来找文件位置、找报错在哪一行。
    • FilesystemMiddleware (4.15) :是"编辑器 "。能力是读、写、改,用来拉取完整代码并打补丁修复。
  • 💡 为什么框架要把它们拆成两个独立的中间件?
    1. 权限解耦与极致安全:如果你只想做一个"代码阅读/答疑助手",你只需要挂载 Search 中间件(甚至配合只读权限),绝对禁止 AI 拥有写文件的权限。如果这两个功能耦合在一个中间件里,就很难做到权限的精细化控制,容易产生越权危险。
    2. 黄金搭档工作流 :在全自动 AI IDE 中,它们分工明确。Agent 先用 Search 中间件 广撒网定位 Bug 位置,再调 Filesystem 中间件 读取完整代码并最终写入修复补丁。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import FilesystemMiddleware

agent = create_agent(
    model="deepseek-coder",
    tools=[], 
    middleware=[
        # 挂载后,大模型即可读写文件
        FilesystemMiddleware(
            root_path="../todo_workspace", # 严格限制可读写改的安全沙箱根目录,防止越权篡改系统文件
        )
    ]
)

4.16 SubagentMiddleware (子代理协同)

  • 主要作用:用于实现**"多智能体协同 (Multi-Agent)"**。它给主 Agent(包工头)注入了一个特殊能力:当遇到复杂任务时,它可以动态地创建和调用其他专业的"子 Agent(打工人)"来帮忙,最后把结果汇总。
  • 💡 核心工作流:包工头与打工人的配合 : 假设用户下达终极任务:"先写一段 Python 爬虫查股票,然后根据数据写一篇金融分析文章"。
    1. 主 Agent (包工头) 收到任务,发现涉及写代码和写文章,自己一个人干容易出错。
    2. 它决定"分包":通过中间件唤醒一个专业的 coder_agent (程序员) 去专门写爬虫代码跑数据。
    3. 拿到数据后,它再唤醒一个专业的 writer_agent (作家) 去专门写分析文章。
    4. 最后,包工头把作家写好的文章统一交付给用户。
  • 实战示例
python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import SubagentMiddleware

# 1. 提前在"人才库"里注册好各种"专家"子 Agent 的配置
agents_registry = {
    "coder": {"model": "deepseek-coder", "system_prompt": "你是一个资深程序员。"},
    "writer": {"model": "deepseek-chat", "system_prompt": "你是一个金融分析作家。"}
}

# 2. 创建主 Agent(包工头),并挂载子代理解析中间件
manager_agent = create_agent(
    model="gpt-4o", # 包工头通常用最聪明的模型来做统筹调度
    system_prompt="你是一个项目经理,请把复杂任务拆解,并调用合适的子 Agent 来完成。",
    tools=[], 
    middleware=[
        # 挂载后,包工头就获得了一个名为 `invoke_subagent` 的隐藏工具
        SubagentMiddleware(agents=agents_registry)
    ]
)

5. 自定义中间件 (Node-style hooks)

当内置中间件不满足需求时,可以通过钩子(Hooks)自己编写中间件。这是干预 Agent 底层运行状态机(LangGraph)的最强武器。

5.1 四大生命周期钩子

在 Agent 的运行循环中,留有四个卡口:

  1. before_agent:整个 Agent 刚启动,一次请求只走一次(大门)。
  2. before_model:即将请求大模型(LLM)前(车间门)。
  3. after_model:大模型刚生成完回复后。
  4. after_agent:整个 Agent 彻底运行结束,一次请求只走一次(大门出门)。

💡 重要设计原则:系统先行,Hook 在后 无论挂载在哪个卡口,底层逻辑永远是:系统官方的内置逻辑先执行 (如捞取数据库记忆、拼装系统 Prompt 等),等系统把数据全部准备好并塞进 state["messages"] 后,才会唤醒你自定义的 Hook。 这意味着你的 Hook 拿到的永远是完整的、最新的数据。同理,因为官方的 before_agent 初始化工作早已做完,所以即便你在自定义的 before_agent 里强行跳去了 tools,工具干完活后也绝对不会再回头,而是理所应当地前往下一站 before_model

5.2 终极杀器:控制流劫持 (can_jump_to) 与循环流转

中间件不仅能打日志,还能强行修改底层 Graph 的走向(类似于操作系统底层的**"中断 Interrupt"**机制)。

  • 如何申请权限 :必须在装饰器上提前画好路线,如声明 can_jump_to=["tools", "model", "end"]
  • 如何踩下油门 :在函数里返回带有指令的字典,如 {"jump_to": "目标节点"}
  • 💡 底层流转逻辑(核心重点!)
    • 跳去工具 jump_to="tools"(硬规则抢方向盘) :系统强行中断大模型的思考,直接跳去执行你伪造好的工具指令。注意:工具执行完毕后,并非从头开始,而是会折返回 before_model -> model,让大模型看到工具的结果并进行最终的人言总结。这是一个闭环。
    • 跳回模型 jump_to="model"(回炉重造) :系统会把大模型打回起点,强制它带着你新加的严厉指令,重新从 before_model 再次出发执行一次,常用于"反思重试 (Retry)"。
    • 跳到末尾 jump_to="end"(拉闸断电):直接熔断整个 Agent 流程,立刻报错结束。

5.3 基础实战:两种经典写法对比

写法一:散装函数版(适用于轻量级的单次拦截)

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

# 1. 声明权限:允许跳到 tools
@before_model(can_jump_to=["tools"])
def force_tool_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    text = state["messages"][-1].content
    if "direct tool" in text:
        # 2. 伪造圣旨(假装是大模型下达的工具调用)
        fake_tool_call = AIMessage(
            content="",
            tool_calls=[{"name": "get_news", "args": {}, "id": "call_123"}]
        )
        # 3. 踩下油门:塞入假圣旨,强行跳转到 tools 节点
        return {
            "messages": [fake_tool_call],
            "jump_to": "tools"
        }
    return None # 不满足拦截条件则返回 None,正常放行

写法二:面向对象类包装版(适用于复杂逻辑、跨卡口共享状态) 优势:可以通过 self.xxx 变量,在 beforeafter 甚至不同轮次之间轻松共享状态数据。

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

class MyComplexMiddleware(AgentMiddleware):
    
    # 注意:在类的方法中,申请权限的装饰器换成了 @hook_config
    @hook_config(can_jump_to=["tools", "end"])
    def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        # 你可以在这里写 self.temp_data = 100 供下面的 after_model 使用
        if "overflow" in state["messages"][-1].content:
            return {"messages": [AIMessage("Token溢出!")], "jump_to": "end"}
        return None

    @hook_config(can_jump_to=["model"])
    def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        # 强行让大模型重写的回炉重造逻辑写在这里...
        return None

# 挂载方式:直接实例化这个类即可
agent = create_agent(model=model, tools=[...], middleware=[MyComplexMiddleware()])

6. 自定义中间件 (Wrap-style hooks)

Node-style 与 Wrap-style 的本质区别:

  • Node-style(宏观控制流) :也就是上一章的 before/after。它就像十字路口的交警,只能决定系统下一步跳向哪个节点(如 jump_to="tools")。
  • Wrap-style(微观执行拦截) :就像 iOS 开发里的 NSURLProtocol 拦截网络请求一样,它直接把即将执行的具体操作(如调用大模型、调用工具)给"挟持"了。在真正执行前,你可以随意魔改参数;拿到结果后,还能继续加工,甚至可以根据结果决定要不要再重新调一次。

核心武器:handler 执行开关 在 Wrap-style 中间件中,系统会把真正去执行底层任务的函数封装成一个 handler 递给你。你不执行 handler(request),大模型(或工具)就永远不会被真正调用。

6.1 wrap_model_call(包裹大模型调用)

接管向大模型发起网络请求的瞬间。可以在真正发包前后做拦截加工。

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

@wrap_model_call
def wrap_model_call_middleware(
    request: ModelRequest, 
    handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse | None:
    # 1. 拦截请求(类似 NSURLProtocol),在真正请求大模型前注入数据
    request.messages[-1].content += "---> 请求前的私货 <---"
    
    # 2. 你亲自扣下扳机,系统才会真正向底层 LLM 发起网络调用
    response = handler(request)
    
    # 3. 拿到大模型回复后,继续拦截加工
    response.result[0].content += "---> 收到结果后的私货 <---"
    
    return response

# 挂载方式:
# agent = create_agent(model=model, tools=[], middleware=[wrap_model_call_middleware])

6.2 wrap_tool_call(包裹工具调用)

接管某次工具的执行。最经典的用法是:拦截并篡改传给工具的参数,或者捕获异常并强迫工具使用新参数重试。

python 复制代码
from langchain_core.tools import tool
from langchain.agents.middleware import wrap_tool_call
from langgraph.prebuilt.tool_node import ToolCallRequest
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from typing import Callable, Any

# 假设我们有一个查天气的底层工具
@tool
def get_weather(city: str, is_forcast: bool) -> str:
    """获取城市天气"""
    res = f"{city}今天天气不错"
    if is_forcast:
        res += "\n明天天气也很好"
    return res

@wrap_tool_call
def wrap_tool_call_middleware(
    request: ToolCallRequest, 
    handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]]
) -> ToolMessage | Command[Any]:
    
    # 1. 第一次调用:用 Agent 给的原始参数尝试执行
    result = handler(request)
    print(f"原始参数:{request.tool_call['args']}")
    print(f"原始参数调用结果:{result.content}")
    
    # 2. 核心操作:偷偷篡改传给底层工具的参数(强行加上查明天天气)
    request.tool_call["args"]["is_forcast"] = True
    
    # 3. 第二次调用:用篡改后的全新参数,重新去执行底层工具!
    result = handler(request)
    print(f"更新以后的参数:{request.tool_call['args']}")
    print(f"更新以后的参数调用结果:{result.content}")
    
    # 4. 把最终结果返给 Agent
    return result

# 挂载方式:
# agent = create_agent(model=model, tools=[get_weather], middleware=[wrap_tool_call_middleware])

6.3 进阶实战:基于类的综合拦截(面向对象写法)

在实际的企业级开发中,我们更推荐使用类(Class)来统筹管理这两种极其强大的中间件。这样不仅代码更清晰,还能在多次拦截、模型与工具之间共享状态变量(比如统计总调用次数、统一日志 ID 等)。

下面的例子展示了如何在一个类中,同时劫持大模型调用和工具调用

python 复制代码
from langchain.agents.middleware import AgentMiddleware
from langchain.agents.middleware import ModelRequest, ModelResponse
from langgraph.prebuilt.tool_node import ToolCallRequest
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from typing import Callable, Any

class MyUltimateWrapperMiddleware(AgentMiddleware):
    
    def __init__(self):
        # 优势:可以在这里定义跨卡口共享的变量
        self.total_model_calls = 0
        self.total_tool_calls = 0

    # 1. 接管所有发向大模型的请求
    def wrap_model_call(
        self, 
        request: ModelRequest, 
        handler: Callable[[ModelRequest], ModelResponse]
    ) -> ModelResponse | None:
        
        self.total_model_calls += 1
        print(f"【APM监控】拦截到第 {self.total_model_calls} 次大模型网络请求,准备发包...")
        
        # 踩下油门:真正调用大模型 API
        response = handler(request)
        
        print("【APM监控】大模型包已返回。")
        return response

    # 2. 接管所有底层工具的执行
    def wrap_tool_call(
        self, 
        request: ToolCallRequest, 
        handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]]
    ) -> ToolMessage | Command[Any]:
        
        self.total_tool_calls += 1
        tool_name = request.tool_call['name']
        print(f"【安全拦截】即将执行第 {self.total_tool_calls} 个工具: {tool_name}")
        
        # 可以在这里做极其复杂的微操:比如权限校验、参数篡改、失败重试循环等
        
        # 踩下油门:真正执行该工具
        result = handler(request)
        
        print(f"【安全拦截】工具 {tool_name} 执行完毕,拦截结束。")
        return result

# 挂载方式:实例化这个强大的监控拦截类
# agent = create_agent(model=model, tools=[get_weather], middleware=[MyUltimateWrapperMiddleware()])

7. 核心避坑:中间件的执行顺序 (洋葱模型)

结论:中间件的列表顺序非常有讲究,绝不能随便乱写!

LangChain 的中间件执行机制采用的是经典的**"洋葱模型 (Onion Model)"**。 您可以把真正的底层大模型(LLM)想象成洋葱的最核心,而各个中间件就是包裹在外面的一层层的洋葱皮:

  • 请求进场时(穿透洋葱皮向内) :必须从最外层,一层一层剥开走到核心。因此,像 before_model 以及 wrap_model_call 真正发包前的代码,会严格按照 middleware 列表从上到下的顺序执行。
  • 响应出场时(带着结果向外走) :大模型计算完毕,必须从最核心一层一层包回去。因此,像 after_model 以及 wrap_model_call 收到结果后的代码,会严格按照 middleware 列表**从下到上(完全逆序)**执行。

7.1 暴力测试验证(硬核实验)

如果我们在代码里写 3 个 before、3 个 wrap、3 个 after 钩子,然后随便打乱顺序塞进 middleware 列表,执行过程如下:

  1. 进场阶段:所有 Hook 抢着拦截,完全遵循"排在列表前面的人,最先拿到原始请求数据"。
  2. 中心执行 :执行洋葱最核心的底层调用(handler(request))。
  3. 出场阶段:所有 Hook 抢着加工结果,完全反转,"排在列表最后的人,最先拿到大模型的返回结果;排在列表最前面的人,由于在最外层,最后一个才拿到被别人层层加工过的结果"。

⚠️ 极其关键的结论: 中间件的执行顺序,完全是由你在这个 middleware=[...] 数组里书写的顺序决定的!跟你在 Python 文件里谁先定义、谁后定义的顺序半毛钱关系都没有!

7.2 终极完全体:系统内置 + 自定义 Hook 的黄金排序法则

如果您在一个极其复杂的企业级 Agent 里,把官方内置的中间件和自己手写的各种自定义 Hook 全都用上了,那么强烈建议按照以下**"五层洋葱"**的顺序从上到下排列:

  1. 第一层:全局监控层(必须最外层)
    • TracingMiddleware(内置):用来监控全局链路。
    • 自定义的全局 wrap_model_call:如果你写了计算网络耗时的全局拦截器,放这里,这样能捕获内部所有异常。
  2. 第二层:安全防护层(越早拦截越好)
    • ModelCallLimit / ToolCallLimit(内置防爆墙):防死循环。
    • 自定义的 before_model / before_agent(安全校验):比如"检查用户账户余额是否足够"、"敏感词拦截",一旦不满足直接抛异常或 jump_to="end",省得往下流转浪费资源。
  3. 第三层:状态与任务准备层
    • TodoListMiddleware(内置):解析当前的任务执行到哪一步了。
    • ContextEditingMiddleware(内置裁剪):把明显没用的废话从上下文中物理切除。
  4. 第四层:Token 瘦身层(必须贴近大模型)
    • SummarizationMiddleware(内置摘要):在发给大模型之前,做最后一次上下文浓缩,确保大模型不会 Token 溢出。
  5. 第五层:深层微操与工具重试层(最内层)
    • 自定义的 wrap_tool_call:如果你需要拦截某个具体工具并魔改参数,放这里(此时上下文已经被清理干净了)。
    • 自定义的 after_model:如果需要对大模型刚刚吐出的新鲜(未经任何加工的)数据做格式化,放这里。
    • ToolRetryMiddleware(内置重试):最最内层,作为最后的保底,只负责当底层网络报错时,默默重试几遍。

错误示范 : 如果你把 SummarizationMiddleware(压缩)放在了第一位,把 TracingMiddleware(日志)放在了最后一位,那么你的日志系统里将永远看不到被压缩前的原始对话长什么样,导致日后根本无法排查 Bug。


8. 全篇总结

LangChain 的中间件(Middleware)机制,是整个框架最具工业化价值的设计。

  • 内置中间件:开箱即用,帮你防爆、防超载、做安全合规、做持久化上下文。
  • Node-style Hook:像交警一样,让你在宏观上任意揉捏大模型的执行路线。
  • Wrap-style Hook:像贴身保镖一样,让你在微观上对任何一次 API 调用进行精准的参数魔改和重试拦截。

吃透了中间件和洋葱模型,你就正式告别了"写玩具脚本"的阶段,真正掌握了亲手打造高可用、企业级 AI Agent 的核心钥匙!

相关推荐
To_OC4 小时前
别再被跑分骗了:大模型 Benchmark 到底在考什么?
人工智能·llm·agent
JaydenAI5 小时前
[AG-UI详解-06]AG-UI针对MAF的服务端实现
ai·agent·ag-ui·maf
JouYY7 小时前
大模型底层学习(二)- 预训练(Pre-training)流程概览
架构·llm·agent
weixin_666593997 小时前
自然资源时空智能体建设方案:从数字化到智能化的演进路径
大数据·人工智能·大模型·云计算·agent·云平台·空间数据库
wWYy.7 小时前
什么是RAG?
人工智能·agent
深念Y8 小时前
CC Switch 显示错误模型名的排查与解决
网关·开源·agent·项目·代理
武子康8 小时前
同一套 ComfyUI 工作流第二次为什么快一倍:4 类消息误解 + 互斥阶段账本 + 10 单元实验矩阵
人工智能·aigc·agent
吃饱了得干活8 小时前
LangChain Agent 高级玩法:命名、结构化输出与流式模式
langchain·llm·agent