☰
Spring AI 接入实战:JDK17+Spring Boot3.5与多轮对话
2026/9/30 7:57:23 网站建设 项目流程

把 Spring AI 接入到项目里,说难不难,说简单也不简单。它的门槛不在概念上,而在版本组合和实际环境适配:JDK 17 要不要升、Spring Boot 3.5 能不能配、模型的依赖坐标用哪个、多轮对话为什么每次都像“失忆”。这些问题我在跑通之前全踩过。这篇文章就沿着一条可复现的链路——JDK 17 + Spring Boot 3.5 + Spring AI 1.0,带你从环境搭建、第一个 ChatClient 调用,到真正能记住上下文的多轮对话,过程里踩过的坑也会一并写出来。适合刚接触 Spring AI 的后端同学,也适合想评估 AI 能力能不能塞进现有 Spring 服务的人。

1. 版本组合为什么这样选

1.1 先别追新:JDK 17 是 Spring Boot 3.5 的稳妥底线

Spring Boot 3.5 官方要求 Java 17 作为最低版本,但也能跑在 21、23 上。为什么推荐 17?首先它是 LTS,很多企业生产环境用了好几年,中间件、APM 探针、反射库大多已经在这个版本上打磨过。其次,Spring Boot 3.5 的新特性在 17 上都能完整使用,不需要为了升级而升级。有人觉得“JDK 降到 17”是倒退,其实不是。同一条代码在 17 和 21 上跑,差异主要在语言特性和 GC 选择,Spring 应用更怕的是字节码版本、模块访问、反射限制这些隐性差异,17 反而是风险最小的落点。如果你电脑上已经装了 21 或者更高版本,也别急着卸载,多 JDK 共存是常态,后面我会讲怎么切。

还有一个很现实的原因:很多公司的发布服务器、容器基础镜像仍然以 JDK 17 为主线,尤其是那堆老旧的中间件客户端,在 21 上偶尔会出现奇怪的module-reader报错。你在本地用 21 写代码没问题,但打包丢到服务器上编译不过或者启动失败,那才是真正的拖进度。与其被迫“降级”,不如一开始就用 17 这个基线。Spring AI 本身对 Java 版本的要求也不激进,1.0 官方支持列表里 Java 17 就是标准配置。

1.2 Spring AI 版本别跟着感觉走

Spring AI 在 2025 年 5 月发布了 1.0.0 GA,这是第一个可以放心上生产的稳定版本。很多老教程还在用 0.8.x、0.10.x 快照,API 差异很大,比如ChatClient的构建方式、Prompt的构造参数都有过变动。我建议统一使用 Spring AI 1.0.0 或当前最新 1.x,配合 Spring Boot 3.5,官方兼容性说明里也明确支持这个组合。选版本时不要只看 Maven 仓库里的最新版本,还要注意 starter 的模块坐标。

这里有个容易搞混的地方:Spring AI 的模块是按“模型提供方”拆的,不是按功能拆。本地模型用spring-ai-starter-model-ollama,在线兼容接口用spring-ai-starter-model-openai,国内云厂商的兼容接入也大多走 OpenAI 协议。如果只是做对话,不需要引入 RAG、向量库那一堆依赖。我开始犯过的错就是把多个 starter 都塞进 pom,结果自动配置互相覆盖,日志里全是 bean 冲突。所以第一原则是:用哪个模型,就只引哪个 starter,BOM 锁版本,其他模块等真正用到再加。

2. 环境搭建:从 JDK 到可运行工程

2.1 JDK 17 安装与多版本切换

如果你已经不是第一次装 JDK,可以直接跳到切换那步。Windows 上,我一般下载 ZIP 包而不是安装器,解压到D:\jdk-17后手动设置JAVA_HOME和 Path,这样卸载也干净。macOS 上更简单,系统自带/usr/libexec/java_home -V可以列出所有 JDK,在 shell 配置里写一行export JAVA_HOME=$(/usr/libexec/java_home -v 17)就能临时切到 17。Linux 上用 sdkman 是最舒服的,一条sdk install java 17.0.11-tem就搞定。

真正麻烦的是你本地已经有 21,而某个老项目必须要 17。我的做法是在 IDEA 里给每个工程单独指定 Project SDK,而不是动全局JAVA_HOME。Maven 那边也要同步改:mvn -version显示的是命令行的JAVA_HOME指向,IDEA 的 Maven Runner 里 JRE 设置很容易被忽略。你会在编译时报“不支持发行版本 21”或者在运行时报“无效的目标发行号”,八成就是编译用的 JDK 和 IDEA SDK 没对上。排查口诀一句话:先看mvn -version,再看 IDEA 的 Project Structure,最后看 Maven Runner 的 JRE 路径,三处一致才是真的切干净。

2.2 用 Spring Initializr 快速生成工程

打开 start.spring.io 或者 IDEA 自带的 New Project,选择 Spring Boot 3.5.x,Java 版本选 17,左侧依赖先勾Spring Web。Spring AI 1.0 发布之后,Initializr 上已经可以勾选Spring AI相关依赖,但要注意它生成的版本号可能不是你想要的。我更习惯手动加依赖,这样心里有数。生成完工程后,第一件事不是写代码,而是确认 pom 里的<java.version>是不是 17,<spring-boot.version>是不是 3.5.x。这两个值对了,后面基本不会在版本问题上翻车。

如果你是用命令行创建,可以参考这个最简结构:

curl https://start.spring.io/starter.zip \ -d bootVersion=3.5.0 \ -d javaVersion=17 \ -d dependencies=web \ -o demo.zip

当然生产项目还是用 IDEA 向导更直观。这个工程不需要立刻引入 Spring AI,先把 Web 依赖跑起来,http://localhost:8080/actuator/health能通,再往下走,排错范围会小很多。

2.3 Maven 依赖引入:用 BOM 锁版本

Spring AI 的模块版本需要和 Spring Boot 版本匹配,纯靠手写版本号很容易记混。官方给了 BOM,你只需要在dependencyManagement里引一次,后面所有spring-ai-starter-*都不用写版本号。下面是 pom 里的关键片段:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> </dependencies>

这里我特意用 Ollama 来演示,原因后面会讲。BOM 的好处是:以后 Spring AI 升级补丁版本,你只需要改一行spring-ai-bom的版本号,所有模块自动对齐。有些人喜欢手动一个个加1.0.0,一旦某个模块没有及时跟着升级,就会出现某个类找不到的诡异报错。我自己是吃过一次亏之后,才老老实实把 BOM 用起来。

3. 让模型跑起来:ChatClient 第一次对话

3.1 模型提供方怎么选:本地 Ollama 还是在线服务

选模型提供方,本质是在解决“你希望数据走到哪”和“你希望调试多方便”这两个问题。本地 Ollama 的优势非常明显:不需要外网、不会有鉴权超时、模型文件在本地,想换模型就一条命令。开发阶段用它跑通链路,速度最快。在线兼容接口的优势在模型能力和规模化部署,但你需要处理网络、密钥、配额和账单。我的建议很直接:第一轮先把 Ollama 跑起来,链路通了之后再切换到云厂商的 OpenAI 兼容接口。这样你排查问题时面对的是本地环境,变量少很多。

Ollama 安装好之后,记得先把模型拉下来,比如ollama pull qwen2.5:7b。很多人忽略这一步,启动项目后调用接口报“model not found”,然后以为是 Spring AI 配置错了,其实模型压根没下载。拉模型的时间跟网络环境关系很大,7b 模型一般几个 GB,建议挂机等一会儿。我一般用ollama list确认模型已经 ready,再去写 Spring Boot 代码。

3.2 最小配置与第一个调用

配置文件非常短,核心就三个字段:base-url、model、temperature。base-url 默认是http://localhost:11434,如果你改过 Ollama 端口再调这里。

spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b temperature: 0.7

然后写一个 Controller。Spring AI 1.0 里,ChatClient是推荐的门面,你可以直接注入ChatClient.Builder来构建:

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt("请用简洁的话回答:" + message) .call() .content(); } }

启动项目后访问/chat?message=你好,如果配置对,能在几秒内看到回复。这里的temperature控制随机性,0.7 是通用值,别在调试时把它调太高,否则同一个问题可能每次都返回不同的答案,反而不好定位问题。ChatClient的链式 API 很顺手,prompt构造提示词,call同步调用,content拿纯文本结果,比直接操作ChatModel简单太多了。

3.3 ChatClient 与 ChatModel 到底谁在干活

ChatModel是模型接口的底层封装,负责跟 Ollama 或在线 API 通信、处理参数序列化、超时重试。ChatClient是在它之上的一层门面,提供链式编程体验,内部帮你组装Prompt、调用合适的模型、把ChatResponse拆成你需要的结果。大多数业务代码应该依赖ChatClient,而不是直接碰ChatModel。

打个比方:ChatModel是发动机,ChatClient是方向盘和后视镜。你要开车去某个地方,不需要天天钻到引擎盖里看气缸怎么点火,你只需要知道方向盘怎么打、仪表盘怎么读。Spring AI 给你提供的 Advisor 机制也挂在ChatClient这层,多轮对话、日志记录、RAG 增强,都是在这层做拦截和装饰。理解这个分层,后面看文档就会轻松很多。

4. 多轮对话:会话记忆实现

4.1 为什么“多轮”不是把多句话连续发出去

很多人以为多轮对话就是用户发一轮、程序回一轮,再发一轮、再回一轮,模型自然就记住了。实际上模型是无状态的,每次调用接口时,你给它的 messages 列表决定了它“记得”什么。聊第二句时如果没有把第一句拼进去,在模型看来你就是一个新用户。

我记得第一次实验时,用户问“我叫张三”,我回复“好的”,然后用户问“我叫什么?”,模型一本正经地告诉你“我不知道”。这不是模型笨,是我根本没把历史传进去。多轮对话的本质,是维护一个“消息列表”,每次调用都把列表里符合规则的对话历史全部或部分回放给模型。这样做的副作用是 token 消耗会增长,尤其聊得越长,请求越慢,所以才有“记忆窗口”的概念——只留最近 N 轮。

4.2 用 ChatMemory 和 Advisor 接住对话历史

Spring AI 1.0 把会话记忆封装成了ChatMemory和ChatMemoryAdvisor,不需要你自己拼 String。先定义一个ChatMemoryBean,我用的是MessageWindowChatMemory,意思是只保留最近 N 条消息,防止上下文无限膨胀:

@Bean public ChatMemory chatMemory() { return new MessageWindowChatMemory(10); }

然后把 Advisor 挂到ChatClient上:

@Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); }

调用的时候,关键是要传一个会话 ID,否则所有用户会共用一份记忆。比如:

@Test void multiTurn() { // 第一轮 String first = chatClient.prompt() .user("我的名字是张三") .advisors(a -> a.param(MessageChatMemoryAdvisor.MEMORY_CONVERSATION_ID, "session-123")) .call() .content(); // 第二轮,模型应该能“想起”上文的张三 String second = chatClient.prompt() .user("我叫什么?") .advisors(a -> a.param(MessageChatMemoryAdvisor.MEMORY_CONVERSATION_ID, "session-123")) .call() .content(); }

MessageChatMemoryAdvisor会在调用前从ChatMemory里读取历史消息,拼接到本次请求里,然后在调用结束后把新的对话写回 memory。你完全不用关心 String 拼接和消息格式,只要保证同一个会话 ID 在前后几轮中保持一致。如果你想要无状态的单轮对话,不传这个参数就行,ChatClient默认就是无记忆,这反而是它比较干净的地方。

4.3 按用户区分记忆,别全局串台

上面示例里手动传session-123,真实系统中这个值就是用户 ID 或会话 ID。如果不区分,A 用户聊到一半,B 用户插一句“继续”,模型会接着 A 的话头往下说,这就是严重的串台事故。我建议在 Controller 层从登录态或者请求头里取 userId,作为MEMORY_CONVERSATION_ID传入。

如果你的场景是单服务器、用户量不大,每个用户用一个MessageWindowChatMemory实例或者一个 key-value 结构就够了。用户量大、多实例部署时,本地 Map 记忆会失效,因为请求被负载均衡到不同节点后,A 节点存的记忆 B 节点读不到。这种场景要么把记忆放到 Redis,要么把会话状态在整个集群里共享。Spring AI 目前的内置实现偏单机,生产上真要搞,得自己做持久化,这是最容易被低估的一块。我先提醒到这里,等新项目再开一篇讲 Redis 版记忆怎么做。

5. 常见问题排查实录

5.1 JDK 17 与 Bouncy Castle 的经典冲突

很多人在 JDK 17 下跑 Spring AI 相关项目时,突然报JCE cannot authenticate the provider BC,或者IllegalAccessError: tried to access method ...。这往往不是 Spring AI 的问题,而是某个中间件绑定了旧版 Bouncy Castle 加密库。JDK 9 之后模块系统对JCE提供者的加载路径和签名校验变得严格,旧命名规则的bcprov-jdk15on容易出问题。

应对方法很明确:用bcprov-jdk18on,比如:

<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78.1</version> </dependency>

很多人误以为 JDK 17 就要选 jdk15on,其实那个命名指的是 JDK 1.5 时代的产物,面向 JDK 1.8 及以上的版本应该选 jdk18on。还需要检查 classpath 里是否同时存在多个 bcprov 版本,有的话用mvn dependency:tree找到冲突依赖,统一排除旧版本。如果你只是跑 Spring AI 的对话链路,一般不会遇到 BC,只有当你同时用了需要加密签名的内部 SDK、或读带密码的私钥、或接某些特殊中间件时才会撞上。

5.2 依赖下载慢和版本号识别

Spring AI 依赖第一次拉取时比较慢,Maven 默认中央仓库对 Spring 的 release 包是支持的,但如果你用了 milestone 或 snapshot 版本,就需要额外加仓库:

<repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>

如果你配置了阿里云镜像,理论上大部分依赖都能加速,但 Spring AI 某些新模块可能还没同步过去,这时不要直接去掉镜像,而是加mirrorOf例外,或者在本地仓库手动安装 jar。还有个很常见的坑:本地~/.m2/repository/org/springframework/ai目录下残留了旧版本,导致明明改成了新版本号,跑起来还是旧代码。我的习惯是把.m2里这个目录整个删掉,再重新mvn clean package,干净利落。

5.3 Spring AI 和 LangGraph4j 到底选谁

这个问题是我最近在社区里看到频率最高的。Spring AI 和 LangGraph4j 解决的不是同一层问题。Spring AI 更适合“给 Spring 应用加 AI 能力”,比如写一个聊天接口、做一个 RAG 问答、把模型接进现有业务流水线;LangGraph4j 更偏 Agent 的状态图编排,适合你需要在多个模型调用之间做条件分支、循环、人工确认的复杂 Agent。如果是企业后端要快速落地一个“AI 助手”,选 Spring AI 性价比最高;如果你的诉求是做一个会自己规划工具使用、多步推理的 Agent,那就要认真评估 LangGraph4j 这类图编排框架。

两者也不冲突。不少团队把 Spring AI 当作模型接入层,再用 LangGraph4j 编排上层状态机。但这个组合对团队要求不低,我建议先把 Spring AI 的基础链路吃透,再慢慢加编排,不要一上来就堆大而全的架构。

最后分享两个我实际用的技巧。开发阶段,一定先在本地 Ollama 上把链路跑通,再切换线上模型,调试日志、看 token 消耗都不会心疼。另一个是打开 ChatClient 的调试日志:

logging: level: org.springframework.ai.chat.client: DEBUG

这样它组装好之后真正发给模型的 messages 列表会原样打出来。排查多轮对话丢记忆的时候特别管用,你能直接看到历史消息有没有被拼进去,是 Advisor 没生效还是会话 ID 传错了,一目了然。按这套组合搭过一遍的同学,如果还遇到其他奇怪问题,欢迎回来一起对一对版本序列号,我已经被版本坑怕了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询