ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring AI 实战:JDK 17+Spring Boot 3.5 快速搭建多轮对话接口

2026/9/30 7:57:21 拓冰建站 浏览量
Spring AI 实战:JDK 17+Spring Boot 3.5 快速搭建多轮对话接口 不废话先说结论Spring AI 没有网上传的那么玄乎。我前阵子在一个内部数据看板项目里接了大模型做自然语言查询从零开始搭环境到跑通多轮对话前后不到半天。这期间踩了不少坑尤其是 JDK 17 版本选择、Spring Boot 3.5 的兼容性、还有那个离谱的 bcprov-jdk 依赖问题网上资料鱼龙混杂翻车概率极高。这篇文章就基于我的实操经验按“环境搭建 → 基本调用 → 多轮对话”这条线把过程完整捋一遍。你不需要提前懂 LangChain不需要了解 Agent 框架只需要熟悉 Spring Boot 基础跟着走完就能得到一个可以本地跑起来的 AI 对话接口。项目最终用的组合是JDK 17 Spring Boot 3.5 Spring AI 1.0.0 GA稳定跑通。1. 为什么我最终选了 Spring AI而不是 LangGraph4j 或 spring-ai-alibaba先说选型。现在 Java 世界里做 AI 接入叫得上名字的无非三条路Spring AI、LangGraph4j、Spring AI Alibaba。B 站和掘金上吹什么的都有但真正落到项目里我自己的判断标准就三条和现有 Spring 技术栈的贴合度、官方维护活跃度、以及多轮对话的坑深不深。1.1 Spring AI 与其他方案的横向比对对比维度Spring AILangGraph4jSpring AI Alibaba定位Spring 官方生态的 AI 客户端LangChain 的 Java 移植版阿里开源的 Spring AI 增强套件学习曲线低熟悉 RestTemplate 就能上手高涉及图结构、状态机概念中多了些阿里云服务绑定多轮对话支持原生 ChatMemory Advisor通过 State 管理上下文支持但更偏云服务场景适合场景大多数 Spring Boot 项目复杂 Agent 流程编排阿里云用户、NL2SQL 类定制需求依赖侵入性无纯 HTTP 调用中概念侵入中部分功能依赖阿里云 SDK我实际对比下来的体感如果你只是想把大模型OpenAI、通义、DeepSeek 等接进 Spring Boot做一个“能对话、能记上下文、能跑 Prompt 模板”的功能Spring AI 是心智负担最低的选择。LangGraph4j 我看了几天文档它的状态机和图执行模型确实强大适合做复杂 Agent但几十个项目里可能只有一个需要那种编排能力剩下的人只是在做普通对话补全。至于 spring-ai-alibaba它在 RAG、NL2SQL 上有很深的定制比如 NL2SQL 它能把自然语言直接转成 SQL 去查库这个确实香。但前提是你的基础设施跟阿里云绑定比较深不然引入一堆自己用不上的能力反而重。我这次项目只需要纯对话 上下文记忆没到需要 NL2SQL 的程度所以还是选了官方 Spring AI。1.2 关于 “Spring AI 2.0” 的说法你可能搜到过 Spring AI 2.0 已经发布之类的说法。我查证下来的结论是Spring AI 目前最新的稳定主线是 1.0.0 GA2025 年年中发布2.0 还在 SNAPSHOT 阶段。如果你的项目要跑生产老老实实用 1.0.0不要碰 2.0.0-SNAPSHOT那个依赖树每天都在变今天能编过明天可能就红一片。2. 环境搭建JDK 17 的降级与版本选型、Spring Boot 3.5、Maven 依赖这是整个环节里最容易翻车的部分。很多人直接用 JDK 21 跑 Spring Boot 3.5一启动报错就懵了。这里先说清楚一件事Spring Boot 3.5 官方支持的基线是 JDK 17也就是说 JDK 17 是完全可以跑的而且是最稳妥的选择。如果你机器上装的是 JDK 21 或更高建议直接给这个项目单独配一个 17 的运行时不要在多个 JDK 版本之间扯皮。2.1 JDK 17 安装与切换实操如果你是 Windows我建议直接下载Temurin 17或Oracle JDK 17这俩都行没有本质区别。安装完之后最关键的是设置JAVA_HOME环境变量指向 JDK 17 的根目录然后在命令行里执行java -version确认输出是openjdk version 17.x.x。如果是 21 或者 1.8环境变量没生效或优先级不对。我在一台机器上同时装了 JDK 8、17、21 三个版本以前吃过大亏——IDEA 里配的 Project SDK 是 17但 Maven 的JAVA_HOME指向了 8结果编译报source/target 8 不支持之类的错。记忆点IDEA、Maven、终端三个地方的 JDK 版本必须一致否则你会浪费一整个下午在排查版本不一致的问题上。另外有一个很多教程不会提的点JDK 17 里增加 Bouncy Castle 依赖时要版本匹配。网上流行一种说法是“JDK 17 需要手动加 bcprov-jdk 什么什么版本”甚至有人推荐直接把bcprov-jdk18on强行塞进pom.xml。我实测下来在 Spring AI 1.0.0 GA Spring Boot 3.5 的情况下不需要手动加任何 Bouncy Castle 依赖因为 Spring Boot 的依赖管理已经内置了安全相关的传递依赖。手动加反而容易引发NoClassDefFoundError这是我踩过最冤枉的坑。2.2 Spring Boot 3.5 项目初始化直接去 start.spring.io 生成一个空项目关键选择如下Group / Artifact随意比如com.example / ai-demoJava Version17Spring Boot 版本3.5.x不要选 3.3 或 3.4因为 Spring AI 1.0.0 GA 与 Spring Boot 3.5 的兼容性最好DependenciesSpring Web、Lombok、Spring Boot Actuator可选生成后pom.xml里需要手动加入 Spring AI 的 BOM 和依赖。注意Spring AI 的 groupId 曾经有过变动老教程里写的是org.springframework.ai:spring-ai-core新版本统一改成了org.springframework.ai:spring-ai-starter-model-openai这种 starter 形式。如果你照抄旧教程的依赖坐标大概率编不过。正确写法如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.0/version relativePath/ /parent properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies !-- Spring Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI OpenAI 模型接入 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency /dependencies这里用了 OpenAI 的 starter但我后面对接的其实是 OpenAI 兼容接口服务。为什么可以这样因为市面上绝大多数大模型服务商DeepSeek、通义千问、Moonshot、各种国产模型都提供 OpenAI 兼容的 REST API这意味着你只需要改配置文件的base-url和api-key一行 Java 代码都不用改。2.3 配置文件中的关键项在application.yml里核心配置项如下spring: ai: openai: api-key: sk-xxxxx base-url: https://api.openai.com chat: options: model: gpt-4o-mini temperature: 0.7如果你要用 DeepSeek只需要把base-url改成https://api.deepseek.com模型名改成deepseek-chat即可。这种“只改配置、不改代码”的姿势太适合 Java 程序员了——你不需要学 Python不需要引入 LangChain就可完成大模型调用。注意api-key千万不要直接提交到 git 仓库里。我习惯的做法是配置里写${OPENAI_API_KEY}占位符然后通过 IDEA 的环境变量面板注入或者用启动脚本里的--spring.ai.openai.api-key${KEY}参数传入。这属于老生常谈但确实还是看到有人把那串 key 直接贴代码里发到开源平台上的一个 key 能被薅掉几百上千块。3. Spring AI 的核心概念ChatClient、Prompt 与 Model拿生活里的类比讲明白Spring AI 本身不是一个框架它更像一个“大模型的统一客户端适配层”。它帮我们把 HTTP 请求、JSON 解析、流式响应、上下文存储这些脏活累活都封装好了你只需要关心三样东西Model模型、Prompt提示词、ChatClient聊天客户端。3.1 三个核心概念帮你建立心智模型拿点奶茶来类比Model 就是给你做奶茶的师傅有不同流派GPT 师傅、DeepSeek 师傅各师傅的风格和价钱不同Prompt 就是你递给师傅的订单小票上面写了“少冰、三分糖、加芋圆”师傅完全按小票来做你字写得越清楚拿到手的奶茶越接近预期ChatClient 就是前台接待员你告诉接待员要什么他帮你填订单、递给师傅、再把做好的奶茶端给你你不需要亲自进后厨跟师傅沟通。在 Spring AI 里这三者的关系用代码表达就是这样Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一个乐于助人的助手回答要简洁而准确。) .build(); } }这里先注入一个ChatClient实例并给它设定了一个默认的“系统角色”。这个系统角色很重要因为大模型本身没有任何角色感你说“你是鲁迅”他才开始模仿鲁迅说话你不设定他就一个平平无奇的通用助手。3.2 Prompt 不只是“一段问题”很多人一开始把 Prompt 理解为“输入的问题”这没错但不完整。Spring AI 里的 Prompt 是一个对象它可以包含历史消息、上下文变量、结构化输出格式等。比如Prompt prompt new Prompt( 用一句话解释 Java 中的 Optional, OpenAiChatOptions.builder() .withModel(gpt-4o-mini) .withTemperature(0.5) .build() );这里的 OpenAiChatOptions 会覆盖配置文件里的默认选项这种“默认配置 局部覆盖”的设计对复杂业务很友好。比如你大多数接口希望模型严谨一些temperature 设为 0.2某一个创意生成类的接口希望发散一些就可以单独传 temperature 0.9非常灵活。强调一下temperature这个参数有什么实际意义它控制模型输出的“随机性”0 到 1 之间越接近 0 越稳定保守适合知识问答、信息抽取越接近 1 越发散适合头脑风暴、文案创意。我在实际项目里凡是面向用户直接展示的内容一律 0.3 以下内部测试、生成灵感类工具才敢开到 0.8。4. 从零到一的实操先跑通单轮对话再接多轮到了最实际的环节直接上代码。我用 Spring Boot 最常见的三层结构来组织Controller 负责接收请求Service 负责调用 Spring AI配置类负责兜底设置。4.1 先跑通最简单的单轮对话第一步永远是最简单的那版。不要一上来就做流式、做记忆先把“问一句答一句”跑通确认链路是通的再往上面叠功能。这里我写了一个接口传入用户问题返回模型回答的字符串。RestController RequestMapping(/api/ai) public class AiController { private final ChatClient chatClient; public AiController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/chat) public String chat(RequestBody ChatRequest request) { String answer chatClient.prompt() .user(request.message()) .call() .content(); return answer; } public record ChatRequest(String message) {} }这代码是不是很直白.prompt()开始一个对话.user(...)塞入用户消息.call()同步调用模型.content()把结果取出来。运行起来之后你 POST 一个 JSON 过去就能收到模型的回复。如果你在浏览器里测试 http://localhost:8080/api/ai/chat 会显示 405这个是正常的因为接口只支持 POST。我建议直接用 IDEA 自带的 HTTP Client.http文件测试或者用 Postman、Apifox 都行。我个人的习惯是用 Apifox因为它可以直接生成 API 文档方便前后端联调。测试的时候记得加请求头Content-Type: application/json请求体长这样{ message: 你好请介绍一下你自己 }如果返回的字符串里带着“你好我是 AI 助手……”之类的内容说明链路已经通了接下来才配谈多轮对话。4.2 多轮对话的两种常见实现方式多轮对话的本质是让模型“记住”之前聊了什么。但大模型本身是无状态的它不会主动记录你和它的聊天历史每次调用都是一次全新的对话。所以多轮对话的实现本质上就是把历史消息一遍遍放入新的请求里让模型假装有记忆。Spring AI 提供了两种做多轮对话的姿势一种偏手工一种偏框架你按项目阶段来选。方式一手动拼接历史消息这种方式最原始也最可控。你把每次用户消息和 AI 回复都存到一个 List 里下次请求时把整个 List 丢给模型Service public class ChatService { private final ChatClient chatClient; private final ListMessage history new ArrayList(); public ChatService(ChatClient chatClient) { this.chatClient chatClient; } public String chatWithMemory(String userMessage) { history.add(new UserMessage(userMessage)); String response chatClient.prompt() .messages(history) .call() .content(); history.add(new AssistantMessage(response)); return response; } }这种方式很快能让你理解“上下文”的本质是什么——它只是历史消息数组而已。但它有个明显的问题历史越长每次请求体就越大费用越高响应越慢。所以这种方式只适合做 Demo 或内部工具生产环境必须做消息裁剪或压缩。方式二使用 Spring AI 的 ChatMemory Advisor推荐Spring AI 自带了一套做对话记忆的设施核心是ChatMemory接口和MessageChatMemoryAdvisor。你可以把它理解成“框架帮你自动管历史记录还自动做裁剪”。如同一台自动咖啡机你只需要说要杯拿铁它自己完成磨豆、压粉、萃取全流程你不需要关心水位和粉量。配置起来也很简单只需要在配置类里注入一个内存版的ChatMemoryConfiguration public class AiConfig { Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } }这里有个非常容易踩的坑不要在 Controller 或 Service 里把 ChatClient 单例给拆掉。因为 ChatClient 是线程安全的全局一个 Bean 就够了Spring AI 会用 conversationId 来区分每一轮对话属于哪个“会话”不会串台。调用方的代码变成这样PostMapping(/chat) public String chat(RequestBody ChatRequest request) { return chatClient.prompt() .user(request.message()) .advisors(advisor - advisor.param(chatId, request.chatId())) .call() .content(); }这里的chatId就是你的会话 ID可以来自前端也可以来自用户 ID。同一个 chatId 的消息会被放进同一个历史上下文里不同 chatId 彼此隔离。这就像是你和客服聊天每次打电话会生成一个“工单号”所有对话挂在同一个工单下。用这种方式即使你不拼接任何历史消息Spring AI 也会自动帮你把之前的问答记录塞进请求里并且支持窗口大小配置spring: ai: advisor: memory: window-size: 10window-size 的意思是保留最近 10 条消息超出部分会被丢弃。这个值不建议调得过大我实测下来 10 到 20 之间比较合适既保证上下文连贯又不至于让单次请求太长。4.3 加一个流式响应让 AI 说话像在打字如果你做的是聊天机器人同步返回一整个字符串的体验是很“钝”的用户体验好的聊天框都是流式输出。Spring AI 支持流式有两种方式SSEServer-Sent Events或者 WebSocket。SSE 更简单我优先推荐。服务端只需要把.call()换成.stream()返回值变成FluxStringPostMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestBody ChatRequest request) { return chatClient.prompt() .user(request.message()) .advisors(advisor - advisor.param(chatId, request.chatId())) .stream() .content(); }前端用原生的EventSource或者 axios 的流式接口都能接每次收到一个分片就追加到聊天框里效果就跟 ChatGPT 官方一样字是一个一个蹦出来的。注意流式模式下如果你的服务背后有 Nginx 或网关记得关闭缓冲设置proxy_buffering off否则前端会长时间收不到数据等模型全部生成完才一次性返回流式就白做了。5. 多轮对话进阶Prompt 模板与系统角色定制跑通多轮对话只是第一步要让 AI 真正适配业务你必须学会用Prompt 模板。很多教程把这玩意儿讲得特别玄乎其实它本质就是“字符串模板 变量替换”跟 MyBatis 里${}拼 SQL 的思路是同源的。5.1 一个实用的 Prompt 模板示例假设我们在做一个餐饮 SaaS 系统的 AI 客服用户问“你们家的会员储值怎么退款”一个合格的 Prompt 模板是这样RestController public class CustomerServiceAiController { private final ChatClient chatClient; public CustomerServiceAiController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/api/cs/chat) public String customerChat(RequestBody CsChatRequest request) { return chatClient.prompt() .system(system - system .text(你是一个{shopName}的智能客服。面对顾客的问题请先查证再回答。如果信息不足请引导顾客提供会员手机号。回答要礼貌、简洁不要编造政策。) .param(shopName, request.shopName())) .user(request.message()) .call() .content(); } public record CsChatRequest(String shopName, String message, String chatId) {} }注意这里我对“系统 Prompt”做了参数化也就是让模型在回答任何问题之前先知道它是在为“哪家店”做客服。这在多商户 SaaS 场景里非常关键因为每家店的会员规则、退款政策、满减活动都不一样如果不在 Prompt 里把商户上下文交代清楚模型就会野生发挥、胡编乱造。我把这称为 Prompt 里的“角色定位 业务边界”组合拳。角色定位让模型知道“你是谁”业务边界让模型知道“什么能说什么不能说”。在面向客户的场景里业务边界甚至比角色定位还重要因为它直接决定了 AI 会不会承诺“退款 100 元”这种公司承受不起的话。5.2 用系统 Prompt 把多轮对话改造成角色扮演我实际跑过一个测试不加系统 Prompt直接问“你是什么模型”模型会老实回答“我是 DeepSeek”或“我是 GPT”。但加了系统 Prompt“你是餐饮店的店长小助手”同样的问题它会说“我是这家店的智能助手很高兴为您服务”。这就是系统 Prompt 的魔力——它决定了模型每一轮回答的姿态和口径。再配合上一节的多轮记忆把整个对话历史交给模型就能构建出“有明确人设的对话机器人”。如果你后续有Agent的需求下一步要学的就是给模型配上“工具调用”和“流程编排”那是个更大的话题不在本文范围内。6. 常见问题与排查技巧实录这部分整理的是我在这套环境里真实踩过的坑按频率从高到低排列。你如果在跑的过程中遇到类似问题直接对照着查能省不少时间。6.1 依赖下载失败或类找不到NoClassDefFoundError现象启动时控制台飘出一长串NoClassDefFoundError或者 Maven 下载依赖时卡死在某个 artifact 上。原因90% 的情况是你没引入 Spring AI 的 BOM或者引入的是老坐标spring-ai-core而不是 starter。排查流程检查pom.xml里是否有spring-ai-bom的 dependencyManagement检查依赖坐标是否包含starter字样如spring-ai-starter-model-openai检查 Spring Boot 版本是不是 3.5.x我试过用 3.3.x 去配最新 Spring AI也会出问题最后看 Maven 仓库里是否真的下载到了 1.0.0 版本的 jar如果卡在 SNAPSHOT删除本地仓库对应目录重新mvn clean install。6.2 报错”OpenAI API response: Invalid API key”现象调用接口时返回 401提示 API key 无效。原因要么是 key 真错了要么是base-url对了但 key 的格式不对。比如 DeepSeek 的 key 是sk-开头OpenAI 的也是sk-开头但两者不通用。有些国产模型平台的 key 前面还带/或Bearer字样而配置里只需要写裸的 key。排查流程先用 curl 直接测一下模型的 API排除代码因素确认application.yml里的 key 没有多余空格、没有换行确认自定义的base-url后面没多带/v1之类的路径。比如 DeepSeek 的 base-url 是https://api.deepseek.com你填了https://api.deepseek.com/v1某些版本也能通但某些接口路径会拼接错我建议填官方文档里给的最精简形式。6.3 多轮对话上下文不生效AI 不记得之前说了什么现象第一次问“我叫张三”第二次问“我叫什么名字”模型答不上来。原因90% 是你没有使用带有记忆机制的 ChatClient。很多人会直接用OpenAiChatModel的底层方法去调用那个方法是完全无状态的你每次都要自己传历史消息才能“记住”。如果你用的是 ChatClient但没配置MessageChatMemoryAdvisor同样不生效。排查流程确认配置类里有没有注册ChatMemoryBean确认 ChatClient 的构建有没有.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))确认每次调用时有没有传chatId且chatId是否是同一个值。这个字段不传或值不同记忆就串不到一起相当于每次都开了个新会话。6.4 JDK 17 环境下启动报 “Unable to instantiate [org.springframework.ai.chat.memory.ChatMemory]”现象项目能编译启动时 Bean 创建失败。原因Spring AI 1.0.0 对部分内存型 Bean 的自动配置存在时序问题特别是在你通过Configuration同时定义ChatMemory和ChatClient时如果两者之间有循环依赖就会启动失败。排查流程把ChatMemory的创建单独抽到一个配置类ChatClient的创建放到另一个配置类并且通过Lazy注解解决循环依赖。或者更简单不要同时在一个配置类里初始化这两个 Bean参考我前文的写法拆开定义就行。这个问题我查了很久才定位到根因网上很少有人说清楚。6.5 响应内容里混入了 Markdown 语法现象模型输出的文本里带**加粗、-列表标记、#标题等 Markdown 语法但你的业务场景只需要纯文本。原因大模型的默认行为就是这么干的它默认你是在“写文档”。Spring AI 没给你做后处理所以你必须自己在 Prompt 里约束或者在后端做清洗。解决Prompt 末尾加一句“请以纯文本形式回答不要使用 Markdown 格式。”或者后端做一个简单正则把**、##等标记去掉如果做的是网页版聊天框其实保留 Markdown 再用marked.js渲染也是常见方案看你的前端能力。6.6 调用越来越慢响应延迟从 1 秒涨到 5 秒以上现象连续对话几轮后接口响应明显变慢。原因不是网络问题而是你的请求体变大了。每轮对话都加上历史消息模型需要处理的历史 token 越来越多生成时间自然变长。解决把window-size调小比如从 20 调到 10或者对历史消息做一次“摘要压缩”每过几轮把之前的对话用模型浓缩成一段背景信息替换掉原始历史。这个方案 Spring AI 有对应的MessageChatMemoryAdvisor扩展点我还没完全吃透但视频和社区里有人实现了你先用 window-size 就够扛住大部分场景。7. 最后分享一个我从坑里爬出来的经验这套东西跑通并不难真正难的是“版本锁定”和“上下文管理”这两件事。我前前后后改了三次 pom 版本、删了两次本地 Maven 仓库、查了好几篇互相矛盾的博客才稳定下来。现在回看Spring AI 的文档已经很良心了但它的 1.0 版本迭代太快网上 80% 的教程还停留在 0.8 或 0.9 的 API 上你看教程时务必看一下发布时间和版本号半年以上的基本就当历史参考别直接照抄。还有一句话值得刻在脑子里多轮对话不是魔法它只是把历史消息一次次重放给模型而已。理解了这一点你就能明白为什么叫“上下文窗口”为什么要做裁剪也就能自己设计出适合业务的记忆策略了。如果你拿这套东西做出点什么或者卡在哪个新坑里欢迎回来交流我很乐意一起看看新版本又改了什么。