概述
Agent middleware 是在不修改 agent 或 model 代码的前提下,向 agent 执行流程中的关键位置注入自定义逻辑(日志、追踪、输入改写、访问控制等)的机制。 AgentScope Java 中,可以在 5 个位置上设置 hook,覆盖了从外层 reply 流程一路下沉到底层模型 API 调用的全链路:
两种类型的差别:
- Onion(洋葱式)—— middleware 包裹下一层 handler,可以在
next.apply(input)前后插入逻辑、观察中间事件流。 - Transformer(变换式)—— middleware 之间串成流水线,前一个的输出作为后一个的输入,不存在「内层」概念。
onSystemPrompt 嵌入在 onReasoning 内部,因为它在 reasoning 步骤组装 system prompt 时被触发:
当前
onActing 只包裹 agent 运行时内部的工具执行;通过 external execution 在 agent 外部执行的工具不会被 onActing 追踪到。装备 Middleware
AgentScope 把一组 hook 装在一个MiddlewareBase 实现里 —— 同一个 middleware 类可以同时实现 5 个位置中任意子集的 hook(其余位置默认 next.apply(input))。把实例传给 builder 的 middlewares(...) 即可装备:
middleware(...)(单数)也可单独添加一个;middlewares(...) 接受 List<? extends MiddlewareBase>,未实现的位置自动跳过,不产生任何调用开销。
内置 Middleware
OtelTracingMiddleware
OtelTracingMiddleware(位于 io.agentscope.core.tracing)为 agent 全生命周期接入 OpenTelemetry 追踪。它在 onAgent、onModelCall、onActing 三个位置打点,按层级生成 span:
invoke_agent <name>—— 包裹整次 replychat <model>—— 包裹每次模型 API 调用execute_tool <name>—— 包裹每次工具执行
next.apply(input),几乎零开销。
OtelTracingMiddleware 从进程级 GlobalOpenTelemetry 实例读取配置。应用如果自行导出 span,除了 AgentScope 之外还需要引入 OpenTelemetry SDK 和 OTLP exporter。使用 OpenTelemetry BOM 保持二者版本一致(下列版本与 AgentScope 当前使用的版本一致):
Basic <base64-credentials> 形式的值,供 Langfuse 等要求 Authorization header 的后端使用:
GlobalOpenTelemetry,直接复用并只添加 middleware 即可。新配置不再调用已弃用的 TracerRegistry.register(...)。应用关闭时应关闭 SdkTracerProvider,让 batch processor 刷新尚未导出的 span。
每次 reply 会产出一棵嵌套 span 树,关键属性包括 agent 名称、session ID、模型名、token 数、工具名与入参等。
TaskReminderMiddleware
TaskReminderMiddleware(位于 io.agentscope.core.middleware)与内置 TodoTools 配合使用,在每个 reasoning step 之前把当前 AgentState.tasksContext 渲染成 <system-reminder> 注入上下文,避免长任务期间 agent 偏离计划。
通过 builder 上的 enableTaskList(true) 开关与 TodoTools 一同启用:
FinalAnswerFilterMiddleware
FinalAnswerFilterMiddleware 仅输出 ReAct 最终推理轮次的文本。产生工具调用的中间轮次文本会被过滤,工具事件及其他非文本事件仍会正常流式输出。
自定义 Middleware
实现MiddlewareBase 接口(位于 io.agentscope.core.middleware),只重写需要的 hook 即可,其它的不用管。
每个洋葱 hook 收到一个 next 函数,调用 next.apply(input) 进入内层逻辑;可以在调用前后插入自己的处理,或者通过 Flux<AgentEvent> 算子(doOnNext / flatMap / map 等)观察、改写中间事件流。
io.agentscope.core.middleware):
需要替换流入下一层的字段时,构造一个新的 input record 后再调用
next.apply(...)。
完整可运行示例:agentscope-examples/documentation/.../middleware/CustomizedMiddlewareExample.java、middleware/ModelCallMiddlewareExample.java、middleware/SystemPromptMiddlewareExample.java。
读取 RuntimeContext
MiddlewareBase 的所有 hook 都将本次 call / stream 绑定的 RuntimeContext 作为第二个参数直接传入——既能读会话字段,也能按类型 / 按 key 取属性,还能反向写入来给下游 hook 和 tool 传值。
- 同一份
RuntimeContext在整个 reply 内被各层 hook / tool 共享,使用线程安全的内部 map,可以安全地put写入。 - 不要把请求级状态缓存到 middleware 实例字段——一个 middleware 实例通常被多个 agent / call 复用;要么放进
RuntimeContext,要么用 ReactorcontextWrite。 - 若 builder 上同时配置了全局
toolExecutionContext,框架在分发给 tool 时会把它合并到 per-call context 之后(per-call 优先级更高)。
执行顺序
Onion 类 hook(onAgent、onReasoning、onActing、onModelCall)按 MiddlewareBase.order() 排序——数值越大越处于最外层。默认值是 1;相同 order 的 middleware 保持其 Builder 注册顺序:
order(),改变其相对默认优先级的位置。例如 order 为 0 时,会进入所有仍保持默认 order 1 的 middleware 内层:
onSystemPrompt)—— middleware 从左到右串行接力:
实用示例
计时 middleware
下面的 middleware 记录每次模型调用的耗时:限速 middleware
下面的 middleware 在两次模型调用之间强制留出最小间隔:动态 system prompt middleware
下面的 middleware 在 system prompt 中注入实时上下文。也可以直接复用示例middleware/SystemPromptMiddlewareExample.java:
模型回退 middleware
下面的 middleware 在主模型失败时切换到备用模型:全部工具被拒绝时停止 agent
当用户通过 HITL 拒绝了一轮推理产出的全部工具调用时,agent 默认会继续下一轮推理(向后兼容)。如果希望在这种场景下停止 agent,可以编写一个onActing middleware 观察 AllToolsDeniedEvent 并发出 RequestStopEvent:
GenerateReason.ALL_TOOLS_DENIED: