提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 1、中间件概述
-
- [1.1 什么是中间件](#1.1 什么是中间件)
- [1.2 为什么需要中间件](#1.2 为什么需要中间件)
- [1.3 中间件的分类](#1.3 中间件的分类)
- [1.4 和模型供应商无关的内置中间件分类](#1.4 和模型供应商无关的内置中间件分类)
- 2、常用内置中间件的使用
-
- [2.1 SummarizationMiddleware中间件](#2.1 SummarizationMiddleware中间件)
- [2.2 HumanInTheLoopMiddleware中间件](#2.2 HumanInTheLoopMiddleware中间件)
- [2.3 PIIMiddleware中间件](#2.3 PIIMiddleware中间件)
- [2.4 TodoListMiddleware中间件](#2.4 TodoListMiddleware中间件)
- 3、其它内置中间件
-
- [3.1 ModelCallLimitMiddleware中间件](#3.1 ModelCallLimitMiddleware中间件)
- [3.2 ToolCallLimitMiddleware中间件](#3.2 ToolCallLimitMiddleware中间件)
- [3.3 ModelFallbackMiddleware中间件](#3.3 ModelFallbackMiddleware中间件)
- [3.4 LLMToolSelectorMiddleware中间件](#3.4 LLMToolSelectorMiddleware中间件)
- [3.5 ToolRetryMiddleware中间件](#3.5 ToolRetryMiddleware中间件)
- [3.6 ModelRetryMiddleware中间件](#3.6 ModelRetryMiddleware中间件)
- [3.7 LLMToolEmulator中间件](#3.7 LLMToolEmulator中间件)
- [3.8 ContextEditingMiddleware中间件](#3.8 ContextEditingMiddleware中间件)
- [3.9 FilesystemFileSearchMiddleware中间件](#3.9 FilesystemFileSearchMiddleware中间件)
- 4、多个中间件组合及执行顺序
- 5、自定义中间件
-
- [5.1 什么是hook函数(钩子函数)](#5.1 什么是hook函数(钩子函数))
- [5.2 LangChain的hook函数分类](#5.2 LangChain的hook函数分类)
- [5.3 Node-style hooks函数用法](#5.3 Node-style hooks函数用法)
- [5.4 Wrap-style hooks函数用法](#5.4 Wrap-style hooks函数用法)
- [5.5 装饰器和类的选择](#5.5 装饰器和类的选择)
- [5.6 hook函数执行顺序(重要)](#5.6 hook函数执行顺序(重要))
1、中间件概述

1.1 什么是中间件



1.2 为什么需要中间件



1.3 中间件的分类

链接:https://docs.langchain.com/oss/python/langchain/middleware/overview

1.4 和模型供应商无关的内置中间件分类
LangChain提供的和模型供应商无关的内置中间件分为六个类别
类型1:成本与资源控制类

类型2:稳定性与容错保障类

类型3:安全与合规风控类

类型4:决策增强与智能编排类

类型5:执行能力扩展类

类型6:开发调试与测试辅助类

2、常用内置中间件的使用

2.1 SummarizationMiddleware中间件








python
from langchain_core.messages import SystemMessage,HumanMessage,AIMessage
from langchain.agents import create_agent
messages = [
SystemMessage("你是个非常友好的AI助手"),
HumanMessage("你好啊,我是老王,你是谁?"),
AIMessage("你好老王,我是小王"),
HumanMessage("好的小王,很高兴认识你"),
AIMessage("你高兴得太早了"),
HumanMessage("呵呵,你什么意思")
]
agent = create_agent(
model="deepseek-v4-flash",
middleware=[
SummarizationMiddleware(
model=model,
trigger=[
("tokens",100),
("messages",6),
("fraction",0.001)
],
keep=("messages",2),
summary_prompt="对历史消息摘要,消息列表如下\n{messages}"
)
]
)
response = agent.invoke({
"messages": messages
})
for msg in response["messages"]:
msg.pretty_print()


2.2 HumanInTheLoopMiddleware中间件



python
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
# 从.env文件中加载环境变量
load_dotenv(override=True)
CLOSEAI_API_KEY = os.getenv("CLOSEAI_API_KEY")
CLOSEAI_BASE_URL = os.getenv("CLOSEAI_BASE_URL")
model = init_chat_model(
model="gpt-5.4-mini",
model_provider="openai",
api_key=CLOSEAI_API_KEY,
base_url=CLOSEAI_BASE_URL
)
python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import HumanMessage
from langchain.tools import tool
from langgraph.types import Command
from rich import print as rprint
@tool
def get_weather(city: str, is_forcast: bool = False) -> str:
"""
查询指定城市天气
Args:
city: 城市名称
is_forcast: 是否包含明日天气预报?
"""
res = f"{city}今天天气不错"
if is_forcast:
res += "\n明天下雨"
return res
@tool
def get_news() -> str:
"""
查询当日新闻
"""
return "中方三艘油轮通过霍尔木兹海峡"
@tool
def read_email_tool(email_id: str) -> str:
"""通过邮件ID读取内容的伪函数"""
return f"邮件ID:{email_id}\n是空的"
@tool
def send_email_tool(recipient: str, subject: str, body: str) -> str:
"""发送邮件伪函数"""
print(">>> 真的执行发送邮件工具了")
return f"发送给 {recipient} 的邮件标题是:{subject},内容:{body}"
agent = create_agent(
model=model,
tools=[get_weather, get_news, read_email_tool, send_email_tool],
checkpointer=InMemorySaver(),
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"get_weather": True,
"get_news": True,
"read_email_tool": False,
"send_email_tool": {
"allowed_decisions": ["approve", "reject"],
"description": "发送邮件中断了..."
},
},
description_prefix="中断啦!!"
),
]
)
config = {"configurable": {"thread_id": "1"}}
response = agent.invoke({
"messages": [HumanMessage(content="请帮我查询今天北京的天气"
"查询今日新闻"
"查看ID为 'sk2131421' 的邮件内容,"
"向15641685664@qq.com发送邮件,标题是'哈哈哈',内容是:'你好啊'"
"同时做这四件事")]
},
config=config
)
rprint(response)
python
{
'messages': [
HumanMessage(
content="请帮我查询今天北京的天气查询今日新闻查看ID为 'sk2131421'
的邮件内容,向15641685664@qq.com发送邮件,标题是'哈哈哈',内容是:'你好啊'同时做这四件事",
additional_kwargs={},
response_metadata={},
id='f764445f-1569-49dc-91fd-7cf55a76ed42'
),
AIMessage(
content='',
additional_kwargs={'refusal': None},
response_metadata={
'token_usage': {
'completion_tokens': 100,
'prompt_tokens': 281,
'total_tokens': 381,
'completion_tokens_details': {
'accepted_prediction_tokens': 0,
'audio_tokens': 0,
'reasoning_tokens': 0,
'rejected_prediction_tokens': 0
},
'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0},
'latency_checkpoint': {
'engine_tbt_ms': 4,
'engine_ttft_ms': 47,
'engine_ttlt_ms': 419,
'pre_inference_ms': 76,
'service_tbt_ms': 4,
'service_ttft_ms': 382,
'service_ttlt_ms': 750,
'total_duration_ms': 684,
'user_visible_ttft_ms': 305
}
},
'model_provider': 'openai',
'model_name': 'gpt-5.4-mini-2026-03-17',
'system_fingerprint': None,
'id': 'chatcmpl-DoT4xNl02fauFlw0fQbzBetsaj4z5',
'service_tier': 'default',
'finish_reason': 'tool_calls',
'logprobs': None
},
id='lc_run--019ea722-f968-7d43-8d47-48f40df7517b-0',
tool_calls=[
{
'name': 'get_weather',
'args': {'city': '北京', 'is_forcast': False},
'id': 'call_5QgsZ1dypAnY1lySmtIXllxt',
'type': 'tool_call'
},
{'name': 'get_news', 'args': {}, 'id': 'call_Qeg2vZRyfHGGffzJRGhA5XDO', 'type': 'tool_call'},
{
'name': 'read_email_tool',
'args': {'email_id': 'sk2131421'},
'id': 'call_WbcLD5MUWCsjozqymdEgXsH6',
'type': 'tool_call'
},
{
'name': 'send_email_tool',
'args': {'recipient': '15641685664@qq.com', 'subject': '哈哈哈', 'body': '你好啊'},
'id': 'call_Yxslc3JHQizxVaSuONr0ZNYP',
'type': 'tool_call'
}
],
invalid_tool_calls=[],
usage_metadata={
'input_tokens': 281,
'output_tokens': 100,
'total_tokens': 381,
'input_token_details': {'audio': 0, 'cache_read': 0},
'output_token_details': {'audio': 0, 'reasoning': 0}
}
)
],
'__interrupt__': [
Interrupt(
value={
'action_requests': [
{
'name': 'get_weather',
'args': {'city': '北京', 'is_forcast': False},
'description': "中断啦!!\n\nTool: get_weather\nArgs: {'city': '北京', 'is_forcast':
False}"
},
{'name': 'get_news', 'args': {}, 'description': '中断啦!!\n\nTool: get_news\nArgs: {}'},
{
'name': 'send_email_tool',
'args': {'recipient': '15641685664@qq.com', 'subject': '哈哈哈', 'body': '你好啊'},
'description': '发送邮件中断了...'
}
],
'review_configs': [
{'action_name': 'get_weather', 'allowed_decisions': ['approve', 'edit', 'reject']},
{'action_name': 'get_news', 'allowed_decisions': ['approve', 'edit', 'reject']},
{'action_name': 'send_email_tool', 'allowed_decisions': ['approve', 'reject']}
]
},
id='03f2757a974f19da1603162367f04f25'
)
]
}
2.2.3 举例过程2:指明工具调用请求决策
python
weather_decision = {
"type" : "edit",
"edited_action" : {
"name" : "get_weather",
"args" : {"city" : "上海市","is_forcast" : True},
}
}
news_decision = {
"type" : "approve"
}
send_email_decision = {
"type" : "approve"
}
decisions = {
"decisions" : []
}
interrupts = response.get("__interrupt__",[])
action_requests = interrupts[0].value["action_requests"]
for action_request in action_requests:
if action_request["name"] == "get_weather":
decisions["decisions"].append(weather_decision)
if action_request["name"] == "get_news":
decisions["decisions"].append(news_decision)
if action_request["name"] == "send_email_tool":
decisions["decisions"].append(send_email_decision)
if interrupts :
resumed_response = agent.invoke(
Command(resume=decisions),
config = config
)
for msg in resumed_response["messages"]:
msg.pretty_print()
python
>>> 真的执行发送邮件工具了
================================ Human Message =================================
请帮我查询今天北京的天气查询今日新闻查看ID为 'sk2131421' 的邮件内容,向15641685664@qq.com发送邮件,标题是'哈哈哈',内容是:'你好啊'同时做这四件事
================================== Ai Message ==================================
Tool Calls:
get_weather (call_5QgsZ1dypAnY1lySmtIXllxt)
Call ID: call_5QgsZ1dypAnY1lySmtIXllxt
Args:
city: 上海市
is_forcast: True
get_news (call_Qeg2vZRyfHGGffzJRGhA5XDO)
Call ID: call_Qeg2vZRyfHGGffzJRGhA5XDO
Args:
read_email_tool (call_WbcLD5MUWCsjozqymdEgXsH6)
Call ID: call_WbcLD5MUWCsjozqymdEgXsH6
Args:
email_id: sk2131421
send_email_tool (call_Yxslc3JHQizxVaSuONr0ZNYP)
Call ID: call_Yxslc3JHQizxVaSuONr0ZNYP
Args:
recipient: 15641685664@qq.com
subject: 哈哈哈
body: 你好啊
================================= Tool Message =================================
Name: get_weather
上海市今天天气不错
明天下雨
================================= Tool Message =================================
Name: get_news
中方三艘油轮通过霍尔木兹海峡
================================= Tool Message =================================
Name: read_email_tool
邮件ID:sk2131421
是空的
================================= Tool Message =================================
Name: send_email_tool
发送给 15641685664@qq.com 的邮件标题是:哈哈哈,内容:你好啊
================================== Ai Message ==================================
已同时完成这四件事,不过有一个小问题:
1. 北京天气:我这里实际查询到的是"上海市今天天气不错,明天下雨"
2. 今日新闻:中方三艘油轮通过霍尔木兹海峡
3. 邮件内容:ID 为 `sk2131421` 的邮件是空的
4. 邮件已发送:收件人 `15641685664@qq.com`,标题"哈哈哈",内容"你好啊"
如果你需要,我可以继续帮你重新按"北京"再查一次天气。
2.3 PIIMiddleware中间件



python
from langchain.agents.middleware import PIIMiddleware
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
# 从.env文件中加载环境变量
load_dotenv(override=True)
CLOSEAI_API_KEY = os.getenv("CLOSEAI_API_KEY")
CLOSEAI_BASE_URL = os.getenv("CLOSEAI_BASE_URL")
model = init_chat_model(
model="gpt-5.4-mini",
model_provider="openai",
api_key=CLOSEAI_API_KEY,
base_url=CLOSEAI_BASE_URL
)
python
from langchain_core.messages import HumanMessage
from langchain.agents import create_agent
from rich import print as rprint
agent = create_agent(
model=model,
tools=[],
middleware=[
PIIMiddleware("email",strategy="redact",apply_to_input=True),
PIIMiddleware("credit_card",strategy="mask",apply_to_input=True),
PIIMiddleware("url",strategy="hash",apply_to_input=True),
PIIMiddleware("mac_address",strategy="mask",apply_to_input=True),
PIIMiddleware("ip",strategy="block",apply_to_input=True),
]
)
response = agent.invoke({
"messages" : [HumanMessage("""
帮我向 156168188@qq.com 发送一封邮件
同时查看银行卡号: 5105-1051-0510-5100 的余额
访问 https://localhost:12345
确认这是不是 MAC地址: 11-11-11-11-11-11
""")]
})
for msg in response["messages"]:
msg.pretty_print()
python
1、PIIMiddleware中间件
举例1:使用内置检测器
{
'messages': [
HumanMessage(
content='\n 帮我向 [REDACTED_EMAIL] 发送一封邮件\n 同时查看银行卡号: ****-****-****-5100
的余额\n 访问 <url_hash:dd5fc2a9>\n 确认这是不是 MAC地址: **-**-**-**-**-11\n ',
additional_kwargs={},
response_metadata={},
id='ecee8958-7b2f-4c7a-a3af-6824eea2f765'
),
AIMessage(
content='抱歉,我不能代你执行这些操作或访问这些敏感信息,包括:\n\n- 发送邮件到指定邮箱\n-
查询银行卡余额\n- 打开/访问你提供的链接\n- 确认或处理看起来像 MAC
地址、银行卡号这类敏感标识\n\n如果你愿意,我可以帮你**安全地**做这些事情的准备工作,例如:\n\n1.
**帮你写一封邮件草稿**\n - 你告诉我收件人、主题、正文要点,我可以直接帮你生成可发送的邮件内容。\n\n2.
**帮你判断某串字符是不是 MAC 地址**\n - 一般 MAC 地址格式是类似 `AA:BB:CC:DD:EE:FF` 或 `AA-BB-CC-DD-EE-FF`。\n
- 你给出的 `**-**-**-**-**-11` 这种被打码的内容,无法可靠确认。\n\n3. **帮你整理银行卡查询的正规方式**\n -
我可以告诉你如何通过银行 App、网银或客服电话查询余额。\n - 但我不能直接查询或处理你的银行卡信息。\n\n4.
**帮你检查链接风险**\n -
如果你把链接内容以文本形式贴出来(不要包含敏感账号/验证码),我可以帮你分析是否可疑。\n\n如果你想,我现在就可以先帮
你起草一封邮件。',
additional_kwargs={'refusal': None},
response_metadata={
'token_usage': {
'completion_tokens': 310,
'prompt_tokens': 80,
'total_tokens': 390,
'completion_tokens_details': {
'accepted_prediction_tokens': 0,
'audio_tokens': 0,
'reasoning_tokens': 0,
'rejected_prediction_tokens': 0
},
'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0},
'latency_checkpoint': {
'engine_tbt_ms': 4,
'engine_ttft_ms': 30,
'engine_ttlt_ms': 1424,
'pre_inference_ms': 88,
'service_tbt_ms': 5,
'service_ttft_ms': 175,
'service_ttlt_ms': 1560,
'total_duration_ms': 1487,
'user_visible_ttft_ms': 87
}
},
'model_provider': 'openai',
'model_name': 'gpt-5.4-mini-2026-03-17',
'system_fingerprint': None,
'id': 'chatcmpl-E1ocVZV4rcmrm3X0SX5YqnQ1ypmYI',
'service_tier': 'default',
'finish_reason': 'stop',
'logprobs': None
},
id='lc_run--019f64be-3d86-7963-9f42-e0423938311e-0',
tool_calls=[],
invalid_tool_calls=[],
usage_metadata={
'input_tokens': 80,
'output_tokens': 310,
'total_tokens': 390,
'input_token_details': {'audio': 0, 'cache_read': 0},
'output_token_details': {'audio': 0, 'reasoning': 0}
}
)
]
}

举例2:自定义检测器/函数

python
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
from langchain.messages import HumanMessage
agent = create_agent(
model=model,
tools=[],
middleware=[
PIIMiddleware("api_key", strategy="hash", apply_to_input=True, detector=r"sk-[a-zA-Z0-9]+"),
PIIMiddleware("phone_number", strategy="mask", apply_to_input=True, detector=detect_phone_number)
]
)
response = agent.invoke({
"messages": [HumanMessage("""
这是不是有效的 API_KEY: sk-awef23AFEfaafaefa
帮我给这个号码打电话: 12345612345
访问 https://localhost:12345
""")]
})
for msg in response["messages"]:
msg.pretty_print()
python
================================ Human Message =================================
这是不是有效的 API_KEY: <api_key_hash:6c678cc0>
帮我给这个号码打电话: ****2345
访问 https://localhost:12345
================================== Ai Message ==================================
我不能验证或确认你给出的 `api_key_hash:6c678cc0` 是否是有效的 API Key,也不能替你拨打电话。
另外,`https://localhost:12345` 是你本机的本地地址,我无法直接访问;如果你在自己机器上运行服务,可以在浏览器或本地工具里打开它排查。
如果你愿意,我可以帮你做这些事:
1. **检查 API Key 是否配置正确**
- 教你如何在代码/环境变量里验证格式
- 帮你写一个最小测试请求
2. **拨号前准备**
- 帮你生成拨号脚本或命令
- 解释如何用运营商/VoIP 工具联系 `****2345`
3. **访问本地 HTTPS 服务排查**
- 帮你检查证书、端口占用、服务是否启动
- 给你一条可执行的 `curl` 测试命令
如果你想,我可以现在直接给你一条用于测试本地服务的命令。

2.4 TodoListMiddleware中间件








python
from langchain.tools import tool
from pathlib import Path
import subprocess
WORKSPACE = Path("../todo_workspace")
@tool
def list_files(path: str = ".") -> str:
"""
列出工作区指定目录下的文件和子目录。path 只能是相对路径。
Args:
path: 工作区下的相对路径,一定指向目录,默认为.,表示工作区根路径,不能访问工作区外的目录
"""
target = (WORKSPACE / path).resolve()
workspace_root = WORKSPACE.resolve()
if not str(target).startswith(str(workspace_root)):
return "错误:只允许访问工作区内的目录。"
if not target.exists():
return f"错误:目录不存在: {path}"
if not target.is_dir():
return f"错误:不是目录: {path}"
items = sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
if not items:
return f"目录为空: {path}"
lines = []
for item in items:
rel = item.relative_to(workspace_root)
kind = "[DIR]" if item.is_dir() else "[FILE]"
lines.append(f"{kind} {rel.as_posix()}")
return "\n".join(lines)
@tool
def read_file(path: str) -> str:
"""
读取工作区中的文本文件内容。path 只能是相对路径。
Args:
path: 工作区内的文件名
"""
file_path = (WORKSPACE / path).resolve()
if not str(file_path).startswith(str(WORKSPACE.resolve())):
return "错误:只允许读取工作区内的文件。"
if not file_path.exists():
return f"错误:文件不存在: {path}"
return file_path.read_text(encoding="utf-8")
@tool
def write_file(path: str, content: str) -> str:
"""
写入工作区中的文本文件。path 只能是相对路径。
Args:
path: 工作区内的文件名
content: 写入文件的内容
"""
file_path = (WORKSPACE / path).resolve()
if not str(file_path).startswith(str(WORKSPACE.resolve())):
return "错误:只允许写入工作区内的文件。"
file_path.write_text(content, encoding="utf-8")
return f"已写入文件: {path}"
@tool
def run_tests() -> str:
"""
在工作区运行 pytest -q,并返回输出。
不接收任何参数,返回格式为
returncode=0|1
STDOUT:
STDERR:
"""
try:
result = subprocess.run(
["pytest", "-q"],
cwd=str(WORKSPACE),
capture_output=True,
text=True,
timeout=20,
)
return (
f"returncode={result.returncode}\n\n"
f"STDOUT:\n{result.stdout}\n\n"
f"STDERR:\n{result.stderr}"
)
except Exception as e:
return f"运行测试失败: {e}"
python
from langchain.agents.middleware import TodoListMiddleware
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from rich import print as rprint
agent = create_agent(
model=model,
tools=[list_files, read_file, write_file,run_tests],
middleware=[TodoListMiddleware()],
system_prompt="你是一个代码修复助手。遇到多步骤任务时,先使用 write_todos 制定待办事项;"
"然后读取文件、修复代码并运行测试。工作全部在工作区下进行。"
)
response = agent.invoke({
"messages" : [HumanMessage("请测试并修复工作区下的my_add.py文件中的代码")]
})
rprint(response)




3、其它内置中间件
3.1 ModelCallLimitMiddleware中间件

python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ModelCallLimitMiddleware(
thread_limit=2, # 每个线程最多2次模型调用
# run_limit=5, # 每次运行最多5次
exit_behavior="end", # 达到限制后退出
),
],
)
config = {"configurable": {"thread_id": "1"}}
print("=" * 30, "> first <", "=" * 30)
response_first = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response_first["messages"]:
msg.pretty_print()
print("=" * 30, "> second <", "=" * 30)
response_second = agent.invoke({
"messages": [HumanMessage("你是谁?")]},
config=config
)
for msg in response_second["messages"]:
msg.pretty_print()
print("=" * 30, "> third <", "=" * 30)
response_third = agent.invoke({
"messages": [HumanMessage("你能帮我做什么?")]},
config=config
)
for msg in response_third["messages"]:
msg.pretty_print()

python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from typing import List
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ModelCallLimitMiddleware(
thread_limit=2,
# run_limit=5,
exit_behavior="error",
),
],
)
config = {"configurable": {"thread_id": "1"}}
print("=" * 30, "> first <", "=" * 30)
response_first = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response_first["messages"]:
msg.pretty_print()
print("=" * 30, "> second <", "=" * 30)
response_second = agent.invoke({
"messages": [HumanMessage("你是谁?")]},
config=config
)
for msg in response_second["messages"]:
msg.pretty_print()
print("=" * 30, "> third <", "=" * 30)
response_third = agent.invoke({
"messages": [HumanMessage("你能帮我做什么?")]},
config=config
)
for msg in response_third["messages"]:
msg.pretty_print()

python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field, SecretStr
from typing import List, Union
from dotenv import load_dotenv
load_dotenv()
model = ChatDeepSeek(
model="any",
api_base="http://localhost:8889",
api_key=SecretStr("<KEY>")
)
class ContactInfo(BaseModel):
"""用户的联系方式"""
name: str = Field(description="用户姓名")
email: str = Field(description="用户邮箱地址")
phone: str = Field(description="用户的手机号")
class EventInfo(BaseModel):
event_name: str = Field(description="事件名称")
date: str = Field(description="事件发生日期")
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ModelCallLimitMiddleware(
# thread_limit=2,
run_limit=3,
exit_behavior="end",
),
],
response_format=Union[ContactInfo, EventInfo]
)
config = {"configurable": {"thread_id": "1"}}
response = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response["messages"]:
msg.pretty_print()

python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field, SecretStr
from typing import List, Union
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="any",
api_base="http://localhost:8889",
api_key=SecretStr("<KEY>")
)
class ContactInfo(BaseModel):
"""用户的联系方式"""
name: str = Field(description="用户姓名")
email: str = Field(description="用户邮箱地址")
phone: str = Field(description="用户的手机号")
class EventInfo(BaseModel):
event_name: str = Field(description="事件名称")
date: str = Field(description="事件发生日期")
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ModelCallLimitMiddleware(
# thread_limit=2,
run_limit=3,
exit_behavior="error",
),
],
response_format=Union[ContactInfo, EventInfo]
)
config = {"configurable": {"thread_id": "1"}}
response = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response["messages"]:
msg.pretty_print()
3.2 ToolCallLimitMiddleware中间件

python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field, SecretStr
from typing import List, Union
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="any",
api_base="http://localhost:8889",
api_key=SecretStr("<KEY>")
)
class ContactInfo(BaseModel):
"""用户的联系方式"""
name: str = Field(description="用户姓名")
email: str = Field(description="用户邮箱地址")
phone: str = Field(description="用户的手机号")
class EventInfo(BaseModel):
event_name: str = Field(description="事件名称")
date: str = Field(description="事件发生日期")
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ToolCallLimitMiddleware(
# thread_limit=2, # 每个线程最多2次工具调用
run_limit=2, # 每次运行最多2次
exit_behavior="end",
),
],
response_format=Union[ContactInfo, EventInfo]
)
config = {"configurable": {"thread_id": "1"}}
response = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response["messages"]:
msg.pretty_print()

python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field, SecretStr
from typing import List, Union
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="any",
api_base="http://localhost:8889",
api_key=SecretStr("<KEY>")
)
class ContactInfo(BaseModel):
"""用户的联系方式"""
name: str = Field(description="用户姓名")
email: str = Field(description="用户邮箱地址")
phone: str = Field(description="用户的手机号")
class EventInfo(BaseModel):
event_name: str = Field(description="事件名称")
date: str = Field(description="事件发生日期")
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ToolCallLimitMiddleware(
# thread_limit=2,
run_limit=2,
exit_behavior="error",
),
],
response_format=Union[ContactInfo, EventInfo]
)
config = {"configurable": {"thread_id": "1"}}
response = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response["messages"]:
msg.pretty_print()

python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field, SecretStr
from typing import List, Union
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="any",
api_base="http://localhost:8889",
api_key=SecretStr("<KEY>")
)
class ContactInfo(BaseModel):
"""用户的联系方式"""
name: str = Field(description="用户姓名")
email: str = Field(description="用户邮箱地址")
phone: str = Field(description="用户的手机号")
class EventInfo(BaseModel):
event_name: str = Field(description="事件名称")
date: str = Field(description="事件发生日期")
agent = create_agent(
model=model,
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ToolCallLimitMiddleware(
# thread_limit=2,
run_limit=2,
exit_behavior="continue",
),
],
response_format=Union[ContactInfo, EventInfo]
)
config = {"configurable": {"thread_id": "1"}}
# seen = set()
response = agent.invoke({
"messages": [HumanMessage("你好")]},
config=config
)
for msg in response["messages"]:
msg.pretty_print()
3.3 ModelFallbackMiddleware中间件


3.4 LLMToolSelectorMiddleware中间件

3.5 ToolRetryMiddleware中间件

python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolRetryMiddleware
from langchain.messages import HumanMessage
import datetime
def write_times(s):
"""将每次工具调用的时间戳和间隔写入本地文件,方便观察退避策略"""
with open("call_times_with_jitter.txt", "a", encoding="utf-8") as f:
f.write(s + "\n")
count = 1
start_time = None
@tool
def get_weather(city: str):
"""查询指定城市天气"""
global count
global start_time
interval = 0
current_time = datetime.datetime.now()
if not start_time:
interval = 0
else:
# 计算当前调用与上一次调用之间的时间差(秒)
interval = (current_time - start_time).total_seconds()
start_time = current_time
res_str = f"第 {count} 次调用,当前时间: {start_time}, 和上次调用间隔 {interval} 秒"
count += 1
# 记录日志
write_times(res_str)
# 故意抛出 TimeoutError,以此触发中间件的重试机制
raise TimeoutError("Not Implemented")
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[
# ToolRetryMiddleware 用于捕获工具执行中的异常并自动重试
ToolRetryMiddleware(
max_retries=6, # 最大重试次数(不包含初始的那次调用,一共最多调 1 + 6 = 7 次)
backoff_factor=2.0, # 指数退避因子(每次重试等待时间乘以 2)
initial_delay=1.0, # 第一次重试前的初始等待时间(1 秒)
max_delay=10.0, # 最大等待延迟上限(防止指数增长无限大,限制在 10 秒)
jitter=True, # 开启抖动(在等待时间中加入随机性,防止并发请求时出现"惊群效应")
retry_on=(TimeoutError,), # 仅针对捕获到特定的 TimeoutError 异常时才触发重试
on_failure="continue" # 当达到最大重试次数依然失败时,Agent 的行为:"continue" 表示将错误信息包装后塞回对话历史,让大模型知道失败了并继续决策
),
],
)
response = agent.invoke({
"messages": [HumanMessage("今天北京天气如何?")]
})
# 1. 你的提问 -> 2. AI 决定调用工具 -> 3. 重试失败后的错误反馈 -> 4. AI 最终给出的兜底回复
for msg in response["messages"]:
msg.pretty_print()



3.6 ModelRetryMiddleware中间件



3.7 LLMToolEmulator中间件

3.8 ContextEditingMiddleware中间件



3.9 FilesystemFileSearchMiddleware中间件


4、多个中间件组合及执行顺序



python
from langchain.agents.middleware import AgentMiddleware
class Middleware1(AgentMiddleware):
def before_model(self, state, runtime):
print("[中间件1] before_model")
return None
def after_model(self, state, runtime):
print("[中间件1] after_model")
return None
class Middleware2(AgentMiddleware):
def before_model(self, state, runtime):
print("[中间件2] before_model")
return None
def after_model(self, state, runtime):
print("[中间件2] after_model")
return None
class Middleware3(AgentMiddleware):
def before_model(self, state, runtime):
print("[中间件3] before_model")
return None
def after_model(self, state, runtime):
print("[中间件3] after_model")
return None
agent = create_agent(
model=model,
tools=[],
middleware=[Middleware3(), Middleware1(), Middleware2()]
)
print("\n执行一次调用,观察顺序:")
agent.invoke({"messages": [{"role": "user", "content": "测试"}]})

5、自定义中间件

5.1 什么是hook函数(钩子函数)



5.2 LangChain的hook函数分类

5.3 Node-style hooks函数用法

python
from langchain.agents.middleware import before_model, after_model, before_agent, after_agent, AgentMiddleware
from typing import Any
from langgraph.runtime import Runtime
from langchain.agents import AgentState
@before_model
def before_model_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> before_model <-----"
return None
@after_model
def after_model_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> after_model <-----"
return None
@before_agent
def before_agent_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> before_agent <-----"
return None
@after_agent
def after_agent_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> after_agent <-----"
return None
python
from langchain_core.messages import HumanMessage
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
# 从.env文件中加载环境变量
load_dotenv(override=True)
model = init_chat_model(
model="gpt-5.4-mini",
model_provider="openai",
api_key=os.getenv("CLOSEAI_API_KEY"),
base_url=os.getenv("CLOSEAI_BASE_URL")
)
agent = create_agent(
model=model,
middleware=[
before_model_middleware,
after_model_middleware,
before_agent_middleware,
after_agent_middleware,
]
)
response = agent.invoke({
"messages": [HumanMessage("你好")]
})
for msg in response["messages"]:
msg.pretty_print()


python
from langchain.agents.middleware import AgentMiddleware
class MyMiddleware(AgentMiddleware):
def before_model(self,state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> before_model <-----"
return None
def after_model(self,state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> after_model <-----"
return None
def before_agent(self,state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> before_agent <-----"
return None
def after_agent(self,state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += "----> after_agent <-----"
return None







- 基于装饰器的实现
python
from typing import Any
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, after_model, AgentState
from langchain.messages import AIMessage, SystemMessage
from langchain.tools import tool
from langgraph.runtime import Runtime
@tool
def get_news() -> str:
"""获取当日新闻"""
return f"美加墨世界杯今日开幕"
# 在模型(LLM)执行前触发。允许跳转到 "tools" 节点。
@before_model(can_jump_to=["tools"])
def force_tool_first(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""
【业务场景:强行拦截并触发工具】
如果用户输入包含 "direct tool",则跳过本次大模型的思考/生成阶段,
直接伪造一个大模型的 tool_calls 意图,强行把控制权移交给工具执行节点。
"""
text = state["messages"][-1].content
# 检查关键词,满足条件则强行干预流程
if isinstance(text, str) and "direct tool" in text.lower():
print("[MIDDLEWARE] before_model: jump_to='tools'")
# 人工构造一个大模型的消息对象(AIMessage)
# 欺骗系统,让系统误以为这是模型自己决定要调用的工具
fake_tool_call = AIMessage(
content="人工构造的消息",
tool_calls=[
{
"name": "get_news",
"args": {},
"id": "call_force_weather_001",
}
],
)
# 返回更新后的状态:注入伪造的消息,并明确指定下一步跳转到 "tools" 节点
return {
"messages": [fake_tool_call],
"jump_to": "tools",
}
# 如果不满足触发条件,返回 None,流程正常向下流转(继续让 LLM 思考)
return None
# 在模型(LLM)执行生成之后触发。允许重新跳转回 "model" 节点。
@after_model(can_jump_to=["model"])
def retry_with_extra_instruction(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""
【业务场景:反思/重试机制】
如果大模型已经生成了回答,但发现用户最初的请求包含 "retry model",
则动态追加一条系统提示词(SystemMessage),强行让模型重新生成(重试)一次。
"""
# 倒序遍历消息历史,找到最近的一次用户输入(human 消息)
user_text = ""
for msg in reversed(state["messages"]):
if getattr(msg, "type", "") == "human":
user_text = getattr(msg, "content", "")
break
# 检查用户输入是否包含触发重试的关键字
if isinstance(user_text, str) and "retry model" in user_text.lower():
# 【核心防御】:防止无限循环重跳(死循环)
# 检查消息历史中是否已经注入过这条特殊的系统提示。如果有,说明已经重试过了,不再重复干预。
already_injected = any(
isinstance(getattr(msg, "content", None), str)
and "你必须以【二次回答】开头" in msg.content
for msg in state["messages"]
)
if already_injected:
return None # 已注入过,直接放行,结束重试流程
print("[MIDDLEWARE] after_model: jump_to='model' with extra system instruction")
# 返回更新后的状态:追加强力约束的系统消息,并将指针跳回 "model" 节点重新执行
return {
"messages": [
SystemMessage("你必须以【二次回答】开头,并且只用一句话回答。")
],
"jump_to": "model",
}
return None
# 在模型(LLM)执行前触发。允许直接跳转到 "end" 节点(强行终止)。
@before_model(can_jump_to=["end"])
def overflow_context_processor(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""
【业务场景:安全卫士/异常拦截】
模拟上下文窗口溢出(Token超限)或其他严重的系统阻断情况。
一旦触发,直接熔断流程,拒绝让大模型继续处理,直接报错或返回兜底文案。
"""
# 假装溢出,模拟检查最后一条消息是否包含 overflow 标识
if "overflow" in state["messages"][-1].content:
print("[MIDDLEWARE] before_model: jump_to='end' when contenxt window overflow")
# 构造兜底的结束消息,并直接指定跳转到 "end" 终止 Agent 运行
return {
"messages": [
AIMessage("上下文窗口溢出,终止")
],
"jump_to": "end",
}
agent = create_agent(
model=model,
tools=[get_news],
# # 将定义的中间件按照顺序挂载到 Agent 中(注意:执行顺序会严格按照列表声明顺序)
middleware=[force_tool_first, retry_with_extra_instruction, overflow_context_processor],
)
def run_once(user_input: str):
result = agent.invoke(
{
"messages": [
{"role": "user", "content": user_input}
]
}
)
for msg in result["messages"]:
msg.pretty_print()
if __name__ == "__main__":
# Case 1: 直接跳 tools
# 预期表现:
# 1. 触发 force_tool_first,打印 "[MIDDLEWARE] before_model: jump_to='tools'"
# 2. 绕过 LLM 的首轮思考,直接调用 `get_news` 工具
# 3. 工具返回结果后,LLM 总结工具结果并输出
print('=' * 30, '-> Case 1 <-', '=' * 30)
run_once("请帮我查今日新闻 direct tool")
# Case 2: 输出后跳回 model
# 预期表现:
# 1. 正常进入 LLM 生成第 1 版回答
# 2. 触发 retry_with_extra_instruction,打印 "[MIDDLEWARE] after_model: jump_to='model'..."
# 3. 注入系统提示词后,LLM 被强行拉回并生成第 2 版回答
# 4. 最终输出应带有"【二次回答】"前缀
print('=' * 30, '-> Case 2 <-', '=' * 30)
run_once("请随便介绍一下 LangChain retry model")
# Case 3:
# 预期表现:
# 1. 触发 overflow_context_processor 中间件
# 2. 直接打印终止信息并退出,LLM 根本不会接收到这个请求
print('=' * 30, '-> Case 3 <-', '=' * 30)
run_once("你好 overflow")
# Case 4: 正常流程
# 预期表现:
# 1. 没有任何中间件被触发(不满足任何关键字)
# 2. Agent 走正常的 OOTB(Out of the box)标准工作流:User -> Model -> Call Tool -> Model -> End
print('=' * 30, '-> Case 4 <-', '=' * 30)
run_once("今日新闻摘要?")

- 基于类的实现
python
from typing import Any
from langchain.agents import create_agent
from langchain.agents.middleware import hook_config, AgentState, AgentMiddleware
from langchain.messages import AIMessage, SystemMessage
from langchain.tools import tool
from langgraph.runtime import Runtime
@tool
def get_news() -> str:
"""获取当日新闻"""
return f"美加墨世界杯今日开幕"
class MyMiddleware(AgentMiddleware):
@hook_config(can_jump_to=["tools", "end"])
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
text = state["messages"][-1].content
# 假装溢出
if "overflow" in text:
print("[MIDDLEWARE] before_model: jump_to='end' when contenxt window overflow")
return {
"messages": [
AIMessage("上下文窗口溢出,终止")
],
"jump_to": "end",
}
if isinstance(text, str) and "direct tool" in text.lower():
print("[MIDDLEWARE] before_model: jump_to='tools'")
fake_tool_call = AIMessage(
content="人工构造的消息",
tool_calls=[
{
"name": "get_news",
"args": {},
"id": "call_force_weather_001",
}
],
)
return {
"messages": [fake_tool_call],
"jump_to": "tools",
}
return None
@hook_config(can_jump_to=["model"])
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
user_text = ""
for msg in reversed(state["messages"]):
if getattr(msg, "type", "") == "human":
user_text = getattr(msg, "content", "")
break
if isinstance(user_text, str) and "retry model" in user_text.lower():
# 防止无限重跳:如果已经加过提示,就不再跳
already_injected = any(
isinstance(getattr(msg, "content", None), str)
and "你必须以【二次回答】开头" in msg.content
for msg in state["messages"]
)
if already_injected:
return None
print("[MIDDLEWARE] after_model: jump_to='model' with extra system instruction")
return {
"messages": [
SystemMessage("你必须以【二次回答】开头,并且只用一句话回答。")
],
"jump_to": "model",
}
return None
agent = create_agent(
model=model,
tools=[get_news],
middleware=[MyMiddleware()],
)
def run_once(user_input: str):
result = agent.invoke(
{
"messages": [
{"role": "user", "content": user_input}
]
}
)
for msg in result["messages"]:
msg.pretty_print()
if __name__ == "__main__":
# Case 1: 直接跳 tools
print('=' * 30, '-> Case 1 <-', '=' * 30)
run_once("请帮我查今日新闻 direct tool")
# Case 2: 输出后跳回 model
print('=' * 30, '-> Case 2 <-', '=' * 30)
run_once("请随便介绍一下 LangChain retry model")
# Case 3:
print('=' * 30, '-> Case 3 <-', '=' * 30)
run_once("你好 overflow")
# Case 4: 正常流程
print('=' * 30, '-> Case 4 <-', '=' * 30)
run_once("今日新闻摘要?")
5.4 Wrap-style hooks函数用法











① 基于装饰器实现
python
from langchain_core.tools import tool
from typing import Any
from langgraph.types import Command
from langchain_core.messages import ToolMessage
from langgraph.prebuilt.tool_node import ToolCallRequest
from langchain.agents.middleware import wrap_tool_call
@tool
def get_weather(city: str, is_forcast: bool) -> str:
"""
获取当日特定城市的天气
Args:
city: 城市名称
is_forcast: 是否包含明天的天气预报
"""
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]:
result = handler(request)
print(f"原始参数:{request.tool_call['args']}")
print(f"原始参数调用结果:{result}")
request.tool_call["args"]["is_forcast"] = True
result = handler(request)
print(f"更新以后的参数:{request.tool_call['args']}")
print(f"更新以后的参数调用结果:{result}")
return result
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[wrap_tool_call_middleware]
)
response = agent.invoke({
"messages": [HumanMessage("帮我查询北京今天的天气如何?")]
})
for msg in response["messages"]:
msg.pretty_print()

python
class WrapToolCallMiddleware(AgentMiddleware):
def wrap_tool_call(self, request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
result = handler(request)
print(f"原始参数:{request.tool_call['args']}")
print(f"原始参数调用结果:{result}")
request.tool_call["args"]["is_forcast"] = True
result = handler(request)
print(f"更新以后的参数:{request.tool_call['args']}")
print(f"更新以后的参数调用结果:{result}")
return result
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[WrapToolCallMiddleware()]
)
response = agent.invoke({
"messages": [HumanMessage("帮我查询上海今天的天气如何?")]
})
for msg in response["messages"]:
msg.pretty_print()


5.5 装饰器和类的选择









5.6 hook函数执行顺序(重要)





