Skip to content

模型发送请求使用异步执行分析任务

java
@PostMapping(value = "/ai_ops", produces = "text/event-stream;charset=UTF-8")
public SseEmitter aiOps() {
    SseEmitter emitter = new SseEmitter(600000L); // 10分钟超时(告警分析可能较慢)
    executor.execute(() -> {
        try {
            // 调用 AiOpsService 执行分析流程
            Optional<OverAllState> overAllStateOptional = aiOpsService.executeAiOpsAnalysis(chatModel, toolCallbacks);
            OverAllState state = overAllStateOptional.get();
            logger.info("AI Ops 编排完成,开始提取最终报告...");

            // 提取最终报告
            Optional<String> finalReportOptional = aiOpsService.extractFinalReport(state);
            // 输出最终报告
            if (finalReportOptional.isPresent()) {
                // 发送
            }
        }
    });
    return emitter;
}

代码中的executor就是把agent请求的任务交给了另一个线程,让他去执行,避免了等待,提高了高并发情况下的稳定性

异步执行分析任务,为什么需要另一个线程?

简单来说,这是为了 “不让一个人(线程)累死,也不让他堵住后面的所有人”

想象一下餐厅的运作:

  • Tomcat 的请求处理线程 = 餐厅门口接待客人的 迎宾员
  • 耗时的 AI 分析任务 = 需要厨师花很长时间才能做好的 复杂菜肴

同步方式(错误示范)

迎宾员(Tomcat 线程)接到一个点餐(请求)后,自己跑到后厨,亲自盯着厨师做完这道菜(耗时 5 分钟),然后端着菜给客人,再回来迎接下一位客人。

问题

  1. 效率极低:迎宾员 5 分钟只能服务一位客人。
  2. 服务瘫痪:如果同时来了 10 位客人,就需要 10 位迎宾员,但餐厅可能总共就 5 位迎宾员。后来的客人只能排队等待,甚至被拒绝服务。这就是你说的 “拖垮服务器”

异步方式(代码的做法)

迎宾员(Tomcat 线程)接到点餐后,立刻把菜单交给一个专门负责传菜的 “跑单员”(自定义线程池 executor),然后马上回头去接待新的客人。

  • 跑单员 的任务很简单:拿着菜单,等着厨师(AI 分析任务)把菜做好,然后端给客人。
  • 厨师 在后厨慢慢做菜,完全不影响前台的迎宾工作。

好处

  1. 迎宾员解放:负责网络请求的 Tomcat 线程可以瞬间返回,继续处理成百上千的其他请求,并发能力大增。
  2. 任务有序执行:耗时的 AI 任务会被放到一个独立的、可控的 “跑道”(线程池)上排队执行,不会让服务器乱套。

一句话总结: 这是网络服务器处理耗时任务的 标准工业实践,目的是为了将 “管理请求的轻量工作”“处理业务的重量工作” 分离开,保证服务的整体稳定和高效

改进建议:如果想更优雅,可以改用 CompletableFuture + SseEmitter 的组合,让代码更健壮(超时控制、异常传递)。

Optional < >一个帮忙判断是否是空的包装盒

Optional<String> 是什么?

这是一个用于 “安全地告诉调用方:我可能有值,也可能没有” 的容器。

为什么会有 Optional

在 Java 中,一个方法如果可能返回 null,调用方很容易忘记检查,直接调用 null 对象的方法就会触发 NullPointerException(空指针异常),导致程序崩溃。Optional 就像一个 “漂亮的包装盒”,它明确地告诉你:“里面的东西可能存在,也可能是空的,你必须拆开检查后才能使用”

在这个场景中

  • extractFinalReport(state) 方法(从 AI 多 Agent 的执行结果中提取最终的告警报告)不能保证 100% 成功。如果内部流程出错,报告字段缺失等,就无法提取出报告。
  • 于是,这个方法返回一个 Optional<String>
    • 如果成功:盒子里装着报告内容(Optional.of("报告内容"))。
    • 如果失败:盒子里是空的(Optional.empty())。

isPresent()的作用

isPresent()ifPresent 方法的作用

它就是 “检查包装盒是否为空” 的官方方法。

  • 返回 true:说明盒子里有东西,可以安全地用 .get() 方法拿出来。
  • 返回 false:说明盒子是空的,不能拿,需要走其他逻辑(比如返回错误信息)。

你看到的这段代码

java
if (finalReportOptional.isPresent()) {
    // 发送
}

它的完整逻辑应该是:

java
if (finalReportOptional.isPresent()) {
    // 1. 用 .get() 方法安全地取出报告内容
    String report = finalReportOptional.get();
    // 2. 通过 SSE 发送给客户端
    emitter.send(report);
    // 3. 表示任务完成,可以关闭连接了
    emitter.complete();
} else {
    // 盒子是空的,说明提取报告失败
    emitter.send("系统内部错误:无法生成最终报告,请检查日志。");
    // 以异常形式完成连接
    emitter.completeWithError(new RuntimeException("No report extracted"));
}

更优雅的写法: 使用 ifPresent 方法,代码更简洁,也完全避免了 get() 可能带来的风险:

java
finalReportOptional.ifPresentOrElse(
    report -> { emitter.send(report); emitter.complete(); }, // 有值
    () -> { emitter.send("无法生成报告"); emitter.completeWithError(...); } // 无值
);

基于 VitePress 构建