5、自定义中间件
某些复杂场景下,官方内置的中间件不能完全满足需求,此时可以通过实现LangChain暴露的中间件 hook函数 构建自定义中间件。
说明:尽可能使用内置中间件。
5.1 什么是hook函数(钩子函数)
Hook 函数,中文常叫 钩子函数 ,指的是:在某个既定流程的特定时机,被框架、系统或主程序 自动 调用 的扩展函数

因此,可以把它理解成:
主流程预留了一些插槽,允许你在这些位置挂上自己的函数,这种被挂进去并在特定时机执行的函 数,就是 hook 函数。
核心特点:
- 不是你主动在业务代码里随便调用的,而是当流程运行到某个"钩子点"时,系统自动触发它
- 它依附于一个更大的执行流程。比如"请求开始前""模型调用前""任务结束后""异常发生时"等。
- 它的作用是让你在不改主流程源码的前提下插入自己的逻辑。例如做 日志 、 鉴权 、 修改输入 、 拦截输出 、 清理资源 等。
LangChain的中间件作用在Agent架构中,后者是基于LangGraph构建的流程图。如下列出了六个hook 函数(钩子函数):

无论是官方内置中间件、自定义中间件、还是下文提到的便捷装饰器中间件,通常都是通过实现其中的 一个或多个hook来生效的。
5.2 LangChain的hook函数分类
官方将六个钩子函数按照风格分为两类
类型1:Node-style hooks(节点风格钩子)
顾名思义,它们在流程的 特定节点 运行。 适合顺序逻辑,如记录日志、验证
- before_agent:在Agent开始运行之前执行
- before_model:在模型调用之前执行。
- after_model:在模型调用之后执行。
- after_agent:在Agent流程全部完成后执行。
类型2:Wrap-style hooks(包装风格钩子)
顾名思义,它们在 模型或工具调用前后 运行。 适合控制流,如重试、回退、缓存。
- wrap_model_call (包裹模型调用)
- wrap_tool_call (包裹工具调用)
5.3 Node-style hooks函数用法
支持两种用法
- 装饰器是函数式挂载,把一个hook快速挂载到Agent的某个节点
- 类写法是对象化中间件,把中间件封装为一个可配置、可复用、可扩展的组件
1 基本用法
基于装饰器实现
from typing import Any
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import before_model, after_model, before_agent, after_agent, AgentMiddleware
from langchain_core.messages import HumanMessage
from langgraph.runtime import Runtime
@before_model
def before_model_middle(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
# 模型调用之前
state["messages"][-1].content += "before_model 额外追加"
return None
@after_model
def after_model_middle(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
# 模型调用之后
state["messages"][-1].content += "after_model 额外追加"
return None
@before_agent
def before_agent_middle(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
# Agent 执行之前
state["messages"][-1].content += "before_agent 额外追加"
return None
@after_agent
def after_agent_middle(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
# Agent 执行之后
state["messages"][-1].content += "after_agent 额外追加"
return None
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="deepseek-v4-pro",
model_provider="deepseek",
extra_body={"thinking": {"type": "disabled"}},
)
agent = create_agent(
model=model,
middleware=[
before_model_middle,
after_model_middle,
before_agent_middle,
after_agent_middle,
]
)
response = agent.invoke({
"messages": [HumanMessage('你好')]
})
for msg in response["messages"]:
msg.pretty_print()
================================ Human Message =================================
你好before_agent 额外追加before_model 额外追加
================================== Ai Message ==================================
你好!我看到你提到了"before_agent"和"before_model"的额外追加,但我需要更多上下文来理解你的具体需求。😊
你是想了解:
某个框架或系统中的消息处理流程?
Agent 和 Model 之间数据传递的机制?
还是需要在代码中添加某些前置处理逻辑?
如果你能提供更多细节,比如:
使用的是什么系统/框架(如 LangChain、自研系统等)
具体想实现什么功能
遇到了什么问题
我会更好地帮助你!请随时补充说明。after_model 额外追加after_agent 额外追加
观察HumanMessage可以发现,before_agent钩子先于before_model被执行,二者都在调用模型 之前被执行
. after_agent晚于after_model执行,二者都在模型调用后执行
2. 基于类实现
关键规则:
-
必须继承 AgentMiddleware ← 这个固定
-
方法名固定 ( before_model , after_model ) ← 这个固定
-
类名随意 ← 这个不固定
LangGraph 只看:
是否继承 AgentMiddleware?
是否有 before_model / after_model 等方法?
from langchain.agents.middleware import AgentMiddleware
from langchain.agents import AgentState
from langgraph.runtime import Runtime
from typing import Any
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:
# Agent 执行之前
state["messages"][-1].content += "before_agent 额外追加"
return None
def after_agent(
self,
state: AgentState,
runtime: Runtime,
) -> dict[str, Any] | None:
# Agent 执行之后
state["messages"][-1].content += "after_agent 额外追加"
return None
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
test_middleware=MyMiddleware()
# 从 .env 文件中加载环境变量
load_dotenv(override=True)
model = init_chat_model(
model="deepseek-v4-pro",
model_provider="deepseek",
extra_body={"thinking": {"type": "disabled"}},
)
agent = create_agent(
model=model,
middleware=[test_middleware]
)
response = agent.invoke({
"messages": [HumanMessage('你好')]
})
for msg in response["messages"]:
msg.pretty_print()
- before_model 通常的场景:
消息修剪(trim messages ,PII 脱敏,输入验证 ,条件路由
- after_model 通常的场景:
输出验证,格式化响应,统计信息,状态更新
2 两种方法的统一
装饰器底层会基于我们重写的方法构造一个 AgentMiddleware子类 的实例,以 @after_model 装饰器 底层实现为例,关键代码如下
return type(
middleware_name,
(AgentMiddleware,),
{
"state_schema": state_schema or AgentState,
"tools": tools or [],
"after_model": wrapped,
},
)()
这是after_model最终返回的内容。
上述代码中的wrapped是after_model内部的装饰器,代码如下
def wrapped(
_self: AgentMiddleware[StateT, ContextT],
state: StateT,
runtime: Runtime[ContextT],
) -> dict[str, Any] | Command[Any] | None:
return func(state, runtime) # type: ignore[return-value]
上述代码等价于
return type(
middleware_name,
(AgentMiddleware,),
{
"state_schema": state_schema or AgentState,
"tools": tools or [],
"after_model": func(state, runtime),
},
)()
而 func(state, runtime) 正是我们定义的、被 @after_model 修饰的函数,在上述案例中对应的是 after_model_middleware,
上述代码的含义是
-
- 创建一个AgentMiddleware的子类
-
- 类名为middleware_name,即创建agent时传递的中间件名称,上述案例中是 after_model_middleware
-
- 这个子类有两个属性 state_schema 和 tools
-
- 有一个方法: after_model ,逻辑等同于 func(state, runtime) 。
-
- 最后的括号 () 表示实例化子类,返回一个对象
所以,用装饰器最终返回的也是一个AgentMiddleware的子类对象,并且重写了after_model方法,和 基于类的自定义方式本质是一样的。
3 参数说明
Node-style hooks函数有两个参数
state: 是一个AgentState实例,维护Agent运行过程中的状态,这类状态会随着Agent的运行而发生变 化,包括 消息列表
runtime: 是一个Runtime实例,维护Agent运行过程中的上下文环境,包括 上下文 、 长期记忆 等。
4 返回值说明
返回 None:不修改状态(不修改Agent状态)
def before_model(self, state, runtime):
print("日志记录")
return None # 不做任何修改,继续流程
返回字典:更新状态
def after_model(self, state, runtime):
count = state.get("count", 0)
return {
"count": count + 1
} # 更新状态中的 count
返回 {"jump_to": "..."}:控制流程
def before_model(self, state, runtime):
if state.get("count", 0) > 10:
return {"jump_to": "__end__"} # 跳过模型,直接结束
return None
jump_to 目标:
- "end" - 结束 Agent
- "tools" - 跳到工具节点
- 其他自定义节点
5 装饰器参数:can_jump_to
这里就涉及到Node-style的四个hook函数可以接收额外参数 can_jump_to 。
钩子函数可以 改变Agent正常的运行轨迹 。比如:发现上下文窗口溢出,直接跳转至结尾,提前终止整 个Agent。
can_jump_to 决定了钩子函数可以直接跳转至流程的哪些位置,可取值如下:
- end:跳转至Agent流程末尾,或第一个after_agent钩子,直接终止整个流程。
- tools:跳转至工具节点。
- model:跳转至模型节点,或第一个before_model钩子。
1. 基于装饰器实现
from typing import Any
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import after_model, before_model
from langchain.messages import AIMessage, SystemMessage
from langchain.tools import tool
from langgraph.runtime import Runtime
import os
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
#读取.env配置文件 verbose相关的环境变量以env为准
load_dotenv(verbose=True)
AICODE_KEY = os.getenv("AICODE_KEY")
AICODE_URL = os.getenv("AICODE_URL")
# 获取大模型
model = ChatOpenAI(
model='gpt-5.5',
api_key=AICODE_KEY,
base_url=AICODE_URL,
)
@tool
def get_news() -> str:
"""获取当日新闻"""
return "美加墨世界杯今日开幕"
# ============================================================
# before_model:在模型(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",
}
],
)
# 返回更新后的状态:
# 1. 注入伪造的 AIMessage
# 2. 明确指定下一步跳转到 "tools" 节点
return {
"messages": [fake_tool_call],
"jump_to": "tools",
}
# 如果不满足触发条件,返回 None,
# 流程正常向下流转(继续让 LLM 思考)
return None
# ============================================================
# after_model:在模型(LLM)执行生成之后触发
# 允许重新跳转回 "model" 节点
# ============================================================
@after_model(can_jump_to=["model"])
def retry_with_extra_instruction(
state: AgentState,
runtime: Runtime,
) -> dict[str, Any] | None:
"""
【业务场景:反思/重试机制】
如果大模型已经生成了回答,
但发现用户最初的请求包含 "retry model",
则动态追加一条系统提示词,
强行让模型重新生成(重试)一次。
"""
# 倒序遍历消息历史,
# 找到最近的一次用户输入(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"
)
# 返回更新后的状态:
# 1. 追加强力约束的系统消息
# 2. 将指针跳回 "model"
# 3. 重新执行模型
return {
"messages": [
SystemMessage(
content="你必须以【二次回答】开头,并且只用一句话回答。"
)
],
"jump_to": "model",
}
return None
# ============================================================
# before_model:在模型(LLM)执行前触发
# 允许直接跳转到 "end" 节点(强行终止)
# ============================================================
@before_model(can_jump_to=["end"])
def overflow_context_processor(
state: AgentState,
runtime: Runtime,
) -> dict[str, Any] | None:
"""
【业务场景:安全卫士/异常拦截】
模拟上下文窗口溢出(Token 超限)
或其他严重的系统阻断情况。
一旦触发,直接熔断流程,
拒绝让大模型继续处理,
直接返回兜底文案。
"""
# 假装发生上下文溢出,
# 模拟检查最后一条消息是否包含 overflow 标识
text = state["messages"][-1].content
if isinstance(text, str) and "overflow" in text.lower():
print(
"[MIDDLEWARE] before_model: "
"jump_to='end' when context window overflow"
)
# 构造兜底的结束消息,
# 并直接指定跳转到 "end" 终止 Agent 运行
return {
"messages": [
AIMessage(
content="上下文窗口溢出,终止"
)
],
"jump_to": "end",
}
return None
# ============================================================
# 创建 Agent
# ============================================================
agent = create_agent(
model=model,
tools=[get_news],
# 将定义的中间件按照顺序挂载到 Agent 中
# 注意:执行顺序会严格按照列表声明顺序
middleware=[
force_tool_first,
retry_with_extra_instruction,
overflow_context_processor,
],
)
# ============================================================
# 执行 Agent
# ============================================================
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
# 2. 打印 jump_to='tools'
# 3. 绕过 LLM 的首轮思考
# 4. 直接调用 get_news 工具
# 5. 工具返回结果后,LLM 总结工具结果并输出
# ========================================================
print("=" * 30, "-> Case 1 <-", "=" * 30)
run_once(
"请帮我查今日新闻 direct tool"
)
# ========================================================
# Case 2:输出后跳回 model
#
# 预期表现:
# 1. 正常进入 LLM,生成第 1 版回答
# 2. 触发 retry_with_extra_instruction
# 3. 注入 SystemMessage
# 4. jump_to='model'
# 5. LLM 被重新拉回并生成第 2 版回答
# 6. 最终输出应该带有"【二次回答】"
# ========================================================
print("=" * 30, "-> Case 2 <-", "=" * 30)
run_once(
"请随便介绍一下 LangChain retry model"
)
# ========================================================
# Case 3:上下文溢出
#
# 预期表现:
# 1. 触发 overflow_context_processor
# 2. 打印终止信息
# 3. 直接跳转到 end
# 4. 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(
"今日新闻摘要?"
)
============================== -> Case 1 <- ==============================
MIDDLEWARE before_model: jump_to='tools'
================================ Human Message =================================
请帮我查今日新闻 direct tool
================================== Ai Message ==================================
人工构造的消息
Tool Calls:
get_news (call_force_weather_001)
Call ID: call_force_weather_001
Args:
================================= Tool Message =================================
Name: get_news
美加墨世界杯今日开幕
================================== Ai Message ==================================
今日新闻:美加墨世界杯今日开幕。
============================== -> Case 2 <- ==============================
MIDDLEWARE after_model: jump_to='model' with extra system instruction
================================ Human Message =================================
请随便介绍一下 LangChain retry model
================================== Ai Message ==================================
LangChain 里的 "retry model" 通常指两类重试能力:一种是对 **LLM 调用失败** 的重试,另一种是对 **模型输出不符合格式** 的重试修复。
**1. LLM 调用失败重试**
如果模型接口因为网络、限流、服务暂时不可用等原因失败,LangChain 可以自动重试。很多 LangChain 的 runnable/model 都支持 `.with_retry()`。
例如:
```python
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_retry = llm.with_retry(
stop_after_attempt=3
)
response = llm_with_retry.invoke("介绍一下 LangChain")
print(response.content)
```
这里的意思是:调用失败时最多尝试 3 次。它主要解决的是工程层面的不稳定问题,比如超时、429、临时 500 错误。
**2. 输出解析失败重试**
另一种常见场景是:你要求模型输出 JSON、结构化字段或某种格式,但模型返回的内容不符合解析器要求。LangChain 提供了 `RetryOutputParser` 之类的工具,让模型根据原始 prompt 和错误输出再试一次。
大致流程是:
```text
用户请求 -> 模型输出 -> parser 解析失败 -> 把错误和原始要求再发给模型 -> 模型重新生成
```
它不是简单地重复调用,而是会告诉模型哪里错了,让它"修正输出"。
典型用途:
```python
from langchain.output_parsers import RetryOutputParser
```
现在新版 LangChain 更推荐用 LCEL / Runnable 风格来组合 parser、model 和 retry 逻辑。
**3. retry 适合解决什么问题**
适合:
API 临时失败
模型返回格式偶尔不稳定
JSON 解析失败
工具调用参数格式错误
Agent 某一步偶发失败
不适合:
prompt 本身写得不清楚
schema 设计不合理
模型能力不足
业务逻辑错误
每次都失败的确定性错误
也就是说,retry 是"兜底机制",不是 prompt 设计或系统设计的替代品。
**4. 常见参数**
常见会配置这些东西:
```python
.with_retry(
stop_after_attempt=3,
wait_exponential_jitter=True
)
```
含义大概是:
`stop_after_attempt`: 最多重试几次
`wait_exponential_jitter`: 使用指数退避并加随机抖动,避免短时间内反复打爆 API
**5. 实际建议**
生产环境里一般会这样用:
```python
llm = ChatOpenAI(
model="gpt-4o-mini",
timeout=30,
max_retries=2
)
chain = prompt | llm | parser
chain = chain.with_retry(stop_after_attempt=3)
```
如果是结构化输出,最好优先用:
```python
llm.with_structured_output(MySchema)
```
再配合 retry,而不是完全依赖模型"自觉"输出合法 JSON。
简单说,LangChain retry model 的核心价值就是:让 LLM 应用在面对接口抖动和模型输出不稳定时更可靠。但它应该作为可靠性补充,而不是主要控制手段。
================================ System Message ================================
你必须以【二次回答】开头,并且只用一句话回答。
================================== Ai Message ==================================
【二次回答】LangChain retry model 通常用于在模型调用失败或输出解析失败时自动重试,例如通过 `.with_retry(stop_after_attempt=3)` 处理网络、限流、超时等临时错误,或用 `RetryOutputParser` 让模型根据解析错误重新生成结构化结果,但它只适合作为可靠性兜底,不能替代清晰的 prompt、合理的 schema 和正确的业务逻辑。
============================== -> Case 3 <- ==============================
MIDDLEWARE before_model: jump_to='end' when context window overflow
================================ Human Message =================================
你好 overflow
================================== Ai Message ==================================
上下文窗口溢出,终止
============================== -> Case 4 <- ==============================
================================ Human Message =================================
今日新闻摘要?
================================== Ai Message ==================================
Tool Calls:
get_news (call_vvyUBdFjyHQkzyR9whajZWpc)
Call ID: call_vvyUBdFjyHQkzyR9whajZWpc
Args:
================================= Tool Message =================================
Name: get_news
美加墨世界杯今日开幕
================================== Ai Message ==================================
今日新闻摘要:美加墨世界杯今日开幕。
我们提前判定需要调用工具,直接在before_model中跳转至工具节点,省去了一次模型调用
通过约定的 retry model标记 ,在after_model之后再次跳转到模型节点,触发模型重复调用
通过约定的 overflow标记 ,模拟上下文窗口溢出,在before_model中直接跳转至结尾,提前终 止流程
Case 4 是没有被干预的正常Agent流程,作为对照。
- 基于类实现
和基于装饰器实现的关键区别在于:需要引入额外的装饰器 @hook_config 为 can_jump_to 传参。
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 context 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("今日新闻摘要?")