Skip to content

LangChain 教程 13|中间件概述:Agent 执行的拦截器

📖 本篇导读:这是 LangChain 系列教程的第 13 篇。本篇将深入讲解 Agent 执行循环、中间件的作用点、两种 Hook 风格,以及预置中间件的使用。读完预计需要 10 分钟。

简单来说

中间件(Middleware)让你能在 Agent 执行的关键节点插入自定义逻辑——就像高速公路上的收费站,每辆车(每次调用)经过时都会被检查、记录或修改。

本节目标

学完本节,你将能够:

  • 理解 Agent 执行循环和中间件的作用点
  • 掌握中间件的两种 Hook 风格:节点式和包装式
  • 了解 LangChain 提供的预置中间件
  • 为后续学习自定义中间件打下基础

业务场景

想象这些真实需求:

  1. 日志追踪:记录每次模型调用的输入输出,用于调试和审计
  2. 成本控制:限制单次会话的 API 调用次数,防止失控
  3. 安全防护:在用户输入到达模型前检测敏感信息
  4. 智能重试:当外部 API 调用失败时自动重试

这些需求都需要在 Agent 执行过程中"插一脚"——中间件正是为此而生。

没有中间件 vs 有中间件的痛点对比


一、Agent 执行循环

1.1 核心循环

Agent 的核心执行循环很简单:

用户输入 → 调用模型 → 模型决定调用工具 → 执行工具 → 返回结果给模型 → 模型继续或结束
┌─────────────────────────────────────────┐
│               Agent Loop                │
│                                         │
│   ┌─────────┐    ┌─────────┐           │
│   │  Model  │───▶│  Tools  │           │
│   │  Call   │◀───│Execute  │           │
│   └─────────┘    └─────────┘           │
│        │              │                 │
│        └──────────────┘                 │
│         (循环直到完成)                   │
└─────────────────────────────────────────┘

1.2 中间件的作用点

中间件在循环的关键位置暴露钩子(Hooks):

                    beforeAgent


              ┌─────────────────┐
              │  Agent 开始执行  │
              └────────┬────────┘

        ┌──────────────┼──────────────┐
        │              ▼              │
        │        beforeModel          │
        │              │              │
        │        wrapModelCall        │
        │              │              │
        │         ▼▼▼▼▼▼▼▼           │
        │      ┌──────────┐          │
        │      │  Model   │          │
        │      └──────────┘          │
        │         ▼▼▼▼▼▼▼▼           │
        │              │              │
        │         afterModel          │
        │              │              │
        │        wrapToolCall         │
        │              │              │
        │         ▼▼▼▼▼▼▼▼           │
        │      ┌──────────┐          │
        │      │  Tools   │          │
        │      └──────────┘          │
        │              │              │
        └──────────────┼──────────────┘
                       │ (循环)
              ┌────────┴────────┐
              │  Agent 执行完成  │
              └────────┬────────┘

                  afterAgent


                   返回结果

中间件 Hook 作用点流程图


二、中间件基础用法

2.1 添加中间件

通过 createAgentmiddleware 参数添加中间件:

typescript
import {
  createAgent,
  summarizationMiddleware,
  humanInTheLoopMiddleware,
} from "langchain";

const agent = createAgent({
  model: "gpt-4o",
  tools: [searchTool, calculatorTool],
  middleware: [
    summarizationMiddleware({ model: "gpt-4o-mini", trigger: { tokens: 4000 } }),
    humanInTheLoopMiddleware({ interruptOn: { dangerousTool: true } }),
  ],
});

2.2 中间件执行顺序

当有多个中间件时,执行顺序如下:

typescript
const agent = createAgent({
  model: "gpt-4o",
  middleware: [middleware1, middleware2, middleware3],
  tools: [...],
});

执行流程

  1. Before 钩子:按顺序执行

    • middleware1.beforeAgent()
    • middleware2.beforeAgent()
    • middleware3.beforeAgent()
  2. Wrap 钩子:嵌套执行(像洋葱一样)

    • middleware1.wrapModelCall()middleware2.wrapModelCall()middleware3.wrapModelCall() → 模型
  3. After 钩子:逆序执行

    • middleware3.afterModel()
    • middleware2.afterModel()
    • middleware1.afterModel()

类比理解:想象三道安检门,进去时按 1→2→3 顺序过,出来时按 3→2→1 顺序过。Wrap 钩子则像俄罗斯套娃,最外层的中间件包裹着里面的。


三、两种 Hook 风格

节点式 Hook vs 包装式 Hook 对比

3.1 节点式 Hook(Node-style)

在特定执行点顺序运行,适合日志记录、验证和状态更新:

Hook触发时机
beforeAgentAgent 开始执行前(每次调用一次)
beforeModel每次模型调用前
afterModel每次模型响应后
afterAgentAgent 执行完成后(每次调用一次)
typescript
import { createMiddleware, AIMessage } from "langchain";

const loggingMiddleware = createMiddleware({
  name: "LoggingMiddleware",
  beforeModel: (state) => {
    console.log(`即将调用模型,当前消息数: ${state.messages.length}`);
    return;
  },
  afterModel: (state) => {
    const lastMessage = state.messages[state.messages.length - 1];
    console.log(`模型返回: ${lastMessage.content}`);
    return;
  },
});

3.2 包装式 Hook(Wrap-style)

包裹模型或工具调用,你决定是否执行、执行几次:

Hook作用
wrapModelCall包裹每次模型调用
wrapToolCall包裹每次工具调用
typescript
import { createMiddleware } from "langchain";

const retryMiddleware = createMiddleware({
  name: "RetryMiddleware",
  wrapModelCall: (request, handler) => {
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        return handler(request);
      } catch (e) {
        if (attempt === 2) throw e;
        console.log(`重试 ${attempt + 1}/3`);
      }
    }
    throw new Error("不可达");
  },
});

包装式 Hook 的超能力

  • 零次调用:短路返回,跳过实际执行
  • 一次调用:正常执行
  • 多次调用:实现重试逻辑

四、预置中间件速览

LangChain 提供了丰富的开箱即用中间件:

预置中间件功能矩阵

4.1 通用中间件

中间件功能使用场景
summarizationMiddleware自动摘要长对话长对话超出上下文窗口
humanInTheLoopMiddleware人工审批工具调用高风险操作需人工确认
modelCallLimitMiddleware限制模型调用次数防止无限循环、控制成本
toolCallLimitMiddleware限制工具调用次数限制 API 调用频率
modelFallbackMiddleware模型故障自动降级提高系统可用性
piiMiddleware检测隐私信息合规要求、日志脱敏
todoListMiddleware任务规划与追踪复杂多步骤任务
llmToolSelectorMiddleware智能筛选相关工具工具数量多时提高准确性
toolRetryMiddleware工具调用失败重试处理临时网络故障
modelRetryMiddleware模型调用失败重试处理 API 限流
toolEmulatorMiddleware用 LLM 模拟工具执行开发测试阶段
contextEditingMiddleware清理旧工具输出长对话上下文管理

4.2 快速示例

typescript
import {
  createAgent,
  summarizationMiddleware,
  modelCallLimitMiddleware,
  piiMiddleware,
} from "langchain";

const agent = createAgent({
  model: "gpt-4o",
  tools: [searchTool],
  middleware: [
    summarizationMiddleware({
      model: "gpt-4o-mini",
      trigger: { tokens: 4000 },
      keep: { messages: 20 },
    }),
    modelCallLimitMiddleware({
      threadLimit: 10,
      runLimit: 5,
      exitBehavior: "end",
    }),
    piiMiddleware("email", { strategy: "redact", applyToInput: true }),
  ],
});

五、中间件能做什么

5.1 日志与监控

typescript
const analyticsMiddleware = createMiddleware({
  name: "AnalyticsMiddleware",
  beforeAgent: (state) => {
    console.log(`[开始] 会话开始,用户消息: ${state.messages[0]?.content}`);
    return;
  },
  afterAgent: (state) => {
    console.log(`[结束] 会话结束,总消息数: ${state.messages.length}`);
    return;
  },
  wrapModelCall: (request, handler) => {
    const start = Date.now();
    const result = handler(request);
    console.log(`[模型] 调用耗时: ${Date.now() - start}ms`);
    return result;
  },
});

5.2 转换与过滤

typescript
const promptEnhanceMiddleware = createMiddleware({
  name: "PromptEnhanceMiddleware",
  beforeModel: (state) => {
    return {
      messages: [
        ...state.messages,
        { role: "system", content: "请用简洁专业的语言回复。" }
      ]
    };
  },
});

5.3 流程控制

typescript
const rateLimitMiddleware = createMiddleware({
  name: "RateLimitMiddleware",
  beforeModel: {
    canJumpTo: ["end"],
    hook: (state) => {
      if (state.messages.length > 50) {
        return {
          messages: [new AIMessage("对话已达上限,请开始新会话。")],
          jumpTo: "end",
        };
      }
      return;
    }
  },
});

六、中间件 vs 其他方案

中间件 vs 直接修改代码 vs 外部包装器对比

方案适用场景优势劣势
中间件Agent 执行过程中的横切关注点解耦、可复用、可组合学习曲线
直接修改代码简单一次性逻辑直接、快速难以复用、代码耦合
外部包装器Agent 调用前后的处理简单无法介入内部执行

选择中间件的信号

  • 需要在多个 Agent 间复用相同逻辑
  • 需要介入模型或工具调用的内部过程
  • 需要条件性地短路或重试执行

常见问题

Q1: 中间件和 LangGraph 的节点有什么区别?

中间件是"横切关注点",在现有执行流程中插入逻辑;LangGraph 节点是"核心业务逻辑",定义执行流程本身。

Q2: 中间件执行失败会怎样?

默认情况下,中间件错误会导致整个 Agent 调用失败。建议在中间件内部进行错误处理,避免影响主流程。

Q3: 可以动态添加/移除中间件吗?

中间件在 createAgent 时固定。如果需要动态行为,可以在中间件内部使用条件判断,或创建多个 Agent 配置。


总结

中间件是 Agent 执行过程中的"拦截器":

概念说明
执行循环Model → Tools → Model(循环直到完成)
作用点beforeAgent/Model、afterAgent/Model、wrapModelCall、wrapToolCall
节点式 Hook顺序执行,用于日志、验证
包装式 Hook嵌套执行,用于重试、缓存、转换
执行顺序before 正序,after 逆序,wrap 嵌套

核心理念:中间件让你在不修改核心逻辑的情况下,灵活地添加日志、安全、重试等横切关注点。

下一篇,我们将深入学习 LangChain 提供的各种预置中间件!

读文档、看源码、写代码,理解 AI Agent 本质 🤖