前面几篇讲了工具、结构化输出、以及把二者串起来的 createAgent。到这里,一个 Agent 的基本骨架就齐了。但真实项目里,我们往往还想在 Agent 的循环里「插一脚」——记录日志、改写提示词、给工具加重试、限制调用次数、拦截敏感信息。中间件(Middleware) 就是官方为此留出的机制。这篇讲清楚它是什么、能做什么、有哪些分类;具体常用中间件的用法,留到后面的文章逐个演示。
一、从 Agent 循环说起
Agent 的本质是一个循环(见 demo-12):
用户提问
-> 调用模型(模型可能输出 tool_calls)
-> 执行工具,把结果塞回消息历史
-> 再调用模型
-> 重复,直到模型不再调用工具
问题来了:如果我们想在这个循环的某些环节加点东西,怎么办?
- 想在每次调用模型前记录日志?
- 想让系统提示词根据当前状态动态变化?
- 想给工具调用加上失败重试?
- 想限制模型最多调用几次,防止死循环烧钱?
- 想在把消息发给模型前,把手机号、邮箱脱敏?
这些需求有一个共同点:它们和具体业务无关,但几乎每个 Agent 都可能需要。 这类「横切关注点」如果都塞进工具函数或系统提示词里,代码会变得又乱又难维护。
二、什么是中间件
中间件是挂在 Agent 循环上的可插拔拦截器。 它在循环的各个关键节点暴露钩子(hook),你写一个中间件挂上去,就能在对应节点介入,而完全不用碰 createAgent 的内部实现。
import { createAgent, createMiddleware } from "langchain";
const myMiddleware = createMiddleware({
name: "MyMiddleware",
beforeModel: (state, runtime) => {
console.log("准备调用模型,当前消息数:", state.messages.length);
},
});
const agent = createAgent({
model,
tools: [...],
middleware: [myMiddleware], // 挂上中间件
});
一句话:中间件让「定制 Agent 行为」从「改源码」变成「加一个插件」。
三、能挂在哪:生命周期钩子
一个中间件可以挂六种钩子,正好对应 Agent 循环的几个关键点:
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
beforeAgent | 整个 Agent 运行「前」执行一次 | 初始化、打点 |
beforeModel | 「每次」调用模型前 | 改状态、动态拼提示词 |
afterModel | 模型返回后、工具执行前 | 修正 tool_calls 参数 |
wrapModelCall | 「包裹」模型调用 | 改请求、重试、降级、后处理响应 |
wrapToolCall | 「包裹」工具执行 | 参数校验、鉴权、缓存、重试、改结果 |
afterAgent | 整个 Agent 运行「后」执行一次 | 汇总、清理 |
其中的钩子分成两类,理解这点很关键:
- 节点式钩子(node-style):
beforeAgent/beforeModel/afterModel/afterAgent。它们在固定节点顺序执行,适合做日志、校验、状态更新。 - 包裹式钩子(wrap-style):
wrapModelCall/wrapToolCall。它们包裹住真正的调用,你决定要不要调用、调用几次(0 次=短路,1 次=正常,多次=重试),适合做重试、缓存、降级、转换。
除了钩子,中间件还能注册一些「资产」:
| 能力 | 作用 |
|---|---|
stateSchema | 自己的状态字段,跨多次调用持久化 |
contextSchema | 只读的运行时上下文,不持久化 |
tools | 给 Agent 追加工具(例如任务清单中间件会加一个 write_todos) |
streamTransformers | 注册流式转换器 |
四、动手:钩子是按什么顺序触发的
理论说了这么多,不如跑一遍看顺序。下面这个中间件什么都不做,只在每个钩子里打印一行——它不是常用中间件,只是把「钩子图」真实地跑出来:
import { createMiddleware, createAgent, tool } from "langchain";
import { z } from "zod";
const tracer = createMiddleware({
name: "Tracer",
beforeAgent: () => console.log("beforeAgent"),
beforeModel: () => console.log("beforeModel"),
afterModel: () => console.log("afterModel"),
afterAgent: () => console.log("afterAgent"),
wrapModelCall: async (request, handler) => {
console.log("wrapModelCall -> 进入");
const response = await handler(request);
console.log("wrapModelCall <- 返回");
return response;
},
wrapToolCall: async (request, handler) => {
console.log(`wrapToolCall -> 进入工具 ${request.toolCall.name}`);
const result = await handler(request);
console.log("wrapToolCall <- 工具执行完毕");
return result;
},
});
const getWeather = tool(({ city }) => `${city} 今天晴,气温 25℃`, {
name: "get_weather",
description: "查询指定城市今天的天气",
schema: z.object({ city: z.string() }),
});
const agent = createAgent({
model,
tools: [getWeather],
middleware: [tracer],
});
await agent.invoke({ messages: [{ role: "user", content: "北京今天天气怎么样?" }] });
输出(模型调了 2 次、工具 1 次):
beforeAgent
beforeModel
wrapModelCall -> 进入
wrapModelCall <- 返回 ← 第 1 次模型调用:决定要调 get_weather
afterModel
wrapToolCall -> 进入工具 get_weather
wrapToolCall <- 工具执行完毕
beforeModel
wrapModelCall -> 进入
wrapModelCall <- 返回 ← 第 2 次模型调用:根据结果生成最终回答
afterModel
afterAgent
把这张输出和第一节的 Agent 循环对照,就能直观看到:中间件的钩子,精确地分布在循环的每个环节。
多个中间件同时存在时的执行顺序规则:
before_*按注册顺序正序执行,after_*逆序执行,wrap_*像洋葱一样嵌套(第一个中间件包住所有后面的)。
五、中间件有什么作用
官方给中间件的定位就是「横切关注点」,常见用途可以归为五类:
- 可观测:logging、埋点、调试,追踪 Agent 每一步在做什么;
- 改造请求:动态改写系统提示词、筛选/追加工具、格式化输出;
- 提升可靠性:失败重试、模型降级、错误兜底、提前终止;
- 控成本 / 防失控:限制模型调用次数、限制工具调用次数、限流;
- 安全合规:PII 检测与脱敏、内容审核、危险操作人工审批。
关键在于:把这些能力交给中间件统一处理,业务工具和系统提示词就能保持干净。
六、有哪些分类
LangChain 内置了一批开箱即用的中间件。按用途大致可分为以下几类(本篇只做分类介绍,具体用法放到后面的文章逐个演示):
A. 上下文管理——控制「喂给模型什么」
| 中间件 | 作用 |
|---|---|
summarizationMiddleware | 对话太长时自动摘要,保留最近消息 |
contextEditingMiddleware | 清理旧的工具调用结果,腾出上下文 |
anthropicPromptCachingMiddleware / bedrockPromptCachingMiddleware | 给提示词加缓存,省钱提速 |
B. 可靠性 / 容错——让 Agent 更稳
| 中间件 | 作用 |
|---|---|
toolRetryMiddleware | 工具调用失败自动重试(指数退避) |
modelRetryMiddleware | 模型调用失败自动重试 |
modelFallbackMiddleware | 主模型失败时自动降级到备用模型 |
toolErrorMiddleware | 把工具抛的异常转成消息交还给模型 |
toolEmulatorMiddleware | 测试时用 LLM 模拟工具,不真正执行 |
C. 流程控制 / 循环干预——改变 Agent 循环怎么走
| 中间件 | 作用 |
|---|---|
modelCallLimitMiddleware | 限制模型调用次数,防死循环 / 烧钱 |
toolCallLimitMiddleware | 限制工具调用次数(全局或指定工具) |
humanInTheLoopMiddleware | 高风险工具执行前暂停,等人工审批 |
todoListMiddleware | 给 Agent 一个「任务清单」工具,做计划 |
llmToolSelectorMiddleware | 工具太多时,先用 LLM 挑出相关工具 |
providerToolSearchMiddleware | 把工具放到厂商的服务端「按需搜索」 |
D. 安全 / 合规——护栏与脱敏
| 中间件 | 作用 |
|---|---|
piiMiddleware | PII 检测(block / redact / mask / hash) |
piiRedactionMiddleware | PII 脱敏的专用封装 |
openAIModerationMiddleware | OpenAI 内容审核(provider 专用) |
E. 动态行为——根据运行时状态改变行为
| 中间件 | 作用 |
|---|---|
dynamicSystemPromptMiddleware | 按当前状态动态拼系统提示词 |
F. 能力扩展 / 厂商集成
- 文件系统、子 Agent 等能力(来自 Deep Agents);
- 各厂商专属中间件(Anthropic / AWS / OpenAI 的缓存、审核等)。
七、Python 对应写法
Python 侧的中间件模型与 JS 一致,只是用装饰器或类来定义。钩子名一一对应,只是换成下划线命名(before_model、wrap_model_call……)。
装饰器风格(单个钩子,简单):
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, wrap_model_call, AgentState
from langgraph.runtime import Runtime
from typing import Any, Callable
from langchain.agents.middleware import ModelRequest, ModelResponse
@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"准备调用模型,当前消息数:{len(state['messages'])}")
return None
@wrap_model_call
def retry_model(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse:
for attempt in range(3):
try:
return handler(request)
except Exception:
if attempt == 2:
raise
agent = create_agent(model="gpt-5.5", tools=[...], middleware=[log_before_model, retry_model])
类风格(多个钩子 / 需要配置):
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime
from typing import Any
class LoggingMiddleware(AgentMiddleware):
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print("before_model")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print("after_model")
return None
agent = create_agent(model="gpt-5.5", tools=[...], middleware=[LoggingMiddleware()])
对应关系:
| 能力 | JS | Python |
|---|---|---|
| 定义方式 | createMiddleware({...}) | 装饰器 / 继承 AgentMiddleware |
| 节点钩子 | beforeAgent / beforeModel / afterModel / afterAgent | before_agent / before_model / after_model / after_agent |
| 包裹钩子 | wrapModelCall / wrapToolCall | wrap_model_call / wrap_tool_call |
| 注册状态 | stateSchema | state_schema |
| 挂载 | createAgent({ middleware: [...] }) | create_agent(..., middleware=[...]) |
内置中间件的名字也一样,只是首字母大写:SummarizationMiddleware、HumanInTheLoopMiddleware、ModelCallLimitMiddleware、PIIMiddleware、TodoListMiddleware 等。
八、小结
- 是什么:中间件是挂在 Agent 循环上的「钩子 / 拦截器」,用来处理横切关注点。不改 Agent 内部代码,就能定制行为。
- 六个钩子:
beforeAgent/beforeModel/wrapModelCall/afterModel/wrapToolCall/afterAgent;还能注册state/context/tools/ 流转换器。 - 作用:可观测、改造请求、容错重试、限流控成本、安全合规。
- 分类:上下文管理、可靠性、流程控制、安全合规、动态行为、能力扩展。
一句话:中间件 = Agent 的可插拔扩展点。 它把「日志、重试、限流、脱敏、审批」这些每个 Agent 都可能需要的通用能力,从业务逻辑里剥离出来,让 Agent 的骨架保持简洁。后续文章会挑其中最常用的几个,逐个演示用法。