AI Agent 开发实录 03:Java 程序员用 Spring AI Alibaba 1.1.2.0 从0到1手搓生产级 AI Agent —— 完结篇

0 阅读12分钟

AI Agent 开发实录 03:Java 程序员用 Spring AI Alibaba 1.1.2.0 从0到1手搓生产级 AI Agent —— 完结篇

本系列是跟着课程《Java转AI高薪领域必备——从0到1打通生产级AI Agent开发》的实现实录:业务场景与实现主线以课程为准,技术栈沿用本库已验证的组合——Spring AI Alibaba 1.1.2.0(Graph)+ tk-mybatis + MySQL 8.x,项目代号 double-ai-agent,共三期连载。本期为第 03 期:在前两期基础上接入人机协同审批——HumanApprovalNode 实现 Graph 执行中断、Redis 持久化检查点解决 Missing Checkpoint 问题、审批回调接口完成状态恢复与分支路由,最终打通「数据采集 → AI 预测 → 邮件通知 → 人工审批 → 审核通过/拒绝 → 创建调拨单/直接结束」的完整闭环。 前置阅读:本期依赖第 01 期的环境搭建与第 02 期的完整 Graph 编排,建议先看完前两期再动手。

前两期已完成环境搭建与全部节点串联——销售数据采集、历史调拨数据采集、AI 预测分析、JSON 提取、邮件通知、调拨单自动创建,整条链路已经跑通。但少了一个关键环节:人工审批。AI 给出的调拨建议不能直接入库,得有人看一眼,点个「采纳」或「拒绝」。本期就把这个缺口补上,把整个业务流程真正闭环。

这里有一个背景得先交代:Spring AI Alibaba 从 1.0.0.4 升到 1.1.2.0 之后,人机交互的 API 做了很大的改变。课程原版用的是旧版 API,我这边只能对着官方文档和源码摸索,踩了不少坑——Missing Checkpoint、数据丢失、拒绝分支不生效——这些踩坑过程都记在下面了。

完整 Graph QQ_1784946846880.png

一. 人机协同

1.1 HumanApprovalNode

Spring AI Alibaba 1.1.2.0 的人机交互机制跟旧版完全不同:旧版用的是 state.withHumanFeedback()graph.call(),新版换成了 InterruptableAction 接口 + InterruptionMetadata + graph.updateState() / graph.invoke() 这套组合。变化之大,我一开始也是对着官方 Demo 一行行改,改了好几版才跑通。

1.1.1 官方 Demo

先看官方给的 Demo,理解中断机制的核心思路:

/**
* 可中断的节点动作
* 实现 InterruptableAction 接口,可以在任意节点中断执行
*/
public static class InterruptableNodeAction implements AsyncNodeActionWithConfig, InterruptableAction {
   private final String nodeId;
   private final String message;

   public InterruptableNodeAction(String nodeId, String message) {
      this.nodeId = nodeId;
      this.message = message;
   }

   @Override
   public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
      // 正常节点逻辑:更新状态
      return CompletableFuture.completedFuture(Map.of("messages", message));
   }

   @Override
   public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
      // 检查是否需要中断
      // 如果状态中没有 human_feedback,则中断等待用户输入
      Optional<Object> humanFeedback = state.value("human_feedback");

      if (humanFeedback.isEmpty()) {
         // 返回 InterruptionMetadata 来中断执行
         InterruptionMetadata interruption = InterruptionMetadata.builder(nodeId, state)
               .addMetadata("message", "等待用户输入...")
               .addMetadata("node", nodeId)
               .build();

         return Optional.of(interruption);
      }

      // 如果已经有 human_feedback,继续执行
      return Optional.empty();
   }
}

核心逻辑就两步:

  1. interrupt() 方法在节点执行前被调用,检查状态中是否已有审批结果。没有 → 返回 InterruptionMetadata,Graph 暂停;有 → 返回 Optional.empty(),继续执行。
  2. apply() 方法是节点正常执行的业务逻辑,中断恢复后走到这里。

这个机制设计得挺巧妙的——它不是把审批做成一个单独的步骤,而是让任何节点都可以变成一个"中断点"。理解了这个思路,后面就好办了。

1.1.2 HumanApprovalNode 第一版

对着 Demo 改出第一版:

package vip.wayhua.ivy.ai.agent.nodes;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;

import com.alibaba.cloud.ai.graph.StateGraph;

import com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig;
import com.alibaba.cloud.ai.graph.action.InterruptableAction;
import com.alibaba.cloud.ai.graph.action.InterruptionMetadata;
import vip.wayhua.ivy.ai.agent.constant.Constant;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/***
 * 人工审批
 */
public class HumanApprovalNode implements AsyncNodeActionWithConfig, InterruptableAction {
    private final String nodeId;
    private final String message;

    public HumanApprovalNode() {
        this.nodeId = "human_approval_node";
        this.message = "等待用户确认";
    }


    @Override
    public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
        // 正常节点逻辑:更新状态
        return CompletableFuture.completedFuture(Map.of("messages", message));
    }

    @Override
    public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {

       // 检查是否需要中断
       // 如果状态中没有 human_feedback,则中断等待用户输入
        Optional<Object> humanFeedback = state.value(Constant.KeyName.APPROVAL);

        if (humanFeedback.isEmpty()) {
          // 返回 InterruptionMetadata 来中断执行
            InterruptionMetadata interruption = InterruptionMetadata.builder(nodeId, state)
                    .addMetadata("message", "等待用户输入...")
                    .addMetadata("node", nodeId)
                    .build();

            return Optional.of(interruption);
        }

        // 如果已经有 human_feedback,继续执行
        return Optional.empty();
    }
}
1.1.3 添加配置

把人工审批节点插到发送邮件和创建调拨单之间——邮件通知发出去之后,停下来等人点确认:

        stateGraph.addNode(Constant.NodeName.HUMAN_APPROVAL_NODE, new HumanApprovalNode());
       //...
        stateGraph.addEdge(Constant.NodeName.SEND_EMAIL_NODE, Constant.NodeName.HUMAN_APPROVAL_NODE);
        stateGraph.addEdge(Constant.NodeName.HUMAN_APPROVAL_NODE, Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE);
        stateGraph.addEdge(Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE, StateGraph.END);
1.1.4 Controller 调整

Controller 调用处也得改——需要传 threadId,不然没法区分是哪个会话的中断:

 @Operation(summary = "销售测试", description = "销售测试,输入productId")
    @GetMapping("/sale")
    public Map<String, Object> sale(@RequestParam("productId") String productId) {

        String threadId = Sid.next();
        RunnableConfig runnableConfig = RunnableConfig.builder().threadId(threadId).build();
        OverAllState overAllState = graph.invoke(
                Map.of(Constant.KeyName.PRODUCT_ID, productId,
                Constant.KeyName.THREAD_ID, threadId),runnableConfig
        ).get();
        Map<String, Object> data = overAllState.data();
        return data;
    }
1.1.5 测试——没反应

第一版跑起来,前面没什么反应,看不到中断效果。于是加上日志,同时把审批后的分支逻辑也补进去。

1.1.6 HumanApprovalNode 修改——加日志、加分支逻辑
package vip.wayhua.ivy.ai.agent.nodes;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;

import com.alibaba.cloud.ai.graph.StateGraph;

import com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig;
import com.alibaba.cloud.ai.graph.action.InterruptableAction;
import com.alibaba.cloud.ai.graph.action.InterruptionMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import vip.wayhua.ivy.ai.agent.constant.Constant;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/***
 * 人工审批
 */
public class HumanApprovalNode implements AsyncNodeActionWithConfig, InterruptableAction {
    private static final Logger log = LoggerFactory.getLogger(HumanApprovalNode.class);
    private final String nodeId;
    private final String message;

    public HumanApprovalNode() {
        this.nodeId = "human_approval_node";
        this.message = "等待用户确认";
    }


    @Override
    public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
        log.error("HumanApprovalNode->------------------");
        Optional<Boolean> humanFeedback = state.value(Constant.KeyName.APPROVAL);
        if (!humanFeedback.isEmpty()) {
            Boolean approval = humanFeedback.get();
            String nextStep = StateGraph.END;
            if (approval) {
                nextStep =Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE  ;
            }
            CompletableFuture.completedFuture(Map.of("messages", message,
                    Constant.KeyName.HUMAN_APPROVAL_NODE, nextStep
            ));
        }

        // 正常节点逻辑:更新状态
        return CompletableFuture.completedFuture(Map.of("messages", message));
    }

    @Override
    public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
        log.error("HumanApprovalNode-->interrupt   " + nodeId);
        // 检查是否需要中断
        // 如果状态中没有 human_feedback,则中断等待用户输入
        Optional<Object> humanFeedback = state.value(Constant.KeyName.APPROVAL);

        if (humanFeedback.isEmpty()) {
            // 返回 InterruptionMetadata 来中断执行
            log.error("已经中断了.....");
            InterruptionMetadata interruption = InterruptionMetadata.builder(nodeId, state)
                    .addMetadata("message", "等待用户输入...")
                    .addMetadata("node", nodeId)
                    .build();

            return Optional.of(interruption);
        }

        // 如果已经有 human_feedback,继续执行
        return Optional.empty();
    }
}

这里的思路是:中断恢复后进入 apply(),检查审批结果是采纳还是拒绝,把对应的下一步路由写到状态里。后面配合条件边,就能走到不同的分支。

1.1.7 测试——中断成功

QQ_1785136393205

已经中断,需要人工审批。日志里能看到"已经中断了.....",说明 interrupt() 方法被正确触发。

QQ_1785136433967.png

先保存 threadId 和 productId,后面审批回调要用:

threadId: "260727736843029540765696",
productId: "1",

二. 审批回调与状态恢复

中断是中断了,但还得有个接口让人点「采纳」或「拒绝」。这就是审批回调接口。

2.1 审批回调接口——第一版



    // 触发邮件回调的接口
    @Operation(summary = "触发邮件回调的接口", description = "触发邮件回调的接口")
    @GetMapping("/approval")
    public R approval(@RequestParam(value = "approval") Boolean approval,
                      @RequestParam(value = "threadId") String threadId) {
        RunnableConfig runnableConfig = RunnableConfig.builder().threadId(threadId).build();
        try {
            // 1. 根据threadId拿到中断时保存的状态快照
            StateSnapshot stateSnapshot = graph.getState(runnableConfig);
            OverAllState state = stateSnapshot.state();
            // 2. 【核心替换】替代旧 state.withHumanFeedback
            // 将审批结果写入状态中 KEY_APPROVAL = "approval"
            Map<String, Object> feedbackData = Map.of(Constant.KeyName.APPROVAL, approval);
            RunnableConfig resumeConfig = graph.updateState(runnableConfig, feedbackData, null);

            // 3. 【核心替换】替代 graph.call(),继续运行工作流
            OverAllState overAllState = graph.invoke(feedbackData,resumeConfig).get();

            return R.success(overAllState.data());
        } catch (Exception e) {
            return R.error(e.getMessage());
        }
    }

这个接口做三件事:

  1. 根据 threadId 拿到中断时的状态快照
  2. 把审批结果写入状态(旧版 withHumanFeedback 的替代方案)
  3. 恢复 Graph 执行(旧版 graph.call() 的替代方案)

2.2 测试——Missing Checkpoint!

前面保存的 threadId:

threadId: "260727736843029540765596",

QQ_1785137103076.png

Missing Checkpoint! 这个报错很明确——Graph 的中断检查点存在内存里,服务重启或者会话超时就没了。要解决这个问题,得把检查点持久化到外部存储。

三. Redis 检查点持久化:解决 Missing Checkpoint

参考官方文档:java2ai.com/docs/framew…

3.1 添加 Redisson

  <dependency>
            <groupId>vip.wayhua.ivy.ai</groupId>
            <artifactId>ivy-starter-redission</artifactId>
            <version>1.0.1</version>
        </dependency>

这个 Redisson 前面在 DocSolo 里已经配好了,直接复用。

3.2 Redis Stack Docker 部署

Redis 不仅要跑起来,还需要 Redis Stack 版本(带 RedisJSON、RediSearch 等模块),因为 Graph 的检查点存储用到了这些能力:

version: '3'

services:
  redis-stack-server:
    image: redis/redis-stack-server:latest
    container_name: redis-stack-server
    restart: always
    ports:
      - "6379:6379"
    environment:
      # 设置访问密码,取消注释启用
      REDIS_ARGS: "--requirepass wayhua"
    volumes:
      - ./redis-data:/data

QQ_1785138243159.png

3.3 添加持久化配置

在 Graph 编译时注册 RedisSaver:

  SaverConfig saverConfig=SaverConfig.builder()
                .register( new RedisSaver
                        .Builder()
                        .redisson(redissonClient)
                        .build())
                .build();

        CompileConfig compileConfig=CompileConfig.builder()
                .saverConfig(saverConfig)
                .build();


        CompiledGraph compile = stateGraph.compile(compileConfig);

3.4 测试——又报错了

QQ_1785139081685

报错了。排查之后发现是重启项目导致之前的数据丢失,拿不到之前的状态。这也从侧面印证了:旧数据在内存中,重启就没了。Redis 持久化只有对新产生的会话才生效。

3.5 添加审批按钮链接

邮件里得带上审批链接,让人点了就能批:

   Constant.KeyName.ADOPT_LINK, "http://localhost:8900/saleProduct/approval?approval=true&threadId="+threadId,
   Constant.KeyName.REJECT_LINK, "http://localhost:8900/saleProduct/approval?approval=false&threadId="+threadId));

3.6 approval 新版的数据丢失问题

一直会丢失一些数据,查了好久,才算解决。

问题出在 graph.updateState() 之后直接调 graph.invoke(feedbackData, resumeConfig),这时候状态还没完全同步。解决方法是 updateState 之后再重新获取一次 OverAllState,用最新的状态去 invoke:

 // 触发邮件回调的接口
    @Operation(summary = "触发邮件回调的接口", description = "触发邮件回调的接口")
    @GetMapping("/approval")
    public R approval(@RequestParam(value = "approval") Boolean approval,
                      @RequestParam(value = "threadId") String threadId) {
        RunnableConfig runnableConfig = RunnableConfig.builder()
                .threadId(threadId)
                .build();
        try {
            // 1. 根据threadId拿到中断时保存的状态快照
            StateSnapshot stateSnapshot = graph.getState(runnableConfig);
            OverAllState state = stateSnapshot.state();
            log.error("-->"+ JsonUtils.toJson(state.data()));
            // 2. 【核心替换】替代旧 state.withHumanFeedback
            // 将审批结果写入状态中 KEY_APPROVAL = "approval"
            Map<String, Object> feedbackData = Map.of(Constant.KeyName.APPROVAL, approval);
            RunnableConfig resumeConfig = graph.updateState(runnableConfig, feedbackData, null);
            StateSnapshot latestSnapshot  = graph.getState(resumeConfig);
            // 3. 【核心替换】替代 graph.call(),继续运行工作流
//            OverAllState overAllState = graph.invoke(feedbackData, resumeConfig).get();
            OverAllState resumeState  = latestSnapshot.state();
            Map<String, Object> data = resumeState.data();

            log.error("-->"+ JsonUtils.toJson(data));
            OverAllState overAllState = graph.invoke(resumeState, resumeConfig).get();
            return R.success(overAllState.data());
        } catch (Exception e) {
            return R.error(e.getMessage());
        }
    }

关键点:updateState 之后调 getState 重新获取 OverAllState,再调 invoke。不这么做的话,invoke 拿到的状态可能是旧的,之前写入的审批结果就丢了。这个问题折腾了我好一阵子。

3.7 发送测试

QQ_1785143520308.png 收到的邮件里带了审批链接。

在页面调用接口:

QQ_1785143557809.png 点击采纳,数据库中成功生成了调拨单:

QQ_1785143331789.png 拒绝测试——出问题了:

点了拒绝,数据库里还是生成了一条调拨单。

QQ_1785143790020.png 这说明什么?说明拒绝之后,Graph 还是走到了 CREATE_INVENTORY_TRANSFER_NODE。边是写死的,不管审批结果如何都会往下走。

四. 条件分支:拒绝退出

原因在这:

       stateGraph.addEdge(Constant.NodeName.HUMAN_APPROVAL_NODE, Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE);
        stateGraph.addEdge(Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE, StateGraph.END);

这是固定边——不管审批结果是什么,一定走到创建调拨单。正确的做法是把这条固定边换成条件边:采纳 → 创建调拨单,拒绝 → 结束。

4.1 ApprovalEdge

新增条件边,根据状态中的路由标记决定下一步:

package vip.wayhua.ivy.ai.agent.edge;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.action.EdgeAction;
import vip.wayhua.ivy.ai.agent.constant.Constant;

public class ApprovalEdge implements EdgeAction {
    @Override
    public String apply(OverAllState state) throws Exception {
        String humanApprovalNextStep = state.value(Constant.KeyName .HUMAN_APPROVAL_NODE, "");
        if (Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE.equals(humanApprovalNextStep)){
            return Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE;
        }
        return StateGraph.END;
    }
}

4.2 配置条件边

 stateGraph.addConditionalEdges(Constant.NodeName.HUMAN_APPROVAL_NODE,
             AsyncEdgeAction.edge_async(new ApprovalEdge()),
             Map.of(Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE,Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE,
                     StateGraph.END ,StateGraph.END)
             );

        stateGraph.addEdge(Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE, StateGraph.END);

同时 HumanApprovalNode 的 apply() 方法也得把路由数据写进状态——前面写了判断逻辑,但要确保返回的 Map 带了 HUMAN_APPROVAL_NODE 的 key。

说到这,我仔细查了一遍之前的代码,发现一个低级错误:

package vip.wayhua.ivy.ai.agent.nodes;

import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;

import com.alibaba.cloud.ai.graph.StateGraph;

import com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig;
import com.alibaba.cloud.ai.graph.action.InterruptableAction;
import com.alibaba.cloud.ai.graph.action.InterruptionMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import vip.wayhua.ivy.ai.agent.constant.Constant;
import vip.wayhua.ivy.ai.core.utils.JsonUtils;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/***
 * 人工审批
 */
public class HumanApprovalNode implements AsyncNodeActionWithConfig, InterruptableAction {
    private static final Logger log = LoggerFactory.getLogger(HumanApprovalNode.class);
    private final String nodeId;
    private final String message;

    public HumanApprovalNode() {
        this.nodeId = "human_approval_node";
        this.message = "等待用户确认";
    }


    @Override
    public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
        log.error("HumanApprovalNode->------------------");
        Map<String, Object> data = state.data();

        log.error("--------------> json-->\n"+ JsonUtils.toJson(data));
        Optional<Boolean> humanFeedback = state.value(Constant.KeyName.APPROVAL);
        if (!humanFeedback.isEmpty()) {
            Boolean approval = humanFeedback.get();
            String nextStep = StateGraph.END;
            if (approval) {
                nextStep = Constant.NodeName.CREATE_INVENTORY_TRANSFER_NODE;
            }

          return   CompletableFuture.completedFuture(Map.of("messages", message,
                    Constant.KeyName.HUMAN_APPROVAL_NODE, nextStep
            ));
        }

        // 正常节点逻辑:更新状态
        return CompletableFuture.completedFuture(Map.of("messages", message));
    }

    @Override
    public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
        log.error("HumanApprovalNode-->interrupt   " + nodeId);
        // 检查是否需要中断
        // 如果状态中没有 human_feedback,则中断等待用户输入
        Optional<Object> humanFeedback = state.value(Constant.KeyName.APPROVAL);

        if (humanFeedback.isEmpty()) {
            // 返回 InterruptionMetadata 来中断执行
            log.error("已经中断了.....");
            InterruptionMetadata interruption = InterruptionMetadata.builder(nodeId, state)
                    .addMetadata("message", "等待用户输入...")
                    .addMetadata("node", nodeId)
                    .build();

            return Optional.of(interruption);
        }

        // 如果已经有 human_feedback,继续执行
        return Optional.empty();
    }
}

原来问题是少了一个 return,哎。

 return   CompletableFuture.completedFuture(Map.of("messages", message,
                    Constant.KeyName.HUMAN_APPROVAL_NODE, nextStep
            ));

之前审批分支逻辑里的 CompletableFuture.completedFuture(...) 前面没有 return,这行代码执行了但返回值被丢弃了,方法继续往下走到最后的 return,返回的 Map 里没有 HUMAN_APPROVAL_NODE 这个 key。所以 ApprovalEdge 读到的永远是空字符串,永远走 StateGraph.END,拒绝能结束但采纳也创建不了调拨单

这种 bug 是最难查的——代码不报错、编译不报错、逻辑看起来也对,就是行为不对。最后还是加了日志把每一步的返回值打印出来才定位到。写 Java 这么多年,还是会在这种地方翻车。

4.3 测试——终于对了

QQ_1785156130447.png

QQ_1785156066998.png 采纳后正常生成调拨单。 QQ_1785156176064.png

QQ_1785156153595.png 拒绝后不再生成调拨单,流程在审批节点后直接结束。

五. 小结

到此,整个流程的控制基本上完成了。回顾这一期,踩了几个实打实的坑:

1. Missing Checkpoint。 Spring AI Alibaba 1.1.2.0 的 Graph 默认把检查点放在内存里,服务一重启就没了。解决办法是用 RedisSaver 持久化到 Redis Stack,这样即使服务重启,中断的会话也能恢复。这个设计在官方文档里有提,但第一次接触很容易忽略。

2. updateState 后数据丢失。 审批回调里调了 graph.updateState() 之后直接 graph.invoke(feedbackData, ...),状态没完全同步。正确的做法是 updateState 之后重新 getState() 拿到最新状态,再 invoke。这个坑是我自己排查了好久才发现的,官方 Demo 里没有这种场景。

3. 拒绝分支不生效。 根本原因是条件边需要从状态中读取路由标记,但 HumanApprovalNode 的 apply() 方法少了一个 return,导致路由标记根本没写进状态。加上条件边 ApprovalEdge 配合修正后的返回值,采纳走创建调拨单,拒绝直接结束,分支逻辑才真正生效。

4. 阻塞调用问题。 目前 Controller 里 graph.invoke(...).get() 是同步阻塞的,如果整个链路比较长,接口容易超时。这个以后可以改成异步或者用 SSE 推送进度,但不在本期范围内。

这一篇的内容和课程《Java转AI高薪领域必备-从0到1打通生产级AI Agent开发》有很大区别——课程用的是 Spring AI Alibaba 1.0.0.4,我这边升级到了 1.1.2.0,人机交互 API 完全变了。好在有官方文档 java2ai.com/ 可以参考,算不上从零摸索,但确实花了不少时间读源码和做实验。

后面的微服务化、接入 Kafka 等,都是数据业务层面的——增加订单时根据告警规则触发仓库调拨,这些是 Spring Cloud 相关的内容,不属于 AI Agent 的范畴了,就不在这过多展开了。

这一系列到此结束,后面是 BI 相关的内容。

代码

gitee.com/wavaya88/Do… 3.all 分支