2 LangChain 1.0 中间件(Middleware)- wrap_model_call、wrap_tool_call

一、问题理解与目标定义

核心问题 :厘清 wrap_model_call 和 wrap_tool_call 两个中间件的职责边界、适用场景及实现差异。
分析目标:帮助开发者在实际项目中准确选择和使用这两个钩子,避免误用,并通过 Python 案例展示其典型应用。

二、中间件功能对比

维度 wrap_model_call wrap_tool_call
拦截对象 大语言模型(LLM)的调用过程 Agent 工具(Tool)的执行过程
触发时机 每次 Agent 调用 LLM 前后(包裹整个调用) 每次 Agent 决定调用某个工具时(包裹该工具执行)
控制粒度 模型输入/输出、模型选择、重试、缓存等 工具参数校验、权限控制、审计、重试等
典型场景 动态模型路由、请求缓存、Token 限制、故障降级 敏感操作审批、PII 脱敏、工具限流、日志记录
是否可修改请求/响应 ✅ 可完全拦截并替换模型调用逻辑 ✅ 可拦截工具调用,修改参数或结果

💡 关键区别:

  • wrap_model_call 关注 "Agent 如何思考"(模型推理层)
  • wrap_tool_call 关注 "Agent 如何行动"(工具执行层)

3 中间件实现对比

3.1 wrap_model_call:动态模型路由 + 缓存

  • 使用场景说明:
    • 当对话历史较短时,使用便宜模型节省成本;
    • 当上下文较长或任务复杂时,自动切换到高性能模型保证质量;
    • 对相同输入缓存结果,避免重复调用。
python 复制代码
# -*- coding: utf-8 -*-
# 运行的时候动态的选择模型

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.tools import tool
from langchain_community.callbacks import get_openai_callback
from langchain.chat_models import init_chat_model
from langchain.agents.middleware import AgentMiddleware, ModelRequest
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.messages import AIMessage
import hashlib
from typing import Optional, Callable
import asyncio
import os
from dotenv import load_dotenv
load_dotenv(override=True)


simple_model = init_chat_model(model="deepseek-v3.1", #
                               model_provider='openai',
                               api_key= os.getenv("api_key"),
                               base_url= os.getenv("base_url"),
                               temperature=0.3,
                               max_retries=4,
                               #max_tokens=10
                               )

complex_model = init_chat_model(model="gpt-4.1-mini", #
                                model_provider='openai',
                                api_key= os.getenv("api_key"),
                                base_url= os.getenv("base_url"),
                                temperature=0.3,
                                max_retries=4,
                                #max_tokens=10
                                )

# 全局缓存(生产环境建议用 Redis),对于已经问过的问题 可以直接返回
_MODEL_CACHE = {}

# 1. 系统提示词
system_prompt = """你是一位幽默的天气预报员。
根据天气给出穿衣建议,用轻松的方式表达。"""


@wrap_model_call
def dynamic_model_select(request: ModelRequest, handler)->ModelResponse:
    """
    根据任务复杂度选择模型
    ModelRequest:封装了输入数据(如 messages)、模型配置(如 model)、会话状态(state)等。
    ModelResponse:标准化输出格式,确保中间件能统一处理。

    wrap_model_call属于 Wrap-Style Hook,其调用时机如下:
    触发条件:每次 Agent 尝试调用模型(如 handler(request))时,该中间件会包裹整个模型调用过程。
    执行顺序:在模型实际执行前(handler(request) 调用前)和返回结果后均可插入逻辑。
    """

    # 获取状态
    state = request.state
    messages = state.get("messages", [])

    # 判断任务复杂度
    if is_complex_task(messages):
        #request.model = complex_model
        request.override(model=complex_model)
        print("使用复杂的模型GPT-mini")
    else:
        request.override(model=simple_model)
        print("使用简单的模型deepseekV3")

    # 生成缓存键
    print("当前的消息:", messages[-1].content)
    cache_key = hashlib.md5(str(messages[-1].content).encode()).hexdigest()

    # 缓存命中
    if cache_key in _MODEL_CACHE:
        print(f"[wrap_model_call] 缓存命中: {cache_key[:8]}")
        return _MODEL_CACHE[cache_key]
        #return AIMessage(content=_MODEL_CACHE[cache_key])

    response = handler(request)
    _MODEL_CACHE[cache_key] = response
    # 调用 handler(request) 会将修改后的请求传递给下一个中间件或最终模型,确保逻辑可组合。
    return response


def is_complex_task(messages):
    """判断任务是否复杂"""
    if not messages:
        return False

    last_message = messages[-1].content if hasattr(messages[-1], 'content') else str(messages[-1])

    # 复杂任务判断逻辑
    complex_keywords = ["分析", "比较", "优化", "设计", "架构", "多步骤"]

    # 长度超过 200 字或包含复杂关键词
    if len(last_message) > 200:
        return True

    for keyword in complex_keywords:
        if keyword in last_message:
            return True

    return False

# 2. 定义工具
@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气"""
    return f"{city}:晴,25度,微风徐徐"

@tool
def get_location() -> str:
    """获取用户位置"""
    return "北京"

# 4. 添加记忆
checkpointer = InMemorySaver()


agent = create_agent(
    model=simple_model,  # 默认模型(会被中间件覆盖)
    tools=[get_weather,get_location],
    middleware=[dynamic_model_select],
)


config = {"configurable": {"thread_id": "user-001"}}
for event in agent.stream(
        {"messages": [{"role": "user", "content": "我在哪里?天气如何?分析一下"}]},
    config=config,
    stream_mode="values"
    ):
    event['messages'][-1].pretty_print()


print("*******************************************************")

config = {"configurable": {"thread_id": "user-001"}}
for event in agent.stream(
        {"messages": [{"role": "user", "content": "我在哪里?天气如何?分析一下"}]},
        config=config,
        stream_mode="values"
):
    event['messages'][-1].pretty_print()

3.2 wrap_tool_call:工具权限控制 + 参数脱敏

使用场景说明:

  • 阻止非授权用户执行高危操作(如删除、转账);
  • 在工具调用前后记录审计日志,满足合规要求;
  • 对敏感参数(如手机号、邮箱)进行日志脱敏,防止数据泄露。
python 复制代码
# -*- coding: utf-8 -*-
"""
agent_with_memory
Author: user
Date: 2026/3/16
Description:
"""
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.tools import tool
from langchain_community.callbacks import get_openai_callback
from langchain.chat_models import init_chat_model
from langchain.agents.middleware import wrap_tool_call
import re
import os
from dotenv import load_dotenv
load_dotenv(override=True)

model = init_chat_model(model="deepseek-v3.1", #
                       model_provider='openai',
                       api_key= os.getenv("api_key"),
                       base_url= os.getenv("base_url"),
                       temperature=0.3,
                       max_retries=4,
                       #max_tokens=10
                       )

# 1. 系统提示词
system_prompt = """你是一位幽默的天气预报员。
根据天气给出穿衣建议,用轻松的方式表达。"""

# 2. 定义工具
@tool
def get_weather(city: str) -> str:
   """获取指定城市的天气"""
   return f"{city}:晴,25度,微风徐徐"

@tool
def get_location() -> str:
   """获取用户位置"""
   return "北京"
@tool
def send_email(email:str,user:str,content:str):
   """
   给用户发送邮件
   :param email: 用户email
   :param user: 用户名字
   :param content: 内容
   :return:
   """
   return f"已经给用户{user} {email},发功成功!内容是:{content}"

@wrap_tool_call
def secure_tool_execution(request,call):
   """
   对敏感工具调用进行权限校验和参数脱敏
   """
   print(request)
   tool_name = request.tool.name
   args = request.tool_call.get('args')  # 工具调用参数字典
   if hasattr(request.runtime,'context'):
       user_role = request.runtime.context.get("user_role")
   else:
       user_role='guest'
   # 权限控制:仅管理员可获取用户地址
   if tool_name == "get_location" and user_role != "admin":
       raise PermissionError("无权执行删除操作")

   # 参数脱敏:对 send_email 工具中的邮箱脱敏(审计用)
   if tool_name == "send_email" and "email" in args:
       original_email = args["email"]
       masked_email = re.sub(r'(.{2}).*(@.*)', r'\1***\2', original_email)
       print(f"[wrap_tool_call] 邮箱脱敏: {original_email} → {masked_email}")
       # 注意:此处仅用于日志,实际调用仍用原始参数
       # 若需真正脱敏,应修改 args(但可能破坏功能)

   # 记录审计日志
   print(f"[AUDIT] 用户({user_role}) 调用工具: {tool_name}, 参数: {list(args.keys())}")

   # 执行原始工具调用
   result = call(request)
   return result


# 4. 添加记忆
checkpointer = InMemorySaver()

# 5. 创建 Agent
agent = create_agent(
   model=model,
   tools=[get_weather, get_location,send_email],
   system_prompt=system_prompt,
   checkpointer=checkpointer,
   middleware=[secure_tool_execution]
)

# 6. 运行对话
config = {"configurable": {"thread_id": "user-001"}}


with get_openai_callback() as cb:
   # 使用 stream 方法
   for event in agent.stream(
           {"messages": [{"role": "user", "content": "今天穿什么好?"}]},
           config=config,
           context={"user_role": "admin", "session_id": "sess_123"},
           stream_mode="values"
   ):
       #print(event)
       event['messages'][-1].pretty_print()
   print("\n--- Token Usage ---")
   print(f"Total Tokens: {cb.total_tokens}")
   print(f"Prompt Tokens: {cb.prompt_tokens}")
   print(f"Completion Tokens: {cb.completion_tokens}")
   print(f"Total Cost (USD): ${cb.total_cost:.6f}")
   print(f"Successful Requests: {cb.successful_requests}")

print()
#print("助手:", response1["messages"][-1].content)

# 第二轮对话 - Agent 记住了上下文
# response2 = agent.invoke(
#     {"messages": [{"role": "user", "content": "那我需要带伞吗?"}]},
#     config=config
# )
# print("助手:", response2["messages"][-1].content)

for event in agent.stream(
       {"messages": [{"role": "user", "content": "给小明发送一个邮件(12345678@qq.com)问好!"}]},
       config=config,
       context={'user_role':'guest'},
       stream_mode="values"
):
   #print(event)
   event['messages'][-1].pretty_print()

上面只是一个思想,无法达到真正的脱密。使用 LangChain 内置 PIIMiddleware(最简单)可实现真正的脱敏。

python 复制代码
from langchain.agents.middleware import wrap_tool_call,PIIMiddleware

agent = create_agent(
    model=model,
    tools=[get_weather, get_location,send_email],
    system_prompt=system_prompt,
    checkpointer=checkpointer,
    middleware=[secure_tool_execution,
                PIIMiddleware("email", strategy="redact", apply_to_input=True)]
)

四 对比

场景 推荐中间件 理由
根据上下文长度切换模型 wrap_model_call 直接控制模型选择逻辑
限制模型最大 Token 数 wrap_model_call 可在调用前截断或报错
缓存模型响应 wrap_model_call 包裹整个调用,可返回缓存结果
工具调用前校验用户权限 wrap_tool_call 可访问工具名和用户角色
记录所有数据库查询日志 wrap_tool_call 可捕获工具参数和结果
对支付工具参数做二次加密 wrap_tool_call 可修改 request.tool_args 后再调用
  • wrap_model_call 不适用于工具逻辑
    它只拦截 LLM 调用,无法感知 Agent 是否打算调用工具。
  • wrap_tool_call 无法干预模型决策
    它在模型已决定调用某工具后才触发,不能改变"是否调用"的决策。
  • 两者可协同工作
    典型流程:
    模型思考(wrap_model_call) → 决定调用工具 → 执行工具(wrap_tool_call)
  • 异常处理
    • 在 wrap_model_call 中抛出异常会中断整个 Agent 流程;
    • 在 wrap_tool_call 中抛出异常仅中断当前工具调用,Agent 可继续尝试其他工具。

五、最佳实践建议

✅ 安全控制优先用 wrap_tool_call:因为高危操作都发生在工具层。

✅ 成本优化优先用 wrap_model_call:模型调用是主要费用来源。

✅ 不要在 wrap_tool_call 中修改业务关键参数(除非你确定后果),避免破坏工具契约。

✅ 结合 before_model / after_model 实现更精细的上下文治理(如摘要、注入)。

相关推荐
huabiangaozhi2 小时前
Spring Cloud Gateway 整合Spring Security
java·后端·spring
羑悻的小杀马特2 小时前
掌握 LangChain 的 Runnable:统一接口、链式组合与流式输出全解析
服务器·langchain·runnable
Keeling17202 小时前
SpringAI学习笔记(三)会话记忆功能
笔记·学习·spring·ai
wangjialelele2 小时前
详解Redis终端操作和Redis-plus-plus接口使用
linux·数据库·c++·redis·分布式·缓存·中间件
Z...........3 小时前
Spring日志管理
java·后端·spring
lj850863 小时前
性能测试总结
java·开发语言·spring
怕浪猫3 小时前
第3章 LangChain 核心抽象:Models 与 Messages
langchain·llm·ai编程
小江的记录本3 小时前
【PageHelper】 【Spring Boot + MyBatis + PageHelper】 完整项目示例+PageHelper核心原理深度解析
java·前端·spring boot·后端·sql·spring·mybatis
weixin_704266053 小时前
Spring AOP事务控制实战指南
java·后端·spring