中间件机制和skill配置

中间件的英文是 Middleware。它不是最终执行业务的对象,而是站在“调用方”和“被调用方”之间的一层处理逻辑

常见能力:

能力 说明 典型场景
日志追踪 记录模型、工具、子智能体什么时候被调用 排查 Agent 执行过程
参数改写 在真正调用工具前修改参数 统一补默认值、参数脱敏
结果改写 在工具返回后统一包装结果 统一输出格式、敏感内容过滤
调用限制 限制模型或工具调用次数 防止死循环、节省 Token
上下文压缩 对过长的对话历史或中间过程进行摘要 防止撑爆上下文窗口
权限认证 在调用高风险工具前判断当前用户是否有权限 企业系统里的付费功能、敏感操作
提前终止或抛异常 达到阈值后主动停止执行,或交给上层统一异常处理 调用次数超限、流程异常

对企业级智能体来说,最常用的是三件事:

  • 压缩上下文:避免多轮对话和长链路任务把上下文窗口撑爆。
  • 限制模型调用次数:避免 Agent 不断思考、不断重试。
  • 限制工具调用次数:避免某个工具被反复调用,造成循环或成本失控。

多智能体为什么需要中间件:

前面几章讲过,多智能体系统能“分而治之”,但也会放大两个问题:

  • 多个 Agent 互相调用,Token 成本更高;
  • Agent 的决策有不确定性,容易陷入重复规划或循环调用

中间件的价值,是给这类系统加上工程边界:

  1. 可以调用模型,但要有次数上限;
  2. 可以调用工具,但某个工具不能无限调用;
  3. 上下文可以增长,但到阈值前要先摘要压缩;
  4. 达到限制后,要么正常结束,要么抛出异常交给业务层处理

LangChain中间件概览:

工程问题 代表中间件 解决什么
上下文太长 SummarizationMiddleware 自动摘要旧消息,保留关键上下文
调用次数失控 ModelCallLimitMiddlewareToolCallLimitMiddleware 限制模型或工具调用次数,防止循环和成本失控
高风险动作审批 HumanInTheLoopMiddleware 在发邮件、删库、写文件等动作前暂停,等待人工确认
隐私与敏感信息 PIIMiddleware 对邮箱、手机号、信用卡等信息做脱敏、遮蔽或阻断
失败恢复 ModelRetryMiddlewareModelFallbackMiddlewareToolRetryMiddleware 模型或外部工具失败时重试,必要时切换备用模型
工具太多难选择 LLMToolSelectorMiddleware 工具列表很长时,先筛出当前任务相关工具
任务规划辅助 TodoListMiddleware 给 Agent 增加待办清单能力,帮助处理多步骤任务

 

middleware 配置

创建 DeepAgent 时,可以通过 middleware 参数传入一个列表。

main_agent = create_deep_agent(
    model=llm,
    tools=[delete_database, delete_file, select_database],
    checkpointer=checkpointer,
    system_prompt="回答使用中文,调用对应的工具实现对应的功能!",
    middleware=[
        ModelCallLimitMiddleware(
            thread_limit=1,  # 同一个 thread_id 下累计最多调用 1 次模型
            run_limit=1,  # 当前这次 invoke 内最多调用 1 次模型
            exit_behavior="error",  # 超限后抛出异常,便于后端统一捕获处理
        )
    ],
    # 本章重点是 middleware 调用限制,因此这里关闭人工审批拦截
    interrupt_on={
        "delete_database": False,
        "delete_file": False,
        "select_database": False,
    },
)

这段代码的意思是:给主智能体加一个模型调用限制中间件。只要主智能体需要调用模型,就会先经过这个中间件

子智能体配置

子智能体本质上也是一个独立的 Agent 配置,因此也可以配置 middleware

# 常见字段
db_agent = {
    "name": "db_helper",
    "description": "负责数据库查询任务。",
    "system_prompt": "你是一个专业的数据库查询助手。",
    "tools": [query_table, query_schema],
    "middleware": [
        # 子智能体自己的中间件
    ],
}
配置位置 影响范围
create_deep_agent(... middleware=...) 影响主智能体自己的执行链路
子智能体字典里的 middleware 影响该子智能体自己的链路

上下文摘要:SummarizationMiddleware

对话轮次很多,或者 Agent 执行过程很长时,messages 里会积累大量历史消息、工具结果和中间过程。最粗暴的处理方式是把旧消息丢掉,但这样容易丢失关键背景。更合理的做法是:在上下文快要过长之前,把一部分历史消息交给模型总结成短摘要,再把摘要放回上下文。这就是摘要中间件的作用

核心参数:

参数 说明
model 使用哪个模型做摘要压缩
trigger 什么时候触发摘要,例如 Token 数、消息数、上下文比例
keep 摘要后保留多少最近上下文,避免最新消息丢失
from langchain.agents.middleware import SummarizationMiddleware

summary_middleware = SummarizationMiddleware(
    model=llm,
    trigger=("tokens", 4000),  # 消息累计到约 4000 token 时触发摘要
    keep=("messages", 20),  # 摘要后保留最近 20 条原始消息
)

main_agent = create_deep_agent(
    model=llm,
    tools=[...],
    middleware=[summary_middleware],
)

位置:

  • 多轮深度搜索;
  • 多次网络检索;
  • 多次数据库查询;
  • 需要不断修订报告的任务;
  • 子智能体返回内容比较长的任务。

模型调用限制:ModelCallLimitMiddleware

Agent 每次“思考下一步”都可能调用模型。一次正常任务里,模型调用几次是合理的:先理解任务,决定调用工具,看工具结果,再整理最终答案。

但如果提示词不清晰、工具结果不稳定,Agent 可能不断重复,模型调用限制就是给这条链路设置上限

thread_limit 与 run_limit

ModelCallLimitMiddleware(
    thread_limit=1,  # 同一个 thread_id 下累计最多调用 1 次模型
    run_limit=1,  # 当前这次 invoke 内最多调用 1 次模型
    exit_behavior="error",  # 超限后抛出异常
)
参数 含义 可以怎么理解
thread_limit 同一个 thread_id 下的累计模型调用次数限制 一个客户端或一条会话线程的总额度
run_limit 单次执行里的模型调用次数限制 当前这次请求最多调用多少次模型

如果要使用 thread_limit,需要有稳定的 thread_id,通常还要配置 checkpointer。否则框架没有可靠位置记录“这条线程已经调用过几次模型

thread_limit 管一整条会话线程;  run_limit 管这一次 invoke / stream。

exit_behavior:end 还是 error

达到限制后,中间件需要决定怎么处理。常见取值有两个:

取值 行为 适合场景
"end" 正常结束,返回一段限制提示 提示内容可以直接给用户看
"error" 抛出异常 后端需要统一捕获异常,返回标准错误格式

更推荐使用 "error"。原因是 "end" 属于“正常业务返回”,前端可能把它当成普通回答展示;而 "error" 可以交给接口层统一处理,返回固定格式

import os

from deepagents import create_deep_agent
from dotenv import find_dotenv, load_dotenv
from langchain.agents.middleware import ModelCallLimitMiddleware
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver

load_dotenv(find_dotenv())

llm = init_chat_model(
    model=os.getenv("LLM_QWEN_MAX"),
    model_provider="openai",
)

# checkpointer 用来记录同一条线程的执行状态
# thread_id 也是 thread_limit 判断“同一个会话线程”的依据
checkpointer = InMemorySaver()
thread_config = {"configurable": {"thread_id": "erdaye"}}

main_agent = create_deep_agent(
    model=llm,
    tools=[delete_database, delete_file, select_database],
    checkpointer=checkpointer,
    system_prompt="回答使用中文,调用对应的工具实现对应的功能!",
    middleware=[
        ModelCallLimitMiddleware(
            thread_limit=1,  # 同一个 thread_id 下累计最多调用 1 次模型
            run_limit=1,  # 当前这次 invoke 内最多调用 1 次模型
            exit_behavior="error",  # 超限后抛出异常,便于后端统一捕获处理
        )
    ],
    # 本章重点是 middleware 调用限制,因此这里关闭人工审批拦截
    # 如果要演示危险动作审批,可以把高风险工具配置为 True 或 allowed_decisions
    interrupt_on={
        "delete_database": False,
        "delete_file": False,
        "select_database": False,
    },
)

# 这个请求通常需要多次“模型规划 -> 工具调用 -> 模型整理结果”
# run_limit=1 会让示例更容易触发模型调用上限
result = main_agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "先查询product表的数据!再删除user表,最后,删除zhaoweifeng.txt文件",
            }
        ]
    },
    config=thread_config,
)

print(f"最终结果{result['messages'][-1].content}")

工具调用限制:ToolCallLimitMiddleware

如果工具没有限制,Agent 可能反复查表结构、反复执行相似 SQL。数据库、网络搜索、RAG 这类工具本身就有成本或性能压力,更需要限制

工具调用限制可以分成两类:

限制类型 说明
全局限制 所有工具加起来最多调用多少次
单工具限制 指定某个工具最多调用多少次
from langchain.agents.middleware import ToolCallLimitMiddleware

global_tool_limit = ToolCallLimitMiddleware(
    thread_limit=10,  # 同一条会话线程里,所有工具累计最多调用 10 次
    run_limit=5,  # 当前这次 invoke 内,所有工具最多调用 5 次
)

search_tool_limit = ToolCallLimitMiddleware(
    tool_name="search_web",  # 只限制 search_web 这个工具
    run_limit=3,  # 当前这次 invoke 内最多搜索 3 次
    exit_behavior="error",  # 超限后直接抛异常,交给业务层处理
)

sql_tool_limit = ToolCallLimitMiddleware(
    tool_name="execute_sql",  # 只限制 execute_sql 这个工具
    run_limit=2,  # 当前这次 invoke 内最多执行 2 次 SQL
    exit_behavior="error",
)

这段配置可以读成:

  • 同一个线程里,所有工具调用总次数最多 10 次;
  • 单次执行里,所有工具调用总次数最多 5 次;
  • 单次执行里,search_web 最多调用 3 次;
  • 单次执行里,execute_sql 最多调用 2 次;
  • 指定工具超限后抛异常。

和模型调用限制一样,只要用到 thread_limit,就要保证同一条会话有稳定的 thread_id,并配置能保存线程状态的检查点

工具调用限制的 exit_behavior 比模型调用限制多一个常见选择:"continue"

取值 行为 适合场景
"continue" 阻止这次工具调用,把错误作为工具消息交回给 Agent 希望 Agent 自己换一种方式处理
"error" 立刻抛异常 后端统一捕获、统一返回错误结构
"end" 直接结束当前执行 单工具场景下给出明确终止提示

然后把它们一起放进 middleware 列表:

main_agent = create_deep_agent(
    model=llm,
    tools=[search_web, execute_sql],
    middleware=[
        global_tool_limit,
        search_tool_limit,
        sql_tool_limit,
    ],
)

自定义工具中间件:@wrap_tool_call

实际开发中,经常会遇到更细的需求:

  • 调用某个工具前记录日志;
  • 调用工具前检查用户权限;
  • 调用工具前统一改写参数;
  • 调用工具后过滤敏感结果;
  • 调用工具后把结果包装成统一格式;
  • 调用失败时改成业务可读的错误信息。

这些逻辑适合用自定义中间件。

在 LangChain Agent Middleware 里,工具调用中间件可以用 @wrap_tool_call 装饰器来定义

也就是说,Agent 原本要直接调用工具,现在先把这次工具调用交给你的函数。你的函数可以在工具执行前做一些事,也可以在工具执行后做一些事

from langchain.agents.middleware import wrap_tool_call


@wrap_tool_call
def log_tool_call(request, handler):
    """
    工具调用中间件:在目标工具执行前后打印调用信息
    """
    print("--------进入了工具中间件----------")
    print(f"request : {request}")  # 本次工具调用请求,包含工具名、工具参数等信息
    print(f"handler : {handler}")  # 真正执行目标工具的调用器

    # 前置增强:这里可以记录日志、做权限校验,或者改写 request 中的工具参数
    # handler(request) 是真正执行目标工具的关键步骤;不调用它,工具就会被中间件拦住
    result = handler(request)

    # 后置增强:这里可以包装返回结果、做敏感信息过滤,或者记录工具耗时
    print("--------退出工具中间件----------")
    print(f"result:{result}")

    return result
参数 作用
request 本次工具调用请求,里面包含工具名、工具参数等信息
handler 真正执行目标工具的调用器

没有中间件时,Agent 调用工具可以抽象成:

Agent -> 工具调用器(handler) + 工具参数(request) -> Tool -> 返回结果

有了中间件后,把“调用器”和“参数”交给中间件:

Agent -> 中间件(request, handler) -> handler(request) -> Tool -> result -> Agent

所以中间件既能看到调用前的参数,也能看到调用后的结果

位置 可以做什么
handler(request) 之前 前置增强:日志、权限、参数改写
handler(request) 之后 后置增强:结果包装、敏感词过滤、格式转换
@wrap_tool_call
def monitor_tool(request, handler):
    tool_name = request.tool_call["name"]
    tool_args = request.tool_call["args"]

    print(f"准备调用工具: {tool_name}")
    print(f"工具参数: {tool_args}")

    result = handler(request)

    print(f"工具调用完成: {tool_name}")
    return result

配置到 DeepAgent

定义好中间件以后,还需要配置到 Agent 的 middleware 列表中。

from deepagents import create_deep_agent
from langgraph.checkpoint.memory import InMemorySaver

# 自定义中间件和框架内置中间件一样,
# 都需要放到 middleware 列表里才会生效
deep_agent = create_deep_agent(
    model=llm,
    tools=[add_numbers],
    checkpointer=InMemorySaver(),
    middleware=[log_tool_call],
    system_prompt="你是一个计算器助手,使用add_numbers工具完成加法计算,回答仅返回计算结果。",
)

只要 Agent 调用了 add_numbers,就会经过 log_tool_call

无论是 LangChain 提供好的中间件,还是用 @wrap_tool_call 自己写出来的中间件,都要放到 middleware 列表里才会生效

工程使用场景

自定义工具中间件适合做“所有工具都要经过的统一逻辑

场景 中间件可以做什么
工具调用日志 记录工具名、参数、耗时、返回摘要
权限控制 某些工具只有登录用户或企业用户可以调用
参数保护 禁止模型传入危险路径或危险 SQL
结果脱敏 数据库结果里隐藏手机号、身份证号等敏感字段
前端进度推送 工具开始和结束时向 WebSocket 推送状态

中间件的优势是集中处理。否则每个工具里都写一遍日志、权限、异常包装,代码会很散

但也不要把所有业务逻辑都塞进中间件。可以按这个标准区分:

逻辑类型 更适合放在哪里
某个工具自己的核心业务 工具函数内部
所有工具都需要的日志、权限、脱敏、异常包装 自定义工具中间件
是否允许执行高风险动作,需要人工确认 interrupt_on
调用次数、上下文长度、成本边界 现成限制类中间件

 

middleware 与 interrupt_on 的区别

能力 关注点 典型用途
interrupt_on 某个工具是否需要人工审批 删除文件、删除表、发送邮件
middleware 调用链路中统一加规则或限制 日志、限流、压缩、权限、结果处理

interrupt_on 更像“危险动作审批”。middleware 更像“执行链路治理”

image

main_agent = create_deep_agent(
    model=llm,
    tools=[delete_database, delete_file, select_database],
    checkpointer=checkpointer,
    middleware=[
        ModelCallLimitMiddleware(
            thread_limit=1,
            run_limit=1,
            exit_behavior="error",
        )
    ],
    interrupt_on={
        "delete_database": False,
        "delete_file": False,
        "select_database": False,
    },
)

工程使用:
1. 先观测再设限制

  1. 先用日志或监控观察正常任务会调用几次模型、几次工具;
  2. 找出常见任务的调用次数区间;
  3. 再设置 run_limit 和 thread_limit
  4. 对超过阈值的异常任务做统一提示。

否则很容易出现:用户问一个稍微复杂的问题,Agent 还没完成就被限制打断

2. 按子智能体设置限制

智能体 推荐限制思路
网络搜索助手 限制搜索工具次数,避免重复搜
数据库查询助手 限制 SQL 执行次数,避免反复试错
RAG 助手 限制知识库问答次数,避免长链路循环
主智能体 限制整体模型调用次数,控制总成本

3. 统一异常处理

如果后续项目对接前端,建议后端统一捕获中间件抛出的异常,再给前端返回标准结构

这样前端能清楚区分:

  • 正常回答;
  • 模型调用次数超限;
  • 工具调用次数超限;
  • 人工审批拒绝;
  • 业务工具异常

否则所有内容都混在普通文本里,前端很难做稳定交互

脱敏。重试与降级

 

能力 可以考虑的中间件 使用场景
敏感信息处理 PIIMiddleware 对邮箱、手机号、身份证号等做遮蔽或阻断
模型失败恢复 ModelRetryMiddlewareModelFallbackMiddleware 模型超时、限流、服务不可用时重试或切换备用模型
工具失败恢复 ToolRetryMiddleware 搜索、网页抓取、RAG 查询这类外部工具偶发失败时重试

Skill配置

FilesystemBackend 的作用

FilesystemBackend 可以把 Agent 的文件系统映射到本地目录。配置 Skill 时,也要先告诉 Agent:技能文件夹在哪里

示例的核心链路是:用户请求 -> Agent 读取 Skill 元数据 -> 按需加载 SKILL.md -> 根据技能规则生成回复

# Skill 文件需要通过 Backend 暴露给 Agent
# 当前文件在 examples/ 下,所以 current_dir 指向 examples/
current_dir = Path(__file__).parent.resolve()

file_backend = FilesystemBackend(
    # Agent 能访问的文件系统根目录
    root_dir=current_dir,
    # 开启虚拟沙箱,限制 Agent 只能在这个根目录下访问文件
    virtual_mode=True,
)

注册 skills 参数

# skills=["skills"] 表示加载 current_dir/skills 目录下的技能包
# 每个技能包至少需要一个 SKILL.md,模型会先读取元数据,再按需读取完整技能说明
main_agent = create_deep_agent(
    model=llm,
    backend=file_backend,
    skills=[
        "skills",
    ],
    system_prompt="你是一个智能助手,可以使用 SKILL 技能",
)

这里的 "skills" 是相对于 file_backend.root_dir 的目录名。

区分

概念 示例 说明
物理路径 examples/skills/ 本地磁盘上真正存放 Skill 的目录
Backend 根目录 examples/ FilesystemBackend(root_dir=...) 指向的位置
Agent 配置路径 skills=["skills"] 相对于 Backend 根目录的 Skill 目录

也就是说,skills=["skills"] 不是随便写一个字符串,而是让 Agent 通过 Backend 去它能访问的文件系统中查找 skills/ 目录

执行调用示例

# 这句话明确要求使用“表情翻译技能”,便于触发 emoji-translator/SKILL.md。
query = "我早上起床晚了,赶公交车差点摔倒,还好最后到了公司。请你只用表情翻译技能。"

# DeepAgents 沿用 messages 输入结构,最终回复在 messages 最后一条。
result = main_agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": query,
            }
        ]
    }
)

print(f"最终输出结果:{result['messages'][-1].content}")

如果 emoji-translator 的 description 写得清楚,模型会根据 emoji-translator/SKILL.md 里的规则输出表情组合

配置注意事项

注意事项 说明
先配置 backend 没有文件系统 Backend,Agent 不知道去哪里找 Skill
skills 写目录名 写的是相对于 FilesystemBackend.root_dir 的技能目录
文件夹名和 name 对齐 技能文件夹名建议等于 SKILL.md 中的 name
description 要清晰 它决定模型什么时候加载该 Skill
采用渐进式加载 先看 YAML 元数据,再按需读取完整 SKILL.md
Skill 不是越多越好 技能过多、功能重复,会降低触发稳定性

 常见问题排查

问题现象 排查方向
Agent 完全不知道 Skill 存在 是否配置了 FilesystemBackendskills 路径是否写对
Skill 目录存在但没有触发 description 是否明确写出触发场景
触发了错误的 Skill 多个 Skill 的描述是否过于接近,职责是否重叠
找不到资源或脚本 SKILL.md 中的相对路径是否和 Skill 目录结构一致
输出格式不稳定 SKILL.md 是否写清楚输入边界、执行步骤和最终输出格式

Skill 不是自动插件系统,它主要靠元数据和提示词让模型判断“什么时候该用哪项能力

 

posted @ 2026-07-01 10:43  幻影之舞  阅读(14)  评论(0)    收藏  举报