一、问题理解与目标定义
核心问题 :厘清 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 实现更精细的上下文治理(如摘要、注入)。