【 LangChain 1.2 实战(四)】构建一个模块化的天气查询 Agent时候已经实现了一个可查天气的智能助手,但它尚缺少生产环境必备的几项能力:
- 成本控制:所有问题都交给同一个模型,简单问候与复杂多步推理成本一样。
- 可观测性:你几乎看不到 Agent 内部做了什么,排查问题全靠猜。
- 安全防护:无法拦截敏感提问,也无法给回答追加免责声明。
- 容错能力:工具调用或模型请求偶发失败时,程序直接崩溃。
Agent Middleware 正是解决这些问题的标准方案。它允许你以插件 的形式,在 Agent 的模型调用、工具调用前后植入自定义逻辑,而不必修改任何业务代码(工具、提示词、模型定义)。
本章将在原有天气查询 Agent 的基础上,依次实现:
- 动态模型路由 ------ 根据对话复杂度自动切换便宜/强大的模型。
- 日志、安全、容错三件套 ------ 记录耗时、拦截敏感词、自动重试。
- LangChain 内置中间件能力一览 ------ 为后续进阶铺路。
最终, Agent 将同时具备这四种能力,中间件各司其职,完美协同。
1 准备工作:回顾项目结构
deepseek_agent_project/
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── llm.py
│ ├── agent.py
│ ├── tools/
│ │ ├── __init__.py
│ │ └── weather.py
│ └── middleware/ # 我们将在本章填满它
│ ├── __init__.py
│ ├── dynamic_model_middleware.py # 动态路由
│ └── weather_middleware.py # 日志/安全/容错
├── .env
├── .gitignore
├── main.py
└── requirements.txt
如果 app/middleware/ 目录和 __init__.py 还未创建,请现在新建,以确保它成为一个合法的 Python 包。
2 动态模型路由:让 Agent 学会"分诊"
目标:当对话很短(如"你好")时,使用便宜的基础模型;当对话消息数增多(意味着可能涉及工具调用、多步推理)时,自动升级为更强的模型。
2.1 配置两个模型
编辑 .env,增加高级模型名称。此处以 SiliconFlow 平台为例,保持与基础模型相同的 API 地址和密钥:
ini
DEEPSEEK_API_KEY=sk-your-key
DEEPSEEK_BASE_URL=https://api.siliconflow.cn/v1
MODEL_NAME=deepseek-ai/DeepSeek-V3
ADVANCED_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct # 新增
在 app/config.py 中读取该配置(在原有基础上增加最后一行):
python
import os
from pathlib import Path
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parent.parent
load_dotenv(PROJECT_ROOT / ".env")
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
if not DEEPSEEK_API_KEY:
raise ValueError("⚠️ .env 中缺少 DEEPSEEK_API_KEY,请检查!")
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.siliconflow.cn/v1")
MODEL_NAME = os.getenv("MODEL_NAME", "deepseek-ai/DeepSeek-V3")
ADVANCED_MODEL_NAME = os.getenv("ADVANCED_MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")
2.2 实例化两个模型
修改 app/llm.py,分别导出基础模型和高级模型:
python
from langchain_openai import ChatOpenAI
from app.config import (
DEEPSEEK_API_KEY,
DEEPSEEK_BASE_URL,
MODEL_NAME,
ADVANCED_MODEL_NAME,
)
basic_llm = ChatOpenAI(
openai_api_key=DEEPSEEK_API_KEY,
openai_api_base=DEEPSEEK_BASE_URL,
model=MODEL_NAME,
temperature=0,
)
advanced_llm = ChatOpenAI(
openai_api_key=DEEPSEEK_API_KEY,
openai_api_base=DEEPSEEK_BASE_URL,
model=ADVANCED_MODEL_NAME,
temperature=0,
)
注意 :原来的
deepseek_llm被替换为basic_llm和advanced_llm两个导出,后续引用时统一使用这两个名称。
2.3 编写动态路由中间件
新建 app/middleware/dynamic_model_middleware.py:
python
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from app.llm import basic_llm, advanced_llm
@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
"""根据对话消息数动态选择模型"""
messages = request.state.get("messages", [])
message_count = len(messages)
print(f"[🔀 路由中间件] 当前消息数:{message_count}")
if message_count >= 3:
print("[🔀 路由中间件] 切换到高级模型")
model = advanced_llm
else:
print("[🔀 路由中间件] 使用基础模型")
model = basic_llm
# 用 override 替换请求中的模型,然后交给真正的模型调用处理器
return handler(request.override(model=model))
为什么使用 wrap_model_call 装饰器?
它可以将一个普通函数包装成中间件,并拦截每一次模型调用。函数内部的 handler(request) 代表原始的模型调用逻辑,我们通过 request.override(model=...) 替换了模型,从而在调用前动态决定使用哪个模型。
2.4 将路由中间件挂载到 Agent
修改 app/agent.py,暂时只挂载这一个中间件(稍后我们会把全部中间件加进来):
python
from langchain.agents import create_agent
from app.llm import basic_llm
from app.tools.weather import get_weather
from app.middleware.dynamic_model_middleware import dynamic_model_selection
agent = create_agent(
model=basic_llm, # 默认模型,实际会被中间件覆盖
tools=[get_weather],
system_prompt="你是一个助手,你可以查询城市的天气。",
middleware=[dynamic_model_selection]
)
2.5 测试动态路由
修改 main.py 进行简单验证:
python
from app.agent import agent
if __name__ == "__main__":
print("=== 测试1:简单问候(应使用基础模型) ===")
resp = agent.invoke({"messages": [{"role": "user", "content": "你好,你能做什么?"}]})
print(f"助手:{resp['messages'][-1].content}\n")
print("=== 测试2:一步查天气(可能触发模型切换) ===")
resp = agent.invoke({"messages": [{"role": "user", "content": "北京今天天气怎么样?"}]})
print(f"助手:{resp['messages'][-1].content}\n")
print("=== 测试3:多步推理(先查北京,再查上海) ===")
resp = agent.invoke({"messages": [{"role": "user", "content": "请先告诉我北京的天气,然后告诉我上海的天气"}]})
print(f"助手:{resp['messages'][-1].content}\n")
运行 python main.py,终端会打印类似日志:
[🔀 路由中间件] 当前消息数:1
[🔀 路由中间件] 使用基础模型
助手:你好!我可以帮你查询天气...
...
[🔀 路由中间件] 当前消息数:3
[🔀 路由中间件] 切换到高级模型
...
效果:Agent 在多步工具调用过程中,消息数达到 3 时自动切换模型,简单问题则始终用基础模型,有效节省费用。
3 监控、安全与容错:给 Agent 戴上"安全帽"和"记录仪"
动态路由解决了成本问题,但 Agent 还缺少可观测性、安全性和鲁棒性。接下来我们添加三个中间件:
- 日志中间件:记录每次模型调用的耗时。
- 安全中间件:拦截含敏感词的提问,并在所有回复后追加免责声明。
- 容错中间件:工具调用失败自动重试,模型调用失败尝试备用模型。
3.1 编写三合一中间件文件
新建 app/middleware/weather_middleware.py(如果已存在则覆盖):
python
import time
from typing import Any, Dict
from langchain.agents.middleware import AgentMiddleware
from langchain_core.messages import AIMessage
class TimingLoggingMiddleware(AgentMiddleware):
"""记录每次模型调用的耗时与消息数量"""
def before_model(self, state: Any, runtime: Dict[str, Any]) -> Any:
msg_count = len(state.get("messages", []))
print(f"[⏱️ 日志中间件] 即将调用大模型,当前上下文消息数:{msg_count}")
state["_model_start_time"] = time.time()
return state
def after_model(self, state: Any, runtime: Dict[str, Any]) -> Any:
start = state.get("_model_start_time", time.time())
elapsed = time.time() - start
print(f"[✅ 日志中间件] 模型调用完成,耗时 {elapsed:.2f} 秒")
return state
class WeatherSafetyMiddleware(AgentMiddleware):
"""安全中间件:拦截敏感词,并在最终回复中追加免责声明"""
def __init__(self, forbidden_keywords: list):
super().__init__()
self.forbidden_keywords = [kw.lower() for kw in forbidden_keywords]
def before_model(self, state: Any, runtime: Dict[str, Any]) -> Any:
messages = state.get("messages", [])
if messages:
last_msg = messages[-1]
if last_msg.type == "human":
content = last_msg.content.lower()
for word in self.forbidden_keywords:
if word in content:
print(f"[🛡️ 安全中间件] 检测到敏感词 "{word}",拦截请求。")
return {
"messages": [
last_msg,
AIMessage(content="⚠️ 抱歉,您的问题包含不合规内容,请修改后重试。")
],
"jump_to": "end"
}
return state
def after_agent(self, state: Any, runtime: Dict[str, Any]) -> Any:
messages = state.get("messages", [])
if messages:
last_msg = messages[-1]
if last_msg.type == "ai":
disclaimer = "\n\n---\n📌 以上天气信息为模拟数据,仅供学习参考。"
last_msg.content += disclaimer
return state
class WeatherRetryMiddleware(AgentMiddleware):
"""容错中间件:工具调用出错时自动重试,模型调用失败时切换备用模型"""
def wrap_tool_call(self, request: Any, handler: callable) -> Any:
max_retries = 2
for attempt in range(max_retries):
try:
return handler(request)
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"[🔄 容错中间件] 工具调用失败 ({e}),正在重试 {attempt+2}/{max_retries}...")
time.sleep(0.5)
def wrap_model_call(self, request: Any, handler: callable) -> Any:
try:
return handler(request)
except Exception as e:
print(f"[⚠️ 容错中间件] 主模型调用失败,尝试使用备用模型...")
# 此处可以切换到其他备用模型,示例中仍用原请求重试
return handler(request)
三个中间件分别使用了不同的钩子,全面展示了 Middleware 的灵活性:
TimingLoggingMiddleware使用before_model/after_model,适合无侵入的监控。WeatherSafetyMiddleware在before_model中通过jump_to: "end"提前终止请求 ,并在after_agent中追加内容,实现安全与合规。WeatherRetryMiddleware重写了wrap_tool_call和wrap_model_call,以代理模式包裹实际调用,实现重试和降级。
3.2 将所有中间件组装到一起
现在更新 app/agent.py,同时挂载所有四个中间件。顺序至关重要:安全中间件应最先执行(拦截恶意请求),然后是日志(记录后续步骤),接着容错重试,最后动态路由(在真正调用模型时决定使用哪个模型)。
python
from langchain.agents import create_agent
from app.llm import basic_llm
from app.tools.weather import get_weather
from app.middleware.weather_middleware import (
TimingLoggingMiddleware,
WeatherSafetyMiddleware,
WeatherRetryMiddleware,
)
from app.middleware.dynamic_model_middleware import dynamic_model_selection
agent = create_agent(
model=basic_llm,
tools=[get_weather],
system_prompt="你是一个助手,你可以查询城市的天气。",
middleware=[
WeatherSafetyMiddleware(forbidden_keywords=["密码", "银行卡", "暴力"]),
TimingLoggingMiddleware(),
WeatherRetryMiddleware(),
dynamic_model_selection, # 动态路由放在最后,真正调用前换模型
],
)
洋葱模型执行流(以一次正常的天气查询为例):
WeatherSafetyMiddleware.before_model检查输入 → 无敏感词,放行。TimingLoggingMiddleware.before_model记录开始时间。WeatherRetryMiddleware.wrap_model_call包裹后续调用。dynamic_model_selection拦截,根据消息数选择basic_llm或advanced_llm,然后交由真正的模型调用。- 模型返回结果。
TimingLoggingMiddleware.after_model打印耗时。WeatherSafetyMiddleware.after_agent追加免责声明。
3.3 全流程演示
保持 main.py 不变,运行:
bash
python main.py
将看到完整的中间件日志输出。例如:
[🛡️ 安全中间件] 输入安全,放行。
[⏱️ 日志中间件] 即将调用大模型,当前上下文消息数:1
[🔀 路由中间件] 当前消息数:1
[🔀 路由中间件] 使用基础模型
[✅ 日志中间件] 模型调用完成,耗时 0.83 秒
助手: 北京 天气(2026-05-04 14:22:10)...
---
📌 以上天气信息为模拟数据,仅供学习参考。
如果测试含敏感词的输入:
python
resp = agent.invoke({"messages": [{"role": "user", "content": "帮我查银行卡密码"}]})
日志会显示:
[🛡️ 安全中间件] 检测到敏感词 "银行卡",拦截请求。
助手: ⚠️ 抱歉,您的问题包含不合规内容,请修改后重试。
动态路由、安全拦截、耗时记录、免责声明,四种能力同时生效,但核心业务代码(weather.py、config.py)完全零改动。
4 内置中间件扩展:生产级 Agent 能力清单
通过前面的实战,已经掌握了如何从零构建自定义中间件。实际上,LangChain 1.2 还提供了一系列开箱即用的内置中间件 ,覆盖了上下文管理、安全合规、稳定容错、人机协同等多个维度。这些中间件可以直接以相同的方式挂载到 create_agent(middleware=[...]) 中,让项目快速获得企业级能力。
| 类别 | 中间件名称 | 作用 |
|---|---|---|
| 上下文管理 | SummarizationMiddleware |
当对话历史过长时,自动总结旧消息,避免超出上下文窗口。 |
| 安全合规 | PIIMiddleware |
自动检测并处理(脱敏/拦截)输入/输出中的个人隐私信息,如邮箱、信用卡号。 |
Guardrails |
一种安全护栏系统,可对内容进行确定性或基于模型的校验。 | |
| 稳定容错 | ModelCallLimitMiddleware |
限制模型的总调用次数,防止死循环或 Bug 导致 API 费用失控。 |
ToolCallLimitMiddleware |
限制工具的总调用次数,防止高频调用付费 API 导致成本飙升。 | |
ModelFallbackMiddleware |
当主模型调用失败时,自动切换到一个或多个备用模型,提升系统可用性。 | |
ToolRetryMiddleware |
当工具调用因网络等临时错误失败时,自动使用指数退避策略进行重试。 | |
ModelRetryMiddleware |
当模型调用因频率限制等原因失败时,自动进行重试。 | |
| 人机协同 | HumanInTheLoopMiddleware |
在执行高风险工具(如转账、发送邮件)前暂停工作流,等待人类审批或修改。 |
| 任务规划 | To-do List / Subagent |
为 Agent 配备任务规划和派生子 Agent 的能力,用于处理复杂的多步、并行子任务。 |
| 效率优化 | LLM Tool Selector |
在调用大模型前,先用一个更快/更便宜的模型筛选出相关的工具列表,减轻大模型负担。 |
Context Editing |
管理对话上下文,例如清除不需要的工具调用历史,保持上下文干净。 |
如何使用它们?
和自定义中间件的挂载方式完全一致,只需从 langchain.agents.middleware 导入相应类,并将实例加入 middleware 列表。例如,要同时启用摘要和 PII 防护:
python
from langchain.agents.middleware import SummarizationMiddleware, PIIMiddleware
agent = create_agent(
model=basic_llm,
tools=[...],
middleware=[
PIIMiddleware(patterns=["email", "phone"]),
SummarizationMiddleware(model=basic_llm, max_tokens_before_summary=4000),
# 还可以跟上自定义的中间件
]
)
这些内置中间件与刚编写的自定义中间件完全兼容、可任意组合。可以把 Middleware 理解成一个"能力插件市场"------LangChain 提供了通用组件,而随时可以按需开发自己的业务专属插件。
5 总结:Middleware 的魔力
通过本章,天气查询 Agent 已经从一个单模型玩具,进化成具备:
- ✅ 自适应模型选择(成本优化)
- ✅ 全链路调用日志(可观测性)
- ✅ 敏感词过滤与合规声明(安全性)
- ✅ 工具/模型调用自动重试(鲁棒性)
- ✅ 知晓 LangChain 开箱即用的中间件库,随时可扩展
的生产级雏形 。这一切只新增了两个中间件文件,并在 agent.py 中添加了几行配置。Agent 的核心逻辑、工具实现、入口脚本均保持原样。
这就是 LangChain 1.2 中间件的设计哲学:把非功能需求从业务代码中剥离,以可组合、可排序、可拔插的方式"挂载"到 Agent 上。 当你未来需要添加限流、缓存、多租户隔离或任务分派等能力时,第一个念头就应该是:"写一个 Middleware 吧!"