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

钩子

中间件提供两种风格的钩子来拦截代理执行:

节点式钩子

在特定执行点顺序运行。

包装式钩子

围绕每次模型或工具调用运行。

节点式钩子

在特定执行点顺序运行。用于日志记录、验证和状态更新。 选择中间件所需的钩子。您可以选择节点式钩子或包装式钩子。 节点式钩子在特定执行点运行: 包装式钩子围绕每次调用运行,让您控制执行: 示例:

包装式钩子

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

状态更新

节点式和包装式钩子都可以更新代理状态。其机制不同:
  • 节点式钩子 (beforeAgent, beforeModel, afterModel, afterAgent): 直接返回一个字典。字典通过图的归约器应用到代理状态。
  • 包装式钩子 (wrapModelCall, wrapToolCall): 对于模型调用,直接返回一个 Command 以在模型响应旁注入状态更新。对于工具调用,直接返回一个 Command。当您需要根据在模型或工具调用期间运行的逻辑来跟踪或更新状态时(例如摘要触发点、使用元数据或根据请求或响应计算的自定义字段),请使用这些钩子。

节点式钩子

从节点式钩子返回一个字典,将更新合并到代理状态中。字典键映射到状态字段。

包装式钩子

直接从 wrapModelCall 返回一个 Command,以从模型调用层注入状态更新:
Command 通过图的归约器流动,因此更新被正确应用,消息是累加的,而不是替换现有状态。

多个中间件的组合

当多个中间件层返回响应时,框架会传递最后产生的 AIMessage:
  • AIMessage 流过: 每个中间件的 handler() 接收来自前一层的 AIMessage。当中间件返回一个 AIMessage 时,它就成为下一个中间件处理程序的输入。
  • 没有消息更新的命令是直接传递的: 如果一个中间件返回的 Command 其状态更新不涉及 messages,框架会将其视为消息流的无操作。下一个中间件的处理程序接收来自 返回 Command 的那个中间件之前 的中间件的 AIMessage。
  • 归约器行为和重试安全: 命令仍然通过归约器应用(消息累加,外层在冲突中获胜)。重试逻辑会丢弃之前调用中的命令。

创建中间件

python 一个 AgentMiddleware 子类可以声明三个类属性,代理工厂在编译时会拾取这些属性:
  • state_schema — 使用自定义字段扩展代理状态。参见 自定义状态模式。
  • tools — 注册随中间件一起提供的额外工具(例如,待办事项列表中间件上的 write_todos)。
  • transformers — 注册作用域感知的流转换器工厂。参见 自定义流转换器。 :::
createMiddleware 接受三个配置字段,代理工厂在编译时会拾取这些字段:
  • stateSchema — 使用自定义字段扩展代理状态。参见 自定义状态模式。
  • tools — 注册随中间件一起提供的额外工具。
  • streamTransformers — 注册作用域感知的流转换器工厂。参见 自定义流转换器。
示例:
何时使用类:
  • 为同一个钩子定义同步和异步实现
  • 单个中间件需要多个钩子
  • 需要复杂配置(例如,可配置阈值、自定义模型)
  • 通过初始化时配置在项目间重用
::: 使用 createMiddleware 函数定义自定义中间件:

自定义状态模式

如果您的中间件需要在钩子之间跟踪状态,中间件可以使用自定义属性扩展代理的状态。这使得中间件能够:
  • 跨执行跟踪状态:维护计数器、标志或其他在代理执行生命周期内持续存在的值
  • 在钩子之间共享数据:将信息从 beforeModel 传递到 afterModel 或在不同的中间件实例之间传递
  • 实现横切关注点:添加速率限制、使用跟踪、用户上下文或审计日志等功能,而无需修改核心代理逻辑
  • 做出条件决策:使用累积的状态来确定是否继续执行、跳转到不同的节点或动态修改行为
状态字段可以是公共的或私有的。以 (_) 开头的字段被视为私有字段,不会包含在代理的结果中。只有公共字段(没有前导下划线的字段)会被返回。 这对于存储不应暴露给调用者的内部中间件状态非常有用,例如临时跟踪变量或内部标志:

自定义流转换器

中间件注册的转换器需要 langchain@1.4.3 或更高版本。
中间件可以注册流转换器工厂,将实时代理流中的事件投影到类型化的扩展通道上。这对于暴露计数器、侧信道工件、部分输出或线路级脱敏非常有用,而无需耦合到框架的内置投影。 在编译时,中间件注册的工厂与调用者直接传递给代理工厂的任何内容合并。最终排序规则 将内置的 ToolCallTransformer 保持在前面,并让调用者提供的条目落在最后。 将 streamTransformers 作为工厂元组传递给 createMiddleware。每个工厂的形状为 () => StreamTransformer<any>(零个参数),并且每个作用域调用一次;每次调用返回一个新的转换器可保持每个子图隔离。
有关完整的排序规则和 PII 脱敏示例,请参阅 在中间件上注册转换器。

自定义上下文

中间件可以定义一个自定义上下文模式来访问每次调用的元数据。与状态不同,上下文是只读的,不会在调用之间持久化。这使其非常适合用于:
  • 用户信息:传递在执行期间不会变化的用户 ID、角色或偏好设置
  • 配置覆盖:提供每次调用的设置,如速率限制或功能标志
  • 租户/工作区上下文:包含多租户应用程序的组织特定数据
  • 请求元数据:传递请求 ID、API 密钥或中间件所需的其他元数据
使用 Zod 定义上下文模式,并通过中间件钩子中的 runtime.context 访问它。上下文模式中的必填字段将在 TypeScript 级别强制执行,确保在调用 agent.invoke() 时必须提供它们。
必填上下文字段:当您在 contextSchema 中定义必填字段时(没有 .optional() 或 .default() 的字段),TypeScript 将强制在 agent.invoke() 调用期间必须提供这些字段。这确保了类型安全,并防止因缺少必填上下文而导致的运行时错误。

执行顺序

使用多个中间件时,了解它们的执行方式:
前置钩子按顺序运行:
  1. middleware1.before_agent()
  2. middleware2.before_agent()
  3. middleware3.before_agent()
代理循环开始
  1. middleware1.before_model()
  2. middleware2.before_model()
  3. middleware3.before_model()
包装钩子像函数调用一样嵌套:
  1. middleware1.wrap_model_call() → middleware2.wrap_model_call() → middleware3.wrap_model_call() → 模型
后置钩子按反向顺序运行:
  1. middleware3.after_model()
  2. middleware2.after_model()
  3. middleware1.after_model()
代理循环结束
  1. middleware3.after_agent()
  2. middleware2.after_agent()
  3. middleware1.after_agent()
关键规则:
  • before_* 钩子:从第一个到最后一个
  • after_* 钩子:从最后一个到第一个(反向)
  • wrap_* 钩子:嵌套(第一个中间件包装所有其他中间件)

代理跳转

要从中间件提前退出,请返回一个包含 jump_to 的字典: 可用的跳转目标:
  • 'end': 跳转到代理执行结束(或第一个 after_agent 钩子)
  • 'tools': 跳转到工具节点
  • 'model': 跳转到模型节点(或第一个 before_model 钩子)

最佳实践

  1. 保持中间件专注 - 每个中间件应做好一件事
  2. 优雅地处理错误 - 不要让中间件错误导致代理崩溃
  3. 使用合适的钩子类型:
    • 节点式用于顺序逻辑(日志记录、验证)
    • 包装式用于控制流(重试、回退、缓存)
  4. 清晰地记录任何自定义状态属性
  5. 在集成之前独立测试中间件
  6. 考虑执行顺序 - 将关键中间件放在列表的前面
  7. 尽可能使用内置中间件

示例

动态提示

在运行时动态修改系统提示,以便在每次模型调用前注入上下文、用户特定指令或其他信息。这是最常见的中间件用例之一。 使用 ModelRequest 中的 systemMessage 字段来读取和修改系统提示。它包含一个 SystemMessage 对象(即使代理是使用字符串 systemPrompt 创建的)。
Google
OpenAI
Anthropic
OpenRouter
Fireworks
Baseten
Ollama
使用 SystemMessage.concat 来保留缓存控制元数据或其他中间件创建的结构化内容块。

动态模型选择

动态选择工具

在运行时选择相关工具以提高性能和准确性。本节涵盖过滤预注册工具。有关注册在运行时发现的工具(例如来自 MCP 服务器),请参阅 运行时工具注册。 好处:
  • 更短的提示 - 通过仅暴露相关工具来降低复杂性
  • 更好的准确性 - 模型从更少的选项中正确选择
  • 权限控制 - 根据用户访问权限动态过滤工具

工具调用监控

提示缓存 (Anthropic)

使用 Anthropic 模型时,使用带有缓存控制指令的结构化内容块来缓存大型系统提示:
注意事项:
  • ModelRequest.system_message 始终是一个 SystemMessage 对象,即使代理是使用 system_prompt="string" 创建的
  • 使用 SystemMessage.content_blocks 以块列表的形式访问内容,无论原始内容是字符串还是列表
  • 修改系统消息时,使用 content_blocks 并附加新块以保留现有结构
  • 您可以直接将 SystemMessage 对象传递给 create_agent 的 system_prompt 参数,用于高级用例,如缓存控制
::: 在中间件中使用 ModelRequest 的 systemMessage 字段修改系统消息。它包含一个 SystemMessage 对象(即使代理是使用字符串 systemPrompt 创建的)。 示例:链接中间件 - 不同的中间件可以使用不同的方法:
生成的系统消息将是:
使用 SystemMessage.concat 来保留缓存控制元数据或其他中间件创建的结构化内容块。

其他资源