AgentScope(Java Harness 版)Middleware 中间件梳理
一句话结论: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 = 直接短路截断,后续逻辑不再执行
二、中间件生产常用场景
- 日志、链路追踪、埋点:记录工具名称、入参、耗时、异常、token 消耗、traceId,RuntimeContext 全链路透传
- 参数改写、入参清洗:自动补全参数、敏感内容脱敏、过滤违规词、改写检索 query
- 结果后置处理:工具返回内容格式化、摘要、脱敏、包装统一返回体
- 限流、熔断、重试:工具调用失败自动重试、QPS 限流、超时控制
- 安全拦截、沙箱管控:文件路径白名单、禁止高危参数、数据操作审计
- 计费、Token 统计:统计每一次模型调用、工具调用的 Token 消耗
- 动态注入上下文:从 RuntimeContext 读取租户、用户信息,注入到 Prompt / 工具参数
- 短路拦截:满足预设条件直接返回自定义结果,跳过 LLM 推理或者跳过工具执行
⚠️ 重要边界:权限组件
PermissionEngine是独立硬闸门,执行顺序在 onActing 中间件之前;中间件无法绕过权限校验。
三、自定义开发与注册方式(完全支持自定义)
两种接入方式:
- 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();
- 工具级 ToolInterceptor:给单个工具单独绑定,在 Tool 注册阶段指定,粒度更细,适合不同工具差异化安全、重试策略。
四、框架内置中间件说明
框架自带少量通用中间件:链路追踪中间件、计时中间件、Token 统计中间件。
重点:压缩、记忆持久化、事实抽取,Harness 默认由 Hook 实现,不是中间件;开发者可以自行编写 Middleware 替代原有 Hook 能力。
五、Middleware 和 Hook 对比表
表格
| 项目 | Middleware(中间件) | Hook(钩子) |
|---|---|---|
| 核心能力 | 洋葱链路,可以修改入参、修改返回、直接短路拦截 | 事件通知,以只读观测为主,一般不能截断流程 |
| 插桩位置 | onAgent / onReasoning / onActing / onModelCall / onSystemPrompt | PreReasoning、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次】
逐个插桩点说明
- onAgent 时机:用户发起一次提问,整个 ReAct 会话的最外层包裹,一轮对话仅执行一次(前置 + 后置)
- 前置:收到用户消息,一切推理、工具调用之前
- 后置:LLM 给出最终回答、不再继续 ReAct 循环之后 适用场景:会话级埋点、全会话计时、会话封禁校验、会话缓存、会话异常兜底 示例:前置记录 sessionId、会话开始时间;后置统计整个会话总 token、总耗时
重点:内部无论循环多少次推理、多少次工具调用,onAgent 只包裹一层
- onReasoning 时机:每一轮 ReAct 推理,包裹【组装上下文 + 调用 LLM + 解析 LLM 返回(toolcall / 直接回答)】,一轮 ReAct 触发一次;本例一共触发 3 次
- 前置:prompt 组装完成之后、调用 LLM 之前
- 后置:拿到 LLM 原始返回,并且解析完成(区分是工具调用还是直接回答)之后 适用场景:推理阶段日志、修改传给 LLM 的完整消息列表、本轮推理短路返回假结果
注意:onReasoning 内部包含 onModelCall;onReasoning 是 Agent 层推理包装,onModelCall 是底层模型 API 通信包装
- onActing 时机:LLM 输出 toolcall、权限校验完成之后,包裹单次工具执行流程;一次工具调用触发一次;本例触发 2 次(query_order、export_excel)
- 前置:权限校验 Pass 之后,工具本体执行之前
- 后置:工具执行完毕,拿到返回结果或者捕获异常之后 适用场景:工具入参脱敏、工具调用日志、工具重试、工具结果改写、工具 QPS 限流
关键点:PermissionEngine 在 onActing 之前执行;本轮推理 LLM 选择直接回答、不调用工具,则不会进入 onActing
- onModelCall 时机:真正发起 HTTP 请求调用大模型 API 时触发,被 onReasoning 包裹在内部;本例 3 次 onReasoning,对应 3 次 onModelCall
- 前置:构造好模型请求体(messages、temperature、topP 等),发起 HTTP 请求之前
- 后置:拿到模型原始返回报文(还没有解析成 toolcall 结构)之后 适用场景:动态修改 temperature、切换模型、修改请求体、拦截模型返回、统计模型接口耗时、模型降级
粒度最细:只管控模型 API 通信,不感知上层 Agent 的 ReAct 逻辑、工具逻辑
- onSystemPrompt(特殊转换回调) 时机:每次框架组装 system prompt 的时候触发回调,用来转换 system 文本;只要准备向 LLM 发请求、组装 prompt 就会触发,本例触发 3 次
不属于洋葱 next 链路,没有截断能力,职责仅为文本转换:输入原始 system,返回修改后的 system 字符串 示例:从 RuntimeContext 读取租户、用户角色,动态追加角色约束到 system 提示词
插桩点能力简表(基于上面订单示例)
表格
| 中间件 | 触发次数(本例) | 触发时机 | 是否支持截断(不调用 next) |
|---|---|---|---|
| onAgent | 1 次(前置 + 后置) | 用户一次提问的整个会话外壳 | ✅ 可以直接短路,不跑任何推理 |
| onReasoning | 3 次 | 每一轮 ReAct 推理、准备调用 LLM | ✅ 可以短路,本轮推理直接返回结果 |
| onActing | 2 次 | LLM 选择调用工具、权限放行之后 | ✅ 可以短路,不执行工具本体 |
| onModelCall | 3 次 | 底层发起大模型 HTTP 接口调用 | ✅ 可以短路,不请求真实 LLM |
| onSystemPrompt | 3 次 | 每次组装 system 提示词 | ❌ 转换型,无 next,只能返回新 prompt |
八、高频误区澄清
❌ 错误理解:onAgent → onReasoning → onActing → onModelCall 串行只跑一遍 ✅ 正确理解:onAgent 包裹外层循环;循环内部反复执行 onReasoning;onReasoning 内部嵌套 onModelCall;只有 onReasoning 解析出 toolcall 时,才额外执行 onActing。
更多推荐


所有评论(0)