128.Agent-LangChain核心组件-中间件-调用模型的时候

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

内容参考于:图灵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)

相关推荐
似梦的苏烟1 小时前
gradio快速入门
agent
布吉岛的石头1 小时前
Java 程序员第 49 阶段5:BERT 预训练目标 MLM+NSP 的工程含义
java·人工智能·深度学习·bert·transformer
玫瑰互动GEO1 小时前
开发者学GEO优化:从算法基础到CoT推理链
大数据·人工智能·ai·geo·ai搜索·geo优化
Zentceh2 小时前
安防监控夜视方案2026:AI全彩+边缘计算+云平台
图像处理·人工智能·科技·计算机视觉·车载系统·无人机·边缘计算
voipmaker2 小时前
AI 赋能安防系统演进:从硬件单品到场景化融合调度架构
人工智能·ai·架构
做cv的小昊2 小时前
【World Model】π0.5:a Vision-Language-Action Model with Open-World Generalization
人工智能·算法·机器学习·大模型·多模态·vla·世界模型
光锥智能2 小时前
具身智能数据,众包时代来了?
人工智能
大嘴皮猴儿2 小时前
2026年9月行业洞察:适合亚马逊跨境卖家的免费自动图片翻译平台
大数据·人工智能·跨境电商ai跨境翻译网站哪个好·ai图片翻译网站推荐·ai图片翻译工具哪个好
莪_幻尘2 小时前
Skill 体检:30 个 Skill 全凭感觉?体检器先自曝了 8 个“假 0 分
前端·人工智能·llm