目录
- [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:
- 多仓库按优先级合并(同名后者覆盖)
- 子类可重写
filterVisible()做灰度 / 白名单 - 用 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 的两种方式:
- 推荐 :直接用 Hook 签名里的第二个参数
ctx(本地示例CustomizedMiddlewareExample/ModelCallMiddlewareExample/SystemPromptMiddlewareExample全部采用这种方式) - 也可以在 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,要么用 ReactorcontextWrite - 如果 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(),别写中间件字段