Agent Scope Java 2.x 系列【10】Middleware

目录

  • [AgentScope Middleware](#AgentScope Middleware)
    • [1. 概述](#1. 概述)
    • [2. 生命周期嵌套](#2. 生命周期嵌套)
    • [3. 两种执行模型:Onion vs Transformer](#3. 两种执行模型:Onion vs Transformer)
      • [3.1 Onion 洋葱式](#3.1 Onion 洋葱式)
      • [3.2 Transformer 变换式](#3.2 Transformer 变换式)
      • [3.3 对比表](#3.3 对比表)
    • [4. 五个 Hook 位置与 Input 类型](#4. 五个 Hook 位置与 Input 类型)
    • [5. MiddlewareBase 接口](#5. MiddlewareBase 接口)
    • [6. 装备 Middleware](#6. 装备 Middleware)
    • [7. 执行顺序(order)](#7. 执行顺序(order))
    • [8. 内置 Middleware](#8. 内置 Middleware)
      • [8.1 OtelTracingMiddleware](#8.1 OtelTracingMiddleware)
      • [8.2 TaskReminderMiddleware](#8.2 TaskReminderMiddleware)
      • [8.3 GracefulShutdownMiddleware](#8.3 GracefulShutdownMiddleware)
      • [8.4 DynamicSkillMiddleware](#8.4 DynamicSkillMiddleware)
    • [9. 自定义 Middleware(完整示例)](#9. 自定义 Middleware(完整示例))
      • [9.1 全生命周期监控(CustomizedMiddlewareExample)](#9.1 全生命周期监控(CustomizedMiddlewareExample))
      • [9.2 模型调用审计(ModelCallMiddlewareExample)](#9.2 模型调用审计(ModelCallMiddlewareExample))
      • [9.3 动态 System Prompt(SystemPromptMiddlewareExample)](#9.3 动态 System Prompt(SystemPromptMiddlewareExample))
    • [10. RuntimeContext 读写](#10. RuntimeContext 读写)
    • [11. 实用场景](#11. 实用场景)
      • [11.1 计时中间件](#11.1 计时中间件)
      • [11.2 限流中间件](#11.2 限流中间件)
      • [11.3 模型回退](#11.3 模型回退)
      • [11.4 全部工具被拒绝时停止 Agent](#11.4 全部工具被拒绝时停止 Agent)
    • [12. 小结](#12. 小结)

AgentScope Middleware

Middleware 是 AgentScope 的无侵入扩展机制:不改动 Agent / Model 源码,在执行链路关键节点插入横切逻辑(日志、追踪、输入改写、权限拦截、限流、动态提示词、异常降级等)。

1. 概述

MiddlewareBase 是 Agent 中间件标准父接口,定义 5 个生命周期钩子,分两种执行模型:

  • 洋葱包裹模式(4 个钩子) :onAgent / onReasoning / onActing / onModelCall
  • 流水线变换模式(1 个钩子) :onSystemPrompt

所有钩子都提供 default 空实现(直接透传),业务中间件只需重写关心的位置。

典型应用场景:

场景 钩子
链路追踪(OpenTelemetry) onAgent + onModelCall + onActing
日志埋点、调用耗时统计 onModelCall
动态注入时间 / 用户信息 / 环境变量 onSystemPrompt
长任务防偏离(Todo 提醒) onSystemPrompt + onReasoning
优雅关闭(阶段边界中断) onReasoning + onActing
动态 Skill 合并 onSystemPrompt
限流、模型回退、工具拒绝拦截 onModelCall / onActing

2. 生命周期嵌套

一次 agent.call() 的嵌套层级:

text 复制代码
onAgent/
└── ReAct loop(每一轮)/
    ├── onReasoning/
    │   ├── onSystemPrompt(组装 system prompt)
    │   └── onModelCall(模型 API 调用)
    └── onActing(每次工具调用)

#mermaid-svg-lIFj8IyVdhslx08P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lIFj8IyVdhslx08P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lIFj8IyVdhslx08P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lIFj8IyVdhslx08P .error-icon{fill:#552222;}#mermaid-svg-lIFj8IyVdhslx08P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lIFj8IyVdhslx08P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lIFj8IyVdhslx08P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lIFj8IyVdhslx08P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lIFj8IyVdhslx08P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lIFj8IyVdhslx08P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lIFj8IyVdhslx08P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lIFj8IyVdhslx08P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lIFj8IyVdhslx08P .marker.cross{stroke:#333333;}#mermaid-svg-lIFj8IyVdhslx08P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lIFj8IyVdhslx08P p{margin:0;}#mermaid-svg-lIFj8IyVdhslx08P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lIFj8IyVdhslx08P .cluster-label text{fill:#333;}#mermaid-svg-lIFj8IyVdhslx08P .cluster-label span{color:#333;}#mermaid-svg-lIFj8IyVdhslx08P .cluster-label span p{background-color:transparent;}#mermaid-svg-lIFj8IyVdhslx08P .label text,#mermaid-svg-lIFj8IyVdhslx08P span{fill:#333;color:#333;}#mermaid-svg-lIFj8IyVdhslx08P .node rect,#mermaid-svg-lIFj8IyVdhslx08P .node circle,#mermaid-svg-lIFj8IyVdhslx08P .node ellipse,#mermaid-svg-lIFj8IyVdhslx08P .node polygon,#mermaid-svg-lIFj8IyVdhslx08P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lIFj8IyVdhslx08P .rough-node .label text,#mermaid-svg-lIFj8IyVdhslx08P .node .label text,#mermaid-svg-lIFj8IyVdhslx08P .image-shape .label,#mermaid-svg-lIFj8IyVdhslx08P .icon-shape .label{text-anchor:middle;}#mermaid-svg-lIFj8IyVdhslx08P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lIFj8IyVdhslx08P .rough-node .label,#mermaid-svg-lIFj8IyVdhslx08P .node .label,#mermaid-svg-lIFj8IyVdhslx08P .image-shape .label,#mermaid-svg-lIFj8IyVdhslx08P .icon-shape .label{text-align:center;}#mermaid-svg-lIFj8IyVdhslx08P .node.clickable{cursor:pointer;}#mermaid-svg-lIFj8IyVdhslx08P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lIFj8IyVdhslx08P .arrowheadPath{fill:#333333;}#mermaid-svg-lIFj8IyVdhslx08P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lIFj8IyVdhslx08P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lIFj8IyVdhslx08P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lIFj8IyVdhslx08P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lIFj8IyVdhslx08P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lIFj8IyVdhslx08P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lIFj8IyVdhslx08P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lIFj8IyVdhslx08P .cluster text{fill:#333;}#mermaid-svg-lIFj8IyVdhslx08P .cluster span{color:#333;}#mermaid-svg-lIFj8IyVdhslx08P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lIFj8IyVdhslx08P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lIFj8IyVdhslx08P rect.text{fill:none;stroke-width:0;}#mermaid-svg-lIFj8IyVdhslx08P .icon-shape,#mermaid-svg-lIFj8IyVdhslx08P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lIFj8IyVdhslx08P .icon-shape p,#mermaid-svg-lIFj8IyVdhslx08P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lIFj8IyVdhslx08P .icon-shape .label rect,#mermaid-svg-lIFj8IyVdhslx08P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lIFj8IyVdhslx08P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lIFj8IyVdhslx08P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lIFj8IyVdhslx08P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-lIFj8IyVdhslx08P .userNode>*{fill:#FFF4E6!important;stroke:#E67E22!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .userNode span{fill:#FFF4E6!important;stroke:#E67E22!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .userNode tspan{fill:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .onionNode>*{fill:#E8F4FD!important;stroke:#2980B9!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .onionNode span{fill:#E8F4FD!important;stroke:#2980B9!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .onionNode tspan{fill:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .transNode>*{fill:#E8F8E9!important;stroke:#27AE60!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .transNode span{fill:#E8F8E9!important;stroke:#27AE60!important;stroke-width:1px!important;color:#333!important;}#mermaid-svg-lIFj8IyVdhslx08P .transNode tspan{fill:#333!important;} 是,有 tool_call
否
用户输入
onAgent

包裹整次 reply
onReasoning

单轮推理
onSystemPrompt

组装 system prompt
onModelCall

底层 LLM API
onActing

工具执行
还有下一轮?
最终回复

注意 :onActing 只捕获 Agent 内部发起的工具执行;通过 external execution 在 Agent 外部调度的工具不会被该钩子拦截。


3. 两种执行模型:Onion vs Transformer

3.1 Onion 洋葱式

中间件层层包裹内层执行逻辑,能观测完整生命周期、拦截 / 改写入参、捕获后置结果与事件流。

text 复制代码
┌─────────────────────────────────────────┐
│            mw1 前置逻辑                  │
│  ┌─────────────────────────────────┐    │
│  │           mw2 前置逻辑           │    │
│  │  ┌───────────────────────────┐  │    │
│  │  │      原生业务核心逻辑       │  │    │
│  │  └───────────────────────────┘  │    │
│  │           mw2 后置逻辑           │    │
│  └─────────────────────────────────┘    │
│            mw1 后置逻辑                  │
└─────────────────────────────────────────┘

执行顺序:mw1 前 → mw2 前 → 核心逻辑 → mw2 后 → mw1 后。

3.2 Transformer 变换式

无嵌套包裹、无前后双切面,纯单向流水线。多个中间件首尾串联,上一个的输出 = 下一个的输入:

text 复制代码
原始 Prompt
   ↓
mw1 变换加工
   ↓
mw2 变换加工
   ↓
最终 Prompt → 送入模型

3.3 对比表

维度 Onion 洋葱式 Transformer 变换式
结构 嵌套包裹分层 平直串行流水线
执行阶段 前置 → 核心 → 后置(双向) 仅正向变换,无后置回调
输入输出 每层可独立控制入参、捕获返回事件流 强依赖上一层输出,只能修改入参本体
干预能力 可拦截阻断、重试、捕获异常、观测完整生命周期 仅能修改文本内容,无法拦截后续流程
适用 Hook onAgent / onReasoning / onActing / onModelCall 仅 onSystemPrompt

4. 五个 Hook 位置与 Input 类型

Hook 模型 Input 类型 可访问内容
onAgent Onion AgentInput msgs() ------ 输入消息列表
onReasoning Onion ReasoningInput messages() + tools() + options()
onActing Onion ActingInput toolCalls() ------ 待执行的 ToolUseBlock 列表
onModelCall Onion ModelCallInput messages() + tools() + options() + model()
onSystemPrompt Transformer String 当前 prompt,返回新 prompt

每个 Hook 第一个参数是 Agent,第二个参数是 RuntimeContext(可读写会话上下文、按类型 / key 取属性、反向写入给下游 Hook 和 Tool 传值)。


5. MiddlewareBase 接口

java 复制代码
public interface MiddlewareBase {

    // ─── Onion 模式:4 个钩子 ───

    /** 拦截整个 Agent 完整调用流程(最外层) */
    default Flux<AgentEvent> onAgent(
            Agent agent,
            RuntimeContext ctx,
            AgentInput input,
            Function<AgentInput, Flux<AgentEvent>> next) {
        return next.apply(input);
    }

    /** 拦截单轮 ReAct 推理阶段 */
    default Flux<AgentEvent> onReasoning(
            Agent agent,
            RuntimeContext ctx,
            ReasoningInput input,
            Function<ReasoningInput, Flux<AgentEvent>> next) {
        return next.apply(input);
    }

    /** 拦截单次工具调用执行 */
    default Flux<AgentEvent> onActing(
            Agent agent,
            RuntimeContext ctx,
            ActingInput input,
            Function<ActingInput, Flux<AgentEvent>> next) {
        return next.apply(input);
    }

    /** 拦截底层 LLM API 调用 */
    default Flux<AgentEvent> onModelCall(
            Agent agent,
            RuntimeContext ctx,
            ModelCallInput input,
            Function<ModelCallInput, Flux<AgentEvent>> next) {
        return next.apply(input);
    }

    // ─── Transformer 模式:1 个钩子 ───

    /** 流水线式加工 system prompt */
    default Mono<String> onSystemPrompt(
            Agent agent,
            RuntimeContext ctx,
            String currentPrompt) {
        return Mono.just(currentPrompt);
    }

    /** 洋葱层优先级,数值越大越外层;默认 1 */
    default int order() {
        return 1;
    }
}

所有方法都是 default,自定义中间件只重写关心的钩子即可,其余自动透传、零开销。


6. 装备 Middleware

java 复制代码
import io.agentscope.core.ReActAgent;
import io.agentscope.core.middleware.MiddlewareBase;
import io.agentscope.core.tracing.OtelTracingMiddleware;
import java.util.List;

ReActAgent agent = ReActAgent.builder()
        .name("assistant")
        .sysPrompt("You are a helpful assistant.")
        .model(model)
        .toolkit(toolkit)
        .middlewares(List.of(
                new OtelTracingMiddleware(),
                new TimestampMiddleware()))
        .build();
  • .middleware(...) 单数:添加一个
  • .middlewares(List.of(...)):批量添加
  • 未实现的钩子自动跳过,不产生调用开销

7. 执行顺序(order)

Onion 类钩子按 MiddlewareBase.order() 排序,数值越大越处于外层 ;默认值是 1。相同 order 的中间件保持 Builder 注册顺序:

java 复制代码
middlewares = [mw1(order=2), mw2(order=1)]
// 执行顺序:
// mw1 前 → mw2 前 → 内部逻辑 → mw2 后 → mw1 后

自定义中间件可覆写 order():返回 0 会进入所有默认 order=1 的中间件内层。

流式事件的观察顺序:

text 复制代码
mw1_pre → mw2_pre → mw2_event → mw1_event → ... → mw2_post → mw1_post

Transformer 类钩子(onSystemPrompt)从左到右串行接力:

text 复制代码
originalPrompt → mw1.onSystemPrompt() → mw2.onSystemPrompt() → final

8. 内置 Middleware

类名 包路径 触发钩子 核心职责
OtelTracingMiddleware core.tracing onAgent + onModelCall + onActing 为 Agent → Model → Tool 三级调用生成 OpenTelemetry Span
TaskReminderMiddleware core.middleware onSystemPrompt + onReasoning 每次推理前注入当前 Todo 列表,防止长任务偏离计划
GracefulShutdownMiddleware core.shutdown onReasoning + onActing 在阶段完成后的安全点执行优雅关闭中断
DynamicSkillMiddleware core.skill onSystemPrompt 每次调用动态合并多仓库 Skill,支持按用户隔离和灰度
FinalAnswerFilterMiddleware core.middleware onReasoning 仅输出最终推理轮次文本,过滤中间工具调用轮次

8.1 OtelTracingMiddleware

三级 Span 结构:

text 复制代码
invoke_agent <name>          ← onAgent:包裹整个 agent 回复
├── chat <model>             ← onModelCall:包裹每次模型 API 调用
└── execute_tool <name>      ← onActing:包裹每次工具执行

每个 Span 记录:Agent 名称 / ID、模型名、消息数、工具数、Token 用量(input/output)、工具调用 ID。未配置 OTel SDK 时(只有 no-op provider)所有钩子直接短路到 next.apply(input),几乎零开销。

8.2 TaskReminderMiddleware

配合内置 TodoTools 使用:

  • onSystemPrompt:一次性注入 todo_write 工具的静态使用说明
  • onReasoning:每次推理前 把 AgentState.tasksContext 渲染成 <system-reminder> 块追加到输入

提醒消息只临时追加、不写入 AgentState.context,因此不会被持久化 / 压缩 / 回溯。

java 复制代码
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new TodoTools());

ReActAgent agent = ReActAgent.builder()
        .name("planner")
        .toolkit(toolkit)
        .enableTaskList(true)   // 与 TodoTools 一起启用
        .build();

8.3 GracefulShutdownMiddleware

在 onReasoning 和 onActing 的 doOnComplete 里检查是否处于 SHUTTING_DOWN 状态。只在当前阶段完全完成后才中断,不浪费已产生的输出;只有全局关闭超时到期时才在阶段中间强制中断。

8.4 DynamicSkillMiddleware

每次 call() 时从有序的 AgentSkillRepository 列表动态组合 Skill:

  1. 多仓库按优先级合并(同名后者覆盖)
  2. 子类可重写 filterVisible() 做灰度 / 白名单
  3. 用 SHA-256 对 Skill 内容做签名,签名相同时短路跳过重建

9. 自定义 Middleware(完整示例)

下面三个示例来自 agentscope-examples/documentation/.../middleware/,是可直接运行的真实代码。

9.1 全生命周期监控(CustomizedMiddlewareExample)

演示 onAgent / onReasoning / onActing 三个洋葱钩子,并通过 doOnNext 观察工具执行时的流式进度块(ToolResultTextDeltaEvent):

java 复制代码
static class MonitoringMiddleware implements MiddlewareBase {

    @Override
    public Flux<AgentEvent> onAgent(
            Agent agent, RuntimeContext ctx, AgentInput input,
            Function<AgentInput, Flux<AgentEvent>> next) {
        System.out.println("[onAgent] start --- " + agent.getName()
                + ", messages: " + input.msgs().size());
        return next.apply(input)
                .doOnComplete(() -> System.out.println("[onAgent] end"));
    }

    @Override
    public Flux<AgentEvent> onReasoning(
            Agent agent, RuntimeContext ctx, ReasoningInput input,
            Function<ReasoningInput, Flux<AgentEvent>> next) {
        int n = input.messages() != null ? input.messages().size() : 0;
        System.out.println("[onReasoning] start --- context size: " + n);
        return next.apply(input)
                .doOnComplete(() -> System.out.println("[onReasoning] end"));
    }

    @Override
    public Flux<AgentEvent> onActing(
            Agent agent, RuntimeContext ctx, ActingInput input,
            Function<ActingInput, Flux<AgentEvent>> next) {
        String toolNames = input.toolCalls().stream()
                .map(ToolUseBlock::getName)
                .collect(Collectors.joining(", "));
        System.out.println("[onActing] start --- tools: " + toolNames);

        return next.apply(input)
                .doOnNext(event -> {
                    // 捕获工具执行过程中的流式进度块
                    if (event instanceof ToolResultTextDeltaEvent delta) {
                        System.out.println("  [tool progress] " + delta.getDelta());
                    }
                })
                .doOnComplete(() -> System.out.println("[onActing] end --- " + toolNames));
    }
}

对应的工具用 ToolEmitter 在执行过程中发射进度块:

java 复制代码
public static class ProgressTools {
    @Tool(name = "process_data", description = "Process a dataset and report progress")
    public String processData(
            @ToolParam(name = "dataset_name", description = "Name of the dataset")
                    String datasetName,
            ToolEmitter emitter) {
        for (int i = 1; i <= 5; i++) {
            Thread.sleep(500);
            int pct = i * 20;
            emitter.emit(ToolResultBlock.text("Processed " + pct + "% of " + datasetName));
        }
        return "Successfully processed '" + datasetName + "'.";
    }
}

9.2 模型调用审计(ModelCallMiddlewareExample)

演示 onModelCall:统计每次模型调用的元数据、耗时、事件数。

java 复制代码
public static class AuditingMiddleware implements MiddlewareBase {

    final AtomicLong callCount = new AtomicLong();
    final AtomicLong eventCount = new AtomicLong();

    @Override
    public Flux<AgentEvent> onModelCall(
            Agent agent, RuntimeContext ctx, ModelCallInput input,
            Function<ModelCallInput, Flux<AgentEvent>> next) {

        long callIndex = callCount.incrementAndGet();
        long startMs = System.currentTimeMillis();

        System.out.printf("[Audit #%d] model=%s | messages=%d | tools=%d%n",
                callIndex,
                input.model().getClass().getSimpleName(),
                input.messages().size(),
                input.tools().size());

        return next.apply(input)
                .doOnNext(event -> eventCount.incrementAndGet())
                .doOnComplete(() -> System.out.printf(
                        "[Audit #%d] completed in %d ms (events: %d)%n",
                        callIndex, System.currentTimeMillis() - startMs, eventCount.get()))
                .doOnError(err -> System.err.printf(
                        "[Audit #%d] error: %s%n", callIndex, err.getMessage()));
    }
}

9.3 动态 System Prompt(SystemPromptMiddlewareExample)

演示 Transformer 模式:两个中间件串联接力,各自往 prompt 追加一段上下文。

java 复制代码
public static class TimestampMiddleware implements MiddlewareBase {
    @Override
    public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
        String ts = Instant.now().toString();
        return Mono.just(currentPrompt + "\n\n[Context] Current UTC time: " + ts);
    }
}

public static class EnvironmentMiddleware implements MiddlewareBase {
    private final String environment;
    private final String userId;

    public EnvironmentMiddleware(String environment, String userId) {
        this.environment = environment;
        this.userId = userId;
    }

    @Override
    public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
        return Mono.just(currentPrompt
                + "\n[Context] Environment: " + environment
                + " | User ID: " + userId);
    }
}

装配时两个中间件串行执行:

java 复制代码
ReActAgent agent = ReActAgent.builder()
        .name("ContextAwareAgent")
        .sysPrompt("You are a helpful assistant.")
        .model(model)
        .middleware(new TimestampMiddleware())
        .middleware(new EnvironmentMiddleware("demo", "user-42"))
        .build();

// 最终 prompt 大致是:
// You are a helpful assistant.
//
// [Context] Current UTC time: 2026-09-28T10:00:00Z
// [Context] Environment: demo | User ID: user-42

10. RuntimeContext 读写

所有 Hook 第二个参数 RuntimeContext 在整次 reply 内被各层 Hook / Tool 共享:

java 复制代码
public class RequestContextMiddleware implements MiddlewareBase {

    @Override
    public Flux<AgentEvent> onAgent(
            Agent agent, RuntimeContext ctx, AgentInput input,
            Function<AgentInput, Flux<AgentEvent>> next) {

        // 读:会话字段
        System.out.printf("[req] user=%s session=%s reqId=%s%n",
                ctx.getUserId(), ctx.getSessionId(), ctx.get("request_id"));

        // 写:下游 Hook 和 Tool 都能读到
        ctx.put("trace_id", java.util.UUID.randomUUID().toString());

        return next.apply(input);
    }
}

获取 ctx 的两种方式:

  1. 推荐 :直接用 Hook 签名里的第二个参数 ctx(本地示例 CustomizedMiddlewareExample / ModelCallMiddlewareExample / SystemPromptMiddlewareExample 全部采用这种方式)
  2. 也可以在 Hook 内部通过 agent.getRuntimeContext() 获取(需判空,非调用线程上可能为 null):
java 复制代码
public Flux<AgentEvent> onAgent(Agent agent, RuntimeContext ctx, AgentInput input, ...) {
    RuntimeContext rc = ctx != null ? ctx : agent.getRuntimeContext();
    if (rc != null) {
        rc.put("trace_id", java.util.UUID.randomUUID().toString());
    }
    return next.apply(input);
}

注意事项:

  • RuntimeContext 内部是线程安全 map,可以安全 put
  • 不要把请求级状态缓存到中间件实例字段 ------一个中间件实例通常被多个 Agent / call 复用;要么放进 RuntimeContext,要么用 Reactor contextWrite
  • 如果 Builder 同时配了全局 toolExecutionContext,框架分发给 Tool 时会把它合并到 per-call context 之后(per-call 优先级更高)

11. 实用场景

11.1 计时中间件

java 复制代码
public class TimingMiddleware implements MiddlewareBase {
    @Override
    public Flux<AgentEvent> onModelCall(
            Agent agent, ModelCallInput input,
            Function<ModelCallInput, Flux<AgentEvent>> next) {
        long start = System.nanoTime();
        return next.apply(input).doFinally(sig -> {
            long ms = (System.nanoTime() - start) / 1_000_000;
            System.out.println("[timing] " + agent.getName() + ": " + ms + "ms");
        });
    }
}

11.2 限流中间件

java 复制代码
public class RateLimitMiddleware implements MiddlewareBase {
    private final long minIntervalMs;
    private final AtomicLong lastCall = new AtomicLong(0);

    public RateLimitMiddleware(Duration minInterval) {
        this.minIntervalMs = minInterval.toMillis();
    }

    @Override
    public Flux<AgentEvent> onModelCall(
            Agent agent, ModelCallInput input,
            Function<ModelCallInput, Flux<AgentEvent>> next) {
        long wait = minIntervalMs - (System.currentTimeMillis() - lastCall.get());
        Mono<Void> delay = wait > 0
                ? Mono.delay(Duration.ofMillis(wait)).then()
                : Mono.empty();
        return delay.thenMany(next.apply(input))
                .doOnSubscribe(s -> lastCall.set(System.currentTimeMillis()));
    }
}

11.3 模型回退

java 复制代码
public class ModelFallbackMiddleware implements MiddlewareBase {
    private final Model fallback;

    public ModelFallbackMiddleware(Model fallback) {
        this.fallback = fallback;
    }

    @Override
    public Flux<AgentEvent> onModelCall(
            Agent agent, ModelCallInput input,
            Function<ModelCallInput, Flux<AgentEvent>> next) {
        return next.apply(input)
                .onErrorResume(err -> next.apply(new ModelCallInput(
                        input.messages(), input.tools(), input.options(), fallback)));
    }
}

简单的主→备回退也可以直接用 ReActAgent.Builder.fallbackModel(...) 和 maxRetries(...),不必自己写中间件;切模型的监听用 failoverListener(...)。

11.4 全部工具被拒绝时停止 Agent

当用户通过 HITL 拒绝了一轮推理产出的全部工具调用时,默认 Agent 会继续下一轮;希望立即停止可以观察 AllToolsDeniedEvent 并发出 RequestStopEvent:

java 复制代码
public class StopOnAllDeniedMiddleware implements MiddlewareBase {
    @Override
    public Flux<AgentEvent> onActing(
            Agent agent, RuntimeContext ctx, ActingInput input,
            Function<ActingInput, Flux<AgentEvent>> next) {
        return next.apply(input).flatMap(event -> {
            if (event instanceof AllToolsDeniedEvent) {
                return Flux.just(event, new RequestStopEvent(
                        "All tools denied by user",
                        GenerateReason.ALL_TOOLS_DENIED));
            }
            return Flux.just(event);
        });
    }
}

12. 小结

text 复制代码
想观测 / 拦截 / 改写入参   →  Onion 钩子(onAgent / onReasoning / onActing / onModelCall)
想加工 system prompt       →  onSystemPrompt(Transformer)
想按优先级分层             →  覆写 order(),数值大在外层
想传请求级数据             →  RuntimeContext.put(),别写中间件字段
相关推荐
Ramble_Naylor1 小时前
async/await:让一个线程同时等很多件事
开发语言·rust
通信瓦工1 小时前
利用浊度和电导率测量确定乙二醇基流体的质量
网络·数据库·ai
ZealSinger2 小时前
Spring7 RestTemplate弃用迁移
java·spring·后端开发·restclient
林伽一2 小时前
100 万输出词元与窄开放,前沿模型发布范式正在改写|2026年10月02日
人工智能·科技·安全·ai
迅猛龙办公室2 小时前
Python实现绘制同切圆
开发语言·python
bigdata-余建新2 小时前
week5
ai
朝朝辞暮i2 小时前
C++ 第 13 课:值传递 —— 为什么函数里改了,外面却没变?
java·c++·算法
弹简特2 小时前
【Java项目-企悦抽】16-抽奖模块01-获取活动完整信息接口实现
java·开发语言·状态模式·springboot
言乐62 小时前
Python语音检索
开发语言·python·django·virtualenv·pygame