AgentScope-Java 入门:用 Middleware 审计 Agent 调用

AgentScope-Java 入门:用 Middleware 审计 Agent 调用

前面几篇,我们已经把 Review Copilot 做成了一个可运行的本地 Git diff 评审助手:

  • 第 1 篇搭好 Spring Boot + Vue 项目骨架。
  • 第 2 篇用 SSE 展示评审进度。
  • 第 3 篇读取 Git diff、执行规则检查,并加入最小 Agent diff 摘要。
  • 第 4 篇保存任务状态和 Markdown 报告。
  • 第 5 篇收紧只读权限边界。

这一篇回到 AgentScope-Java 的另一个核心能力:Middleware。

目标很明确:

让我们能看清 Agent 调用了什么、用了多久、是否失败,以及整个评审流水线最终生成了多少 findings。

第 6 篇不再把所有模型 provider、prompt 解析和前端发布细节都塞进来。模型调用链路只讲必要背景,重点放在审计。

本篇对应代码

本篇对应分支:

bash 复制代码
git checkout chapter/06-audit-middleware

完成后合并回 main,并打 tag:

bash 复制代码
git tag chapter-06-complete

项目仓库:

text 复制代码
https://github.com/ynzz-j/agentscope-review-copilot

为什么需要审计

Agent 应用和普通 CRUD 应用不一样。

普通接口出问题时,我们通常看:

  • HTTP 状态码。
  • 入参。
  • SQL。
  • 业务日志。

Agent 应用还要额外关心:

  • 模型是否真的被调用。
  • 调用了几次模型。
  • 每次调用有多少消息。
  • 是否带了工具定义。
  • 调用耗时多久。
  • 模型调用失败时错误是什么。
  • 最终生成了多少结构化发现项。

否则很容易出现一种假象:

页面有结果,但结果其实全来自规则检查,模型根本没有参与。

所以第 6 篇要补上可观测性。

当前评审链路

配置模型 provider 后,Review Copilot 里会有两类 Agent 调用。

第一类是第 3 篇加入的最小 Agent 摘要:

text 复制代码
Git diff
  -> DiffSummaryService
  -> ReActAgent.call()
  -> DIFF_SUMMARIZED 事件

第二类是完整模型评审:

text 复制代码
Git diff + 文件上下文 + 规则 findings
  -> ModelReviewService
  -> ReActAgent.call()
  -> 解析模型 JSON
  -> 合并 ReviewFinding

未配置 provider 时,这两类模型调用都会跳过,确定性规则检查仍然执行。

这条约束不变:

不指定默认模型提供商。DashScope、OpenAI、Anthropic、Gemini、Ollama 或其他 RC4 支持方式,都在实际项目配置时显式决定。

ModelReviewService 的核心链路

第二类 Agent 调用是完整模型评审。它的核心做了三件事:构建评审 prompt、调用 ReActAgent.call()、解析 JSON 输出。

关键代码:

java 复制代码
// ModelReviewService.java
public List review(
        ReviewJob job,
        GitDiffTool.GitDiffResult diff,
        Map contexts,
        List ruleFindings) {

    String prompt = buildReviewPrompt(diff, contexts, ruleFindings, job.focusCategories());

    RuntimeContext runtimeContext = RuntimeContext.builder()
            .sessionId(job.sessionId())
            .build();

    try (ReActAgent agent = agentFactory.createModelOnlyReviewer(job.sessionId())) {
        Msg response = agent.call(prompt, runtimeContext).block(MODEL_CALL_TIMEOUT);
        if (response == null) {
            return List.of();
        }
        return parseFindings(response.getTextContent());
    }
}

Prompt 里约束模型只输出 JSON 数组:

java 复制代码
private String buildReviewPrompt(
        GitDiffTool.GitDiffResult diff,
        Map contexts,
        List ruleFindings,
        List focusCategories) {

    return """
            你是代码评审专家。基于以下输入生成结构化发现项。

            ## Git Diff
            %s

            ## 规则检查已发现的问题(供参考,不要重复)
            %s

            ## 重点关注类别
            %s

            请只输出 JSON 数组,不要输出其他内容。
            每个元素包含:severity, category, file, line, evidence, impact, suggestion, confidence
            """.formatted(
            diff.diffText(),
            formatRuleFindings(ruleFindings),
            formatCategories(focusCategories));
}

模型输出不一定完全合规,所以解析需要容错:

java 复制代码
private List parseFindings(String modelOutput) {
    // 1. 提取第一个 [ 到最后一个 ] 之间的内容
    String json = extractJsonArray(modelOutput);
    if (json == null) {
        return List.of();
    }
    try {
        // 2. 反序列化为 ReviewFinding 列表
        return objectMapper.readValue(json, new TypeReference>() {});
    } catch (Exception e) {
        // 3. 解析失败不中断流程,规则 findings 仍然可用
        return List.of();
    }
}

这里有两个设计决策值得注意:

  • prompt 里带上规则 findings 作为参考:让模型知道哪些问题已经被规则发现,避免重复输出。
  • 解析失败时返回空列表而非抛异常:模型评审是规则检查的补充,不应该因为模型输出不合规而中断整个评审。

完整的 ModelReviewService 实现(包括 formatRuleFindingsextractJsonArray 等辅助方法)可以在仓库 chapter/06-audit-middleware 分支中查看。

AuditRecord:先定义审计数据

先定义一条审计记录应该包含什么:

java 复制代码
public record AuditRecord(
        String type,
        String subject,
        Status status,
        int messageCount,
        int toolCount,
        int findingCount,
        long elapsedMs,
        String error,
        Instant timestamp) {

    public enum Status {
        SUCCESS,
        FAILED
    }
}

字段含义:

字段 含义
type 审计类型,例如 agent_model_callreview_pipeline
subject 审计对象,例如 agent 名称或 review job id
status 成功或失败
messageCount 模型调用消息数量
toolCount 模型调用可见工具数量
findingCount 评审流水线生成的 finding 数量
elapsedMs 耗时
error 失败时的错误信息
timestamp 审计时间

这里故意把模型调用审计和评审流水线审计放进同一个结构里。

好处是后续可以统一写入日志、数据库或可观测平台。

ReviewAuditSink:先做可替换出口

不要把审计逻辑写死在 Middleware 里。

先定义接口:

java 复制代码
public interface ReviewAuditSink {

    void record(AuditRecord record);
}

当前实现可以只是写日志:

java 复制代码
@Component
public class LoggingReviewAuditSink implements ReviewAuditSink {

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

    @Override
    public void record(AuditRecord record) {
        if (record.status() == AuditRecord.Status.SUCCESS) {
            log.info(
                    "{} subject={} messages={} tools={} findings={} elapsedMs={}",
                    record.type(),
                    record.subject(),
                    record.messageCount(),
                    record.toolCount(),
                    record.findingCount(),
                    record.elapsedMs());
            return;
        }
        log.warn(
                "{} subject={} messages={} tools={} findings={} elapsedMs={} error={}",
                record.type(),
                record.subject(),
                record.messageCount(),
                record.toolCount(),
                record.findingCount(),
                record.elapsedMs(),
                record.error());
    }
}

后续如果要落数据库,只需要换 ReviewAuditSink 的实现。

AuditingMiddleware:审计模型调用

AgentScope-Java 的 Middleware 可以包住模型调用。

本项目实现 onModelCall

java 复制代码
@Component
public class AuditingMiddleware implements MiddlewareBase {

    private final ReviewAuditSink auditSink;

    @Override
    public Flux onModelCall(
            Agent agent,
            RuntimeContext ctx,
            ModelCallInput input,
            Function> next) {
        long started = System.nanoTime();
        int messageCount = input.messages() == null ? 0 : input.messages().size();
        int toolCount = input.tools() == null ? 0 : input.tools().size();
        return next.apply(input)
                .doOnComplete(() -> auditSink.record(AuditRecord.modelCall(
                        agent.getName(),
                        AuditRecord.Status.SUCCESS,
                        messageCount,
                        toolCount,
                        (System.nanoTime() - started) / 1_000_000,
                        null)))
                .doOnError(error -> auditSink.record(AuditRecord.modelCall(
                        agent.getName(),
                        AuditRecord.Status.FAILED,
                        messageCount,
                        toolCount,
                        (System.nanoTime() - started) / 1_000_000,
                        error.toString())));
    }
}

这段代码不关心业务逻辑。

它只回答一个问题:

AgentScope 的模型调用有没有发生,结果如何。

如果配置了 provider,但日志里没有 agent_model_call,就说明模型没有走到 AgentScope 调用链路。

ReviewService:审计整条评审流水线

Middleware 能审计模型调用,但它不知道最终生成了多少 findings。

这个信息属于业务流水线,所以放在 ReviewService 里记录。

成功时:

java 复制代码
auditSink.record(AuditRecord.reviewPipeline(
        job.id(),
        AuditRecord.Status.SUCCESS,
        findingCount,
        (System.nanoTime() - started) / 1_000_000,
        null));

失败时:

java 复制代码
auditSink.record(AuditRecord.reviewPipeline(
        failed.id(),
        AuditRecord.Status.FAILED,
        findingCount,
        (System.nanoTime() - started) / 1_000_000,
        e.toString()));

这样我们能同时看到两层信息:

审计类型 记录什么
agent_model_call AgentScope 模型调用是否成功、消息数、工具数、耗时
review_pipeline 评审任务是否成功、生成 findings 数量、总耗时、错误

这比只看最终页面结果更可靠。

和 Spring AI Advisor 的区别

如果你熟悉 Spring AI,可以把 AgentScope-Java Middleware 类比为 Spring AI Advisor。

但两者的关注点不同。

Spring AI Advisor 更贴近 ChatClient 调用链:

  • prompt 增强。
  • 上下文注入。
  • 日志记录。
  • RAG 检索。
  • 调用前后处理。

AgentScope-Java Middleware 更贴近 Agent 执行过程:

  • model call。
  • tool call。
  • runtime context。
  • agent event。
  • 多轮执行链路。

Review Copilot 里我们用 Middleware 做模型调用审计,用业务 service 做流水线审计,两者分工清楚。

验证测试

后端测试:

powershell 复制代码
cd D:\workspace\whd\ynzz\articles\agentscope-review-copilot\backend
mvn test

本篇重点看两个测试:

  • DiffSummaryServiceTest:确认 diff 摘要通过 ReActAgent.call() 执行,并携带 sessionId
  • AuditingMiddlewareTest:确认模型调用成功、失败都会生成审计记录,并覆盖 findingCount

测试通过后,可以再跑完整验证脚本:

powershell 复制代码
cd D:\workspace\whd\ynzz\articles\agentscope-review-copilot
.\scripts\verify.ps1 -SkipFrontendInstall

运行时如何观察

启动后端:

powershell 复制代码
cd D:\workspace\whd\ynzz\articles\agentscope-review-copilot\backend
mvn spring-boot:run

配置模型 provider 后发起评审。

日志里应该能看到类似记录:

text 复制代码
agent_model_call subject=AgentScope Review Copilot messages=... tools=0 findings=0 elapsedMs=...
review_pipeline subject=... messages=0 tools=0 findings=... elapsedMs=...

如果没有配置 provider,模型调用审计不会出现,但 review_pipeline 仍然会记录规则检查结果。

常见问题

为什么 findingCount 不放在 Middleware 里

因为 Middleware 只知道模型调用输入和输出过程,不知道业务上最后合并了多少 findings。

Review Copilot 的 findings 来自两部分:

  • 规则检查。
  • 模型评审。

合并逻辑在 ReviewService,所以 finding 数量应该在业务流水线层记录。

为什么 toolCount 现在经常是 0

模型评审阶段使用的是 createModelOnlyReviewer,没有把 Toolkit 交给模型。

这是刻意设计。

本项目先由服务端确定性读取 diff 和源码上下文,再把受控输入交给模型。这样能避免模型绕过权限边界自主读取文件。

工具本身仍然保留 @Tool 元信息,后续要扩展 Agent 自主工具调用时可以继续使用。

为什么第 6 篇不展开所有 provider 配置

provider 配置不是第 6 篇的重点。

这一篇只需要知道:

  • 不指定默认 provider。
  • 显式配置 provider 后才调用模型。
  • 模型调用会经过 Middleware 审计。

具体 provider 细节放在 README 和项目配置里即可。

本篇小结

这一篇完成了:

  • ModelReviewService 核心链路(prompt 构建 + JSON 解析容错)。
  • AuditRecord
  • ReviewAuditSink
  • LoggingReviewAuditSink
  • AuditingMiddleware
  • 模型调用成功/失败审计。
  • 评审流水线成功/失败审计。
  • finding 数量和耗时记录。
  • diff 摘要 Agent 调用的测试覆盖。

到这里,Review Copilot 不只是能跑,还能回答"Agent 到底有没有工作、工作结果如何"。

下一篇,我们完成前端体验、中文 README、GitHub 发布和整个系列的扩展路线。

系列导航

篇目 标题 状态
1 AgentScope-Java 入门:搭建 Review Copilot 项目骨架 已发布
2 AgentScope-Java 入门:用 SSE 展示评审任务进度 已发布
3 AgentScope-Java 入门:让评审助手读懂 Git diff 已发布
4 AgentScope-Java 入门:保存评审状态并生成 Markdown 报告 已发布
5 AgentScope-Java 入门:给代码评审助手加上只读安全边界 已发布
6 AgentScope-Java 入门:用 Middleware 审计 Agent 调用 本文
7 AgentScope-Java 入门:完善 Vue 前端、发布 GitHub,并规划下一步 待发布

作者:亦暖筑序

系列仓库:https://github.com/ynzz-j/agentscope-review-copilot

AgentScope-Java 版本:2.0.0-RC4

相关推荐
你驴我2 小时前
WhatsApp 消息撤回与编辑的幂等性设计实践
java·服务器·前端·后端·python
子昕2 小时前
Opus 5只要Fable一半价格,我准备把Claude续上了
ai编程
青山木2 小时前
Hot 100 ---腐烂的橘子
java·数据结构·后端·算法·leetcode·广度优先
aqi002 小时前
15天学会AI应用开发(十六)LangChain实现对话记忆功能
人工智能·python·大模型·ai编程·ai应用
小龙报2 小时前
【优选算法】1. 水果成蓝 2.找到字符串中所有字母的异位词
java·c语言·数据结构·数据库·c++·redis·算法
自强的小白3 小时前
重点!起始时间和结束时间与创建时间和修改时间的区别(localdate与LocaldateTIME区别)以及增删改查什么时候设置目前时间(添加和更新员工时)
java
雪的季节3 小时前
Python 线程同步与异步编程 全解析
java·开发语言
吃饱了得干活3 小时前
缓存与数据库一致性:从理论到实战
java·后端·面试