Agent Scope Java 2.x 系列【11】中间件(Middleware):核心设计

文章目录

  • [1. 概述](#1. 概述)
    • [1.1 Onion 洋葱式](#1.1 Onion 洋葱式)
    • [1.2 Transformer 变换式](#1.2 Transformer 变换式)
    • [1.3 五个位置](#1.3 五个位置)
  • [2. MiddlewareBase 接口](#2. MiddlewareBase 接口)
  • [3. MiddlewareBase 实现类](#3. MiddlewareBase 实现类)
    • [3.1 TaskReminderMiddleware](#3.1 TaskReminderMiddleware)
    • [3.2 OtelTracingMiddleware](#3.2 OtelTracingMiddleware)
    • [3.3 GracefulShutdownMiddleware](#3.3 GracefulShutdownMiddleware)
    • [3.4 DynamicSkillMiddleware](#3.4 DynamicSkillMiddleware)

1. 概述

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

AgentScope Java 划分两类中间件模型:Onion 洋葱型、Transformer 变换型,共 5 个可挂载 Hook 点位。

下图呈现 Agent 生命周期里各个 Hook 的嵌套层级:

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

链路说明:

  1. 整体最外层是onAgent
  2. 内部循环为每一轮 ReAct 流程
    • 推理阶段onReasoning中嵌套两个钩子:
      • onSystemPrompt:拼接系统提示词时触发,内嵌于推理流程
      • onModelCall:发起模型接口调用
    • 行动阶段onActing:负责捕获 Agent 内部执行工具的全过程

补充说明onActing 仅监控 Agent 自身发起的工具运行,外部调度执行的工具不会被该钩子捕获。


1.1 Onion 洋葱式

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

以两个中间件 mw1(外层)、mw2(内层)包裹原生执行业务逻辑演示层级:

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

执行顺序步骤拆解:

  1. 先跑最外层 mw1 前置逻辑
  2. 进入内层,执行 mw2 前置逻辑
  3. 运行框架本身的业务流程
  4. 反向回退,先走 mw2 后置收尾
  5. 最后执行最外层 mw1 后置收尾

适用点位onAgent / onReasoning / onActing / onModelCall


1.2 Transformer 变换式

无嵌套包裹、无前后双切面,纯单向流水线串行结构 。多个中间件首尾串联,上一个的输出 = 下一个的输入,只有正向加工步骤,不存在执行完成后的反向后置回调。

以两个中间件 mw1mw2 串行原生执行业务逻辑演示层级:

复制代码
原始Prompt文本
       ↓
┌───────────────┐
│ mw1 变换加工逻辑 │
└───────┬───────┘
        ↓ mw1输出结果作为mw2入参
┌───────────────┐
│ mw2 变换加工逻辑 │
└───────┬───────┘
        ↓
最终成型 Prompt → 送入模型推理

执行顺序步骤拆解:

  1. 拿到未经修改的原始系统提示词
  2. 执行 mw1 变换逻辑,产出第一段修改后的文本
  3. mw1 的输出直接传入 mw2 作为输入
  4. 执行 mw2 变换逻辑,生成最终提示词
  5. 直接流向底层模型,全程没有反向后置步骤

和洋葱 Onion 关键差异对比

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

唯一点位onSystemPrompt(仅用于系统提示词加工)。


1.3 五个位置

可以在 5 个位置上设置 hook,覆盖了从外层 reply 流程一路下沉到底层模型 API 调用的全链路:

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

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


2. MiddlewareBase 接口

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

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

源码如下:

java 复制代码
/**
 * Agent中间件基础接口
 * 提供智能体生命周期5个关键执行节点的拦截扩展能力
 *
 * <p>分为两种执行设计模式:
 * <ol>
 *   <li>洋葱包裹模式(4个钩子):通过next函数包裹内层执行,可在执行前后插入自定义逻辑
 *       <ul>
 *         <li>{@link #onAgent} --- 拦截整个Agent完整调用流程(最外层)</li>
 *         <li>{@link #onReasoning} --- 拦截单轮ReAct推理阶段</li>
 *         <li>{@link #onActing} --- 拦截单次工具调用执行动作</li>
 *         <li>{@link #onModelCall} --- 拦截底层原始大模型API请求</li>
 *       </ul>
 *   </li>
 *   <li>流水线转换模式(1个钩子):串行文本转换,无嵌套包裹逻辑
 *       <ul>
 *         <li>{@link #onSystemPrompt} --- 对系统提示词做内容加工转换</li>
 *       </ul>
 *   </li>
 * </ol>
 *
 * <p>所有钩子均提供default默认空实现,默认直接透传给下一层执行逻辑;
 * 业务自定义中间件仅需重写需要扩展的钩子,无需实现全部方法,降低接入成本。
 *
 * <p>生命周期嵌套层级:
 * <pre>
 * onAgent(全局)
 * └── 循环多轮ReAct
 *     ├── onReasoning(推理层)
 *     │   ├── onSystemPrompt(提示词组装)
 *     │   └── onModelCall(模型调用)
 *     └── onActing(工具执行)
 * </pre>
 * <p>补充:onActing仅追踪Agent内部发起执行的工具,外部调度执行的工具不会被拦截捕获
 *
 * <p>使用示例:自定义推理日志埋点中间件
 * <pre>{@code
 * MiddlewareBase logging = new MiddlewareBase() {
 *     @Override
 *     public Flux<AgentEvent> onReasoning(
 *             Agent agent, RuntimeContext ctx, ReasoningInput input,
 *             Function<ReasoningInput, Flux<AgentEvent>> next) {
 *         System.out.println("推理开始,会话ID=" + ctx.getSessionId());
 *         // 执行内层推理逻辑,流完成后打印后置日志
 *         return next.apply(input)
 *             .doOnComplete(() -> System.out.println("推理执行完毕"));
 *     }
 * };
 * }</pre>
 */
public interface MiddlewareBase {

    /**
     * 拦截整个Agent单次完整调用流程(最外层钩子)
     * @param agent 当前运行的智能体实例
     * @param ctx 单次调用独立运行上下文,存储会话、用户、自定义属性等全局临时数据
     * @param input Agent顶层入参(对话消息集合)
     * @param next 透传函数,调用后执行下一个中间件或Agent核心原生逻辑
     * @return Agent全生命周期流式事件流(响应式Flux多事件)
     */
    default Flux<AgentEvent> onAgent(
            Agent agent,
            RuntimeContext ctx,
            AgentInput input,
            Function<AgentInput, Flux<AgentEvent>> next) {
        // 默认实现:直接放行,无额外拦截逻辑
        return next.apply(input);
    }

    /**
     * 拦截单轮ReAct推理阶段
     * 推理阶段包含:系统提示词组装、大模型调用、流式输出内容解析
     * @param agent 当前运行的智能体实例
     * @param ctx 单次调用独立运行上下文
     * @param input 推理阶段入参:对话消息、可用工具列表、模型配置参数
     * @param next 透传函数,执行下游推理逻辑
     * @return 推理过程产生的事件流
     */
    default Flux<AgentEvent> onReasoning(
            Agent agent,
            RuntimeContext ctx,
            ReasoningInput input,
            Function<ReasoningInput, Flux<AgentEvent>> next) {
        // 默认直接透传内层推理逻辑
        return next.apply(input);
    }

    /**
     * 拦截工具调用执行阶段
     * 仅捕获Agent内部发起运行的工具,外部独立服务调度的工具不受此钩子管控
     * @param agent 当前运行的智能体实例
     * @param ctx 单次调用独立运行上下文
     * @param input 工具执行入参:待执行的工具调用信息、入参
     * @param next 透传函数,执行下游工具执行逻辑
     * @return 工具运行产生的事件流
     */
    default Flux<AgentEvent> onActing(
            Agent agent,
            RuntimeContext ctx,
            ActingInput input,
            Function<ActingInput, Flux<AgentEvent>> next) {
        // 默认放行工具执行流程
        return next.apply(input);
    }

    /**
     * 拦截底层原始大模型API调用
     * 粒度比onReasoning更细,只包裹真实发起LLM网络请求的环节
     * @param agent 当前运行的智能体实例
     * @param ctx 单次调用独立运行上下文
     * @param input 模型调用完整入参:消息、工具定义、模型参数、模型标识
     * @param next 透传函数,执行真实模型请求逻辑
     * @return 模型调用返回的流式事件
     */
    default Flux<AgentEvent> onModelCall(
            Agent agent,
            RuntimeContext ctx,
            ModelCallInput input,
            Function<ModelCallInput, Flux<AgentEvent>> next) {
        // 默认直接发起模型调用
        return next.apply(input);
    }

    /**
     * 流水线模式转换系统提示词文本
     * 多个中间件按注册顺序串行执行:上一个处理完的prompt作为下一个的入参
     * 无洋葱嵌套结构,只做字符串变换,无执行前后包裹回调
     * @param agent 当前运行的智能体实例
     * @param ctx 单次调用独立运行上下文
     * @param currentPrompt 当前待加工的原始系统提示词
     * @return 加工完成后的系统提示词(Mono单字符串异步结果)
     */
    default Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
        // 默认原样返回,不修改提示词
        return Mono.just(currentPrompt);
    }
}

所有实现类:


3. MiddlewareBase 实现类

基于 agentscope-core-2.0.0-RC2,共 4 个内置实现类。

类名 触发钩子 核心职责
TaskReminderMiddleware onSystemPrompt + onReasoning 每次推理前注入当前 Todo 列表,防止长任务偏离计划
OtelTracingMiddleware onAgent + onModelCall + onActing 为 Agent → Model → Tool 三级调用生成 OpenTelemetry Span
GracefulShutdownMiddleware onReasoning + onActing 在阶段完成后的安全点执行优雅关闭中断,不浪费已产生的输出
DynamicSkillMiddleware onSystemPrompt 每次调用动态合并多仓库技能,支持按用户隔离和灰度发布

3.1 TaskReminderMiddleware

包路径: io.agentscope.core.middleware

长任务执行过程中,模型容易因为上下文过长而"忘记"最初的计划。该中间件通过在每次推理前显式注入当前 Todo 列表来解决此问题。

其设计有两个要点:

  1. onSystemPrompt:一次性注入 todo_write 工具的静态使用说明,告诉模型如何使用该工具管理任务列表。
  2. onReasoning:在每次推理步骤之前AgentState.tasksContext 的最新状态渲染为 <system-reminder> 块,追加到推理输入中。这样无论前面发生了多少次工具调用和对话轮次,模型始终能看到最新的任务状态。

关键实现细节:

  • 提醒消息仅临时追加 到推理输入,不写入 AgentState.context,因此不会被持久化、压缩或回溯。
  • 消息标记了 METADATA_SYNTHETIC,下游消费者可以据此识别并跳过。
  • opencode 方案不同(后者把最新列表放在最近一次工具输出中),agentscope 选择每轮重新注入 :对话压缩后工具输出可能被清除,但 tasksContext 作为独立状态持久存在,能保证列表始终可见。

核心源码:

java 复制代码
public class TaskReminderMiddleware implements MiddlewareBase {

    private static final String GROUNDING =
            """

            ## Task List
            You have a `todo_write` tool that maintains a structured task list for this session.
            Use it for multi-step work: capture the plan as todos, keep exactly one task
            `in_progress`, and update the whole list as you make progress. Your current list (if
            any) is shown to you before each step inside a `<system-reminder>` block --- treat that
            block as the source of truth for task status.\
            """;

    @Override
    public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
        String base = currentPrompt != null ? currentPrompt : "";
        return Mono.just(base + GROUNDING);
    }

    @Override
    public Flux<AgentEvent> onReasoning(
            Agent agent, RuntimeContext ctx, ReasoningInput input,
            Function<ReasoningInput, Flux<AgentEvent>> next) {
        AgentState state = RuntimeContext.resolveAgentState(ctx, agent);
        List<Task> tasks = state == null ? List.of() : state.getTasksContext().getTasks();
        if (tasks.isEmpty()) {
            return next.apply(input);
        }
        Msg reminder = Msg.builder()
                .role(MsgRole.USER).name("system")
                .content(TextBlock.builder().text(render(tasks)).build())
                .metadata(Map.of(
                    Msg.METADATA_SYNTHETIC, true,
                    Msg.METADATA_REMINDER_KIND, "todo_state"))
                .build();
        List<Msg> messages = new ArrayList<>(input.messages());
        messages.add(reminder);
        return next.apply(new ReasoningInput(messages, input.tools(), input.options()));
    }

    private static String render(List<Task> tasks) {
        StringBuilder sb = new StringBuilder();
        sb.append("<system-reminder>\n");
        sb.append("Your current todo list is shown below. This is the source of truth --- do not"
                + " assume earlier statuses still hold. Keep exactly one task in_progress."
                + " Update the whole list with todo_write as you progress.\n\n");
        for (Task t : tasks) {
            sb.append(marker(t.getState())).append(' ').append(t.getSubject());
            Object priority = t.getMetadata() == null ? null : t.getMetadata().get("priority");
            if (priority != null) {
                sb.append(" (priority: ").append(priority).append(')');
            }
            sb.append('\n');
        }
        sb.append("</system-reminder>");
        return sb.toString();
    }

    private static String marker(Task.State state) {
        return switch (state) {
            case COMPLETED -> "- [x]";
            case IN_PROGRESS -> "- [~]";
            case PENDING -> "- [ ]";
        };
    }
}

3.2 OtelTracingMiddleware

包路径: io.agentscope.core.tracing

Agent 全生命周期接入 OpenTelemetry 分布式追踪,按层级生成三级 Span。使用方式是在构建 ReActAgent 时通过 .middleware(new OtelTracingMiddleware()) 注册。

产生的 Span 层级结构:

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

每个 Span 都会记录关键属性:Agent 名称和 ID、模型名称、消息/工具数量、Token 用量(input/output tokens)、工具调用 ID 等。

当没有配置 OpenTelemetry SDK 时(只有默认的 no-op provider),所有钩子直接短路到 next.apply(input),几乎零开销。

核心源码:

java 复制代码
public class OtelTracingMiddleware implements MiddlewareBase {

    private static final String INSTRUMENTATION_NAME = "io.agentscope";

    private Tracer getTracer() {
        return GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME);
    }

    // onAgent --- invoke_agent span,包裹整个回复
    @Override
    public Flux<AgentEvent> onAgent(Agent agent, RuntimeContext ctx,
            AgentInput input, Function<AgentInput, Flux<AgentEvent>> next) {
        return Flux.defer(() -> {
            Tracer tracer = getTracer();
            Span span = tracer.spanBuilder("invoke_agent " + agent.getName())
                    .setAttribute("gen_ai.operation.name", "invoke_agent")
                    .setAttribute("gen_ai.agent.name", agent.getName())
                    .setAttribute("gen_ai.agent.id",
                            agent.getAgentId() != null ? agent.getAgentId() : "")
                    .setAttribute("gen_ai.request.messages.count",
                            (long) input.msgs().size())
                    .startSpan();
            AtomicReference<String> replyIdRef = new AtomicReference<>();
            try (Scope ignored = span.makeCurrent()) {
                return next.apply(input)
                        .doOnNext(event -> {
                            if (event instanceof AgentStartEvent rse) {
                                replyIdRef.set(rse.getReplyId());
                            }
                        })
                        .doOnComplete(() -> {
                            setReplyIdIfPresent(span, replyIdRef.get());
                            span.setStatus(StatusCode.OK);
                            span.end();
                        })
                        .doOnError(e -> {
                            setReplyIdIfPresent(span, replyIdRef.get());
                            span.setStatus(StatusCode.ERROR, e.getMessage());
                            span.recordException(e);
                            span.end();
                        });
            }
        });
    }

    // onModelCall --- chat span,包裹每次模型 API 调用
    @Override
    public Flux<AgentEvent> onModelCall(Agent agent, RuntimeContext ctx,
            ModelCallInput input, Function<ModelCallInput, Flux<AgentEvent>> next) {
        return Flux.defer(() -> {
            Tracer tracer = getTracer();
            Model model = input.model();
            String modelName = model != null ? model.getModelName() : "unknown";
            Span span = tracer.spanBuilder("chat " + modelName)
                    .setAttribute("gen_ai.operation.name", "chat")
                    .setAttribute("gen_ai.request.model", modelName)
                    .setAttribute("gen_ai.request.messages.count",
                            (long) input.messages().size())
                    .setAttribute("gen_ai.request.tools.count",
                            input.tools() != null ? (long) input.tools().size() : 0L)
                    .startSpan();
            try (Scope ignored = span.makeCurrent()) {
                return next.apply(input)
                        .doOnNext(event -> {
                            if (event instanceof ModelCallEndEvent mce) {
                                setModelResponseAttributes(span, mce);
                            }
                        })
                        .doOnComplete(() -> { span.setStatus(StatusCode.OK); span.end(); })
                        .doOnError(e -> {
                            span.setStatus(StatusCode.ERROR, e.getMessage());
                            span.recordException(e);
                            span.end();
                        });
            }
        });
    }

    // onActing --- execute_tool span,包裹每次工具执行
    @Override
    public Flux<AgentEvent> onActing(Agent agent, RuntimeContext ctx,
            ActingInput input, Function<ActingInput, Flux<AgentEvent>> next) {
        return Flux.defer(() -> {
            Tracer tracer = getTracer();
            String toolNames = input.toolCalls() != null
                    ? input.toolCalls().stream()
                            .map(ToolUseBlock::getName)
                            .collect(Collectors.joining(", "))
                    : "unknown";
            Span span = tracer.spanBuilder("execute_tool " + toolNames)
                    .setAttribute("gen_ai.operation.name", "execute_tool")
                    .setAttribute("gen_ai.tool.name", toolNames)
                    .setAttribute("gen_ai.tool.call.count",
                            input.toolCalls() != null
                                    ? (long) input.toolCalls().size() : 0L)
                    .startSpan();
            try (Scope ignored = span.makeCurrent()) {
                return next.apply(input)
                        .doOnNext(event -> {
                            if (event instanceof ToolResultEndEvent tre) {
                                span.setAttribute("gen_ai.tool.call.id",
                                        tre.getToolCallId() != null
                                                ? tre.getToolCallId() : "");
                            }
                        })
                        .doOnComplete(() -> { span.setStatus(StatusCode.OK); span.end(); })
                        .doOnError(e -> {
                            span.setStatus(StatusCode.ERROR, e.getMessage());
                            span.recordException(e);
                            span.end();
                        });
            }
        });
    }

    private void setModelResponseAttributes(Span span, ModelCallEndEvent event) {
        if (event.getUsage() != null) {
            var usage = event.getUsage();
            span.setAttribute("gen_ai.usage.input_tokens", (long) usage.getInputTokens());
            span.setAttribute("gen_ai.usage.output_tokens", (long) usage.getOutputTokens());
        }
    }
}

3.3 GracefulShutdownMiddleware

包路径: io.agentscope.core.shutdown

系统级中间件,负责在 Agent 生命周期中嵌入优雅关闭逻辑。其设计关注点是在安全的时机中断 Agent 执行,避免浪费已完成的计算

职责分工:

  • AgentBase.call() 负责每次调用的请求注册和注销:通过 Reactor Context 中的 SHUTDOWN_REQUEST_ID_KEY 传递独立的 requestId,并通过 doFinally 确保每个请求无论成功、出错还是取消都会被注销。同一 Agent 实例的并发调用会被独立追踪。
  • 本中间件 负责在安全检查点触发中断:
    • onReasoningdoOnComplete --- 推理阶段完成后
    • onActingdoOnComplete --- 工具执行完成后

中断策略:当系统处于 SHUTTING_DOWN 状态时,只在当前阶段完全完成后 才中断。这意味着推理/执行的输出 token 不会浪费------模型生成的回复或工具调用的结果都能完整返回。只有当全局关闭超时到期时(由 GracefulShutdownManager#enforceTimeoutAndInterrupt 处理),才会在阶段中间强制中断。

核心源码:

java 复制代码
public final class GracefulShutdownMiddleware implements MiddlewareBase {

    private static final Logger log = LoggerFactory.getLogger(GracefulShutdownMiddleware.class);

    private final GracefulShutdownManager manager;

    public GracefulShutdownMiddleware(GracefulShutdownManager manager) {
        this.manager = manager;
    }

    @Override
    public Flux<AgentEvent> onReasoning(
            Agent agent, RuntimeContext ctx,
            ReasoningInput input, Function<ReasoningInput, Flux<AgentEvent>> next) {
        return Flux.deferContextual(cv ->
                next.apply(input)
                        .doOnComplete(() ->
                                manager.interruptIfShuttingDown(currentRequestId(cv))));
    }

    @Override
    public Flux<AgentEvent> onActing(
            Agent agent, RuntimeContext ctx,
            ActingInput input, Function<ActingInput, Flux<AgentEvent>> next) {
        return Flux.deferContextual(cv ->
                next.apply(input)
                        .doOnComplete(() ->
                                manager.interruptIfShuttingDown(currentRequestId(cv))));
    }

    private static String currentRequestId(reactor.util.context.ContextView cv) {
        return (String) cv.getOrDefault(AgentBase.SHUTDOWN_REQUEST_ID_KEY, null);
    }
}

3.4 DynamicSkillMiddleware

包路径: io.agentscope.core.skill

解决静态 skill prompt 无法适应多用户、多仓库、动态变更场景的问题。核心思路是每次 call() 时从有序的 AgentSkillRepository 列表中动态组合技能 ,生成当次调用专用的 skill prompt

关键设计决策:

  1. 多仓库合并策略:仓库列表按低优先级到高优先级迭代。当两个仓库提供同名技能时,后者胜出。这支持了"基础仓库 + 用户专属仓库"的分层覆盖模式。

  2. 每次调用都重构 (而非缓存):因为按用户命名空间的仓库可能在同一技能名下为不同 RuntimeContext 返回不同内容。但框架做了优化------通过 SHA-256 签名对技能内容做"内容寻址",签名相同时直接复用上次的 SkillBox,实际运行时多数调用都能命中短路。

  3. 可见性控制 :子类可重写 filterVisible(List, RuntimeContext) 实现按请求的灰度发布、环境开关或白名单控制。默认原样返回。

  4. 幂等重建bindToolkitregisterSkillLoadTool 在新 SkillBox 上是幂等的,因此重建开销可控。

每次 call() 的执行流程:

复制代码
onSystemPrompt 触发
  │
  ├─ 遍历所有仓库 → 按名称合并(高优先级覆盖)
  ├─ filterVisible() → 子类灰度/白名单过滤
  ├─ computeSignature() → 签名相同则短路,跳过重建
  └─ 重建 SkillBox → registerSkill → bindToolkit → uploadSkillFiles
       │
       └─ currentSkillBox.getSkillPrompt() → 追加到 system prompt

核心源码:

java 复制代码
public class DynamicSkillMiddleware implements MiddlewareBase {

    private final List<AgentSkillRepository> repositories;
    private final Toolkit toolkit;
    private final SkillFilter builderFilter;
    private final boolean codeExecutionEnabled;

    private volatile Path stableWorkDir;
    private volatile SkillBox currentSkillBox;
    private volatile String lastSignature;   // 内容寻址的哈希,用于短路跳过重建

    public DynamicSkillMiddleware(List<AgentSkillRepository> repositories, Toolkit toolkit) {
        this(repositories, toolkit, null, false, null);
    }

    public DynamicSkillMiddleware(
            List<AgentSkillRepository> repositories, Toolkit toolkit,
            SkillFilter builderFilter, boolean codeExecutionEnabled, Path workDir) {
        this.repositories = repositories != null ? List.copyOf(repositories) : List.of();
        this.toolkit = toolkit;
        this.builderFilter = builderFilter != null ? builderFilter : SkillFilter.all();
        this.codeExecutionEnabled = codeExecutionEnabled;
        this.stableWorkDir = workDir;
    }

    public SkillBox getCurrentSkillBox() {
        return currentSkillBox;
    }

    @Override
    public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
        RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty();
        reloadSkills(rc);
        if (currentSkillBox == null) {
            return Mono.just(currentPrompt);
        }
        SkillFilter effectiveFilter = resolveFilter(rc);
        String prompt = currentSkillBox.getSkillPrompt(effectiveFilter);
        if (prompt == null || prompt.isEmpty()) {
            return Mono.just(currentPrompt);
        }
        String base = currentPrompt != null ? currentPrompt : "";
        String separator = base.isEmpty() || base.endsWith("\n") ? "" : "\n";
        return Mono.just(base + separator + prompt);
    }

    /**
     * 子类可重写,按请求过滤技能可见性(灰度发布、白名单、环境开关)。
     * 每次 call() 调用一次,在合并之后、SkillBox 重建之前。
     * 返回空列表将短路本次 prompt 更新。
     */
    protected List<AgentSkill> filterVisible(List<AgentSkill> raw, RuntimeContext ctx) {
        return raw;
    }

    private void reloadSkills(RuntimeContext ctx) {
        if (repositories.isEmpty()) {
            currentSkillBox = null;
            lastSignature = null;
            return;
        }
        // 按名称合并,高优先级覆盖
        Map<String, AgentSkill> skillsByName = new LinkedHashMap<>();
        for (AgentSkillRepository repo : repositories) {
            List<AgentSkill> skills;
            try {
                skills = repo.getAllSkills();
            } catch (Exception e) {
                log.warn("Skill repository {} failed to load: {}",
                        repo.getClass().getSimpleName(), e.getMessage());
                continue;
            }
            if (skills == null) continue;
            for (AgentSkill skill : skills) {
                if (skill == null || skill.getName() == null) continue;
                skillsByName.put(skill.getName(), skill);
            }
        }
        if (skillsByName.isEmpty()) {
            currentSkillBox = null;
            lastSignature = null;
            return;
        }
        // 可见性过滤
        List<AgentSkill> visible;
        try {
            visible = filterVisible(new ArrayList<>(skillsByName.values()), ctx);
            if (visible == null) visible = new ArrayList<>(skillsByName.values());
        } catch (Exception e) {
            visible = new ArrayList<>(skillsByName.values());
        }
        if (visible.isEmpty()) {
            currentSkillBox = null;
            lastSignature = null;
            return;
        }

        // 内容签名短路:签名相同则跳过重建
        String signature = computeSignature(visible);
        if (signature.equals(lastSignature) && currentSkillBox != null) {
            return;
        }

        // 重建 SkillBox
        SkillBox box = new SkillBox(toolkit);
        box.setWorkDir(ensureStableWorkDir());
        for (AgentSkill skill : visible) box.registerSkill(skill);
        if (toolkit != null) {
            box.bindToolkit(toolkit);
            box.registerSkillLoadTool();
        }
        if (box.isAutoUploadSkill()) box.uploadSkillFiles();
        box.getSkillPromptProvider().setCodeExecutionEnable(codeExecutionEnabled);
        currentSkillBox = box;
        lastSignature = signature;
    }

    // 对技能列表内容计算 SHA-256,避免无变化时的重复重建
    private static String computeSignature(List<AgentSkill> visible) {
        Set<String> sortedNames = new TreeSet<>();
        for (AgentSkill s : visible) sortedNames.add(s.getName());
        Map<String, AgentSkill> byName = new LinkedHashMap<>();
        for (AgentSkill s : visible) byName.put(s.getName(), s);
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            for (String name : sortedNames) {
                AgentSkill s = byName.get(name);
                md.update(name.getBytes(StandardCharsets.UTF_8));
                md.update((byte) 0);
                md.update(s.getSkillContent() != null
                        ? s.getSkillContent().getBytes(StandardCharsets.UTF_8) : new byte[0]);
                md.update((byte) 0);
                Set<String> sortedResources = new TreeSet<>(s.getResources().keySet());
                for (String key : sortedResources) {
                    md.update(key.getBytes(StandardCharsets.UTF_8));
                    String val = s.getResources().get(key);
                    if (val != null) md.update(val.getBytes(StandardCharsets.UTF_8));
                    md.update((byte) 0);
                }
                s.getOriginDir().ifPresent(
                        p -> md.update(p.toString().getBytes(StandardCharsets.UTF_8)));
                md.update((byte) 1);
            }
            byte[] hash = md.digest();
            StringBuilder hex = new StringBuilder(hash.length * 2);
            for (byte b : hash) hex.append(String.format("%02x", b));
            return hex.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 unavailable", e);
        }
    }
}

相关推荐
2401_885665191 小时前
基于OpenCV的模板匹配OCR实战:银行卡与身份证数字识别完整教程
人工智能·python·opencv·计算机视觉·ocr
装不满的克莱因瓶1 小时前
了解3D卷积原理——从空间感知到时空建模的深度学习核心算子
人工智能·pytorch·python·深度学习·机器学习·3d·ai
心之伊始1 小时前
Spring AI Chat Memory 实战:用 JDBC 给 Java Agent 加会话记忆
java·spring boot·agent·spring ai·chat memory
凡人叶枫1 小时前
Effective C++ 条款40:明智而审慎地使用多重继承
java·数据库·c++·嵌入式开发·effective c++
SuperHeroWu71 小时前
【HarmonyOS 7】鸿蒙应用 AI Coding 工具链 DevEco Code 到 DevEco CLI
人工智能·华为·ai编程·harmonyos·cli·code
放弃 治疗1 小时前
宝塔面板安装 JDK 完整教程|Java 环境配置详解
java·开发语言
虾壳云官方1 小时前
openclaw 一键安装教程(2026年6月15最新)
运维·人工智能·windows·自动化·openclaw
不爱土豆唯爱马铃薯1 小时前
AiPy 是什么?
人工智能
deephub1 小时前
Flash-KMeans:快速且内存高效的精确 K-Means,可在单张 GPU 进行亿级数据的聚类
人工智能·机器学习·kmeans·聚类·rag