Appearance
上一篇的 Agent 已经会自主调工具了。但真实项目里,我们还想在它运行的关键节点插入自己的逻辑:调用模型前记日志、调用工具时做监控和权限校验、根据场景动态切换提示词、对话太长时自动摘要……这些「横切关注点」正是 中间件(middleware) 要解决的——它也是把一个 Agent 从「能跑」打磨到「可上生产」的核心机制。这一篇讲 LangChain 1.x 的 Agent 中间件。顶部可切换 Python / Node.js。
中间件是什么
回忆一下 Agent 的 ReAct 循环:模型思考 → 调用工具 → 拿到结果 → 再思考…。中间件让你在这个循环的每个关键点挂上钩子(hook),最常用的三个:
- before_model / beforeModel:每次调用模型之前触发。适合记日志、裁剪历史、注入上下文。
- wrap_tool_call / wrapToolCall:包裹每次工具调用。适合监控、计时、鉴权,甚至拦截替换结果。
- dynamic_prompt / dynamicSystemPrompt:每轮动态决定系统提示词。适合按用户身份或任务场景切换人设。
写法上,Python 用装饰器把普通函数变成中间件,Node 用 createMiddleware 传入对应的钩子函数;最后都通过 create_agent / createAgent 的 middleware 参数挂上去。
一、日志与工具监控
先做两个最实用的:模型调用前打日志、每次工具调用时监控。
python
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, wrap_tool_call
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
import os, time
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
return f"{city}今天晴,26℃。"
# 钩子一:每次调模型前触发。参数 state 里有当前所有消息
@before_model
def log_before_model(state, runtime):
print(f"[日志] 即将调用模型,当前消息数:{len(state['messages'])}")
return None # 返回 None 表示不修改状态,正常继续
# 钩子二:包裹工具调用。可在前后加逻辑,handler(request) 才是真正执行
@wrap_tool_call
def monitor_tool(request, handler):
name = request.tool_call["name"]
start = time.time()
result = handler(request) # 执行真正的工具
print(f"[监控] 工具 {name} 耗时 {time.time() - start:.3f}s")
return result
model = ChatOpenAI(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
temperature=0,
)
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[log_before_model, monitor_tool],
)
agent.invoke({"messages": [{"role": "user", "content": "杭州天气怎么样?"}]})javascript
import { createAgent, createMiddleware } from "langchain";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
const getWeather = tool(async ({ city }) => `${city}今天晴,26℃。`, {
name: "get_weather",
description: "查询指定城市的天气。",
schema: z.object({ city: z.string() }),
});
// 用 createMiddleware 一次性定义多个钩子
const observability = createMiddleware({
name: "Observability",
// 钩子一:每次调模型前触发,state 里有当前所有消息
beforeModel: (state) => {
console.log(`[日志] 即将调用模型,当前消息数:${state.messages.length}`);
return undefined; // 不修改状态,正常继续
},
// 钩子二:包裹工具调用,handler(request) 才是真正执行
wrapToolCall: async (request, handler) => {
const name = request.toolCall.name;
const start = Date.now();
const result = await handler(request);
console.log(`[监控] 工具 ${name} 耗时 ${Date.now() - start}ms`);
return result;
},
});
const model = new ChatOpenAI({
model: "qwen-plus",
apiKey: process.env.DASHSCOPE_API_KEY,
temperature: 0,
configuration: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
});
const agent = createAgent({ model, tools: [getWeather], middleware: [observability] });
await agent.invoke({ messages: [{ role: "user", content: "杭州天气怎么样?" }] });运行时你会在控制台看到日志和工具耗时打印出来——业务逻辑(工具)和横切逻辑(日志/监控)干净地分开了,这正是中间件的价值。
二、动态切换提示词
举个典型需求:平时是普通客服人设,但当用户要「生成报告」时,切换成专门的报告生成提示词。用 dynamic_prompt / dynamicSystemPromptMiddleware 可以每轮根据状态动态返回系统提示词。
python
from langchain.agents.middleware import dynamic_prompt
@dynamic_prompt
def switch_prompt(request):
# 根据最近的用户消息判断场景,返回不同的系统提示词
last = request.state["messages"][-1].content if request.state["messages"] else ""
if "报告" in last:
return "你是专业的报告撰写助手,用正式、结构化的语言输出。"
return "你是亲切的生活助手,回答简短口语化。"
agent = create_agent(model=model, tools=[get_weather], middleware=[switch_prompt])javascript
import { dynamicSystemPromptMiddleware } from "langchain";
const switchPrompt = dynamicSystemPromptMiddleware((state) => {
const msgs = state.messages;
const last = msgs.length ? String(msgs[msgs.length - 1].content) : "";
if (last.includes("报告")) {
return "你是专业的报告撰写助手,用正式、结构化的语言输出。";
}
return "你是亲切的生活助手,回答简短口语化。";
});
const agent = createAgent({ model, tools: [getWeather], middleware: [switchPrompt] });同一个 Agent,会根据用户是在闲聊还是要报告,自动换上不同的「人格」——而工具和主流程完全不用改。
三、别忘了还有一堆「开箱即用」的中间件
很多常见需求,官方已经内置了中间件,直接用即可,不用自己写。下面挑四个最实用的详细说说(括号里分别是 Python 类名 / Node 函数名):
摘要压缩 SummarizationMiddleware / summarizationMiddleware
对话越滚越长,token 越烧越多,还可能撑爆模型的上下文窗口。这个中间件会监控历史长度,一旦接近设定的阈值,就自动把靠前的旧对话总结成一小段摘要,用摘要替换掉原始的冗长消息,从而在保留关键信息的同时大幅压缩上下文。它需要你传入一个用来做摘要的模型(可以就用主模型),并指定触发时机——最稳妥的是按消息条数或绝对 token 数触发(按窗口比例触发需要模型提供上下文窗口信息,通义千问这类模型可能不带,会报错)。做长对话客服、长文档助手时几乎必备。
工具调用限流 ToolCallLimitMiddleware / toolCallLimitMiddleware
Agent 是自主决策的,万一它陷入「反复调同一个工具」的死循环,就会白白烧钱、甚至卡死。这个中间件统计工具调用次数并强制上限:thread_limit 限制整个会话累计调用次数、run_limit 限制单次运行内的调用次数,还能用 tool_name 只针对某个特定工具限流。触及上限后的行为可配(继续、结束或报错)。它是给自主 Agent 上的一道「保险丝」。
敏感信息处理 PIIMiddleware / piiMiddleware
用于检测并处理个人隐私信息(PII),比如邮箱、信用卡号、IP、URL 等。你为想防护的每种类型各挂一个,并选择处理策略:redact(替换成占位符,默认)、mask(部分打码,如只留后四位)、hash(换成哈希值)、或 block(直接拦截阻止)。默认作用于用户输入,也可配置成同时处理模型输出或工具返回结果。合规、脱敏场景用得上。
人工确认 HumanInTheLoopMiddleware / humanInTheLoopMiddleware
有些操作风险高(下单、退款、删数据),不该让 Agent 自己拍板。这个中间件让你指定哪些工具在执行前必须「暂停、等人工批准」:通过 interrupt_on 声明需要拦截的工具,命中时 Agent 会中断并抛出待确认请求,由你的程序(或人)决定放行、修改还是拒绝,再继续往下跑。这就是所谓「human-in-the-loop(人在回路)」,是高风险 Agent 的安全闸门。
下面把四个一次性挂上,看清每个怎么配、参数是什么意思。注意 pii 要为每种敏感类型各挂一个,human_in_the_loop 想「暂停后还能恢复」需要配合 checkpointer(第五篇讲过):
python
from langchain.agents import create_agent
from langchain.agents.middleware import (
SummarizationMiddleware, ToolCallLimitMiddleware,
PIIMiddleware, HumanInTheLoopMiddleware,
)
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model=model,
tools=[get_weather, delete_order],
middleware=[
# ① 对话超过 20 条消息就自动摘要压缩(也可用 ("tokens", 3000) 按 token 触发)
SummarizationMiddleware(model=model, trigger=("messages", 20)),
# ② 整个会话最多调 10 次工具、单次运行最多 5 次;超了就停,防止死循环烧钱
ToolCallLimitMiddleware(thread_limit=10, run_limit=5),
# ③ 把用户输入里的邮箱替换成占位符;信用卡号只留后四位(每种类型挂一个)
PIIMiddleware("email", strategy="redact"),
PIIMiddleware("credit_card", strategy="mask"),
# ④ delete_order 这个危险工具,执行前必须暂停等人工批准
HumanInTheLoopMiddleware(interrupt_on={"delete_order": True}),
],
checkpointer=InMemorySaver(), # ④ 需要它来保存「暂停点」以便之后恢复
)javascript
import { createAgent,
summarizationMiddleware, toolCallLimitMiddleware,
piiMiddleware, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model,
tools: [getWeather, deleteOrder],
middleware: [
// ① 对话超过 20 条消息就自动摘要压缩(也可用 { model, tokens: 3000 } 按 token 触发)
summarizationMiddleware({ model, messages: 20 }),
// ② 整个会话最多调 10 次工具、单次运行最多 5 次;超了就停,防止死循环烧钱
toolCallLimitMiddleware({ threadLimit: 10, runLimit: 5 }),
// ③ 把用户输入里的邮箱替换成占位符;信用卡号只留后四位(每种类型挂一个)
piiMiddleware("email", { strategy: "redact" }),
piiMiddleware("credit_card", { strategy: "mask" }),
// ④ deleteOrder 这个危险工具,执行前必须暂停等人工批准
humanInTheLoopMiddleware({ interruptOn: { delete_order: true } }),
],
checkpointer: new MemorySaver(), // ④ 需要它来保存「暂停点」以便之后恢复
});配好之后的实际效果串起来看:用户发来一句带邮箱的长消息 → ③ PII 先把邮箱抹成占位符 → 对话如果太长,① 摘要把旧消息压缩掉 → Agent 开始干活,② 限流盯着别让它调工具调疯 → 如果它想调用 delete_order,④ 人工确认会把流程暂停、抛出一个待批准请求,你的程序确认后再继续。四道关卡各司其职,业务代码一行没动。
各中间件的参数以官方文档为准,不同版本可能微调。思路是:先找有没有现成的,没有再自己写。
小结与预告
这一篇你掌握了 Agent 的「进阶控制」:用 before_model 记日志、wrap_tool_call 监控和拦截工具、dynamic_prompt 动态切换人设,还认识了一批开箱即用的官方中间件。这些正是把一个「能跑」的 Agent 打磨成「可上生产」的关键。
至此,RAG 和 Agent 两条线的零件都齐了。下一篇是综合实战:我们把 RAG(查知识库)、多工具(查天气/定位/取数据)、中间件(日志/报告切换)全部整合,做一个「智扫通机器人智能客服」——一个真正麻雀虽小五脏俱全的 Agent 项目。