LangChain 中间件入门:六个钩子让 Agent 可插拔
搞 LangChain Agent 的人多少都撞过这面墙:想给 Agent 加点日志、做权限校验、过滤敏感词、统计调用次数,结果要么往
create_agent里塞一堆参数,要么直接改 Agent 源码,改完还不敢升版本。LangChain 的 Middleware(中间件)就是来解开这个死结的------不动 Agent 一行代码,在执行的各个环节插进你自己的逻辑。本文带你从零看懂中间件是什么、六个钩子各管什么,并用能直接跑通的例子把最常用的四个观察型钩子用起来。
一、先厘清:Middleware 到底在解决什么
先说结论:中间件是挂在 Agent 执行流程上的钩子(Hook),让你在特定时间点插入自定义逻辑,而不需要修改 Agent 本身的代码。
要理解它,先得看清 Agent 的执行循环长什么样。create_agent 本质上是一个基于 LangGraph 的 ReAct 循环,核心链路就五步:
1. 用户输入
2. 模型思考
3. 可能调用工具
4. 模型再思考
5. 输出结果
这个循环能力很强,但它很"黑盒"------你很难在这五步之间塞逻辑。以前的做法是继承 Agent 类去改,或者干脆换用 LangGraph 显式画图。代价都很高。
中间件就是这层"可插拔胶水"。它像 Web 开发里的中间件一样,是个小插件,在请求(这里指 Agent 循环)的各个关口拦截、追加逻辑。官方文档把它分成三类:
| 类别 | 钩子 | 说明 |
|---|---|---|
| Node-style | before_agent、before_model、after_model、after_agent |
在固定执行点顺序运行,偏"观察/注入" |
| Wrap-style | wrap_model_call、wrap_tool_call |
包裹真实调用,偏"接管/控制" |
| Convenience | dynamic_prompt |
动态生成 system prompt 的快捷钩子 |
🔴 重点:Node-style 是"看"和"加",Wrap-style 是"控"。 前者在模型/工具调用前后跑一段代码,后者直接把调用包起来,可以决定"要不要真的调、调几次、用什么调"。你先掌握前者的思维,进阶篇再进入后者。
让我把这些钩子挂到执行循环上看,就一目了然了:
1. 用户输入
↓ [before_agent: 一次性。初始化、权限检查、输入预处理]
2. 模型思考
↓ [before_model: 每次循环。消息裁剪、内容过滤、上下文注入]
↓ [wrap_model_call: 每次循环。重试、降级、缓存、改写请求]
↓ [after_model: 每次循环。响应审核、内容追加、日志]
3. 工具执行
↓ [wrap_tool_call: 每次工具调用。工具重试、结果缓存、参数改写]
4. 回到模型思考(循环直到完成)
↓ [after_agent: 一次性。结果格式化、统计、清理]
5. 输出结果
二、六个钩子全景:一张表看懂
| 钩子 | 执行频率 | 执行位置 | 主要用途 |
|---|---|---|---|
before_agent |
一次 | Agent 开始前 | 初始化、权限检查、输入预处理 |
before_model |
每次循环 | 模型调用前 | 消息预处理、动态上下文注入 |
wrap_model_call |
每次循环 | 包裹模型调用 | 重试、降级、缓存、请求改写 |
after_model |
每次循环 | 模型调用后 | 内容审核、响应过滤、日志 |
wrap_tool_call |
每次工具调用 | 包裹工具执行 | 工具重试、结果缓存、参数改写 |
after_agent |
一次 | Agent 结束后 | 格式化输出、统计、清理资源 |
这里的关键记忆点是执行频率:
- Agent 级 (
before_agent/after_agent):每个问题只跑一次,是整个"会话"的入口和出口。 - 循环级 (
before_model/after_model):每调一次模型就跑一次。一个需要工具的提问会调两次模型(先思考要不要调工具、再整合结果),所以这两个钩子也会各跑两次。 - 调用级 (
wrap_tool_call):每个工具真正执行一次就跑一次。
✅ 判断标准:"这个问题要不要算一次完整会话?" 决定用哪个层级。记录会话总次数用 Agent 级;记录每次模型调用、做上下文控制用循环级。
三、两种使用方式
3.1 装饰器(推荐,最直观)
装饰器方式最大优点是简单:函数签名统一是 (state, runtime),你要改消息就操作 state,要对外发事件就用 runtime,runtime相关内容参考:langchain-runtime
python
from langchain.agents.middleware import before_model, after_model
@before_model
def log_before(state, runtime):
"""在每次模型调用前记录日志"""
msg_count = len(state.get("messages", []))
print(f"[before_model] 当前消息数: {msg_count}")
return None
@after_model
def log_after(state, runtime):
"""在每次模型调用后记录日志"""
last_msg = state["messages"][-1] if state.get("messages") else None
if last_msg and hasattr(last_msg, 'tool_calls') and last_msg.tool_calls:
print(f"[after_model] 模型请求了工具调用")
return None
3.2 类继承(复杂逻辑、需要状态时)
当你要在多个钩子之间共享状态、或逻辑很重时,继承 AgentMiddleware 更清晰:
python
from langchain.agents.middleware import AgentMiddleware
class LoggingMiddleware(AgentMiddleware):
"""自定义日志中间件"""
@property
def name(self) -> str:
return "logging"
def before_agent(self, state, runtime):
print("[Logging] Agent 开始执行")
return None
def before_model(self, state, runtime):
msg_count = len(state.get("messages", []))
print(f"[Logging] 准备调用模型,当前 {msg_count} 条消息")
return None
def after_model(self, state, runtime):
print("[Logging] 模型调用完成")
return None
def after_agent(self, state, runtime):
print("[Logging] Agent 执行结束")
return None
💡
name属性可自定义中间件名称(默认是类名),在多中间件日志里用来区分谁是谁。
两种方式都能通过 create_agent(middleware=[...]) 挂载。若可读性优先用装饰器;若逻辑复杂、钩子多、要跨钩子记状态,用类。
四、Agent 级钩子:@before_agent 与 @after_agent
这两个钩子各执行一次,是完整 Agent 会话的开头和结尾。适合做一次性的准备工作、收尾统计。
4.1 @before_agent:开始前的准备
能力:输入预处理、用户信息验证、资源初始化、权限检查。
场景一:输入预处理------自动修正用户输入
python
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain.agents.middleware import before_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool
@before_agent
def preprocess_input(state, runtime):
"""在 Agent 开始前处理用户输入"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
# 这里可以做清洗、归一化、补全上下文等
return None
@tool
def search_course(keyword: str) -> str:
"""在菜鸟教程 RUNOOB 搜索课程"""
courses = {
"python": "Python3 基础教程(免费,30章)",
"html": "HTML 基础教程(免费,25章)",
}
return courses.get(keyword.lower(), f"未找到 {keyword} 相关课程")
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[search_course],
middleware=[preprocess_input],
system_prompt="你是菜鸟教程 RUNOOB 的课程顾问。",
)
result = agent.invoke({"messages": [HumanMessage(content="Python 课程")]})
print(f"回复: {result['messages'][-1].content}")
场景二:访问控制------权限检查
before_agent 最实用的场景------在 Agent 真正开始前就拦截,避免浪费模型调用。返回带 jump_to="end" 的字典可以提前终止整个 Agent。
python
from langchain.agents.middleware import before_agent
from langchain.messages import HumanMessage
@before_agent
def access_control(state, runtime):
"""检查用户是否有权限使用 Agent"""
context = runtime.context
if context is None:
return None
user_role = context.get("user_role", "guest")
if user_role == "guest":
messages = state.get("messages", [])
if messages:
last_content = str(messages[-1].content)
restricted_keywords = ["删除", "管理", "配置", "admin"]
if any(kw in last_content for kw in restricted_keywords):
return {
"jump_to": "end",
"messages": [HumanMessage(
content="您当前的权限不足,无法执行此操作。请登录后重试。"
)]
}
return None
4.2 @after_agent:完成后的处理
能力:格式化最终输出、记录统计信息、清理资源。
场景三:统计分析------记录对话数据
这个场景引入了 runtime.stream_writer,用来在流式输出里推送自定义事件。要接收它,需用 agent.stream(..., stream_mode=["updates", "custom"])。
python
from langchain.agents.middleware import after_agent
@after_agent
def conversation_stats(state, runtime):
"""统计对话信息并追加到结果中"""
messages = state.get("messages", [])
model_calls = 0
tool_calls = 0
total_chars = 0
for msg in messages:
if msg.type == "ai":
model_calls += 1
if hasattr(msg, 'tool_calls') and msg.tool_calls:
tool_calls += len(msg.tool_calls)
if hasattr(msg, 'content') and msg.content:
total_chars += len(str(msg.content))
runtime.stream_writer({
"type": "stats",
"model_calls": model_calls,
"tool_calls": tool_calls,
"total_messages": len(messages),
"total_chars": total_chars,
})
return None
# 使用 stream_mode=["updates", "custom"] 接收自定义事件
for mode, chunk in agent.stream(
{"messages": [HumanMessage(content="查一下 Python 课程")]},
stream_mode=["updates", "custom"],
):
if mode == "custom" and chunk.get("type") == "stats":
print(f"统计信息: {chunk}")
运行结果:
统计信息: {'type': 'stats', 'model_calls': 2, 'tool_calls': 1,
'total_messages': 4, 'total_chars': 127}
场景四:格式化输出------统一回复风格
在 after_agent 里可以替换/追加最终消息,比如给每次回答统一加个页脚:
python
from langchain.agents.middleware import after_agent
from langchain.messages import AIMessage
@after_agent
def format_output(state, runtime):
"""在结果中追加格式化的总结信息"""
messages = state.get("messages", [])
if not messages:
return None
last_ai = None
for msg in reversed(messages):
if msg.type == "ai" and msg.content:
last_ai = msg
break
if last_ai:
tool_msgs = [m for m in messages if m.type == "tool"]
tool_count = len(tool_msgs)
footer = (
f"\n\n---\n"
f"> 本次对话共进行 {len(messages)} 条消息,"
f"调用了 {tool_count} 次工具。\n"
f"> 由菜鸟教程 RUNOOB AI 助手提供支持。"
)
return {
"messages": [AIMessage(content=last_ai.content + footer)]
}
return None
运行结果:
菜鸟教程 RUNOOB 中有 Python3 基础教程,共30章,完全免费,非常适合 Python 初学者入门学习。
---
> 本次对话共进行 4 条消息,调用了 1 次工具。
> 由菜鸟教程 RUNOOB AI 助手提供支持。
五、模型级钩子:@before_model 与 @after_model
这两个是最常用的 钩子------它们在每次模型调用前后执行,天然适合做内容把关和上下文管理。注意它是循环级的,一个带工具的提问会触发两次。
5.1 @before_model:模型调用前拦截
能力:修改消息、注入上下文条件、或直接跳过模型调用。
场景一:消息预处理------限制对话长度
长对话会把上下文撑爆、白白烧 token。在 before_model 里裁剪历史消息是最常见的做法:
python
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain.agents.middleware import before_model
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
@before_model
def limit_context(state, runtime):
"""限制消息历史长度,防止上下文过长"""
messages = state.get("messages", [])
MAX_MESSAGES = 6
if len(messages) > MAX_MESSAGES:
trimmed = messages[-MAX_MESSAGES:]
if trimmed and trimmed[0].type != "human":
trimmed = trimmed[1:]
return {"messages": trimmed}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
middleware=[limit_context],
system_prompt="你是菜鸟教程 RUNOOB 的助手。",
)
result = agent.invoke({"messages": [
HumanMessage(content="第一轮"),
HumanMessage(content="第二轮"),
HumanMessage(content="第三轮"),
HumanMessage(content="第四轮"),
HumanMessage(content="第五轮"),
HumanMessage(content="第六轮"),
HumanMessage(content="第七轮"),
]})
print(f"消息数: {len(result['messages'])}")
print(f"回复: {result['messages'][-1].content}")
运行结果:
消息数: 8
回复: 你好!看起来你正在进行多轮对话测试。有什么可以帮你的吗?
场景二:内容过滤------屏蔽敏感词
配合 jump_to="end" 可以在模型介入前直接拦截,既安全又省钱:
python
from langchain.agents.middleware import before_model
SENSITIVE_WORDS = ["密码", "银行卡号", "身份证号"]
@before_model
def content_filter(state, runtime):
"""检查用户消息是否包含敏感词,如果包含则拦截"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
for word in SENSITIVE_WORDS:
if word in content:
print(f"[拦截] 检测到敏感词: {word}")
return {
"jump_to": "end",
"messages": [HumanMessage(
content=f"抱歉,为了安全,不能处理包含「{word}」的请求。"
)]
}
return None
5.2 @after_model:模型调用后处理
能力:审核模型输出、提取关键信息、追加后续指令。
场景三:响应内容审核
after_model 可以替换模型输出------比如检测到回复涉及敏感话题时,用预设话术覆盖:
python
from langchain.agents.middleware import after_model
FORBIDDEN_TOPICS = ["政治", "暴力", "色情"]
@after_model
def response_audit(state, runtime):
"""审核模型回复,如果涉及禁止话题则替换"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
content = str(last_msg.content) if hasattr(last_msg, 'content') else ""
for topic in FORBIDDEN_TOPICS:
if topic in content:
runtime.stream_writer({
"type": "warning",
"message": f"检测到回复包含「{topic}」相关内容,已被替换"
})
from langchain.messages import AIMessage
return {
"messages": [AIMessage(
content="抱歉,我无法回答这个问题。请询问编程学习相关的内容。"
)]
}
return None
场景四:自动追加提示信息
这是个高频需求------每次模型回复后统一加个免责声明。注意判断条件:只在最终回复 (没有 tool_calls 时)追加,避免把页脚加到工具调用中间环节的消息上。
python
from langchain.agents.middleware import after_model
from langchain.messages import AIMessage
@after_model
def append_disclaimer(state, runtime):
"""在每次模型回复后自动追加免责声明"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
if (last_msg.type == "ai"
and last_msg.content
and not (hasattr(last_msg, 'tool_calls') and last_msg.tool_calls)):
return {
"messages": [AIMessage(
content=(
last_msg.content
+ "\n\n---\n*以上内容由菜鸟教程 RUNOOB AI 助手生成,仅供参考。*"
)
)]
}
return None
⚠️ 细节点:
after_model返回的 messages 走的是add_messagesreducer(追加而非覆盖)。所以上面用"替换为一条新的 AIMessage"等价于"把最后一条 AI 回复换成带页脚的新回复"。这个机制在下一节统一讲。
六、Middleware 的返回值机制
这一节是理解中间件"能不能改状态、能不能控流程"的核心。中间件的返回值决定了后续行为:
| 返回值 | 效果 | 示例 |
|---|---|---|
None |
不修改任何状态,继续正常流程 | 纯日志记录 |
dict |
更新 Agent 状态(合并到当前状态) | return {"custom_field": "value"} |
含 jump_to 的 dict |
跳转到指定节点 | return {"jump_to": "end"} |
关键细节:
- 返回的
dict会通过 Agent 状态的 reducer 合并。 - 对于
messages字段,用的是add_messagesreducer ------所以返回的 messages 会追加而非覆盖。 jump_to配合can_jump_to使用:在before_agent、before_model、after_model中,通过can_jump_to参数声明可以跳转的目标,这是一种安全机制,防止中间件意外跳到不合法节点。
python
from langchain.agents.middleware import before_model
@before_model(can_jump_to=["end"]) # 声明可以跳转到的目标
def conditional_exit(state, runtime):
"""在特定条件下直接结束 Agent"""
messages = state.get("messages", [])
if not messages:
return None
last_content = str(messages[-1].content)
if last_content.strip() in ["再见", "拜拜", "bye"]:
return {
"jump_to": "end",
"messages": [{"role": "assistant", "content": "再见!期待下次为您服务。"}]
}
return None
can_jump_to 可选值一览:
| can_jump_to 值 | 含义 | 适用场景 |
|---|---|---|
["end"] |
可跳转到结束 | 条件退出、安全拦截 |
["model"] |
可跳转回模型 | 需要让模型重新处理 |
["tools"] |
可跳转到工具节点 | 跳过模型直接执行工具 |
["model", "end"] |
可跳转到模型或结束 | 多种条件分支 |
⚠️ 避坑 :如果不在
can_jump_to里声明目标,jump_to会被忽略。这是官方的安全设计,别忘了声明。
七、完整协作示例
把这四个观察型钩子串起来,看它们在一个 Agent 里如何按顺序协作:
python
from dotenv import load_dotenv
load_dotenv()
from langchain.agents.middleware import (
before_agent, after_agent, before_model, after_model
)
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool
@before_agent
def init_session(state, runtime):
print(">>> [before_agent] 会话开始")
return None
@before_model
def pre_model_check(state, runtime):
msg_count = len(state.get("messages", []))
print(f" -> [before_model] 消息数: {msg_count}")
return None
@after_model
def post_model_check(state, runtime):
last = state["messages"][-1] if state.get("messages") else None
if last and hasattr(last, 'tool_calls') and last.tool_calls:
tools = [tc['name'] for tc in last.tool_calls]
print(f" <- [after_model] 请求工具: {tools}")
return None
@after_agent
def finish_session(state, runtime):
total = len(state.get("messages", []))
print(f"<<< [after_agent] 会话结束,共 {total} 条消息")
return None
@tool
def get_weather(city: str) -> str:
"""查询天气"""
return f"{city}: 晴"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[init_session, pre_model_check, post_model_check, finish_session],
system_prompt="你是助手。",
)
result = agent.invoke({"messages": [HumanMessage(content="杭州天气?")]})
print(f"\n最终回复: {result['messages'][-1].content}")
运行结果:
>>> [before_agent] 会话开始
-> [before_model] 消息数: 2
<- [after_model] 请求工具: ['get_weather']
-> [before_model] 消息数: 3
<<< [after_agent] 会话结束,共 4 条消息
最终回复: 杭州今天晴天,适合出行。
从输出能看到关键规律:
before_agent/after_agent:整个会话只执行一次。before_model/after_model:每次模型调用都执行。这个提问需要工具(先思考要不要调 → 再整合结果),所以各执行了两次。- 顺序正好印证了官方的执行模型:before 正序 → 模型调用 → after 逆序,Agent 级包在最外层。
八、总结:你真正需要记住的 6 件事
- 中间件 = 挂在 Agent 执行流程上的钩子,不侵入 Agent 代码,就能加日志、鉴权、过滤、统计等横切逻辑。
- 钩子分两类 :Node-style(
before_agent/before_model/after_model/after_agent)偏观察,Wrap-style(wrap_model_call/wrap_tool_call)偏接管。 - 按频率记钩子 :Agent 级一次(
before_agent/after_agent),循环级每次模型调用(before_model/after_model),调用级每次工具执行(wrap_tool_call)。 - 返回值三档 :
None不改状态,dict合并状态,含jump_to的 dict 控制流程。 jump_to要配can_jump_to声明 ,否则会被忽略;messages走add_messagesreducer,返回即追加。- 两种写法 :简单用装饰器(函数签名
(state, runtime)),复杂用AgentMiddleware类继承。
验证清单
- 把
middleware=[...]传给create_agent时,确认@before_agent只被打了一次 - 用带工具的提问测试,确认
before_model/after_model各执行两次 - 在
before_agent里写权限检查,确认返回{"jump_to": "end"}能提前结束 Agent - 在
before_model里做长度裁剪,确认长对话下消息数被限制 - 用
agent.stream(stream_mode=["updates", "custom"])验证runtime.stream_writer能推送自定义事件 - 测试含
jump_to的返回时,确认已声明对应的can_jump_to
参考资源
- LangChain 官方文档 - Custom middleware:https://docs.langchain.com/oss/python/langchain/middleware/custom
- LangChain 官方文档 - Agents:https://docs.langchain.com/oss/python/langchain/agents
- LangChain Reference - middleware:https://reference.langchain.com/python/langchain/middleware
- 菜鸟教程 - LangChain 中间件教程:https://www.runoob.com/langchain/