Spring AI 实战:用 Spring Boot 快速接入大模型对话能力
如果你和我一样是做 Java 后端的人这两年多少有点焦虑。AI 的大潮铺天盖地但圈子里聊得多的都是 Python、Node.jsJava 这边很长一段时间都处于“能用但很费劲”的状态。想接一个大模型对话能力得自己封装 HTTP 调用、处理 SSE 流式返回、维护多轮上下文、写一堆 JSON 解析代码项目一复杂还容易散落各处维护起来特别痛苦。直到 Spring AI 出现我发现情况终于开始变了——它直接对标 Spring 家族的风格把大模型接入这件事抽象成了一组统一 API再配合 Spring Boot 的自动装配第一次跑通一个可对话的接口我只花了不到 20 分钟。这篇文章就是我基于 Spring Boot Spring AI 做的一次完整快速体验记录。我不打算给你罗列一堆官方文档翻译而是把从零搭项目、写接口、调参数、踩坑排查的整个过程讲讲清楚包括每一步为什么要这么做以及代码背后到底发生了什么。适合两类人看一类是想给现有 Spring Boot 项目加 AI 能力的后端开发另一类是刚接触 Java 生态、想知道 Java 世界怎么接大模型的初学者。看完你应该能自己动手搭出一个能聊天的 AI 服务并且知道怎么让它从“能聊”变成“能用”。1. Spring AI 到底解决什么问题1.1 Java 接入大模型的三个尴尬在 Spring AI 之前Java 开发者接大模型基本靠三样东西HttpClient、JSON 库和一颗强大的心。缺点非常明显。第一是接口协议碎片化。市面上主流的大模型服务商虽然都号称兼容 OpenAI 协议但真正用起来鉴权方式、请求字段、流式返回格式、错误提示多多少少有差异。今天接这家明天换那家调用代码得跟着改一遍没有一个统一抽象。第二是样板代码太多。一次对话请求要构建请求头、设置 JSON body、序列化参数、处理响应、解析错误、管理超时重试。这些逻辑放到业务代码里非常碍眼而且每个接口都要写一遍。第三是生态没有一个“官方答案”。社区里也有各种封装但大多围绕某一两家厂商或者某几个模型生命周期短文档也参差不齐。作为后端团队很难把它当基础设施往上垒。1.2 Spring AI 的定位一套“方言”接入多家大模型Spring AI 聪明的地方在于它不解决“模型怎么训练”的问题那是模型厂商的事它解决的是“应用怎么连模型”的问题。它本身不是一个 AI 引擎更像是 Spring 生态里的一个集成层定义了统一的 ChatModel、ChatClient、Prompt、Message 等概念然后让各个模型厂商用自己的适配器来接这套规范。如果你熟悉 Spring Data JPA 的思路上手就很快——JPA 抽象了数据库方言你写 JPQL底层是 MySQL 还是 PostgreSQL 由方言实现决定。Spring AI 也是类似的逻辑你的业务代码面向统一的客户端接口编程底层接入哪个模型由配置决定。换模型服务商不用改业务代码只改配置文件和依赖这就是它作为“框架”的价值。1.3 快速体验的核心思路先跑通再深化我第一次拿到 Spring AI 的时候并没有直接去研究它内部有多少组件而是先搭了个最小工程跑通一个“发消息-拿回答”的接口。这个策略看起来很朴素但真的很管用。因为只有链路通了你才知道哪些报错是配置问题哪些是代码问题哪些是网络问题。之后再逐步加入 System Prompt、结构化输出、多轮会话每加一个能力都能明确知道它在链路中的位置。所以这篇文章的实操部分也按这个顺序来先搭建可运行工程再写最小对话接口最后加进阶能力。2. 环境准备搭出一个能跑的最小工程2.1 版本选型与依赖引入Spring AI 目前的版本节奏比较快而且对 Spring Boot 的版本有明确要求。我自己用的时候Spring Boot 3.2.x 配 Spring AI 的某个里程碑版本是能跑通的但如果你想少踩坑最好看一眼你选用的 Spring AI 版本对应的 Spring Boot 版本要求别直接用最新的 Spring Boot 去配一个老版本的 Spring AI很容易出现自动装配失效。工程方面建议直接用 Spring Initializr 生成一个基础项目只需要勾选 Web 依赖其它后面手动加。Spring AI 相关的依赖核心坐标大致长这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0-M6/version /dependency注意这里有个大坑Spring AI 的很多版本是里程碑版本M 后缀默认的中央仓库可能没有需要在 pom.xml 里额外配置 Spring 的里程碑仓库repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshots enabledfalse/enabled /snapshots /repository /repositories我最初没加这个仓库Maven 一直报依赖找不到还以为是网络问题后来才发现是仓库没有配。如果你是第一次接触先把这一步搞定比后面调代码省心十倍。2.2 application.yml 与 API Key 管理依赖引入之后真正决定能不能跑通的是配置文件。一个最小配置如下spring: ai: model: provider: your-provider openai: base-url: https://api.your-provider.example.com api-key: ${AI_API_KEY} chat: options: model: your-model-name temperature: 0.7这里的 base-url 指向你选择的大模型服务商兼容接口地址api-key 从环境变量读取千万不要把密钥硬编码进 YAML。model 换成你服务商实际提供的模型名称。为什么 API Key 一定要用环境变量因为项目代码迟早要提交到仓库哪怕现在是私有仓库也有泄露风险。我见过有人直接把密钥写在 application.yml 里然后提交到 Git 仓库结果过了几个月发现密钥被刷爆了。从第一天就养成从环境变量读取的习惯成本为零收益长远。2.3 两个方法验证工程健康依赖和配置都就绪之后不用急着写业务代码先做两个验证。第一个验证项目能不能正常启动。直接运行启动类如果控制台没有任何异常说明自动装配没有大问题。第二个验证Spring 容器里有没有生成 chatClient 这个 Bean。可以临时在一个启动类里打印一下SpringBootApplication public class DemoApplication { public static void main(String[] args) { ConfigurableApplicationContext context SpringApplication.run(DemoApplication.class, args); System.out.println(context.containsBean(chatClient)); } }如果输出 true说明 ChatClient 已经准备好了接下来就可以直接注入使用。如果这里就报错优先回头检查依赖版本、仓库配置和 YAML 里的模型名是否正确。3. 第一个对话接口从 Controller 到大模型3.1 写一个最简的 ChatService跑通最小验证之后接下来就是让应用具备真正的对话能力。Spring AI 的核心入口是 ChatClient它就像 Spring 里的 RestTemplate 一样封装了整个调用链路。我习惯给 AI 调用单独做一个 Service不让 Controller 直接操作 ChatClient这样以后换模型、加日志、做缓存都有一个统一的地方。Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String chat(String message) { return chatClient.prompt(message) .call() .content(); } }这段代码一共做了三件事通过 Builder 构建一个 ChatClient接收用户消息并构建 Prompt发起调用并拿回模型生成的文本。注意这里用的是构造器注入推荐的依赖注入方式字段注入能不用就不用。3.2 暴露 HTTP 接口并完成自测Service 写好后Controller 就非常简单了RestController public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService chatService; } PostMapping(/chat) public MapString, String chat(RequestBody ChatRequest request) { String answer chatService.chat(request.message()); return Map.of(answer, answer); } public record ChatRequest(String message) {} }启动项目之后用 curl 测一下curl -X POST http://localhost:8080/chat \ -H Content-Type: application/json \ -d {message:用一句话介绍你自己}正常情况下返回的 JSON 里就会带一个 answer 字段内容就是模型生成的回答。我记得自己第一次看到这个返回的时候第一反应是“这么简单吗”——确实就这么简单Spring AI 把请求构建、鉴权、响应解析全部消化在 ChatClient 内部了。3.3 从代码反推 ChatClient 的工作原理看完了能跑的代码我建议你花五分钟想一想这背后发生了什么因为只有理解了链路后面遇到问题才知道去查哪一段。当调用chatClient.prompt(message).call().content()时内部大致经历了这么几步先把普通字符串包装成 Prompt 对象再交给 ChatModel 实现类也就是某个模型服务商的适配器适配器负责把 Prompt 转换成厂商要求的 HTTP 请求结构并附加 API Key、模型名、temperature 这些参数然后发起远程调用拿到厂商返回的原始响应最后解析出文本内容返回给业务代码。也就是说你写的代码是稳定的换模型厂商只是换底层适配器而已。这也是为什么我推荐你从一开始就基于 ChatClient 写业务而不是自己去调用底层厂商 SDK——两者都能跑通但前者给了你未来换模型不伤筋动骨的底气。4. 从“能聊”到“能用”模板与结构化输出4.1 PromptTemplate 让提示词不再写死在代码里跑通对话接口只是第一步真实业务里几乎没有裸聊天的需求。很多时候你希望模型扮演某个角色按照既定规则处理输入。比如做一个“代码审查助手”希望用户提交代码片段模型按固定维度输出审查意见。这时最忌讳的做法是把一大段提示词拼在业务代码里。正确做法是用 Spring AI 的 PromptTemplate把提示词主体放到模板中动态内容用占位符替换Service public class CodeReviewService { private final ChatClient chatClient; public CodeReviewService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String reviewCode(String code) { String promptTemplate 你是一名经验丰富的 Java 架构师请对下面的代码做审查。 重点检查潜在 bug、性能问题、可读性、安全隐患。 请给出具体问题和修改建议。 需要审查的代码 %s .formatted(code); return chatClient.prompt(promptTemplate) .call() .content(); } }用系统提示词的方式会更符合 Spring AI 的习惯把“角色设定”和“用户输入”分离系统提示词定义角色和行为规则用户输入放具体内容。这样模型会更稳定不会因为用户的一句话跑偏。Spring AI 里面可以直接在 prompt 上指定系统文本String answer chatClient.prompt() .system(你是一名严谨的 Java 架构师输出时使用中文逐条列出问题。) .user(code) .call() .content();这两段代码跑起来效果差别不大但关注点分离之后维护提示词会轻松很多。随着项目变复杂你肯定希望提示词能独立管理甚至将来用提示词文件替代硬编码字符串。4.2 让模型按固定结构返回 JSON聊天返回一段文本没问题但如果模型输出要直接对接业务系统比如从用户反馈中抽取“情绪、分类、紧急程度”三个字段你再拿一段自由文本去做 Promise 解析就太累了。更现实的需求是让模型直接返回一个 JSON 对象Java 这边反序列化成 Bean。Spring AI 的结构化输出能力正好干这个。它能在提示词中附加格式约束让模型按你的 Java 类定义输出 JSON。假设你定义了一个反馈分析结果类public record FeedbackAnalysis( String sentiment, String category, int urgency ) {}然后使用 BeanOutputConverter 生成格式说明并转换结果BeanOutputConverterFeedbackAnalysis converter new BeanOutputConverter(FeedbackAnalysis.class); String formatInstruction converter.getFormat(); String prompt 分析下面的用户反馈返回结果严格按 JSON 格式字段包括 sentiment、category、urgency。 %s 用户反馈 服务态度很好但响应太慢了希望能改进。 .formatted(formatInstruction); String content chatClient.prompt(prompt).call().content(); FeedbackAnalysis analysis converter.convert(content);这样拿到的 content 就不再是自由文本而是能被converter.convert转成 Java 对象的稳定结构。我在实际体验中发现模型在结构化输出模式下“走神”的概率明显降低偶尔还是会漏字段所以代码里对 convert 要做异常捕获别直接往下游传。这种方式特别适合做信息抽取、内容分类、客服工单自动打标一类的场景。一旦模型输出能跟 Java 类型直接绑定AI 就不再是一个“聊天玩具”而是可以真正参与业务逻辑处理。4.3 多轮对话的会话状态管理思路聊到上下文这是新手最容易忽略的地方默认情况下每次调用 ChatClient 都是无状态的模型不会记得你上一轮说了什么。要实现“多轮对话”必须把之前的历史消息一起发给模型。Spring AI 提供了会话内存机制常见做法是使用消息窗口容器按会话 ID 保存聊天历史。大致思路是ChatMemory chatMemory new MessageWindowChatMemory(10); ChatClient chatClient ChatClient.builder(model) .defaultChatMemory(chatMemory) .build();然后每次请求传入一个 conversationId框架会自动从内存里取出该会话的历史记录拼进下一次请求。窗口大小需要控制因为大模型的上下文窗口有限聊天记录太长既费 token 又可能把核心指令“挤”出注意力范围。10 到 20 条通常是比较稳妥的值。这里要提醒一句内存方案只适合单机、低并发、测试体验场景。如果要做生产级的多轮对话历史消息得落到 Redis 这类外部存储里并且在传入模型前做长度裁剪或摘要压缩。Spring AI 本身也在持续完善存储抽象但基础原理不会变——上下文本质上就是把历史消息递给模型。5. 参数调优与控制让模型“听话”5.1 三个最常用的生成参数聊完功能再聊聊调参。Spring AI 暴露的生成参数里有三个最常用而且直接影响输出质量。第一个是 temperature控制随机性。取值越低输出越确定、越保守适合代码生成、事实抽取取值越高回答越发散、越有创造性适合文案生成、头脑风暴。我在做代码审查助手时temperature 一般设 0.2 左右几乎不让模型自由发挥。第二个是 max-tokens限制模型生成的最大 token 数。这个参数除了控制成本还直接影响接口响应时间。如果模型需要输出很长的内容结果被截断可以先确认是不是这个值设置太小。第三个是 top-p也是采样控制。它和 temperature 作用类似但机制不同通常两者不要同时猛调调一个就够了。多数场景下固定 temperature把 top-p 保持默认是比较稳妥的做法。下面是我自己常用的参考表仅供参考因为不同模型的实际表现有差异参数通俗理解适用场景建议值temperature越大越“敢说”文案、创意0.7-0.9temperature越小越“严谨”代码、抽取0.0-0.3max-tokens最多说多长长文、短答按需设置top-p候选词范围一般不用调默认 1参数不是越多越好先调 temperature 和 max-tokens基本就能覆盖大部分需求。参数都写在配置里也可以按接口动态覆盖Spring AI 允许在 prompt 级别追加选项这个灵活性特别实用。5.2 超时与流式响应的控制真实体验中还有一个容易被忽视的点大模型接口的响应时间不稳定有时 2 秒有时 20 秒。如果 HTTP 客户端超时设置太短业务侧会频繁报错。Spring AI 底层用的是 RestClient可以参考它的超时配置方式在 application.yml 里调整 connect 和 read 超时时间。我一般把连接超时设为 10 秒读取超时设为 60 秒避免因为网络抖动导致正常请求被中断。如果你的场景是“边生成边展示”比如聊天机器人打字机效果那么要研究流式响应。Spring AI 提供了流式 API大致是chatClient.prompt(...).stream()返回的是一个 Flux前端可以配合 SSE 实时接收逐字输出。这个我没有在快速体验里做但如果你要接实时对话方向是明确了的后端用 WebFlux 或者控制器返回 SseEmitter前端接 EventSource 或 fetch 流式读取。先跑通同步接口再升级流式是比较合适的节奏。6. 我踩过的坑和排查思路实录6.1 依赖版本冲突这是我遇到的第一个坑。Spring 生态对版本要求向来严格Spring AI 更是如此。当时我把 Spring Boot 升到了 3.4结果 Spring AI 的里程碑版本还停在对应 3.3 的适配启动时直接提示某个自动配置类不存在。排查思路比较简单先看启动日志的 Caused by 部分找最底下的那个异常通常就是版本不兼容。解法也比较粗暴回退 Spring Boot 版本或者升级 Spring AI 到对应版本。如果你也遇到奇怪启动报错优先怀疑版本组合而不是业务代码。6.2 请求超时和连接异常第二个坑是超时。第一次调用大模型接口默认超时配置只有几秒而我选的模型响应偏慢结果业务侧频繁抛超时异常。这个问题很容易被误判成网络问题或者 API Key 问题实际上就是超时阈值太短。排查方法很简单先用 curl 直接请求一次模型服务商的接口如果 curl 能拿到结果但应用里超时那基本就是客户端超时配置的问题。把读取超时调到 60 秒左右再试大概率解决。6.3 API Key 的安全隐患第三个坑是关于配置的。第一次我图省事把 API Key 直接写在 application.yml 里项目启动后朋友帮我做代码 review第一眼就发现了这个问题。后来我把密钥迁移到了环境变量并且检查了 Git 历史确认这个 Key 没有真的被提交到远程仓库。这里我要特别提醒如果你的 Key 已经提交到任何远程仓库不要只是删掉文件视为已泄露去服务商控制台把旧 Key 作废换新 Key。密钥泄露的代价有时候不是钱的问题而是整个项目的安全信任问题。6.4 模型输出不稳定第四个坑来自模型本身结构化输出偶尔不老实。我遇到过明明给了 JSON 格式约束模型还是输出了一行解释文字导致反序列化失败。这不是代码 bug而是大模型的概率性行为。我的应对方案有三层第一层在提示词里反复强调“只输出 JSON不要解释”第二层解析时加 try-catch失败则记录原始输出方便人工介入第三层设计上不要把结构化输出当成绝对可靠的基础设施重要链路要做人工兜底或二次校验。模型输出再稳定也是概率事件生产系统必须对此有预期。7. 最后补充一点个人经验这套 Spring Boot Spring AI 的快速体验做下来我最大的感受是Java 生态在 AI 集成这件事上终于有了一个可以认真对待的框架。它未必像 Python 生态那样自由但它的抽象方式和 Spring 一贯的风格高度一致对于已经用 Spring Boot 构建了大量业务系统的团队来说学习成本低集成路径顺滑。我给新手的建议是先从最小工程跑通不要一开始就研究高级特性。ChatClient 的同步对话接口跑通之后再加模板、结构化输出最后才是流式和会话管理每一步都验证一次。中间那些坑版本冲突、密钥泄露、超时设置、输出解析失败都是可以提前预防的希望这篇文章能让你少走几轮弯路。如果你照这个过程跑通了接下来值得探索的方向是把 AI 能力嵌入到一个真实业务场景里例如工单分类、文档问答、代码审查工具这些场景才能真正体现 Spring AI 的价值。毕竟框架只是一个开始让它解决实际问题才是我们做后端的人最该关心的事情。