一句话结论:Middleware(中间件)是 AgentScope 洋葱式的扩展拦截机制,用来在 Agent / 模型 / 工具执行链路里插入自定义逻辑;支持开发者自行开发、注册、配置,框架也自带少量内置中间件。 核心区分(极易混淆):

  • Middleware(中间件):洋葱嵌套模型,可以修改入参、修改返回结果、直接截断链路不走后续逻辑,依靠 nextHandler 链式向下传递;Harness 2.0 主推。
  • Hook(钩子):事件通知机制,以观测为主,一般仅做监听、只读采集,不能直接拦截、篡改返回结果;Harness 1.x 大量使用,2.0 保留用于兼容(如 CompactionHook、MemoryFlushHook)。

一、两大类中间件(Java Harness)

1. Agent 全局中间件(基类:MiddlewareBase,挂载在 HarnessAgent 上)

拦截整个 Agent 的生命周期,共 5 个插桩点,其中前 4 个是标准洋葱链路,onSystemPrompt 为特殊转换回调:

  • onAgent:包裹完整一轮用户对话(包含全部 ReAct 循环、多次工具调用、最终回复)
  • onReasoning:包裹单次推理步骤(组装 prompt、调用 LLM、解析解码返回)
  • onActing:包裹单次工具调用全流程(权限校验通过之后、工具本体执行之前)
  • onModelCall:包裹底层大模型 API 调用(最贴近 LLM,可修改模型入参、拦截原始返回)
  • onSystemPrompt:转换型回调,每次组装 system prompt 时修改提示词;不属于洋葱 next 链路,没有截断能力

2. Tool 独立中间件 / ToolInterceptor(工具维度拦截器)

作用域绑定单个工具或者一组工具,不和 Agent 实例绑定;即使工具脱离 Agent、被单独直接调用,拦截逻辑依然生效,适合工具专属的参数校验、沙箱控制、审计、重试。

洋葱执行顺序:按注册顺序依次前置处理,后置处理逆序执行 示例:M1、M2 两个中间件 M1前置 → M2前置 → 工具本体 → M2后置 → M1后置 规则:调用 next 才会向下放行;不调用 next = 直接短路截断,后续逻辑不再执行

二、中间件生产常用场景

  1. 日志、链路追踪、埋点:记录工具名称、入参、耗时、异常、token 消耗、traceId,RuntimeContext 全链路透传
  2. 参数改写、入参清洗:自动补全参数、敏感内容脱敏、过滤违规词、改写检索 query
  3. 结果后置处理:工具返回内容格式化、摘要、脱敏、包装统一返回体
  4. 限流、熔断、重试:工具调用失败自动重试、QPS 限流、超时控制
  5. 安全拦截、沙箱管控:文件路径白名单、禁止高危参数、数据操作审计
  6. 计费、Token 统计:统计每一次模型调用、工具调用的 Token 消耗
  7. 动态注入上下文:从 RuntimeContext 读取租户、用户信息,注入到 Prompt / 工具参数
  8. 短路拦截:满足预设条件直接返回自定义结果,跳过 LLM 推理或者跳过工具执行

⚠️ 重要边界:权限组件 PermissionEngine 是独立硬闸门,执行顺序在 onActing 中间件之前;中间件无法绕过权限校验。

三、自定义开发与注册方式(完全支持自定义)

两种接入方式:

  1. Agent 全局中间件:自定义类继承 MiddlewareBase,按需实现需要的 onXXX 方法,不需要的方法空实现;构建 HarnessAgent 时链式 .middleware() 注册,支持多个中间件,注册顺序决定洋葱层级。
// 自定义日志中间件示例
public class LogMiddleware implements MiddlewareBase {
    @Override
    public Mono<AgentResult> onActing(Agent agent, ActingInput input, Function<ActingInput, Mono<AgentResult>> next) {
        // 前置:打印工具调用信息
        RuntimeContext ctx = input.runtimeContext();
        System.out.println("准备调用工具:" + input.toolCall().name());
        // 放行,进入内层逻辑
        return next.apply(input)
                .doOnSuccess(res -> {
                    // 后置:打印返回结果
                    System.out.println("工具返回:" + res);
                });
    }
}

// 挂载到Agent
HarnessAgent agent = HarnessAgent.builder()
        .middleware(new LogMiddleware())
        .middleware(new RetryMiddleware())
        .build();
  1. 工具级 ToolInterceptor:给单个工具单独绑定,在 Tool 注册阶段指定,粒度更细,适合不同工具差异化安全、重试策略。

四、框架内置中间件说明

框架自带少量通用中间件:链路追踪中间件、计时中间件、Token 统计中间件。

重点:压缩、记忆持久化、事实抽取,Harness 默认由 Hook 实现,不是中间件;开发者可以自行编写 Middleware 替代原有 Hook 能力。

五、Middleware 和 Hook 对比表

表格

项目Middleware(中间件)Hook(钩子)
核心能力洋葱链路,可以修改入参、修改返回、直接短路拦截事件通知,以只读观测为主,一般不能截断流程
插桩位置onAgent / onReasoning / onActing / onModelCall / onSystemPromptPreReasoning、PreActing、PostActing、PostCall
版本定位2.0 主推,新项目优先选用1.x 原生,2.0 保留兼容(CompactionHook、MemoryFlushHook)
典型适用场景限流、重试、参数篡改、熔断、拦截、动态改写 Prompt记忆抽取、上下文压缩、持久化、事件通知、日志采集

六、中间件在工具调用完整链路中的位置

LLM输出ToolCall
    ↓
PreActing Hook(事件通知,只读)
    ↓
PermissionEngine权限校验(独立硬闸门,优先执行)
    ↓
onActing 中间件洋葱链【可拦截、改参数、短路】
    ↓
工具本体执行 / ToolInterceptor 工具拦截器
    ↓
onActing中间件后置处理
    ↓
PostActing Hook
    ↓
结果写入AgentState短期记忆

七、五个中间件插桩点执行时机详解(配套场景示例)

前置场景说明:用户提问:帮我查一下订单 1001 的金额,如果大于 1000,导出 Excel。 该任务会产生多轮 ReAct: 轮 1:LLM 决定调用 query_order 工具 轮 2:拿到订单结果,金额 > 1000,LLM 决定调用 export_excel 轮 3:导出完成,LLM 判断不再调用工具,输出最终回答 执行时序总览:

用户输入消息,进入Agent
✅ onAgent 开始(前置)【全局大包裹,整个会话只执行1次】
    ├─ 组装Prompt,构造system prompt
    ✅ onSystemPrompt 触发(修改system提示词)
    ├─ ✅ onReasoning 开始(前置)【第一轮推理】
    │    ├─ 准备调用LLM
    │    ✅ onModelCall 开始(前置)
    │    ├─ 调用大模型API、拿到返回
    │    ✅ onModelCall 结束(后置)
    │    ├─ LLM输出:调用query_order工具
    │    ✅ onReasoning 结束(后置)【第一轮推理结束】
    │
    ├─ 权限校验通过 → 进入工具中间件
    ✅ onActing 开始(前置)【第一次工具调用:query_order】
    │    ├─ 执行工具本体
    │    ✅ onActing 结束(后置)【query_order完成】
    │
    ├─ 回到ReAct循环,再次组装prompt(带上工具返回结果)
    ✅ onSystemPrompt 触发
    ├─ ✅ onReasoning 开始(前置)【第二轮推理】
    │    ├─ ✅ onModelCall 前后包裹
    │    ├─ LLM输出:调用export_excel
    │    ✅ onReasoning 结束(后置)【第二轮推理结束】
    │
    ├─ 权限校验通过
    ✅ onActing 开始(前置)【第二次工具调用:export_excel】
    │    ├─ 执行导出
    │    ✅ onActing 结束(后置)【export_excel完成】
    │
    ├─ 再次回到ReAct循环,组装prompt
    ✅ onSystemPrompt 触发
    ├─ ✅ onReasoning 开始(前置)【第三轮推理】
    │    ├─ ✅ onModelCall 前后包裹
    │    ├─ LLM判断:不需要工具,直接输出答案
    │    ✅ onReasoning 结束(后置)【第三轮推理结束】
✅ onAgent 结束(后置)【整个用户对话全部结束,只执行1次】

逐个插桩点说明

  1. onAgent 时机:用户发起一次提问,整个 ReAct 会话的最外层包裹,一轮对话仅执行一次(前置 + 后置)
  • 前置:收到用户消息,一切推理、工具调用之前
  • 后置:LLM 给出最终回答、不再继续 ReAct 循环之后 适用场景:会话级埋点、全会话计时、会话封禁校验、会话缓存、会话异常兜底 示例:前置记录 sessionId、会话开始时间;后置统计整个会话总 token、总耗时

重点:内部无论循环多少次推理、多少次工具调用,onAgent 只包裹一层

  1. onReasoning 时机:每一轮 ReAct 推理,包裹【组装上下文 + 调用 LLM + 解析 LLM 返回(toolcall / 直接回答)】,一轮 ReAct 触发一次;本例一共触发 3 次
  • 前置:prompt 组装完成之后、调用 LLM 之前
  • 后置:拿到 LLM 原始返回,并且解析完成(区分是工具调用还是直接回答)之后 适用场景:推理阶段日志、修改传给 LLM 的完整消息列表、本轮推理短路返回假结果

注意:onReasoning 内部包含 onModelCall;onReasoning 是 Agent 层推理包装,onModelCall 是底层模型 API 通信包装

  1. onActing 时机:LLM 输出 toolcall、权限校验完成之后,包裹单次工具执行流程;一次工具调用触发一次;本例触发 2 次(query_order、export_excel)
  • 前置:权限校验 Pass 之后,工具本体执行之前
  • 后置:工具执行完毕,拿到返回结果或者捕获异常之后 适用场景:工具入参脱敏、工具调用日志、工具重试、工具结果改写、工具 QPS 限流

关键点:PermissionEngine 在 onActing 之前执行;本轮推理 LLM 选择直接回答、不调用工具,则不会进入 onActing

  1. onModelCall 时机:真正发起 HTTP 请求调用大模型 API 时触发,被 onReasoning 包裹在内部;本例 3 次 onReasoning,对应 3 次 onModelCall
  • 前置:构造好模型请求体(messages、temperature、topP 等),发起 HTTP 请求之前
  • 后置:拿到模型原始返回报文(还没有解析成 toolcall 结构)之后 适用场景:动态修改 temperature、切换模型、修改请求体、拦截模型返回、统计模型接口耗时、模型降级

粒度最细:只管控模型 API 通信,不感知上层 Agent 的 ReAct 逻辑、工具逻辑

  1. onSystemPrompt(特殊转换回调) 时机:每次框架组装 system prompt 的时候触发回调,用来转换 system 文本;只要准备向 LLM 发请求、组装 prompt 就会触发,本例触发 3 次

不属于洋葱 next 链路,没有截断能力,职责仅为文本转换:输入原始 system,返回修改后的 system 字符串 示例:从 RuntimeContext 读取租户、用户角色,动态追加角色约束到 system 提示词

插桩点能力简表(基于上面订单示例)

表格

中间件触发次数(本例)触发时机是否支持截断(不调用 next)
onAgent1 次(前置 + 后置)用户一次提问的整个会话外壳✅ 可以直接短路,不跑任何推理
onReasoning3 次每一轮 ReAct 推理、准备调用 LLM✅ 可以短路,本轮推理直接返回结果
onActing2 次LLM 选择调用工具、权限放行之后✅ 可以短路,不执行工具本体
onModelCall3 次底层发起大模型 HTTP 接口调用✅ 可以短路,不请求真实 LLM
onSystemPrompt3 次每次组装 system 提示词❌ 转换型,无 next,只能返回新 prompt

八、高频误区澄清

❌ 错误理解:onAgent → onReasoning → onActing → onModelCall 串行只跑一遍 ✅ 正确理解:onAgent 包裹外层循环;循环内部反复执行 onReasoning;onReasoning 内部嵌套 onModelCall;只有 onReasoning 解析出 toolcall 时,才额外执行 onActing。

Logo

更多推荐