创见博客
内置中间件巡礼(二):剩下 12 个内置中间件一次讲完
七崽爱吃小饼干2026/10/16阅读 0

demo-14 讲了中间件是什么、怎么挂;demo-15 动手演示了 4 个最常用的(摘要、人在环、PII、任务清单)。这一篇把剩下 12 个内置中间件各跑一个最小示例,最后再介绍 3 个厂商专属的。每种的用法都高度统一——import 进来,放进 createAgent({ middleware: [...] })。

代码基于 JavaScript 版 langchain v1,模型用 DeepSeek。为便于理解,把 12 个按用途分成六组。

一、可靠性:失败怎么办

1. toolRetryMiddleware —— 工具重试

工具调用失败时自动重试(指数退避),适合调用外部 API、网络工具等。

js
toolRetryMiddleware({
  maxRetries: 3,        // 重试次数(默认 2)
  backoffFactor: 2.0,   // 退避倍数:等待 = initialDelayMs * factor^retry
  initialDelayMs: 1000, // 首次重试等待
  retryOn: (err) => true, // 只重试哪些错误
  onFailure: "continue",  // 重试耗尽后:continue(交还给模型)/ error(抛出)/ 函数
  tools: ["search"],      // 只对指定工具生效(省略则全部)
});

示例:造一个「前两次失败、第三次成功」的工具,看它最终成功:

js
let calls = 0;
const flakyTool = tool(({ q }) => {
  calls += 1;
  if (calls < 3) throw new Error("临时失败");
  return `第 ${calls} 次调用成功`;
}, { name: "flaky_tool", description: "不稳定的工具", schema: z.object({ q: z.string() }) });

const agent = createAgent({
  model, tools: [flakyTool],
  middleware: [toolRetryMiddleware({ maxRetries: 3, backoffFactor: 0, initialDelayMs: 1 })],
});
// 工具最终返回:"第 3 次调用成功",实际被调用 3 次

2. modelRetryMiddleware —— 模型重试

和工具重试对称,作用于模型调用失败:网络抖动、限流等。

js
modelRetryMiddleware({ maxRetries: 3, onFailure: "continue" });

演示时用一个自定义模型模拟「前两次抛错、第三次成功」:

js
class FlakyChatModel extends BaseChatModel {
  constructor(fields) { super(fields); this.attempts = 0; this.failures = fields.failures; }
  _llmType() { return "flaky"; }
  bindTools() { return this; }
  async _generate() {
    this.attempts += 1;
    if (this.attempts <= this.failures) throw new Error("模拟服务不稳定");
    const text = `成功!模型共被调用 ${this.attempts} 次。`;
    return { generations: [{ text, message: new AIMessage(text) }] };
  }
}

const flaky = new FlakyChatModel({ failures: 2 });
const agent = createAgent({
  model: flaky, tools: [],
  middleware: [modelRetryMiddleware({ maxRetries: 3, backoffFactor: 0, initialDelayMs: 1 })],
});
// 最终回答:"成功!模型共被调用 3 次。"

3. modelFallbackMiddleware —— 模型降级

主模型失败时,按顺序自动尝试备用模型。适合容灾、成本优化、多厂商冗余。

js
const agent = createAgent({
  model: "deepseek:no-such-model-xyz",              // 主模型(必然失败)
  tools: [],
  middleware: [modelFallbackMiddleware("deepseek:deepseek-chat")], // 备用模型,可写多个
});
// 主模型失败后,由备用模型正常回答

4. toolErrorMiddleware —— 工具错误兜底

把工具抛出的异常转成一条 error ToolMessage 交还给模型,而不是让整个 Agent 崩掉。它只「兜底」,不负责重试(重试交给 toolRetry)。

js
toolErrorMiddleware({
  onError: (error, request) => {
    if (error instanceof Error) {
      // 只暴露错误类型,不泄露原始信息
      return `工具 ${request.toolCall.name} 执行失败(${error.name}),请稍后重试。`;
    }
    // 不返回 = 让异常继续向上抛
  },
  tools: [...], // 可选,限定工具
});

让模型看到错误后,它有机会换个方式重试或给用户一个交代。

二、限流边界:别过头

5. toolCallLimitMiddleware —— 限制工具调用次数

限制工具的调用次数,防止狂调昂贵 API、死循环。runLimit 是单次调用内,threadLimit 是整个会话(需 checkpointer)。

js
toolCallLimitMiddleware({
  toolName: "search", // 省略则限制全部工具
  runLimit: 1,        // 单次调用最多 1 次
  threadLimit: 20,    // 整个会话最多 20 次
  exitBehavior: "continue", // continue(拦截并返回 error 消息)/ error(抛错)/ end(停止)
});

示例:runLimit: 1、用户一次问三个城市,超出的调用被拦截:

[error]   "Tool call limit exceeded. Do not make additional tool calls."
[error]   "Tool call limit exceeded. Do not make additional tool calls."
[success] "北京 晴 25℃"

6. modelCallLimitMiddleware —— 限制模型调用次数

限制模型调用次数,exitBehavior 默认 "end"(优雅收尾),也可设 "error" 抛异常。

js
modelCallLimitMiddleware({ runLimit: 1, threadLimit: 10, exitBehavior: "end" });

示例:runLimit: 1 时,超出后最后一条消息变成:

"Model call limits exceeded: run level call limit reached with 1 model calls"

三、上下文管理

7. contextEditingMiddleware —— 清理旧工具结果

上下文变长时,清理较早的工具调用结果以腾出空间(保留最近几条),比整体摘要更轻量。默认策略是 ClearToolUsesEdit。

js
import { contextEditingMiddleware, ClearToolUsesEdit } from "langchain";

contextEditingMiddleware({
  edits: [
    new ClearToolUsesEdit({
      trigger: { tokens: 100000 }, // 触发条件(也可 messages / fraction)
      keep: { messages: 3 },       // 保留最近几条工具结果
      clearToolInputs: false,      // 是否同时清空工具调用参数
      excludeTools: [],            // 不清理的工具
      placeholder: "[cleared]",    // 被清理后的占位符
    }),
  ],
});

示例:把阈值调低,工具结果被替换:

工具消息内容已被清理为:"[cleared]"

四、动态行为

8. dynamicSystemPromptMiddleware —— 动态系统提示词

在每次调用模型前,根据运行时状态 / 上下文动态生成系统提示词。

js
const mw = dynamicSystemPromptMiddleware(
  (state, runtime) => `你是助手,用户所在区域是 ${runtime.context.region ?? "未知"}。先复述这个区域。`
);

const agent = createAgent({
  model, tools: [],
  contextSchema: z.object({ region: z.string().optional() }),
  middleware: [mw],
});

const res = await agent.invoke(
  { messages: [{ role: "user", content: "你好" }] },
  { context: { region: "EU" } }
);
// 回答里会带上 "EU"

五、工具治理

9. llmToolSelectorMiddleware —— 工具太多先筛选

工具很多(10+)时,先用一个(通常更小的)模型挑出与当前问题最相关的几个,再交给主模型,减少 Token、提升准确率。

js
llmToolSelectorMiddleware({
  model,             // 选择用的模型,默认主模型
  maxTools: 1,       // 最多选几个
  alwaysInclude: ["search"], // 永远保留的工具
});

示例:注册 4 个工具、maxTools: 1,用一层自定义中间件打印主模型实际看到的工具:

主模型实际看到的工具:get_weather

(这里问的是天气,get_weather 被选中,其余 3 个被过滤掉。)

10. toolEmulatorMiddleware —— 用 LLM 模拟工具

测试时不真正执行工具,而是让 LLM 编一个「像模像样」的结果,适合还没接好外部依赖时开发。

js
toolEmulatorMiddleware({ tools: ["get_weather"], model });

示例中真实工具函数不会被调用,工具消息由 LLM 生成:

工具消息(由 LLM 模拟生成):{ "city":"北京","temperature":18,"condition":"多云", ... }
真实工具函数是否被调用:false

11. piiRedactionMiddleware —— 请求脱敏 + 响应还原

和 demo-15 的 piiMiddleware 不同:它在发给模型的请求里把 PII 抹成标记,但在模型响应 / 工具调用里把标记还原回原值,保证工具仍能拿到真实数据。

js
piiRedactionMiddleware({ rules: { ssn: /\b\d{3}-\d{2}-\d{4}\b/g } });

示例:用户消息里有 SSN,工具最终仍收到原文:

工具实际收到的 SSN(已从脱敏标记还原):"123-45-6789"

注意:该中间件已标记 deprecated,官方建议用 piiMiddleware。

六、厂商能力

12. providerToolSearchMiddleware —— 服务端工具搜索

把一部分工具「藏」在厂商的服务端工具搜索里,模型按需搜索发现,减少上下文占用。依赖厂商能力(Anthropic Claude Sonnet 4+/Opus 4+ 或 OpenAI gpt-5.4+),DeepSeek 不支持。

js
import { providerToolSearchMiddleware } from "langchain";
import { ChatAnthropic } from "@langchain/anthropic";

const agent = createAgent({
  model: new ChatAnthropic({ model: "claude-sonnet-4-5" }),
  tools: [getWeather, lookupOrder],
  middleware: [
    providerToolSearchMiddleware({ searchableTools: [lookupOrder] }),
  ],
});

附:3 个厂商专属中间件

这三个只在对应厂商上生效:

中间件厂商作用
anthropicPromptCachingMiddlewareAnthropic给提示词加缓存,降低重复前缀的成本与延迟
bedrockPromptCachingMiddlewareAWS Bedrock同上,针对 Bedrock Converse
openAIModerationMiddlewareOpenAI用 OpenAI 审核接口拦截违规内容
js
anthropicPromptCachingMiddleware({ ttl: "5m" });
bedrockPromptCachingMiddleware();
openAIModerationMiddleware({ model, exitBehavior: "end" });

Python 对应写法

名字与 JS 基本一致(首字母大写),定义在 langchain.agents.middleware:

python
from langchain.agents import create_agent
from langchain.agents.middleware import (
    ToolRetryMiddleware,
    ModelRetryMiddleware,
    ModelFallbackMiddleware,
    ToolErrorMiddleware,
    ToolCallLimitMiddleware,
    ModelCallLimitMiddleware,
    ContextEditingMiddleware,
    LLMToolSelectorMiddleware,
    ToolEmulatorMiddleware,
    ProviderToolSearchMiddleware,
)

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[
        ToolRetryMiddleware(max_retries=3, on_failure="continue"),
        ToolCallLimitMiddleware(run_limit=10),
        ModelCallLimitMiddleware(run_limit=5, exit_behavior="end"),
        LLMToolSelectorMiddleware(max_tools=3),
    ],
)

动态提示词在 Python 里常用装饰器 @dynamic_prompt:

python
from langchain.agents.middleware import dynamic_prompt, ModelRequest

@dynamic_prompt
def add_context(request: ModelRequest) -> str:
    return f"你是助手,用户区域:{request.runtime.context.get('region', '未知')}"

参数命名规律与 demo-15 一致:JS 驼峰 → Python 下划线(maxRetries ↔ max_retries、exitBehavior ↔ exit_behavior)。

小结

分组中间件
可靠性toolRetry / modelRetry / modelFallback / toolError
限流边界toolCallLimit / modelCallLimit
上下文contextEditing
动态行为dynamicSystemPrompt
工具治理llmToolSelector / toolEmulator / piiRedaction(已废弃)
厂商能力providerToolSearch(需 Anthropic/OpenAI)

记忆规律:

  • 名字带 retry / fallback / error 的,管「失败怎么办」;
  • 带 limit 的,管「别过头」;
  • 带 selector / emulator 的,管「工具怎么用」;
  • 需要跨轮状态或中断的中间件,记得配 checkpointer。

结合 demo-15 的四个,LangChain 内置中间件这套「可插拔的横切能力」就基本铺满了——按需挑选、自由叠加即可。

评论
0/100