
做 Java 后端的人应该都有一种感觉AI 大模型火了两三年但 Spring 生态里一直缺一个“官方答案”。Python 那边有各种 Agent 框架、工具链Java 这边大多数时候还是在 Service 层里硬编码 HTTP 调用自己拼 JSON、自己处理流式响应、自己维护对话历史。Spring AI 的出现直接把这个局面改变了。它是 Spring 官方面向 AI 应用的一站式框架把对接大模型这件事做成了类似 JDBC 统一数据库访问的样子。这篇文章会从零开始带你搞清楚 Spring AI 到底是什么、它的核心抽象设计成什么样、怎么在 Spring Boot 3 项目里几分钟跑通第一个对话案例再逐步拆解提示词模板、结构化输出、上下文记忆这几个高频需求。无论你是刚接触 AI 的 Java 新人还是已经在项目里做过 AI 集成的老手这篇文章都值得看完因为接下来这些内容基本是日常开发里一定会碰到的。1. Spring AI 到底是什么它解决了哪个痛点1.1 从“手写 HTTP 调用”到“统一抽象”先聊一个问题在没有 Spring AI 之前Java 项目接一个大模型要做什么第一你得照着某个大模型服务商的接口文档写一个 HTTP 客户端。第二你要定义好请求体、响应体的 DTO处理各种异常状态码。第三大模型接口的认证方式、参数格式、返回结构各不相同换一个模型厂商就得重写一遍客户端。第四流式输出需要用 SSE 解析不同厂商的 Event 格式还不一样。第五多轮对话的历史消息要自己维护提示词要拼字符串输出拿回来可能是 Markdown 格式的字符串你还得自己正则去提取关键信息。Spring AI 的核心思路就是把上面这些“脏活累活”全部收编成一套标准接口。你只需要面向 Spring AI 的抽象编程底层具体接哪家大模型由配置文件决定。这就好比 JDBC 让你不用关心底层是 MySQL 还是 PostgreSQLSpring AI 让你不用关心模型服务商换了谁你的业务代码可以保持稳定。1.2 核心抽象Model、Prompt 与 ResponseSpring AI 的设计并不复杂核心就几个概念ChatModel统一的大语言模型调用入口不管底层是哪个厂商对外都是call(Prompt)返回ChatResponse。Prompt把用户消息、系统消息、历史消息、模型参数全部封装在一起一次调用就是一个完整请求。ChatResponse模型的返回结果里面包含生成内容、Token 用量等元信息。ChatClient在ChatModel之上封装出的流式 API风格更贴近业务代码官方推荐日常开发优先用这个。PromptTemplate提示词模板类似 Java 里的字符串模板把动态参数注入到 Prompt 里。StructuredOutputConverter把模型返回的文本转换成 Java 对象省去手动解析 JSON 的步骤。EmbeddingModel与VectorStore负责文本向量化和向量存储这是做 RAG 的基础。理解这套抽象之后你会发现 Spring AI 本质上是“一套 API适配多家大模型”而不是“某个大模型的专用 SDK”。它把模型之间的差异隔离在了一层层适配器里你的业务代码只依赖 Spring AI 的接口。1.3 和直接调 HTTP 接口比优势到底在哪我见过不少团队在项目里自己封装过一个 LLM 工具类刚开始感觉挺顺手后来换了模型服务商或者加了流式响应、多轮记忆、结构化输出就开始疯狂打补丁。自己封装的问题在于你永远在重复造轮子而且造出来的轮子很难覆盖所有边界情况。Spring AI 的优势有几个特别明显自动装配配置即用。只要引入对应的 starter配置好密钥Spring 容器里自动就有了ChatModel、ChatClient这些 Bean不需要手写任何客户端初始化代码。流式输出开箱即用。ChatClient.stream()返回的是响应式流配合 Spring WebFlux 很容易推给前端。可插拔的模型适配。今天用 A 厂商的模型明天想换 B 厂商改一个配置项就行别的都不用动。贴近 Spring 生态。模型调用的超时控制、重试、Actuator 监控这些能力可以直接复用 Spring 积累多年的基础设施这是个人封装完全不具备的。2. 动手前的准备环境、依赖与配置2.1 技术栈要求与版本选择Spring AI 从诞生开始就是一个“吃新版本”的框架它的目标环境是 Java 17 及以上、Spring Boot 3.x。如果你还在用 Java 8 加 Spring Boot 2.x那很遗憾这个框架用不了你需要先把项目整体升级上来。这里有一个很重要的版本认知Spring AI 的版本迭代节奏非常快早期版本叫0.8.x后来走完了1.0.0-M1、1.0.0-M2、1.0.0-RC1这些里程碑最终发布了1.0.0正式版。不同版本的 Maven 坐标差异很大网上很多教程停留在旧版本你照着写大概率直接依赖冲突。我个人建议直接用 1.0.0 GA 及以上版本API 已经稳定官方文档也最齐全。2.2 Maven 依赖坐标到底怎么写在 1.0.0 正式版里官方把之前的各种 starter 做了整合核心依赖写法如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent properties java.version17/java.version /properties 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注意看这里没有写version因为从 1.0.0 版本开始Spring AI 的依赖版本已经通过 Spring Boot 的 BOMBill of Materials统一管理了。如果你用的 Spring Boot 版本是 3.3.x 或 3.4.xAI starter 版本会自动对齐不需要自己指定这样可以最大程度避免版本不匹配的问题。如果你接的是本地模型方案那对应的 starter 名字会不一样这个留到后面第 5 部分排查案例里专门说。2.3 API Key 与基础配置的正确姿势接下来是配置。在application.yml里最基础的配置长这样spring: application: name: spring-ai-demo ai: model: chat: openai openai: api-key: ${LLM_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7这里要特别强调一个安全习惯api-key千万不要硬编码在配置文件里提交到代码仓库。用${LLM_API_KEY}这种方式引用环境变量本地开发的时候在 IDE 里配置环境变量服务器部署的时候在运维平台配置密钥始终不进 Git。temperature是控制模型随机性的参数取值 0 到 1 之间值越低回答越稳定保守值越高越有创造性。如果你做的是知识问答类应用建议调到 0.2 到 0.3如果做创意文案生成可以调到 0.8 以上。这属于工程上的调参经验不是官方硬性规定但实测下来对回答质量影响很大。3. 第一个入门案例让代码和模型说上话3.1 先跑通一个最基础的对话配置完成后我们写一个最简单的调用。在 Spring AI 1.0 的 API 设计里最推荐的方式是注入ChatClient它的链式调用风格非常贴合业务逻辑。Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }看到这段代码你会觉得这几乎就像在写自然语言。prompt()开始构建一次对话user()塞入用户消息call()发起同步调用content()取出模型返回的文本内容。然后写一个 Controller 暴露接口RestController RequestMapping(/api/chat) public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService chatService; } GetMapping(/ask) public String ask(RequestParam String message) { return chatService.chat(message); } }启动项目后浏览器访问http://localhost:8080/api/chat/ask?message请用一句话介绍Spring AI这时候你就会看到模型返回的内容出现在页面上。整个调用链路在 Spring AI 内部已经处理好了参数封装、HTTP 调用、响应解析、异常处理你什么都没干就完成了第一次大模型集成。3.2 换一种更“底层”的写法ChatModelChatClient是官方推荐的脚手架但它的底层其实是ChatModel。在某些特殊场景下比如你想直接控制Prompt对象、自己处理元信息可以绕过ChatClient直接用ChatModelService public class RawChatService { private final ChatModel chatModel; public RawChatService(ChatModel chatModel) { this.chatModel chatModel; } public String chat(String userMessage) { Prompt prompt new Prompt(userMessage); ChatResponse response chatModel.call(prompt); return response.getResult().getOutput().getText(); } }两种写法各有适用场景。ChatClient适合绝大多数业务代码因为它把重复操作封装得更好ChatModel适合进阶使用者让你能直接操作更底层的 Prompt 和 Response 结构。我个人的建议是新项目默认用ChatClient碰到特殊需求再往下降级使用ChatModel。3.3 流式输出从等待到实时显示上面那个例子是同步调用模型要把完整内容生成完才返回。但在真实产品体验里这种“一直转圈等结果”的方式已经过时了现在的主流体验是“打字机式”逐字输出。Spring AI 对这块的支持非常成熟GetMapping(/stream) public FluxString stream(RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }只需要把call()换成stream()返回值从String变成FluxStringSpring WebFlux 会自动以 SSEServer-Sent Events的格式把内容推给前端。前端接上 EventSource 或者 fetch 流式读取就能实现逐字显示。这里有个容易踩的坑如果你用的是spring-boot-starter-web而 Controller 方法返回Flux底层其实是异步 Servlet 支持的也是可行的。一旦引入spring-boot-starter-webflux会更好。但如果项目里同时引入了 Web MVC 和 WebFluxSpring Boot 会优先选择 WebFlux可能导致原来 Web MVC 的接口行为发生变化。这个在混合架构项目里要特别小心。4. 进阶案例从“能对话”到“能干活”4.1 提示词模板把 Prompt 从代码里抽出来第一个案例跑通之后你会发现如果每次都在代码里拼 Prompt很快会变成一场灾难。业务场景里的 Prompt 动辄几百字里面有系统角色设定、业务背景、输出格式要求、示例样本硬拼字符串既难维护又容易出错。Spring AI 提供了PromptTemplate用法和 Java 的字符串模板非常像public String generateWithTemplate(String question) { PromptTemplate template new PromptTemplate( 你是一位拥有十年经验的{role}专家。 请用{language}回答以下问题回答要条理清晰、通俗易懂 {question} ); Prompt prompt template.create(Map.of( role, Java后端架构师, language, 简体中文, question, question )); return chatClient.prompt(prompt).call().content(); }把参数用{变量名}占位然后通过Map注入实际值Prompt 的结构一目了然。我更推荐的做法是把这些模板放在单独的资源目录里比如src/main/resources/prompts/customer-service.st然后用Resource读取这样业务人员也能参与模板维护而不是只靠程序员在代码里改字符串。4.2 结构化输出把模型回复变成 Java 对象大模型输出的本质是文本但业务系统需要的是结构化数据。比如你让模型分析一段用户评价希望它返回“情感倾向、关键词列表、打分”这三个字段如果模型返回一段 Markdown 文本你还得自己写解析逻辑这里最容易出 bug。Spring AI 的BeanOutputConverter可以帮你把模型输出直接绑定到 Java 对象上。先定义一个 recordpublic record ReviewAnalysis( String sentiment, ListString keywords, int score ) {}然后这样调用public ReviewAnalysis analyzeReview(String reviewContent) { BeanOutputConverterReviewAnalysis converter new BeanOutputConverter(ReviewAnalysis.class); String promptContent 请分析以下用户评价提取情感倾向、关键词和评分1-5分 %s %s .formatted(reviewContent, converter.getFormat()); Prompt prompt new Prompt(promptContent); ChatResponse response chatModel.call(prompt); return converter.convert(response.getResult().getOutput().getText()); }关键点在于converter.getFormat()它会在 Prompt 末尾自动附加一段格式说明引导模型输出标准的 JSON 结构然后convert方法负责把 JSON 反序列化成 Java 对象。这个过程中你不用自己写一行 JSON 解析代码也不用担心模型输出里的代码块标记干扰解析。4.3 上下文记忆让多轮对话不再“失忆”大模型本身是无状态的每一次调用都是独立的。做聊天机器人时你必须自己把历史消息传给模型否则上一句说的话模型根本记不住。Spring AI 提供了MessageWindowChatMemory可以把它注入到ChatClient里实现自动维护对话历史Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(10) .build(); } Bean public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) { return ChatClient.builder(chatModel) .defaultChatMemory(chatMemory) .build(); }配置好之后同一个会话内的多轮对话就会自动携带历史消息。这里的maxMessages(10)要特别注意不是指 10 轮对话而是指消息窗口里保留 10 条消息记录一般建议 5 到 10 轮。设得太长Token 消耗会非常大设得太短模型的“短期记忆”又不够用。如果你不想引入状态管理也可以手动拼接历史消息类似把之前几轮的user和assistant消息放进Prompt里。这种方式更简单但所有状态管理逻辑都得自己写适合对会话状态有特殊要求的场景。5. 常见问题与排查经验5.1 API Key 报错与认证失败这是新手遇到最多的问题。错误现象调用时报401 Unauthorized或403 Forbidden。排查顺序先确认环境变量LLM_API_KEY是否正确设置很多 IDE 里重启应用后环境变量才会生效。再确认配置文件里的spring.ai.openai.api-key有没有被正确读取可以在启动日志里看看有没有报“API key not configured”之类的提示。如果在云服务器上部署还要确认密钥对应的服务是否开通了访问权限。一个很容易被忽略的细节spring.ai.model.chatopenai这一行配置决定了底层加载的是哪一个ChatModel实现。如果配置写成了别的值比如本地模型方案但代码里注入的却是 OpenAI 的ChatClient启动时可能不报错但调用时行为会非常诡异。5.2 模型名称与参数配置不当错误现象调用时报404 Not Found或Model Not Found。原因是模型名称没匹配上服务商实际提供的模型 ID。每个模型服务商的模型列表不一样同一个厂商还会分不同版本配置里写的模型 ID 必须和实际开通的完全一致。还有一类参数坑temperature、maxTokens、topP这些参数有的模型支持有的模型不支持。如果某个模型参数在服务商那边是只读的你硬传会导致400 Bad Request。我自己在项目里就遇到过换了一个新模型后之前好用的配置直接报错最后把非必要参数全部注释掉才恢复正常。给你一个排查模板先写死最简配置只保留api-key和模型名其他参数一律不设跑通之后再一项一项加回来。这样能快速定位是哪一个参数在捣乱。5.3 流式响应超时或断连错误现象流式输出时长时间没有数据返回或者输出到一半中断。这类问题大多数和网络环境有关常见原因有三个模型服务商接口响应慢超过了项目里配置的连接超时时间。服务端反向代理或网关不支持 SSE缓冲了所有数据导致前端看起来像“卡住”。模型生成的内容太长超过了maxTokens限制输出被强制截断。我一般会先在后端日志里确认调用状态码和耗时如果后端已经正常返回完整内容那问题基本出在中间链路如果后端本身也超时那就要调整 Spring 客户端的超时配置了。5.4 本地模型方案连不上如果你的场景必须完全本地化部署模型那不需要配置任何外部服务商的密钥。在 Spring AI 里选择本地模型方案时依赖坐标和配置方式会完全不同dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-ollama/artifactId /dependencyspring: ai: model: chat: ollama ollama: base-url: http://localhost:11434 chat: options: model: llama3.2这里给新手一个提醒本地模型方案先把模型文件下载好启动本地服务后用命令行工具直接测试接口是否通。假如直接测试都不通Spring AI 这边怎么调都不可能通排查顺序要从底层链路往上走。5.5 依赖冲突与版本兼容错误现象项目启动时报NoClassDefFoundError、BeanCreationException或者各种奇怪的类冲突。Spring AI 的依赖树比较复杂和 Guava、Jackson、Spring Cloud 等库的版本冲突很常见。我遇到过最典型的问题是项目里已经引入了老版本的某工具库Spring AI 需要新版本Maven 仲裁后选错了版本导致运行时方法找不到。排查方法很简单mvn dependency:tree -Dincludesorg.springframework.ai mvn dependency:tree -Dincludescom.fasterxml.jackson.core看输出结果重点检查有没有多个版本共存。如果有在 pom 里用dependencyManagement显式指定统一版本或者排除旧版本依赖。6. 最后分享几条实战心得文章写到这Spring AI 的入门链路已经完整了。最后聊几个比较主观但真实有用的经验。关于学习路径我的建议是先跑通ChatClient的同步调用再研究流式输出然后立刻做结构化输出。很多同学卡在“能对话”这一步之后不知道该做什么其实结构化输出才是把 AI 能力接入业务系统的关键。你想想一个客服系统要的是“工单标题、问题分类、紧急程度、建议解决方案”这些字段而不是一段没有规则的对话文本没有结构化输出AI 能力根本没法落地。关于调参我自己的经验是别迷信网上那些“最佳参数”每个业务场景适合的参数都不同。我见过同一个模型做知识问答时temperature0.1效果最好拿来写营销文案却需要0.9才能有亮点。你要做的是在项目里预留参数配置入口方便随时调整而不是在代码里硬编码。关于架构设计我强烈建议在 Spring AI 之上再抽象一层自己的服务接口。为什么因为底层模型服务商随时可能调整定价、模型下线、变更接口如果你所有业务代码都直接依赖 Spring AI 的 API未来换模型会很痛苦。正确的做法是把你的AiService接口作为业务边界Spring AI 只是接口实现里的一个组件这样无论底层怎么换上游业务代码完全不受影响。关于成本控制有一点可能很多人没意识到Spring AI 的ChatClient返回结果里有 Token 用量元数据。你不一定要用但建议至少在开发环境把每次请求的 Token 消耗记录下来跑一段时间你就能摸清业务的大致成本。尤其是生产环境的日志里这些数据能直接指导你做缓存策略——完全相同的请求缓存命中就能省掉一大笔模型调用费用。最后再赠你一个小技巧调试 Prompt 的时候让模型先把思考过程打出来再给结论。像“请先分析关键因素再给出最终答案”这样的提示往往能显著提升输出质量。这不是 Spring AI 特有的功能但结合结构化输出一起用效果出奇地好。把上面这些内容消化掉Spring AI 对你来说就不再是一个陌生的新框架了。从基础对话到流式输出从提示词模板到结构化数据从上下文记忆到问题排查这条完整的入门路径已经全部走通。剩下的就是在你自己的业务场景里多试、多踩坑、多总结这才是真正把 AI 能力变成工程能力的过程。