中间件机制和skill配置
中间件的英文是 Middleware。它不是最终执行业务的对象,而是站在“调用方”和“被调用方”之间的一层处理逻辑
常见能力:
| 能力 | 说明 | 典型场景 |
|---|---|---|
| 日志追踪 | 记录模型、工具、子智能体什么时候被调用 | 排查 Agent 执行过程 |
| 参数改写 | 在真正调用工具前修改参数 | 统一补默认值、参数脱敏 |
| 结果改写 | 在工具返回后统一包装结果 | 统一输出格式、敏感内容过滤 |
| 调用限制 | 限制模型或工具调用次数 | 防止死循环、节省 Token |
| 上下文压缩 | 对过长的对话历史或中间过程进行摘要 | 防止撑爆上下文窗口 |
| 权限认证 | 在调用高风险工具前判断当前用户是否有权限 | 企业系统里的付费功能、敏感操作 |
| 提前终止或抛异常 | 达到阈值后主动停止执行,或交给上层统一异常处理 | 调用次数超限、流程异常 |
对企业级智能体来说,最常用的是三件事:
- 压缩上下文:避免多轮对话和长链路任务把上下文窗口撑爆。
- 限制模型调用次数:避免 Agent 不断思考、不断重试。
- 限制工具调用次数:避免某个工具被反复调用,造成循环或成本失控。
多智能体为什么需要中间件:
前面几章讲过,多智能体系统能“分而治之”,但也会放大两个问题:
- 多个 Agent 互相调用,Token 成本更高;
- Agent 的决策有不确定性,容易陷入重复规划或循环调用
中间件的价值,是给这类系统加上工程边界:
- 可以调用模型,但要有次数上限;
- 可以调用工具,但某个工具不能无限调用;
- 上下文可以增长,但到阈值前要先摘要压缩;
- 达到限制后,要么正常结束,要么抛出异常交给业务层处理
LangChain中间件概览:
| 工程问题 | 代表中间件 | 解决什么 |
|---|---|---|
| 上下文太长 | SummarizationMiddleware |
自动摘要旧消息,保留关键上下文 |
| 调用次数失控 | ModelCallLimitMiddleware、ToolCallLimitMiddleware |
限制模型或工具调用次数,防止循环和成本失控 |
| 高风险动作审批 | HumanInTheLoopMiddleware |
在发邮件、删库、写文件等动作前暂停,等待人工确认 |
| 隐私与敏感信息 | PIIMiddleware |
对邮箱、手机号、信用卡等信息做脱敏、遮蔽或阻断 |
| 失败恢复 | ModelRetryMiddleware、ModelFallbackMiddleware、ToolRetryMiddleware |
模型或外部工具失败时重试,必要时切换备用模型 |
| 工具太多难选择 | 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 更像“执行链路治理”

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. 先观测再设限制
- 先用日志或监控观察正常任务会调用几次模型、几次工具;
- 找出常见任务的调用次数区间;
- 再设置
run_limit和thread_limit; - 对超过阈值的异常任务做统一提示。
否则很容易出现:用户问一个稍微复杂的问题,Agent 还没完成就被限制打断
2. 按子智能体设置限制
| 智能体 | 推荐限制思路 |
|---|---|
| 网络搜索助手 | 限制搜索工具次数,避免重复搜 |
| 数据库查询助手 | 限制 SQL 执行次数,避免反复试错 |
| RAG 助手 | 限制知识库问答次数,避免长链路循环 |
| 主智能体 | 限制整体模型调用次数,控制总成本 |
3. 统一异常处理
如果后续项目对接前端,建议后端统一捕获中间件抛出的异常,再给前端返回标准结构
这样前端能清楚区分:
- 正常回答;
- 模型调用次数超限;
- 工具调用次数超限;
- 人工审批拒绝;
- 业务工具异常
否则所有内容都混在普通文本里,前端很难做稳定交互
脱敏。重试与降级
| 能力 | 可以考虑的中间件 | 使用场景 |
|---|---|---|
| 敏感信息处理 | PIIMiddleware |
对邮箱、手机号、身份证号等做遮蔽或阻断 |
| 模型失败恢复 | ModelRetryMiddleware、ModelFallbackMiddleware |
模型超时、限流、服务不可用时重试或切换备用模型 |
| 工具失败恢复 | 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 存在 | 是否配置了 FilesystemBackend,skills 路径是否写对 |
| Skill 目录存在但没有触发 | description 是否明确写出触发场景 |
| 触发了错误的 Skill | 多个 Skill 的描述是否过于接近,职责是否重叠 |
| 找不到资源或脚本 | SKILL.md 中的相对路径是否和 Skill 目录结构一致 |
| 输出格式不稳定 | SKILL.md 是否写清楚输入边界、执行步骤和最终输出格式 |
Skill 不是自动插件系统,它主要靠元数据和提示词让模型判断“什么时候该用哪项能力

浙公网安备 33010602011771号