尧图精选

Spring AI 实战入门:从零构建 Java AI 应用与流式输出

🕒 发布时间:2026/10/1 3:48:53 📁 来源:尧图网络
1. 为什么 Java 开发者现在该认真看一眼 Spring AIJava 生态里做 AI 集成这件事过去两年一直有点尴尬。Python 那边 LangChain、LlamaIndex 玩得风生水起Java 开发者想接个大模型要么自己封装 HTTP 客户端要么在项目里塞一堆非 Spring 风格的胶水代码维护起来相当别扭。Spring AI 出现之后这个局面算是有了正经的解法——它把大模型调用抽象成了 Spring 生态里熟悉的 Bean、Template、Advisor 这一套东西写起来跟用 JdbcTemplate 或者 RestTemplate 的感觉差不多。这篇内容面向的是有 Java 和 Spring Boot 基础、但还没真正上手过 Spring AI 的开发者。我会从零开始把构建第一个 Java AI 应用的完整路径走一遍环境怎么搭、ChatClient 怎么配、Prompt 怎么写、流式输出怎么做、常见坑怎么排。中间涉及到的参数选择、依赖取舍、代码组织方式我都会把背后的理由讲清楚而不是只丢一段能跑的代码给你。需要先说明一点Spring AI 目前迭代速度很快1.0 之前的版本 API 变动比较频繁我下面用的写法基于较新的稳定版本如果你用的是更早的里程碑版本部分类名和方法签名可能会有差异遇到对不上的地方优先查官方文档的对应版本说明。2. 环境准备与项目骨架搭建2.1 JDK 与构建工具的最低要求Spring AI 对 JDK 的要求跟着 Spring Boot 走。当前主流版本要求 JDK 17 起步如果你还在用 JDK 8 或者 11第一步就是升级。这不是可选项因为 Spring AI 内部大量使用了 record、密封接口、文本块这些新特性低版本 JDK 直接编译不过。构建工具用 Maven 或 Gradle 都行我个人偏向 Maven因为 Spring AI 的 BOM 在 Maven 里引入比较直观。Gradle 用户注意一下依赖管理需要用 platform 语法引入 BOM否则版本号得一个个手写很容易出现某个模块版本对不齐导致 NoSuchMethodError。具体版本选择上我的建议是组件推荐版本说明JDK17 或 2121 是 LTS虚拟线程对高并发调用有帮助Spring Boot3.2.x 及以上3.2 之前的部分自动配置不完整Spring AI最新稳定版里程碑版本慎用于生产构建工具Maven 3.8低版本对 BOM 支持有瑕疵提示不要混用 Spring Boot 2.x 和 Spring AISpring AI 的自动配置类是基于 Spring Boot 3 的 AutoConfiguration.imports 机制注册的2.x 根本加载不到。2.2 用 Spring Initializr 生成骨架的正确姿势最省事的起步方式还是 Spring Initializr。打开网页或者用 IDE 内置的创建向导选好 Maven、JDK 17、Spring Boot 3.2.x然后在依赖里勾选 Spring Web后面做流式接口要用和 Spring AI 相关的 starter。这里有个细节很多人会踩Spring AI 的 starter 不在默认的依赖列表里需要手动在 pom.xml 里加仓库配置。因为部分版本还托管在 Spring 的里程碑仓库如果你的项目拉不到依赖八成是仓库没配。稳妥的做法是在 pom.xml 里显式声明repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshots enabledfalse/enabled /snapshots /repository /repositories然后在 dependencyManagement 里引入 Spring AI 的 BOM这样后面加具体模型 starter 的时候就不用写版本号了。BOM 的好处是统一版本避免你手动指定 openai starter 是 0.8.0、core 是 0.9.0 这种错配。2.3 模型 starter 的选择逻辑Spring AI 支持多种模型提供方OpenAI、Azure OpenAI、Anthropic、Ollama、以及国内的几家都有对应的 starter。选哪个取决于你的实际场景想快速验证、不折腾本地环境用 OpenAI 的 starter配一个 API Key 就能跑。数据不能出内网、要本地推理用 Ollama本地拉个模型跑起来starter 直接连本地端口。企业内已经有 Azure 资源用 Azure OpenAI starter配置项多一些但合规性好。我下面以 OpenAI starter 为主线演示因为它的 API 最典型换成其他 starter 时核心的 ChatClient 用法几乎不变只是配置项前缀和模型名不一样。这个抽象层设计正是 Spring AI 的价值所在——业务代码不绑死具体厂商。3. ChatClient 核心用法拆解3.1 ChatClient 与 ChatModel 的关系刚接触 Spring AI 的人容易把 ChatClient 和 ChatModel 搞混。简单说ChatModel 是底层接口负责真正跟模型服务通信不同厂商有不同实现ChatClient 是上层门面提供 fluent API让你用链式调用的方式组织请求。类比一下ChatModel 像 JdbcTemplate 底层的 DataSourceChatClient 像你直接用的 JdbcTemplate。实际写业务代码时绝大多数情况你只需要注入 ChatClient不用直接碰 ChatModel。Spring AI 的自动配置会根据你引入的 starter 自动创建对应的 ChatModel Bean然后你可以基于它构建一个 ChatClientConfiguration public class AiConfig { Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem(你是一个严谨的 Java 技术助手回答尽量给出可运行的代码示例。) .build(); } }这里用 builder 而不是直接 new是因为 builder 允许你设置默认的 system prompt、默认的 advisor、默认的选项参数。把 system prompt 放在这里统一管理比每次调用都手写一遍要清爽得多也方便后续统一调整语气和约束。3.2 一次完整调用的代码结构注入 ChatClient 之后最简单的调用长这样Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient chatClient) { this.chatClient chatClient; } public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); } }这段代码里有几个关键点值得展开。prompt()开启一次请求构建user()设置用户消息call()表示同步调用content()取出模型返回的文本内容。整条链是懒执行的只有到call()或者stream()的时候才真正发请求。如果你需要更细的控制比如同时设置 system 和 user 消息可以这样String answer chatClient.prompt() .system(用不超过三句话回答) .user(解释一下什么是 AQS) .call() .content();注意.system()会覆盖掉 builder 里设置的默认 system prompt如果你希望保留默认的再追加得用 advisor 的方式处理这个后面讲。3.3 Prompt 模板与参数化硬编码 prompt 在 demo 里没问题真实项目里几乎一定要用模板。Spring AI 提供了 PromptTemplate支持占位符替换PromptTemplate template new PromptTemplate( 请用{style}的风格解释{concept}这个概念控制在{words}字以内。 ); Prompt prompt template.create(Map.of( style, 通俗易懂, concept, 面向对象编程, words, 200 )); String result chatClient.prompt(prompt).call().content();用模板的好处是 prompt 可以外置到配置文件或者数据库改文案不用重新编译。我一般会把常用的 prompt 模板放在 resources 目录下的独立文件里通过 Resource 加载这样产品和运营也能参与调整。注意占位符的 key 如果传了 nullPromptTemplate 默认会抛异常。如果你的参数可能为空要么提前做默认值处理要么在模板里用条件语法别指望它自动忽略。3.4 结构化输出让模型返回对象而不是字符串直接拿字符串在很多场景下不够用比如你想让模型返回一个 JSON 然后映射成 Java 对象。Spring AI 提供了.entity()方法做这件事record BookInfo(String title, String author, int year) {} BookInfo info chatClient.prompt() .user(给我介绍一下《Effective Java》这本书的作者和出版年份) .call() .entity(BookInfo.class);底层它会自动在 prompt 里追加格式说明然后把返回的文本反序列化成你指定的类型。实测下来对于 record 和简单的 POJO 效果不错但字段一多、嵌套一深模型偶尔会漏字段或者格式跑偏。我的经验是结构化输出尽量保持扁平字段控制在五六个以内嵌套层级不要超过两层稳定性会好很多。4. 流式输出与接口层实现4.1 为什么流式输出是刚需大模型生成一段几百字的回答同步调用可能要等好几秒甚至十几秒。用户盯着一个转圈的加载图标等十秒体验是很差的。流式输出让内容一个字一个字往外蹦首字延迟通常在一秒以内感知上快很多。做聊天类应用流式基本是标配。Spring AI 的流式调用用.stream()替代.call()返回的是一个 Flux响应式流FluxString stream chatClient.prompt() .user(讲讲 Java 的垃圾回收机制) .stream() .content();这里返回的 Flux 每个元素是一小段文本增量不是完整句子前端拼接起来才是完整回答。4.2 用 SSE 把流推到前端后端要做的就是把 Flux 转成 SSEServer-Sent Events推给浏览器。Spring MVC 和 WebFlux 的写法略有不同用 WebFlux 更自然RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String q) { return chatClient.prompt() .user(q) .stream() .content(); } }前端用 EventSource 接收即可。这里有个容易忽略的点produces必须显式声明为text/event-stream否则浏览器不会按 SSE 解析你会看到一堆原始文本堆在一起。4.3 流式场景下的异常处理流式接口的异常处理比同步麻烦因为响应头可能已经发出去了这时候再想返回一个 500 错误页已经来不及。我的做法是在 Flux 上加onErrorResume把异常转成一段友好的文本推给前端return chatClient.prompt() .user(q) .stream() .content() .onErrorResume(e - Flux.just([生成中断] e.getMessage()));同时后端要打日志记录完整堆栈方便排查。前端收到这种带标记的文本时可以特殊渲染成错误提示样式。提示流式接口不要设置过短的超时。有些网关默认 30 秒超时长回答会被截断。检查一下你的 Nginx 或者网关配置里的 proxy_read_timeout。5. 常见问题排查与避坑经验5.1 依赖冲突与 Bean 找不到最常见的报错是启动时提示找不到 ChatModel 或者 ChatClient 相关的 Bean。原因通常有三个一是 starter 没引对比如引了 core 但没引具体厂商的 starter二是 API Key 没配自动配置因为缺少必要属性而跳过了 Bean 创建三是包扫描路径不对自定义的配置类没被扫到。排查顺序建议这样走先看启动日志里有没有ChatModel相关的自动配置报告Spring Boot 的--debug模式会打印条件评估结果能直接告诉你哪个条件没满足。然后检查 application.yml 里的配置前缀是否正确不同 starter 的前缀不一样OpenAI 是spring.ai.openaiOllama 是spring.ai.ollama写错了不会报错只会静默不生效。5.2 Prompt 被拦截或返回异常有时候请求发出去返回的是一段错误提示说 prompt 违反了使用政策。这种情况多半是 prompt 里包含了敏感词或者被模型判定为不当请求。排查时先把 prompt 简化到最短确认基础调用没问题再逐步加回内容定位是哪部分触发的。另外注意 prompt 长度。每个模型都有上下文窗口限制超了会直接报错。粗略估算一个中文字符大约对应 1 到 2 个 token英文单词大约 1.3 个 token。如果你要拼接很长的历史对话记得做截断或者摘要别一股脑全塞进去。5.3 常见问题速查表现象可能原因处理方式启动报找不到 ChatModelstarter 未引入或 Key 未配检查依赖和配置前缀调用返回 401API Key 无效或过期重新生成 Key 并更新配置流式接口无输出produces 未声明或网关超时检查注解和网关超时设置结构化输出字段缺失prompt 约束不够或模型能力不足简化结构、加强格式说明响应特别慢模型选择或网络问题换更小的模型或检查网络链路中文乱码编码未统一为 UTF-8检查请求和响应编码配置5.4 几个我踩过的坑第一个坑是 advisor 的顺序。Spring AI 的 advisor 链是有顺序的如果你同时用了日志 advisor 和记忆 advisor顺序不对会导致日志里看不到完整的上下文。默认顺序不一定符合你的预期必要时显式指定 order。第二个坑是对话记忆。默认情况下每次调用都是无状态的模型不记得上一轮说了什么。要做多轮对话得引入 ChatMemory 相关的 advisor并且注意内存的清理策略否则长时间运行会越占越多。第三个坑是并发。ChatClient 本身是线程安全的可以单例注入。但如果你在 advisor 里放了可变状态并发下就会出问题。我见过有人在自定义 advisor 里用一个成员变量存当前请求的上下文高并发时串得一塌糊涂。记住 advisor 要么无状态要么用 ThreadLocal 隔离。6. 从 Demo 到可用应用的扩展方向跑通第一个应用之后往生产方向走还有几块要补。一是重试和降级模型服务偶尔会抖动加个带退避的重试策略能显著提升稳定性Spring AI 本身对 RetryTemplate 有支持配置一下就行。二是可观测性把每次调用的耗时、token 消耗、模型名打成指标接进 Micrometer后面做成本分析和容量规划都用得上。三是 prompt 的版本管理把 prompt 当代码一样管理改动走评审出问题能回滚。RAG 是另一个绕不开的方向。单纯靠模型自身知识回答企业私有领域的问题往往不准。把文档切块、向量化、存进向量库检索后再拼进 prompt这套流程 Spring AI 有对应的模块支持Advisor 里也有现成的 QuestionAnswerAdvisor 可以用。不过 RAG 的坑主要在切块策略和检索质量上这块展开又是另一个话题了。至于到底用 Spring AI 还是别的 Java 方案我的判断标准很简单如果你的项目本来就是 Spring Boot 技术栈团队熟悉 Spring 的编程模型那 Spring AI 的迁移成本最低心智负担最小。它的抽象层设计让你在换模型厂商时几乎不用改业务代码这个价值在模型快速迭代的当下很实在。最后分享一个实际用下来的小技巧把 system prompt 里加上明确的输出格式约束和角色设定比事后用代码去清洗模型输出要省事得多。比如要求只返回 JSON不要任何解释性文字能省掉大量解析异常的处理逻辑。模型这东西你越早把规矩立清楚后面越省心。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →