通过实现在代理执行流程中特定点运行的钩子来构建自定义 middleware。

钩子

Middleware 提供两种钩子样式来拦截代理执行:

Node-style hooks

在特定执行点顺序运行。用于日志记录、验证和状态更新。 可用的钩子:
  • beforeAgent - 代理启动前(每次调用执行一次)
  • beforeModel - 每次模型调用前
  • afterModel - 每次模型响应后
  • afterAgent - 代理完成后(每次调用执行一次)
示例:
import { createMiddleware, AIMessage } from "langchain";

const createMessageLimitMiddleware = (maxMessages: number = 50) => {
  return createMiddleware({
    name: "MessageLimitMiddleware",
    beforeModel: (state) => {
      if (state.messages.length === maxMessages) {
        return {
          messages: [new AIMessage("Conversation limit reached.")],
          jumpTo: "end",
        };
      }
      return;
    },
    afterModel: (state) => {
      const lastMessage = state.messages[state.messages.length - 1];
      console.log(`Model returned: ${lastMessage.content}`);
      return;
    },
  });
};

Wrap-style hooks

拦截执行并控制何时调用处理器。用于重试、缓存和转换。 您可以决定处理器是调用零次(短路)、一次(正常流程)还是多次(重试逻辑)。 可用的钩子:
  • wrapModelCall - 包装每次模型调用
  • wrapToolCall - 包装每次工具调用
示例:
import { createMiddleware } from "langchain";

const createRetryMiddleware = (maxRetries: number = 3) => {
  return createMiddleware({
    name: "RetryMiddleware",
    wrapModelCall: (request, handler) => {
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
          return handler(request);
        } catch (e) {
          if (attempt === maxRetries - 1) {
            throw e;
          }
          console.log(`Retry ${attempt + 1}/${maxRetries} after error: ${e}`);
        }
      }
      throw new Error("Unreachable");
    },
  });
};

创建 middleware

使用 createMiddleware 函数定义自定义 middleware:
import { createMiddleware } from "langchain";

const loggingMiddleware = createMiddleware({
  name: "LoggingMiddleware",
  beforeModel: (state) => {
    console.log(`About to call model with ${state.messages.length} messages`);
    return;
  },
  afterModel: (state) => {
    const lastMessage = state.messages[state.messages.length - 1];
    console.log(`Model returned: ${lastMessage.content}`);
    return;
  },
});

自定义状态模式

Middleware 可以使用自定义属性扩展代理的状态。这使得 middleware 能够:
  • 跨执行跟踪状态 :维护在代理执行生命周期中持续存在的计数器、标志或其他值
  • 在钩子之间共享数据 :将信息从 beforeModel 传递到 afterModel 或在不同的 middleware 实例之间传递
  • 实现横切关注点 :添加速率限制、使用跟踪、用户上下文或审计日志等功能,而无需修改核心代理逻辑
  • 做出条件决策 :使用累积状态来确定是否继续执行、跳转到不同节点或动态修改行为
import { createMiddleware, createAgent, HumanMessage } from "langchain";
import * as z from "zod";

const callCounterMiddleware = createMiddleware({
  name: "CallCounterMiddleware",
  stateSchema: z.object({
    modelCallCount: z.number().default(0),
    userId: z.string().optional(),
  }),
  beforeModel: (state) => {
    if (state.modelCallCount > 10) {
      return { jumpTo: "end" };
    }
    return;
  },
  afterModel: (state) => {
    return { modelCallCount: state.modelCallCount + 1 };
  },
});

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

const result = await agent.invoke({
  messages: [new HumanMessage("Hello")],
  modelCallCount: 0,
  userId: "user-123",
});
状态字段可以是公共的或私有的。以下划线( _ )开头的字段被视为私有字段,不会包含在代理的结果中。只有公共字段(不带前导下划线的字段)会被返回。 这对于存储不应暴露给调用者的内部 middleware 状态非常有用,例如临时跟踪变量或内部标志:
const middleware = createMiddleware({
  name: "ExampleMiddleware",
  stateSchema: z.object({
    // Public field - included in invoke result
    publicCounter: z.number().default(0),
    // Private field - excluded from invoke result
    _internalFlag: z.boolean().default(false),
  }),
  afterModel: (state) => {
    // Both fields are accessible during execution
    if (state._internalFlag) {
      return { publicCounter: state.publicCounter + 1 };
    }
    return { _internalFlag: true };
  },
});

const result = await agent.invoke({
  messages: [new HumanMessage("Hello")],
  publicCounter: 0
});

// result only contains publicCounter, not _internalFlag
console.log(result.publicCounter); // 1
console.log(result._internalFlag); // undefined

自定义上下文

Middleware 可以定义自定义上下文模式来访问每次调用的元数据。与状态不同,上下文是只读的,不会在调用之间持久化。这使其非常适合:
  • 用户信息 :传递执行期间不变的用户 ID、角色或偏好设置
  • 配置覆盖 :提供每次调用的设置,如速率限制或功能标志
  • 租户/工作区上下文 :包含多租户应用程序的组织特定数据
  • 请求元数据 :传递 middleware 所需的请求 ID、API 密钥或其他元数据
使用 Zod 定义上下文模式,并在 middleware 钩子中通过 runtime.context 访问它。上下文模式中的必填字段将在 TypeScript 级别强制执行,确保您在调用 agent.invoke() 时必须提供它们。
import { createAgent, createMiddleware, HumanMessage } from "langchain";
import * as z from "zod";

const contextSchema = z.object({
  userId: z.string(),
  tenantId: z.string(),
  apiKey: z.string().optional(),
});

const userContextMiddleware = createMiddleware({
  name: "UserContextMiddleware",
  contextSchema,
  wrapModelCall: (request, handler) => {
    // Access context from runtime
    const { userId, tenantId } = request.runtime.context;

    // Add user context to system message
    const contextText = `User ID: ${userId}, Tenant: ${tenantId}`;
    const newSystemMessage = request.systemMessage.concat(contextText);

    return handler({
      ...request,
      systemMessage: newSystemMessage,
    });
  },
});

const agent = createAgent({
  model: "gpt-4o",
  middleware: [userContextMiddleware],
  tools: [],
  contextSchema,
});

const result = await agent.invoke(
  { messages: [new HumanMessage("Hello")] },
  // Required fields (userId, tenantId) must be provided
  {
    context: {
      userId: "user-123",
      tenantId: "acme-corp",
    },
  }
);
必需的上下文字段 :当您在 contextSchema 中定义必需字段时(没有 .optional() .default() 的字段),TypeScript 将强制要求在 agent.invoke() 调用期间必须提供这些字段。这确保了类型安全并防止因缺少必需上下文而导致的运行时错误。
// This will cause a TypeScript error if userId or tenantId are missing
const result = await agent.invoke(
  { messages: [new HumanMessage("Hello")] },
  { context: { userId: "user-123" } } // Error: tenantId is required
);

Execution order

当使用多个中间件时,了解它们的执行方式:
const agent = createAgent({
  model: "gpt-4o",
  middleware: [middleware1, middleware2, middleware3],
  tools: [...],
});
前置钩子按顺序运行:
  1. middleware1.before_agent()
  2. middleware2.before_agent()
  3. middleware3.before_agent()
智能体循环开始
  1. middleware1.before_model()
  2. middleware2.before_model()
  3. middleware3.before_model()
Wrap hooks nest like function calls:
  1. middleware1.wrap_model_call() middleware2.wrap_model_call() middleware3.wrap_model_call() → model
After hooks run in reverse order:
  1. middleware3.after_model()
  2. middleware2.after_model()
  3. middleware1.after_model()
Agent loop ends
  1. middleware3.after_agent()
  2. middleware2.after_agent()
  3. middleware1.after_agent()
Key rules:
  • before_* hooks: First to last
  • after_* hooks: Last to first (reverse)
  • wrap_* hooks: Nested (first middleware wraps all others)

Agent jumps

To exit early from middleware, return a dictionary with jump_to : Available jump targets:
  • 'end' : Jump to the end of the agent execution (or the first after_agent hook)
  • 'tools' : Jump to the tools node
  • 'model' : Jump to the model node (or the first before_model hook)
import { createAgent, createMiddleware, AIMessage } from "langchain";

const agent = createAgent({
  model: "gpt-4o",
  middleware: [
    createMiddleware({
      name: "BlockedContentMiddleware",
      beforeModel: {
        canJumpTo: ["end"],
        hook: (state) => {
          if (state.messages.at(-1)?.content.includes("BLOCKED")) {
            return {
              messages: [new AIMessage("I cannot respond to that request.")],
              jumpTo: "end" as const,
            };
          }
          return;
        },
      },
    }),
  ],
});

const result = await agent.invoke({
    messages: "Hello, world! BLOCKED"
});

/**
 * Expected output:
 * I cannot respond to that request.
 */
console.log(result.messages.at(-1)?.content);

Best practices

  1. 保持中间件专注 - 每个中间件应该只做好一件事
  2. 优雅地处理错误 - 不要让中间件错误导致智能体崩溃
  3. 使用适当的钩子类型
    • Node 风格用于顺序逻辑(日志记录、验证)
    • Wrap 风格用于控制流(重试、回退、缓存)
  4. 清楚地记录任何自定义状态属性
  5. 在集成之前独立对中间件进行单元测试
  6. 考虑执行顺序 - 将关键中间件放在列表首位
  7. 尽可能使用内置中间件

Examples

Dynamic model selection

import { createMiddleware, initChatModel } from "langchain";

const dynamicModelMiddleware = createMiddleware({
  name: "DynamicModelMiddleware",
  wrapModelCall: (request, handler) => {
    const modifiedRequest = { ...request };
    if (request.messages.length > 10) {
      modifiedRequest.model = initChatModel("gpt-4o");
    } else {
      modifiedRequest.model = initChatModel("gpt-4o-mini");
    }
    return handler(modifiedRequest);
  },
});

Tool call monitoring

import { createMiddleware } from "langchain";

const toolMonitoringMiddleware = createMiddleware({
  name: "ToolMonitoringMiddleware",
  wrapToolCall: (request, handler) => {
    console.log(`Executing tool: ${request.toolCall.name}`);
    console.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`);
    try {
      const result = handler(request);
      console.log("Tool completed successfully");
      return result;
    } catch (e) {
      console.log(`Tool failed: ${e}`);
      throw e;
    }
  },
});

Dynamically selecting tools

Select relevant tools at runtime to improve performance and accuracy. Benefits:
  • Shorter prompts - Reduce complexity by exposing only relevant tools
  • Better accuracy - Models choose correctly from fewer options
  • Permission control - Dynamically filter tools based on user access
import { createAgent, createMiddleware } from "langchain";

const toolSelectorMiddleware = createMiddleware({
  name: "ToolSelector",
  wrapModelCall: (request, handler) => {
    // Select a small, relevant subset of tools based on state/context
    const relevantTools = selectRelevantTools(request.state, request.runtime);
    const modifiedRequest = { ...request, tools: relevantTools };
    return handler(modifiedRequest);
  },
});

const agent = createAgent({
  model: "gpt-4o",
  tools: allTools,
  middleware: [toolSelectorMiddleware],
});

Working with system messages

Modify system messages in middleware using the systemMessage field in ModelRequest . It contains a SystemMessage object (even if the agent was created with a string systemPrompt ). Example: Chaining middleware - Different middleware can use different approaches:
import { createMiddleware, SystemMessage, createAgent } from "langchain";

// Middleware 1: Uses systemMessage with simple concatenation
const myMiddleware = createMiddleware({
  name: "MyMiddleware",
  wrapModelCall: async (request, handler) => {
    return handler({
      ...request,
      systemMessage: request.systemMessage.concat(`Additional context.`),
    });
  },
});

// Middleware 2: Uses systemMessage with structured content (preserves structure)
const myOtherMiddleware = createMiddleware({
  name: "MyOtherMiddleware",
  wrapModelCall: async (request, handler) => {
    return handler({
      ...request,
      systemMessage: request.systemMessage.concat(
        new SystemMessage({
          content: [
            {
              type: "text",
              text: " More additional context. This will be cached.",
              cache_control: { type: "ephemeral", ttl: "5m" },
            },
          ],
        })
      ),
    });
  },
});

const agent = createAgent({
  model: "anthropic:claude-3-5-sonnet",
  systemPrompt: "You are a helpful assistant.",
  middleware: [myMiddleware, myOtherMiddleware],
});
生成的系统消息将是:
new SystemMessage({
  content: [
    { type: "text", text: "You are a helpful assistant." },
    { type: "text", text: "Additional context." },
    {
        type: "text",
        text: " More additional context. This will be cached.",
        cache_control: { type: "ephemeral", ttl: "5m" },
    },
  ],
});
使用 SystemMessage.concat 来保留其他中间件创建的缓存控制元数据或结构化内容块。

Additional resources


连接这些文档 到 Claude、VSCode 等,通过 MCP 获取实时答案。