Agent Middleware
概述
AgentMiddleware 是位于 Agent 执行主链中的扩展点,可以包装三个层次:整个 step、一次模型调用和一次工具调用。它适合实现租户鉴权、限流、Prompt 临时增强、模型降级、工具参数校验、缓存和 Trace。
Middleware 会影响执行结果;只观察生命周期时应使用 Listener 或事件。
注册 Middleware
Agent agent = Agent.builder("order-agent")
.chatModel(chatModel)
.middleware(new TimingMiddleware())
.middleware(new TenantGuardMiddleware())
.build();Middleware 是 Agent 定义的一部分,按注册顺序形成责任链。第一个注册项最先进入、最后退出。
三个切入点
public final class TimingMiddleware implements AgentMiddleware {
@Override
public AgentStepResult aroundStep(
AgentMiddlewareContext context, AgentStepChain chain) {
long start = System.nanoTime();
try {
return chain.proceed(context);
} finally {
record("step", System.nanoTime() - start);
}
}
@Override
public AiMessageResponse aroundModelCall(
AgentMiddlewareContext context, AgentModelCallChain chain) {
return chain.proceed(context);
}
@Override
public Object aroundToolCall(
AgentMiddlewareContext context, AgentToolCallChain chain) {
return chain.proceed(context);
}
}aroundStep:覆盖预算检查之后的模式推进,适合 step 级治理。aroundModelCall:访问当前 Prompt,适合模型路由、缓存和临时 Prompt 处理。aroundToolCall:通过非空的context.getToolContext()访问已解析的Tool与原始ToolCall,适合工具授权和审计。其他 Middleware 阶段的getToolContext()返回null。
责任链规则
实现通常必须调用且只调用一次 chain.proceed(context):
- 不调用表示短路,必须返回符合当前状态机的有效结果。
- 多次调用可能重复请求模型或执行有副作用工具。
- 在
finally中做耗时上报仍会增加请求延迟。 - Middleware 被多个 Turn 共享,实例字段必须线程安全。
使用 Turn Metadata
@Override
public Object aroundToolCall(AgentMiddlewareContext context,
AgentToolCallChain chain) {
String tenantId = String.valueOf(
context.getRun().getMetadata().get("tenantId"));
if (!permissionService.allowed(
tenantId, context.getToolContext().getToolName())) {
throw new SecurityException("tool access denied");
}
return chain.proceed(context);
}metadata 会随 Snapshot 持久化,因此 Worker 恢复后仍可读取租户等业务标识。鉴权所需的 permissionService 由 Middleware 自身通过依赖注入持有,并且必须能够安全地被多个 Turn 并发复用。密钥和服务对象不能放入 metadata。
临时修改 Prompt
AgentMiddlewareContext.setPrompt(...) 只替换当前责任链中的模型 Prompt,不直接替换 Turn 的持久化 Prompt。这适合添加一次性的路由提示或脱敏视图。需要跨恢复保留的业务信息应写入可序列化 metadata; 需要永久整理会话历史时,由业务系统读取 ChatMemory 后构造新的历史窗口,再创建 Turn。
动态解析工具
当工具来自动态目录,无法在构建 Agent 时全部注册,可以由 Middleware 同时声明 AgentToolResolver:
public final class CatalogMiddleware implements AgentMiddleware {
private final ToolCatalog catalog;
@Override
public AgentToolResolver getToolResolver() {
return (turn, toolName) -> {
if (!isActivated(turn, toolName)) {
return null;
}
return catalog.resolve(toolName);
};
}
@Override
public AiMessageResponse aroundModelCall(
AgentMiddlewareContext context, AgentModelCallChain chain) {
// 只把当前 Turn 已激活的 Tool 定义加入本次模型 Prompt。
exposeActivatedTools(context);
return chain.proceed(context);
}
}Agent 构建时会自动收集每个 Middleware 的非空 Resolver。模型产生 ToolCall 后,Runner 先查找 Agent 静态注册的 Tool,再查询这些动态 Resolver,因此业务代码不需要单独向 Runner 注册工具目录。
Resolver 只解决“当前名称对应哪个本地可执行 Tool”,不会自动让模型看到 Tool。Middleware 仍需在 aroundModelCall 中把允许使用的 Tool 定义放入当前 Prompt,并把跨恢复所需的激活状态写入 Turn metadata。Resolver 返回的 Tool 名称必须与请求名称一致;多个 Resolver 同时解析出同一个名称会被 视为配置冲突。
ToolSearch 集成
agents-flex-toolsearch 已提供 ToolSearchAgentMiddleware。它负责按搜索结果控制模型可见工具, 并通过 Resolver 恢复可执行 Tool,无需业务侧重复实现上述逻辑。
与 ToolInterceptor 的关系
执行顺序为 Agent Tool Middleware 链,然后进入核心 ToolExecutor 与 Agent 配置的 ToolInterceptor,最后调用 Tool 函数。Middleware 能访问 Turn 和 Agent 上下文;ToolInterceptor 更接近通用工具执行层。跨 Agent 的通用工具治理可放在 ToolInterceptor,任务级策略放在 Middleware。
短路示例
模型缓存可以直接返回缓存响应,但必须保证响应与正常模型协议等价,并考虑 ToolCall、Token usage 和事件语义。工具缓存只适用于无副作用且参数完全决定结果的工具。对写工具绝不能用简单缓存代替业务幂等。
错误处理
Middleware 抛出的运行时异常会进入 Runner 的统一失败/重试逻辑。参数与权限错误应使用确定性异常,避免自动重试;瞬时依赖失败可以让 Runner 按策略调度重试。不要捕获异常后返回伪造成功结果。
测试建议
为每个 Middleware 验证正常链、短路、异常、并发复用和恢复场景;特别检查 proceed 只调用一次。包含外部副作用时,用稳定 toolCallId 断言重试不会重复写入。