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

一、问题理解与目标定义

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

二、中间件功能对比

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

💡 关键区别:

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

3 中间件实现对比

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

  • 使用场景说明:
    • 当对话历史较短时,使用便宜模型节省成本;
    • 当上下文较长或任务复杂时,自动切换到高性能模型保证质量;
    • 对相同输入缓存结果,避免重复调用。
# -*- 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_calldefdynamic_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 defis_complex_task(messages):"""判断任务是否复杂"""ifnot messages:returnFalse last_message = messages[-1].content ifhasattr(messages[-1],'content')elsestr(messages[-1])# 复杂任务判断逻辑 complex_keywords =["分析","比较","优化","设计","架构","多步骤"]# 长度超过 200 字或包含复杂关键词iflen(last_message)>200:returnTruefor keyword in complex_keywords:if keyword in last_message:returnTruereturnFalse# 2. 定义工具@tooldefget_weather(city:str)->str:"""获取指定城市的天气"""returnf"{city}:晴,25度,微风徐徐"@tooldefget_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:工具权限控制 + 参数脱敏

使用场景说明:

  • 阻止非授权用户执行高危操作(如删除、转账);
  • 在工具调用前后记录审计日志,满足合规要求;
  • 对敏感参数(如手机号、邮箱)进行日志脱敏,防止数据泄露。
# -*- 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. 定义工具@tooldefget_weather(city:str)->str:"""获取指定城市的天气"""returnf"{city}:晴,25度,微风徐徐"@tooldefget_location()->str:"""获取用户位置"""return"北京"@tooldefsend_email(email:str,user:str,content:str):""" 给用户发送邮件 :param email: 用户email :param user: 用户名字 :param content: 内容 :return: """returnf"已经给用户{user}{email},发功成功!内容是:{content}"@wrap_tool_calldefsecure_tool_execution(request,call):""" 对敏感工具调用进行权限校验和参数脱敏 """print(request) tool_name = request.tool.name args = request.tool_call.get('args')# 工具调用参数字典ifhasattr(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":"给小明发送一个邮件([email protected])问好!"}]}, config=config, context={'user_role':'guest'}, stream_mode="values"):#print(event) event['messages'][-1].pretty_print()
在这里插入图片描述


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

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 实现更精细的上下文治理(如摘要、注入)。

Read more

Flutter 三方库 matrix 鸿蒙终端底层复杂超维数学算力适配突破:无缝植入极限级张量系统与密集线性代数矩阵运算推演算法,解锁端侧图形处理边界-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 matrix 鸿蒙终端底层复杂超维数学算力适配突破:无缝植入极限级张量系统与密集线性代数矩阵运算推演算法,解锁端侧图形处理边界-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 matrix 鸿蒙终端底层复杂超维数学算力适配突破:无缝植入极限级张量系统与密集线性代数矩阵运算推演算法,全面解锁端侧图形视觉处理边界并拔高数据分析算力上限 在图形学渲染、物理引擎模拟、复杂地理坐标转换以及端侧小型机器学习框架中,底层的矩阵运算(Matrix Operations)是决速步骤。matrix 库是一个专注于高性能线性代数计算的 Dart 库。本文将详解该库在 OpenHarmony 环境下的适配与实战应用。 封面 前言 什么是 matrix?它为 Dart 提供了一套类似于 NumPy 的多维数组运算接口。在鸿蒙操作系统这种强调极致流畅度和复杂视觉动效的系统中,利用高效的矩阵算法可以显著提升自定义 Canvas 绘图或实时传器数据处理的性能,避免因 Dart 层的低效循环导致的 UI 掉帧。 一、原理解析 1.1 基础概念 matrix 库核心基于

By Ne0inhk
【C语言】排序算法——希尔排序以及插入排序 ——详解!!!

【C语言】排序算法——希尔排序以及插入排序 ——详解!!!

【C语言】排序算法——希尔排序以及插入排序详解 * 前言 * 一 、插入排序 * 1. 视频演示 * 2. 算法思想 * 3. 实现思路 * 4. 代码演示 * 二 、希尔排序 * 1. 视频演示 * 2. 算法思想 * 3. 实现思路 * (1)分组 * (2)预排序 * (3)最终排序 * (4)gap的取值 * 4. 代码演示 * 结语 前言 在学习循环的时候,我们学习到了冒泡排序这个算法 那么,除了冒泡排序,还有什么排序算法呢? 今天给大家带来的是插入排序以及希尔排序 一 、插入排序 1. 视频演示 首先给大家看一段视频,让大家先看看插入排序是怎么运行的 插入排序演示 2. 算法思想 我们可以从视频里看见,

By Ne0inhk

Python Arduino 编程指南(一)

原文:zh.annas-archive.org/md5/a92daa7ed7e63d78de20d04520325dc2 译者:飞龙 协议:CC BY-NC-SA 4.0 前言 在物联网(IoT)时代,快速开发和测试你的硬件产品的原型,同时使用软件特性对其进行增强,变得非常重要。Arduino 运动在这一硬件革命中一直处于领先地位,通过其简单的板设计,它使任何人都能方便地开发 DIY 硬件项目。开源社区提供的巨大支持使得与硬件原型开发相关的困难已成为过去。在软件方面,Python 已经很长时间一直是开源软件社区的瑰宝。Python 拥有大量的库来开发各种特性,如图形用户界面、图表、消息和云应用。 本书旨在将硬件和软件世界的最佳结合带给您,帮助您使用 Arduino 和 Python 开发令人兴奋的项目。本书的主要目标是帮助读者解决将 Arduino 硬件与 Python 库接口的难题。同时,作为次要目标,本书还提供了可以用于您未来物联网项目的练习和项目。 本书的设计方式是,

By Ne0inhk
(第二篇)Spring AI 基础入门:从环境搭建到模型接入全攻略(覆盖国内外模型 + 本地部署)

(第二篇)Spring AI 基础入门:从环境搭建到模型接入全攻略(覆盖国内外模型 + 本地部署)

前言:为什么要学 Spring AI?         最近在做 AI 应用开发时,发现很多朋友卡在了工具链整合这一步:用原生 SDK 调用 OpenAI 要处理一堆 HTTP 请求,切换到通义千问又得改大量代码,本地部署 Llama3 更是不知道怎么和 Spring 项目结合…         直到接触了 Spring AI 才发现,这个框架简直是为 Java 开发者量身定做的 AI 开发工具 —— 它把不同模型的调用逻辑标准化了,不管是 OpenAI、通义千问还是本地 Llama3,都能用几乎一样的 API 调用。         这篇教程从基础环境讲到实战接口,全程手把手操作,哪怕是 AI 开发新手,跟着走也能跑通第一个 Spring AI 应用。 目录 基础环境搭建:JDK17+

By Ne0inhk