第 26 课: Agent 中间件与节点系统

2 阅读6分钟

课程目标

精读 Agent 的中间件系统和节点架构。理解 createMiddleware() 的生命周期钩子、内置中间件库、RunnableCallable 的桥接作用,以及 Agent 图中节点的编排方式。


26.1 中间件的设计理念

中间件是 Agent 系统最强大的扩展机制。它允许在 Agent 执行的关键节点插入自定义逻辑,而不改变核心 ReAct 循环。

六个生命周期钩子

START
  │
  ▼
beforeAgent ──────── 整个 Agent 调用开始前(只执行一次)
  │
  ▼
beforeModel ──────── 每次 LLM 调用前
  │
  ▼
[AgentNode 调用 LLM]
  │
  ├── wrapModelCall ── 包装 LLM 调用(可修改请求/响应)
  │
  ▼
afterModel ───────── 每次 LLM 调用后
  │
  ▼
[ToolNode 执行工具]
  │
  ├── wrapToolCall ─── 包装工具调用(可修改参数/结果)
  │
  ▼
(循环回到 beforeModel)
  │
  ▼
afterAgent ──────── 整个 Agent 调用结束后(只执行一次)
  │
  ▼
END

26.2 createMiddleware — 创建中间件

源码位置: libs/langchain/src/agents/middleware.ts

export function createMiddleware<
  TSchema extends StateDefinitionInit | undefined = undefined,
  TContextSchema extends InteropZodObject | undefined = undefined,
  const TTools extends readonly (ClientTool | ServerTool)[] = readonly [],
>(config: {
  name: string;
  stateSchema?: TSchema;
  contextSchema?: TContextSchema;
  tools?: TTools;
  wrapToolCall?: WrapToolCallHook<TSchema, ...>;
  wrapModelCall?: WrapModelCallHook<TSchema, ...>;
  beforeAgent?: BeforeAgentHook<TSchema, ...>;
  beforeModel?: BeforeModelHook<TSchema, ...>;
  afterModel?: AfterModelHook<TSchema, ...>;
  afterAgent?: AfterAgentHook<TSchema, ...>;
}): AgentMiddleware {
  return {
    [MIDDLEWARE_BRAND]: true,  // 品牌标记,防止误将函数当作中间件
    name: config.name,
    stateSchema: config.stateSchema,
    ...config,
  };
}

MIDDLEWARE_BRAND 是一个 Symbol,用于区分中间件实例和普通对象。这防止了 JavaScript 函数(也有 name 属性)被误识别为中间件。


26.3 钩子类型详解

26.3.1 beforeAgent / afterAgent — 一次性钩子

只在 Agent 调用的开始和结束各执行一次:

源码位置: libs/langchain/src/agents/middleware/types.ts:249

type BeforeAgentHandler<TSchema, TContext> = (
  state: InferSchemaValueType<TSchema>,
  runtime: Runtime<TContext>
) => PromiseOrValue<MiddlewareResult<Partial<InferSchemaUpdateType<TSchema>>>>;

// 支持两种形式
export type BeforeAgentHook<TSchema, TContext> =
  | BeforeAgentHandler<TSchema, TContext>         // 简单函数
  | {
      hook: BeforeAgentHandler<TSchema, TContext>; // 带路由配置
      canJumpTo?: JumpToTarget[];                  // 允许跳转的目标
    };

26.3.2 beforeModel / afterModel — 循环钩子

在 ReAct 循环中的每次 LLM 调用前后执行:

type AfterModelHandler<TSchema, TContext> = (
  state: InferSchemaValueType<TSchema>,
  runtime: Runtime<TContext>
) => PromiseOrValue<MiddlewareResult<Partial<InferSchemaUpdateType<TSchema>>>>;

26.3.3 wrapModelCall — 包装 LLM 调用

可以修改发送给 LLM 的请求和返回的响应:

export type WrapModelCallHook<TSchema, TContext> = (
  request: ModelRequest<...>,
  handler: WrapModelCallHandler<TSchema, TContext>
) => PromiseOrValue<AIMessage | Command>;

26.3.4 wrapToolCall — 包装工具调用

可以拦截和修改工具调用:

export type WrapToolCallHook<TSchema, TContext> = (
  request: ToolCallRequest<...>,
  handler: ToolCallHandler<...>
) => PromiseOrValue<ToolMessage | Command>;

26.4 中间件如何变成图节点

ReactAgent 构造函数中,每个中间件的钩子被转化为独立的图节点:

源码位置: libs/langchain/src/agents/ReactAgent.ts:303

for (let i = 0; i < middleware.length; i++) {
  const m = middleware[i];
  if (m.beforeAgent) {
    const node = new BeforeAgentNode(m);
    const name = `${m.name}.before_agent`;
    beforeAgentNodes.push({ index: i, name, allowed: getHookConstraint(m.beforeAgent) });
    allNodeWorkflows.addNode(name, node, node.nodeOptions);
  }
  if (m.beforeModel) {
    const node = new BeforeModelNode(m);
    const name = `${m.name}.before_model`;
    beforeModelNodes.push({ index: i, name });
    allNodeWorkflows.addNode(name, node, node.nodeOptions);
  }
  // afterModel, afterAgent 类似...
}

边的连接规则

  • beforeAgent 节点:链式串联,最后连接到 beforeModelmodel_request
  • beforeModel 节点:链式串联,最后连接到 model_request
  • afterModel 节点:反序串联(最后定义的先执行)
  • afterAgent 节点:反序串联,最后连接到 END

26.5 内置中间件库

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

源码位置: libs/langchain/src/agents/middleware/index.ts

中间件用途关键配置
humanInTheLoopMiddleware人机协作审批interruptOn 指定哪些工具需要审批
summarizationMiddleware上下文摘要消息超过阈值时自动摘要历史
modelCallLimitMiddleware模型调用限制runLimit, threadLimit
toolCallLimitMiddleware工具调用限制防止 Agent 无限调用工具
modelRetryMiddleware模型调用重试失败时自动重试
modelFallbackMiddleware模型降级主力模型失败时切换备选
toolRetryMiddleware工具调用重试工具失败时重试
dynamicSystemPromptMiddleware动态系统提示根据状态动态修改 system prompt
contextEditingMiddleware上下文编辑清理/修改消息历史
piiMiddlewarePII 检测检测并处理个人信息
piiRedactionMiddlewarePII 脱敏自动脱敏个人信息
llmToolSelectorMiddlewareLLM 工具选择使用 LLM 动态选择可用工具
toolEmulatorMiddleware工具模拟用 LLM 模拟工具执行
todoListMiddleware任务清单维护 Agent 的任务跟踪列表

26.6 RunnableCallable — 函数到 Runnable 的桥接

RunnableCallable 是 Agent 节点系统的基础类,将普通函数包装为 Runnable。

源码位置: libs/langchain/src/agents/RunnableCallable.ts

export class RunnableCallable<I = unknown, O = unknown> extends Runnable<I, O> {
  lc_namespace: string[] = ["langgraph"];
  func: (...args: I[]) => O | Promise<O>;
  #state: Awaited<O>;

  constructor(fields: RunnableCallableArgs<I, O>) {
    super();
    this.name = fields.name ?? fields.func.name;
    this.func = fields.func;
    this.recurse = fields.recurse ?? true;
  }

  /** 获取节点的当前状态 */
  getState(): Awaited<O> { return this.#state; }

  /** 设置节点状态(用于 middleware 和 model 节点) */
  setState(state: Awaited<O>) {
    this.#state = { ...this.#state, ...state };
  }

  async invoke(input: I, options?: Partial<RunnableConfig>): Promise<O> {
    const mergedConfig = mergeConfigs(this.config, options);
    const returnValue = await AsyncLocalStorageProviderSingleton.runWithConfig(
      mergedConfig,
      async () => this.func(input, mergedConfig as I)
    );

    // 如果返回值也是 Runnable 且开启了递归,则继续调用
    if (Runnable.isRunnable(returnValue) && this.recurse) {
      return await returnValue.invoke(input, mergedConfig);
    }

    this.#state = returnValue;
    return returnValue;
  }
}

关键特性

  • getState() / setState():节点间共享状态的机制
  • recurse 模式:如果函数返回的是 Runnable,自动递归调用
  • 通过 AsyncLocalStorageProviderSingleton 传播配置上下文

26.7 wrapToolCall 的组合模式

多个中间件的 wrapToolCall 钩子通过组合模式串联为管道:

源码位置: libs/langchain/src/agents/utils.ts:502

function chainToolCallHandlers(handlers: WrapToolCallHook[]): WrapToolCallHook | undefined {
  if (handlers.length <= 1) return handlers[0];

  function composeTwo(outer: WrapToolCallHook, inner: WrapToolCallHook): WrapToolCallHook {
    return async (request, handler) => {
      const innerHandler: ToolCallHandler = async (passedRequest) => {
        return inner(passedRequest, handler);
      };
      return outer(request, innerHandler);
    };
  }

  // 从右到左组合: outer(inner(innermost(handler)))
  let result = handlers[handlers.length - 1];
  for (let i = handlers.length - 2; i >= 0; i--) {
    result = composeTwo(handlers[i], result);
  }
  return result;
}

这意味着 middleware: [auth, retry, cache]wrapToolCall 执行顺序是:auth 包装 retry 包装 cache 包装基础处理器。


26.8 jumpTo — 中间件控制流程

canJumpTo 配置的钩子可以改变 Agent 的执行流:

const middleware = createMiddleware({
  name: "ConditionalRouter",
  afterModel: {
    hook: async (state, runtime) => {
      // 检查条件,决定跳转目标
      if (someCondition) {
        return { jumpTo: "model" };   // 重新调用 LLM
      }
      if (anotherCondition) {
        return { jumpTo: "end" };     // 直接终止
      }
      // 返回 undefined 或 void → 正常继续
    },
    canJumpTo: ["model", "end"],      // 声明允许的跳转目标
  },
});

canJumpTo 决定了图中条件边的目标节点集合。没有声明的跳转目标不会被添加到图中。


26.9 实战练习:自定义中间件

import { z } from "zod";
import { createAgent, createMiddleware, tool } from "langchain";
import { StateSchema, ReducedValue } from "@langchain/langgraph";

// 中间件 1:工具调用审计
const auditMiddleware = createMiddleware({
  name: "audit",
  stateSchema: new StateSchema({
    _auditLog: new ReducedValue(
      z.array(z.string()).default(() => []),
      {
        inputSchema: z.string(),
        reducer: (current: string[], next: string) => [...current, next],
      }
    ),
  }),
  wrapToolCall: async (request, handler) => {
    const entry = `[${new Date().toISOString()}] 工具: ${request.toolCall.name}`;
    const result = await handler(request);
    return result;
  },
});

// 中间件 2:模型调用限制
const limitMiddleware = createMiddleware({
  name: "callLimit",
  stateSchema: z.object({
    _callCount: z.number().default(0),
  }),
  beforeModel: async (state) => {
    if (state._callCount >= 5) {
      return { jumpTo: "end" as const };
    }
    return { _callCount: state._callCount + 1 };
  },
});

const searchTool = tool(
  async ({ query }) => `结果:${query}`,
  {
    name: "search",
    description: "搜索信息",
    schema: z.object({ query: z.string() }),
  }
);

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [searchTool],
  middleware: [auditMiddleware, limitMiddleware],
});

const result = await agent.invoke({
  messages: [{ role: "user", content: "帮我搜索一些信息" }],
});

26.10 源码精读路线

优先级文件关注点
P0agents/middleware.tscreateMiddleware() — 中间件工厂函数
P0agents/middleware/types.ts钩子类型定义:BeforeModelHook, WrapToolCallHook
P1agents/RunnableCallable.ts函数到 Runnable 的桥接
P1agents/ReactAgent.ts:300-670中间件节点如何添加到图中
P1agents/utils.ts:502-540chainToolCallHandlers — 工具调用的组合模式
P2agents/middleware/index.ts内置中间件导出列表
P2agents/nodes/BeforeModelNode.ts中间件节点的具体实现
P2agents/nodes/AfterModelNode.tsafterModel 节点的实现

本课收获总结

级别你应该掌握的
🟢 基础理解 Agent 内部的图结构:节点 + 边;知道有哪些内置中间件
🔵 中阶掌握 createMiddleware() 的六个生命周期钩子
🟡 高阶理解中间件如何被编译为图节点;掌握 wrapToolCall / wrapModelCall 的使用
🟠 资深分析 RunnableCallable 在 Agent 中的桥接作用;理解 jumpTo 控制流
🔴 架构设计 Agent 中间件体系:审计、限流、缓存、安全的分层方案

下一课预告

第 27 课深入多 Agent 协作 —— 理解 supervisor 模式、子 Agent 封装为工具、Agent 间通信与 withAgentName() 的使用。