摘要 :本文深入讲解 LangChain Agent 中间件中的
wrap_model_call包裹型钩子。首先介绍其第一个入参ModelRequest的完整字段结构,包括模型实例、消息列表、系统提示词、工具列表、响应格式、Agent 状态与运行时上下文等;随后演示如何通过override方法临时修改请求内容(如动态注入系统提示词、按用户权限过滤工具列表),并说明包裹型钩子如何替代before_model与after_model实现模型调用前后的统一处理;最后梳理各类钩子的执行顺序规则,并给出完整的可运行代码示例,涵盖before_agent敏感词拦截、VIP 用户权限区分、after_modelJSON 修复等实战场景。
内容参考于:图灵AI大模型全栈
它是通过 wrap_model_call 注解来实现

它的第一个入参是一个 ModelRequest 类型,通过它可以得到,下图红框的内容

| 字段 | 类型 | 含义与用途 |
|---|---|---|
model |
BaseChatModel |
本次要调用的聊天模型实例。你可以在 wrap_model_call 中替换成另一个模型。 |
messages |
list[AnyMessage] |
对话消息列表,不包含系统消息 。系统消息被单独放在 system_message 字段。 |
system_message |
`SystemMessage | None` |
tool_choice |
`Any | None` |
tools |
`list[BaseTool | dictstr, Any]` |
response_format |
`ResponseFormatAny | None` |
state |
AgentState[Any] |
当前 Agent 的完整状态。包含消息历史、自定义状态字段等。 |
runtime |
Runtime[ContextT] |
运行时上下文。包含 context、store、stream_writer 等,类型由 ContextT 决定。 |
model_settings |
dict[str, Any] |
模型调用的额外设置,如 temperature、max_tokens、top_p 等。默认空字典。 |
可以通过下图红框的 override 函数对值进行修改,它的修改只是本次生效,是临时的,不会添加到历史中
下图红框它可以修改的内容

在下图红框这一步,也就是调用模型的时候,可以通过override修改它的tools对工具进行过滤,比如原本有20个工具,在这一步过程成5个工具,内容少大模型分析的就会准确,还可以做vip用户工具和普通用户工具区分,如果是vip用户就给它高级的工具,如果是普通用户就给它普通的工具

第二个参数它是一个函数,通过它可以调用大模型,通过它可以实现在调用 handler,也就是调用模型之前和之后做一些处理,这种也叫做包裹型钩子,它可以替代 before_model 和 after_model

这些钩子的执行顺序,如下图红框,

相同类型的钩子是按照下图红框middleware的值上下顺序决定的,上图官网写了 before_和wrap_钩子是按照第一个到最后一个的顺序执行,也就是按照 middleware的值上下顺序,从上往下执行,而after_钩子是从下往上执行

效果图:

代码
python
from dataclasses import dataclass
from langchain.agents import create_agent, AgentState
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
import re
import json
from langgraph.runtime import Runtime
from typing import Callable
from langchain_qwq import ChatQwen
from dotenv import load_dotenv
import os
# 加载模型
# 加载环境变量
load_dotenv()
# 初始化模型
llm = ChatQwen(
model="qwen3.7-flash",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL")
)
# dataclass会自动创建init、repr、eq方法,frozen能够保证对象初始化之后不能修改
@dataclass(frozen=True)
class Context:
user_id: int
user_permissions: str
# 引入装饰器
# 从 LangChain 的 Agent 中间件模块中导入以下组件:
from langchain.agents.middleware import (
before_agent, # 装饰器:在 Agent 开始执行前触发一次。适合初始化、权限校验、日志、修改初始状态或强制跳转。
after_agent, # 装饰器:在 Agent 执行结束后触发一次。适合资源清理、最终日志、结果后处理等。
before_model, # 装饰器:每次调用模型前触发。适合修改消息、动态调整提示词、注入上下文等。
after_model, # 装饰器:每次模型返回后触发。适合输出校验、内容过滤、解析结果、记录日志等。
wrap_model_call, # 装饰器:包裹模型调用。可在模型调用前后插入逻辑,如重试、缓存、限流、监控、修改请求/响应。
wrap_tool_call, # 装饰器:包裹工具调用。可在工具执行前后插入逻辑,如参数校验、权限检查、重试、结果处理。
ModelRequest, # 类型:模型请求对象。通常包含 messages、tools、配置等,供 wrap_model_call 中读取或修改。
ModelResponse, # 类型:模型响应对象。通常包含生成消息、工具调用等,供 wrap_model_call 中读取或修改。
AgentMiddleware, # 基类:用于以类的方式定义中间件,可集中组织多个钩子方法(before/after/wrap 等)。
)
# 通过 before_agent 注解设置进入Agent之前调用的函数
# 它的 can_jump_to 参数表示当前函数可以跳转到什么节点中
# 通过返回 jump_to 这个字段来实现跳转
# can_jump_to是告诉LangChain可以跳转什么节点
# 返回 jump_to 来实现跳转
# 如果 不写 can_jump_to 那么 jump_to 会无效
# can_jump_to 有三个值,如下
# "tools"表示可以跳转到工具节点,进入Agent之前和之后不可以跳转tools
# "model"表示可以跳转到模型节点或进入模型之前的钩子
# "end"表示跳转到Agent末尾 或 第一个进入Agent之后的钩子,也就是用来结束Agent的执行
@before_agent(can_jump_to=[])
def manage_human_message_before_agent(state: AgentState, runtime: Runtime[Context]):
"""
在Agent启动之前调用
Runtime:用来传递全局变量(只是用来读取的内容-上下文的常量),还经常用来传递数据库连接池、日志对象一些配置信息
"""
# 从后往前找第一个类别为 HumanMessage 的消息
user_content = ""
for message in reversed(state["messages"]):
if isinstance(message, HumanMessage):
user_content = message.content
# 打印用户最新的问题
print(f"在before_agent中,用户最新问题:{user_content}")
# 1.处理用户敏感用词
sensitive_words = ["TM", "TMD", "CNM", "挂了", "垃圾"]
if any(word in user_content.upper() for word in sensitive_words):
# 发现敏感词,直接构造一个 AI 响应,不再交给模型思考
return {
"messages": [AIMessage(content="检测到不当言论,请文明交流。")],
"jump_to": "end" # 直接结束这次对话
}
# 2.vip用户特殊处理
# 获取用户权限
user_permissions = runtime.context.user_permissions
if user_permissions == "vip":
# vip权限能够进行所有知识库的访问
print("我是VIP用户,能够查看全部的内容")
else:
print("我是普通用户,能够查看部分的内容")
return None
# 创建json格式验证
def repair_json_string(raw_str: str) -> str:
# 这一行是去掉 markdown 代码块的标记,比如 ```json 和 ```
# r"```json\s*|```" 这个正则里面有一个 |,表示或者
# 前面部分 ```json\s* 匹配的是 ```json 后面跟着空白(空格、换行都算)
# 后面部分 ```匹配的就是三个反引号
# 把匹配到的这些内容都替换成空字符串 "",也就是删掉
# 然后 .strip() 是去掉字符串最前面和最后面的空白
# 比如原来字符串是 "```json\n{\"a\": 1}\n```"
# 经过这一行就变成了 "{\"a\": 1}"
raw_str = re.sub(r"```json\s*|```", "", raw_str).strip()
# 这一行是去掉 json 里面多出来的逗号
# 比如 {"a": 1,} 或者 [1, 2,] 这种最后多一个逗号的情况
# r",\s*([}\]])" 这个正则里面,逗号 , 匹配一个逗号
# \s* 匹配逗号后面的空白,空格换行都行
# ([}\]]) 是一个捕获组,括号里面写的是 } 或者 ],所以它匹配 } 或 ]
# 整个正则匹配到的内容是:逗号 + 空白 + } 或 ]
# 替换成 r"\1",这里的 \1 代表的是第一个捕获组,也就是 ([}\]]) 这个括号匹配到的内容
# 因为 ([}\]]) 这个括号里写的是 } 或者 ],所以 \1 就是 } 或者 ]
# 这样替换后,逗号和空白就被删掉了,只剩下 } 或 ]
# 比如 {"a": 1,} 整个正则匹配到 ",}",其中 ([}\]]) 匹配到 "}",替换成 \1 就是 "}",所以变成 {"a": 1}
# 比如 [1, 2,] 整个正则匹配到 ",]",其中 ([}\]]) 匹配到 "]", 替换成 \1 就是 "]", 变成 [1, 2]
raw_str = re.sub(r",\s*([}\]])", r"\1", raw_str)
# ([{,]\s*)([a-zA-Z0-9_]+)(\s*:) 这个正则表达式可以分成三组
# 第一组 ([{,]\s*) 它的作用是匹配文字中的 { 或 ,,后面可跟空白
# 第二组 ([a-zA-Z0-9_]+) 它的作用是匹配所有大小写字母和数字,这里是验证json也就是匹配一个json的key名字
# 第三组 (\s*:) 它的作用是匹配冒号和空格
# 也就是被括号包括起来可以看做出一组,如 (这是一组匹配规则) 这样
# 连起来就是匹配这样的内容 {jjjj :
# 正则表达式匹配完成后按照 \1"\2"\3 方式替换
# \1代表第一组的内容也就是 ([{,]\s*) 匹配出来的内容
# \2代表第二组的内容也就是([a-zA-Z0-9_]+)
# \3代表第三组的内容也就是(\s*:)
# 注意\2被加了双引号,假设正则表达式匹配出了 {jjjj : 这样的内容,经过 \1"\2"\3 替换后就变成了 {"jjjj" : 这样
# \1 的内容是{
# \2的内容是jjjj
# \3的内容是 :
raw_str = re.sub(r"([{,]\s*)([a-zA-Z0-9_]+)(\s*:)", r'\1"\2"\3', raw_str)
return raw_str
# 模型输出后
@after_model
def fix_json_structure(state: AgentState, runtime: Runtime[Context]):
# 1. 获取模型最后一条回复
last_message = state["messages"][-1]
if not isinstance(last_message, AIMessage):
return
# 这里我们模拟模型返回错误的json
# raw_content = last_message.content
raw_content = """
```json
{
"user_id": "123",
"action": "send_package",
"items": ["book", "pen"],
}
"""
print(f"开始进行json格式修复:{raw_content}")
try:
# 解析json,如果解析失败会出错,出错就会执行except里面的逻辑
json.loads(raw_content)
except json.JSONDecodeError:
# 修复json
fixed_content = repair_json_string(raw_content)
print(f"json格式修复完成:{fixed_content}")
try:
# 再次验证修复结果
json.loads(fixed_content)
# 【关键】写回消息对象
last_message.content = fixed_content
# 也可以记录一个标记位说明发生过修正
last_message.additional_kwargs["is_fixed"] = True
except Exception:
# 如果修复后还是不行,可以抛出异常触发重试,或记录错误
pass
return {"messages": state["messages"]}
@wrap_model_call
def smart_model_wrapper(
# ModelRequest类型可以得到模型信息、当前消息、当前系统提示词、工具、工具描述、结构化输出、state、runtime
request: ModelRequest,
# Callable表示函数类型,[ModelRequest]函数的入参类型,ModelResponse函数返回值的类型
# 连起来说就是当前handler变量是一个函数,入参是ModelRequest类型,返回值是ModelResponse
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
user_preference = "用户喜欢二次元"
context_msg = SystemMessage(content=f"用户偏好:{user_preference}。请根据此偏好回答问题。")
# 通过override可以临时修改request里面的内容,它修改的内容只会在本次生效
new_request = request.override(
system_message=context_msg
)
# 调用真正执行模型的内容
response = handler(new_request)
# 模拟 after_model 的逻辑:结构化修正 (可选)
# 大模型返回的内容
return response
# 创建智能体
agent = create_agent(model=llm,
middleware=[manage_human_message_before_agent,
fix_json_structure,
smart_model_wrapper,])
result = agent.invoke({"messages": [HumanMessage("你好,我是计算机王")]}, context=Context(user_permissions="vip", user_id=1))
# 获取AI回复
print(result["messages"][-1].content)
print(result)
