课程目标
精读 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节点:链式串联,最后连接到beforeModel或model_requestbeforeModel节点:链式串联,最后连接到model_requestafterModel节点:反序串联(最后定义的先执行)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 | 上下文编辑 | 清理/修改消息历史 |
piiMiddleware | PII 检测 | 检测并处理个人信息 |
piiRedactionMiddleware | PII 脱敏 | 自动脱敏个人信息 |
llmToolSelectorMiddleware | LLM 工具选择 | 使用 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 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | agents/middleware.ts | createMiddleware() — 中间件工厂函数 |
| P0 | agents/middleware/types.ts | 钩子类型定义:BeforeModelHook, WrapToolCallHook 等 |
| P1 | agents/RunnableCallable.ts | 函数到 Runnable 的桥接 |
| P1 | agents/ReactAgent.ts:300-670 | 中间件节点如何添加到图中 |
| P1 | agents/utils.ts:502-540 | chainToolCallHandlers — 工具调用的组合模式 |
| P2 | agents/middleware/index.ts | 内置中间件导出列表 |
| P2 | agents/nodes/BeforeModelNode.ts | 中间件节点的具体实现 |
| P2 | agents/nodes/AfterModelNode.ts | afterModel 节点的实现 |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解 Agent 内部的图结构:节点 + 边;知道有哪些内置中间件 |
| 🔵 中阶 | 掌握 createMiddleware() 的六个生命周期钩子 |
| 🟡 高阶 | 理解中间件如何被编译为图节点;掌握 wrapToolCall / wrapModelCall 的使用 |
| 🟠 资深 | 分析 RunnableCallable 在 Agent 中的桥接作用;理解 jumpTo 控制流 |
| 🔴 架构 | 设计 Agent 中间件体系:审计、限流、缓存、安全的分层方案 |
下一课预告
第 27 课深入多 Agent 协作 —— 理解 supervisor 模式、子 Agent 封装为工具、Agent 间通信与 withAgentName() 的使用。