
Spring AI 实战教程一入门示例Spring AI 这个项目我盯了挺长时间从它早期还是个雏形的时候就一直在关注。作为一个常年用 Spring 生态写业务系统的开发者说实话之前接 AI 能力的时候心里是有点抗拒的OpenAI 的 SDK、Anthropic 的 SDK、各种模型的 SDK 各写各的参数风格不一样返回结构不一样换个模型就要重新适配一遍。这感觉就像家里买了台新电器结果每个电器都自带一个遥控器桌面上一排遥控器看着就头疼。Spring AI 要解决的就是这个问题。它给 Java 开发者提供了一套统一的抽象层把主流的 AI 模型、向量数据库这些资源全部包装成 Spring 风格的 Bean你只需要通过配置注入然后用统一的 API 去调用。这篇入门教程就是用最简单的代码把整个链路跑通让还没接触过 Spring AI 的同学在半小时内就能做出自己的第一个 AI 对话程序。适合谁来读呢如果你已经熟悉 Spring Boot 的基本用法会写 Controller、会配 application.yml那完全够了。不需要你懂深度学习也不需要你研究过大模型 API 的底层细节这些都被框架帮你处理掉了。1. Spring AI 到底是什么为什么值得学老规矩先搞清楚一个框架解决什么问题再决定要不要学它。你回想一下自己在项目里对接第三方服务时的过程。对接短信平台找一个 SDK配置 appKey封装一个 Service发消息。过了两个月换了家短信服务商又得换 SDK、换配置、改代码。这套流程我相信每个写业务系统的人都经历过说实话挺烦人的。Spring AI 干的事就相当于在你和多家人工智能服务之间加了一个适配层它能让你用同样的代码风格去访问不同的模型供应商。今年的模型服务商是真的多各家能力也各有侧重有的擅长代码生成有的偏逻辑推理有的上下文窗口大。厂商多了之后切换的成本就会成为项目选型时要考虑的重要因素Spring AI 的价值在这里就被放大了。用一段代码来说明。在不使用 Spring AI 的情况下你直接调 OpenAI 的接口可能要这么写// 直接用 HTTP 客户端调用 String requestBody { ... 一大堆参数拼装 ... }; String response httpClient.send(requestBody, headers); // 然后再解析那一大坨 JSON而使用 Spring AI 之后Autowired private ChatClient chatClient; public String chat(String message) { return chatClient.call(message); }差距非常明显。前者你需要自己搞 HTTP 通信、接口鉴权、参数封装、结果解析而后者这些都帮你封装好了。而且后面你想切换不同的模型不用改业务代码只需要调整配置文件。Spring AI 项目本身的理念也很 Spring —— 可移植性、可配置性、可扩展性。它的编程模型跟 Spring 生态的经典组件做到了一脉相承的思路。2. 搭建开发环境与项目骨架2.1 前置条件准备缺一不可在开始写代码之前建议先把环境准备好。我踩过的坑基本都是环境问题引起的所以这部分认真说一下。你需要准备的基础环境如下JDK 17 及以上版本Maven 3.9 或 Gradle一个支持 Java 17 的 IDE一个可以正常访问 AI 模型服务的 API KeyJDK 版本这里划个重点 —— Spring AI 官方建议用 17 以上的版本。我之前图省事用了手头现成的 JDK 11结果编译直接报错查了半天才发现是版本不匹配。如果你本地已经有 JDK 8 或 JDK 11稳妥起见还是单独装一个 JDK 17免得在环境问题上浪费时间。另外提一下 Maven 和 Gradle 的选择两个都行看你平时的习惯。我个人习惯用 Maven后面讲配置的时候也都基于 Maven 来讲用 Gradle 的读者配置思路是一样的把依赖声明方式对照着改一下即可。2.2 初始化项目两种方式任选创建项目有两种方式。第一种方式使用 Spring Initializr 网站。打开 Spring Initializr选择对应的构建工具Maven 或 Gradle、语言Java、Spring Boot 版本然后在依赖列表里搜索并勾选 Spring AI。第二种方式直接在 IDE 里新建 Spring 项目比如 IDEA 的 Spring Initializr 向导操作基本一样。不过我建议你自己手动创建一个工程把 pom.xml 里的依赖直接配置进去。为什么这么建议因为这样你能清楚地知道到底引入了哪些东西尤其是 Spring AI 的依赖结构看到依赖树之后心里会更有底。一个最小化的 pom.xml 核心内容大致长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.1/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency /dependencies有几点细节值得注意。Spring AI 的版本号发布节奏很快版本更新时不一定要选最前沿的正式版本建议选择一个经过社区验证的稳定版运行起来会更省心。另外 Spring AI 和相关组件都引入了模块化的管理在 Spring Boot 3.x 的项目里Boot 版本与 Spring AI 版本之间最好保持对应的搭配避免出现不兼容的情况。2.3 配置 application.yml 和 API Key项目创建好之后接着就是核心配置环节。在使用 OpenAI 的情况下需要在 application.yml 里配置好模型服务的 key 和基础地址。spring: application: name: spring-ai-demo ai: openai: api-key: ${OPENAI_API_KEY} base-url: https://api.openai.com chat: options: model: gpt-4o-mini temperature: 0.7有些同学可能习惯把 key 直接硬编码在配置文件里本地演示的时候确实方便但任何时候都不建议在生产环境这么做。建议用环境变量的方式来注入也就是我上面写法里的${OPENAI_API_KEY}这样即使代码传到仓库里也不会把 key 泄露出去。如果你使用的是国内的模型服务商或者开源模型的托管服务只需要把 base-url 替换成对应的服务地址同时调整一下 key 的获取方式就好比你在用短信服务商时换一个平台地址一样都是很常规的操作。后续讲到更多模型时再详细展开。3. 完成第一个 AI 问答接口3.1 使用 ChatClient 装配核心客户端Spring AI 封装了多种编程风格官方主推的是 ChatClient。可以把它理解为“面向 AI 模型的一个客户端”类似 JDBC 帮你屏蔽了不同数据库的差异一样。通过在配置类里注入相关 Bean然后在 Service 层注入 ChatClientConfiguration public class AIConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder.build(); } }如果你的需求比较简单也可以直接在用到的地方直接注入 ChatClient.Builder 然后 build。Spring Boot 的自动配置会自动把可用的模型处理器装配进来底层具体调的是哪家服务从你当前的代码层面来看其实已经感知不到了。使用它的核心方式非常直接就是调用 call 方法Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient chatClient) { this.chatClient chatClient; } public String chat(String message) { return chatClient.call(message); } }3.2 设计一个简洁明快的 Controller 接口有了 Service 之后再把访问入口暴露出来一个 HTTP 接口就完成了。RestController RequestMapping(/api/chat) public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService chatService; } GetMapping(/message) public String chat(RequestParam(msg) String message) { return chatService.chat(message); } }到这里你已经拥有一个能通过浏览器或 Postman 访问的对话接口了。在浏览器里输入http://localhost:8080/api/chat/message?msg你好介绍一下你自己然后耐心等一下返回的内容就是模型生成的回答结果。这个接口就是这么简单从启动到跑通理论上用不了多少行代码。如果只想做极简验证也可以直接在启动类里写一个 CommandLineRunner在应用启动时立刻发一句测试看控制台输出结果。这种方式适合先快速确认配置没有问题。4. 深入 ChatClient 的核心 API 机制4.1 call 方法的内部行为逻辑把 call 方法当成黑盒直接来用当然没问题。但如果你稍微深挖一层理解它内部的流转流程后面处理复杂问题时就不会两眼一抹黑。整个调用过程大致可以拆解为几个环节。当你调用chatClient.call(message)时框架会把消息对象封装成内部的标准消息结构传给模型网关由网关根据当前配置中的模型名称和模型类型找到对应的适配器然后通过适配器向模型服务发起 HTTP 调用。模型服务返回结果后框架解析出回答文本最终通过响应对象返回给你。这个过程中所有细节都被隐藏了这也是框架本身最大的价值。如果你对这些细节感兴趣可以在 debug 模式下运行时观察调用栈会发现框架底层帮你处理了很多容易出错的环节。4.2 用 prompt() 方法自定义消息上下文单纯的 call 方法虽然简单但实用性有限。更多时候你需要给模型设定角色、提供背景信息、传递上下文甚至设置输出格式。这个时候就需要改用 prompt() 方式了。public String chatWithContext(String userMessage) { return chatClient.prompt() .system(你现在是一名经验丰富的 Java 架构师请用专业且简洁的语言回答问题。) .user(userMessage) .call() .content(); }这里 system 方法设置的是系统提示词相当于给模型做一个“角色设定”。user 方法传递用户输入。call 之后拿到的是一个 ChatResponse 对象调用 content() 方法取出回答文本。还有一个使用频率很高的场景是给 prompt 填充参数。比如你有一个模板化的提问需求只需要把动态内容填进去String template 请用一段话解释什么是{concept}并给出一个实际应用场景。; String content chatClient.prompt() .user(u - u.text(template).param(concept, 依赖注入)) .call() .content();这种方式在处理重复性业务问答、批量生成报告、变体内容生成等场景时特别好用可以让代码保持简洁又能灵活适应不同的入参。4.3 用流式响应改善用户等待体验不知道你有没有遇到过这样的情况请求发出去了界面转圈圈转好几秒结果突然一次性蹦出一大段文字。如果是内部工具倒还好如果是面向用户的聊天产品这体验说实话有点拉跨。更好的方式是使用流式响应也就是 streaming。模型每生成一个 token就把它立刻推给前端用户能感受到内容一个词一个词蹦出来就像在跟真人聊天一样。Spring AI 对流式输出提供了几个使用方式。简单场景下可以使用 stream 方法配合 FluxService public class StreamingChatService { private final ChatClient chatClient; public StreamingChatService(ChatClient chatClient) { this.chatClient chatClient; } public FluxString streamChat(String message) { return chatClient.prompt().user(message).stream().content(); } }Controller 侧对应改造一下返回类型GetMapping(value /stream, produces text/event-stream) public FluxString streamChat(RequestParam(msg) String message) { return streamingChatService.streamChat(message); }注意这里使用了text/event-stream作为响应类型这是标准的事件流格式。前端用 EventSource 或 fetch API 的流式读取就能直接接住这个数据源。不过有一点需要提醒流式输出的调试难度会明显比一次性返回要高一些很多代理和网关默认会缓存响应导致你在本地调试的时候收不到流式效果所以如果没看到逐字输出的效果先检查一下自己的网络链路是最自然的排查方向。5. 为模型配置合适的参数与提示词5.1 最核心的参数temperature 和 model 怎么理解配置里最常见的两个参数是 model 和 temperature。model 参数很好理解它告诉框架要调用哪个模型。单论 OpenAI 这边的模型不同模型专精方向不一样价格和响应速度差别也比较大。作为开发者需要根据自己的业务场景选择成本与效果之间的平衡点。temperature 参数是模型生成随机性的一个控制旋钮。它的取值范围通常在 0 到 1 之间。值越低模型的输出越保守、越确定适合代码生成、数据提取、分类处理等要求稳定的场景。值越高模型的输出越有创造性适合写文案、头脑风暴、创意生成这类需要灵活度的场景。举几个实际例子感受一下。比如你要模型帮你写一段 SQL或者做一份信息抽取temperature 设成 0.2 左右比较稳妥。如果你想让它帮忙生成一段营销口号、写一个小红书风格的种草文案temperature 调到 0.8 甚至更高出来的文字会更加放飞。5.2 在配置类里动态设置参数参数除了写在 yml 里做全局配置也支持在代码里按需覆盖public String creativeChat(String message) { return chatClient.prompt() .user(message) .options(OpenAiChatOptions.builder() .withModel(gpt-4o-mini) .withTemperature(0.9) .build()) .call() .content(); }这样做的好处是同一个应用里可以根据不同业务场景切换不同模型配置。比如一个接口走精准回答模式另一个接口走创意发散模式两者互不影响。这里要注意一下新版 SDK 中 OpenAiChatOptions 的具体名称和 builder 风格可能会随版本变化不同版本的 API 存在差异。我在文档里看到过新旧版本写法的切换提醒所以如果你发现 withModel 方法在当前版本里报错查一下对应版本的 release note 就能找到原因。5.3 让系统提示词发挥应有的作用系统提示词是决定模型输出质量和风格的重要组件但也是最容易被新手忽略的。很多人一上来直接把用户问题扔给模型然后抱怨结果不够理想其实大概率是系统提示词没写好。一个好的系统提示词应该具备这些特征明确角色定位、说明任务目标、界定输出格式、针对错误情况做约束。举个具体例子如果你希望模型帮你做信息抽检String systemPrompt 你是一个信息提取助手。请从用户输入中提取出 1. 日期格式为 YYYY-MM-DD 2. 金额以元为单位 3. 相关主体名称 如果找不到对应信息请返回未提取到。 只输出 JSON 格式的结果不要输出其他内容。 ;在实际项目里系统提示词通常会放在配置中心或单独的提示词管理模块里方便运营人员或产品同学调整同时也方便做版本管理。这也是有经验的团队和刚开始接触 AI 应用的团队在工程化程度上的一个常见差异。6. 结合大模型开发中常见的扩展点6.1 让模型输出结构化数据不走 JSON 解析的老路如果你只是简单地让模型返回文本然后用正则去抠里面的信息那你很快就会遇到问题 —— 模型的回答格式天然不稳定偶尔多一个解释、少一个字段你的解析代码就会出错。Spring AI 提供了结构化输出的能力你可以在调用时直接告诉模型你想要的返回类型框架会负责把模型返回的内容转换成对应的对象。public record StockInfo(String name, String code, double latestPrice) { } public StockInfo getStockInfo(String stockName) { return chatClient.prompt() .user(查询 stockName 的最新信息) .call() .entity(StockInfo.class); }当你调用 entity 方法时框架会要求模型按照适合转化为这个类型的方式来组织回答内容然后再做解析。这样一来输出的数据可以直接输入到业务代码里使用省掉了面对文本时棘手的处理工作。有一点需要说明在实际使用中模型返回内容的可解析性并不总是百分百保证。即使框架有人工干预能力模型也可能偶尔输出一些不符合预期的内容导致转换报错。所以生产环境里强烈建议对转换结果做一次数据校验兜底也就是设置用户提示与模型输出之间的约束结合异常捕获和重试机制。6.2 给对话注入记忆能力让多轮问答保持连贯默认情况下每次 call 都是没记忆的独立请求。这就相当于你在跟一个初次见面的陌生人说话你说一句他答一句但完全不记得你们刚才说过什么。要让对话有记忆就需要引入 ChatMemory。Spring AI 提供了内存版本的实现适合单机小规模场景同时也支持接入向量数据库来做大规模持久化。Configuration public class ChatMemoryConfig { Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } }然后在构建 ChatClient 时指定Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultSystem(你是一个友好的 AI 助手) .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); }使用这个模式后后续的对话上下文就会自动带上之前的部分。需要注意的一个坑是内存是有上限的而且不带持久化服务一重启历史对话就全部清空。所以当你发现对话时间长了之后发出去的内容越来越长、费用越来越高就要考虑对话窗口管理策略比如接向量数据库做历史记录归档或者只装配最近几轮对话。6.3 Output Content 的自定义与内容安全使用框架时还需要注意内容输出侧的合规控制。Spring AI 允许我们通过配置来对输出进行过滤和审核比如对包含敏感信息的响应进行屏蔽。这是面向真实用户的产品上线时必经的一环不要等到上线被反馈了才想起来处理。一个简单的过滤实现示例public class SafeWordFilter implements ContentFilter { private final ListString blacklist List.of(违禁词1, 违禁词2); Override public String filter(String content) { for (String word : blacklist) { if (content.contains(word)) { return 抱歉无法回答该问题。; } } return content; } }从更广义的角度来说AIGC 应用的安全是个系统工程需要在提示词约束、输入过滤、输出审核多个层面做防御。作为开发者这个意识从第一个应用上线前就要建立起来。7. 实际落地运行与常见问题排查实录7.1 完整跑通一次推理过程从创建项目到最后的实际操作建议按这个顺序走一遍创建 Spring Boot 3.x 项目在 pom.xml 中加入 Spring AI 依赖在 application.yml 中写入模型供应商配置和 API Key启动应用观察控制台日志编写一个最简单的测试接口验证连通性我第一次跑通的时候其实卡在了依赖版本上花了十几分钟才意识到是 Spring AI 版本不兼容。后来学聪明了统一到官方文档对应的版本组合上再动手这样最省时间。7.2 高频踩坑点整理按优先级排列这里总结一些我实际开发中遇到过的问题和解决办法。问题一配置文件不生效症状表现是服务能启动但调用时报出模型相关错误。排查思路优先检查 spring.ai 的配置前缀路径是否正确版本较低时配置项路径经历过调整很可能你已经配了但框架压根没读到。解决办法在配置类中临时加一个日志输出确认配置属性是否被正确加载。问题二API Key 无效或过期异常信息通常会直接提示鉴权失败。排查方式先手工调一下模型服务商提供的测试接口确认 key 本身可用再确认 base-url 是否正确。常见的低级错误包括 key 前后带了空格、环境变量没生效。问题三请求超时企业网络环境下比较常见。默认的 HTTP 客户端超时时间可能太短大模型推理本身又比较慢。解决办法给 RestClient 配置或底层 HTTP 客户端设置更长的连接和读取超时时间。问题四返回内容被截断这一般和 maxTokens 参数有关。这个参数控制模型最多生成的 token 数量如果设置过小长回答就会被截断。适当调大即可。注意这个值同时影响响应速度和成本。问题五中文乱码或 Unsupported Media Type排查一下 Content-Type 的编码设置以及是否引入了正确的消息转换器。Spring Boot 默认配置一般没问题但如果你手动改了消息转换链就容易出这种问题。7.3 对比一次调用与流式调用的表现差异为了更直观地感受两种方式的区别我做了一组简单对比。同样的提问、同样的模型、同样的 temperature不同调用方式在客户端观察到的体验差异明显对比项一次性 call流式 stream首字返回速度需要等完整结果耗时较长首个 token 返回更快用户体验长时间白屏等待反馈弱逐字输出交互感强实现复杂度低略高需要处理流式编程适用场景后端任务、异步处理、简单问答聊天机器人、实时对话、生成式内容个人建议面向用户交互的界面优先考虑流式实现后端内部逻辑用一次调用即可。至于是用 WebFlux 还是 SSE取决于你的整体技术栈不要为了流式而流式。7.4 ChatClient 还支持哪些实用 APIChatClient 除了基础的 call、stream、prompt还有几个实用操作值得关注。比如你可以为它设置默认消息顾问或默认系统提示词这样在构建 Client 的时候就把统一规范定好不用每次调用都重复写Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一个乐于助人的助手) .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); }另外在流式的调用中可以使用stream().response()获取包含更多元数据的响应对象而不只是纯文本。这在做调用链追踪和计费统计的时候比较好用。还能通过.entity()方法配合 Java Record 或自定义 POJO 实现强类型数据映射前面已经提到过。建议把这几个 API 组合起来用能覆盖绝大多数业务需求。8. 从入门到进阶还有哪些方向值得探索首篇教程做的是最基础的对话链路。当你把这套流程跑通之后后续可以沿着如下几个方向继续深入第一个方向是接入更多的模型服务商。Spring AI 的抽象层已经让切换成本大幅降低你可以把多家模型供应商都配置好然后做模型路由根据不同的任务类型选择不同性价比的模型。比如一个应用里简单情感分析走快而省的小模型复杂逻辑推理走能力强的大模型配合路由策略能省不少钱。第二个方向是结合向量数据库做私有知识库问答。这是目前企业落地最广的场景之一。简单说说思路把企业文档切块、向量化后存入向量数据库用户提问时先做相似度检索再把检索结果连同问题一起发给大模型让它基于这些资料进行回答。这也就是 RAG 的基本过程。Spring AI 在这个方向也有对应的组件支持。第三个方向是把 AI 能力接入更复杂的业务流程。比如让模型自主决定调用哪些工具这就是 Function Calling 或者说 Tool Calling。你把自己的业务方法注册为 AI 可调用的工具让模型根据用户的意图自主选择并调用这些工具再把得到的结果组织成自然语言回复。这对企业内部的信息整合、任务自动化非常有价值。第四个方向是关注工程化治理。AI 应用上线后的可观测性、提示词的版本管理、调用链追踪、成本监控、模型评估等这些在 demo 阶段不会被注意到但进入生产环境后每一个都足以让团队头疼。Spring AI 提供了部分可观测性接口也可以接入主流日志平台来完善。最后再聊一点我在实践中的体会教程到了这里核心的入门内容已经完整覆盖了。从配置项目到跑通对话接口从理解内部运行逻辑到处理常见报错已经能够支撑你完成一个具备基本功能的应用。最后想分享一点个人的经验刚开始接触 Spring AI 的时候不建议一上来就追着新特性跑先把最基础的能力吃透特别是 ChatClient 的 API 风格和配置原理后面碰到任何问题都能顺着链路去排查。实际开发中我自己的体会是Spring AI 最大的魅力不是帮你省几行代码而是给了 Java 生态一个拥抱 AI 的标准方式。过去 Java 在 AI 应用开发领域相对于 Python 生态存在感弱一些大部分人的第一反应是 Python FastAPI LangChain。Spring AI 的成熟让后端团队可以直接在原有的 Spring Cloud、微服务架构里平滑地融入 AI 能力不需要额外搭建一套新的服务架构这对大量存量系统来说价值非常高。如果你正准备在现有项目里接入 AI 能力我的建议是先不要急着把模型接入核心链路可以先搭一个独立的小模块把这篇教程的示例跑通然后逐步探索多轮对话、流式输出、结构化输出这些基础能力等技术积累到一定程度再考虑大规模引入。知识库问答和工具调用这些进阶玩法等基础打得足够牢之后再慢慢推进也不迟。后续我还会继续分享更多实战内容希望能对你的项目落地带来切实的帮助。