ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring AI 实战:用 Spring Boot 构建第一个 Java AI 应用

Spring AI 实战:用 Spring Boot 构建第一个 Java AI 应用 1. 为什么 Java 开发者现在该认真看一眼 Spring AI做 Java 后端的这几年我最大的感受是AI 能力正在从“另起一个 Python 服务”变成“直接长在现有业务系统里”。以前团队想接个大模型常见做法是单独搭一套 Python 服务Java 这边通过 HTTP 去调中间隔着一层网关、一层序列化、一层鉴权调试起来链路长得让人头大。Spring AI 出现之后这件事的逻辑变了——它把大模型调用抽象成了 Spring 生态里一个普通的 Bean你注入进来就能用跟注入一个 JdbcTemplate 没有本质区别。这篇内容我想聊的是怎么用 Spring Boot 加 Spring AI从零构建第一个能跑起来的 Java AI 应用。核心会围绕ChatClient、Prompt这两个最基础的抽象展开把依赖怎么引、配置怎么写、调用怎么发、返回怎么接、坑怎么避一条线讲透。适合已经有 Java 和 Spring Boot 基础、想快速把 AI 能力接进自己项目的同学如果你连 Spring Boot 的起步依赖都还没写过建议先把“第一个 Spring Boot 程序”跑通再回来不然中间一些自动配置的细节会看得云里雾里。需要先说明一点Spring AI 这个项目迭代非常快1.0 之前的版本 API 变动不小网上很多老教程里的ChatClient.builder()用法、配置项前缀都可能已经过时。我下面讲的是基于当前主流稳定版本的常见实践具体到你项目里务必以你引入的那个版本对应的官方文档为准。这一点很关键我踩过不止一次“照着博客写、编译不过”的坑。另外本文只讲通用的大模型接入思路和 Spring AI 的工程化用法不涉及任何特定地区、特定厂商的敏感内容选型上你根据自己团队合规要求来定即可。2. 整体设计思路为什么是 ChatClient 而不是直接发 HTTP2.1 从“手写 HTTP 调用”到“面向接口编程”的转变先说说不用 Spring AI 时一个 Java 应用调大模型长什么样。通常是这样拼一个 JSON 请求体里面塞 model、messages、temperature 这些字段然后用 RestTemplate 或 WebClient 发出去拿到响应再手动解析 choices 数组取出 content。这套流程能跑但问题很明显——每换一个模型厂商请求体结构、鉴权方式、返回格式都可能不一样你的业务代码里就会散落一堆 if-else 和字段映射。Spring AI 的核心价值就是把这层差异抽象掉。它定义了一套统一的接口比如ChatModel、ChatClient你面向接口编程底层换模型只需要换配置和依赖业务代码基本不动。这跟当年 Spring 用JdbcTemplate统一各种数据库驱动的思路是一脉相承的。我个人的判断是只要你的项目是 Spring 技术栈接入 AI 时优先考虑 Spring AI比自己在上面再封装一层要省事得多。2.2 ChatClient 与 ChatModel 的分工这里要理清两个容易混的概念。ChatModel是更底层、更贴近模型本身的接口它负责真正把请求发出去、把响应拿回来偏向“能力提供者”。而ChatClient是更高层的门面Facade它提供了流式fluent的调用风格让你能像写链式代码一样组织提示词、设置参数、处理响应。打个比方ChatModel像是发动机ChatClient像是方向盘和仪表盘。你日常开发绝大多数时候用的是ChatClient因为它更好用、更贴近业务表达。只有当你需要做一些非常底层的定制比如自己控制请求重试、自己处理原始响应结构时才会直接去碰ChatModel。理解这个分层后面看自动配置和 Bean 注入就不会乱。2.3 Prompt 的角色它不只是“一句话”很多人以为 Prompt 就是用户输入的那句话其实在 Spring AI 里Prompt是一个结构化的对象它包含了消息列表messages和可选的模型参数options。消息又分角色系统消息System、用户消息User、助手消息Assistant。系统消息用来设定模型的“人设”和约束用户消息是实际提问助手消息通常用于多轮对话时把历史回复带回去。为什么要把这个结构讲清楚因为实际项目里90% 的“模型回答不符合预期”问题根源都在 Prompt 的组织方式上而不是模型本身不行。你把系统消息写清楚、把上下文带对效果往往立竿见影。这也是我后面会重点展开的部分。3. 环境准备与依赖引入把地基打稳3.1 版本选择别一上来就追最新Spring Boot 和 Spring AI 的版本匹配是第一道坎。我的建议是Spring Boot 用当前主流的 3.x 稳定版Spring AI 用与之兼容的稳定版不要图新鲜去用快照版SNAPSHOT。快照版 API 随时可能变你今天写的代码明天可能就编译不过对于要落地到项目里的东西稳定性永远优先。具体怎么确认兼容性最靠谱的办法是去看 Spring AI 官方仓库的版本说明里面会写明它依赖的 Spring Boot 版本范围。我一般会先在本地建一个最小 Demo 工程把依赖引进去跑一个最简单的调用确认没问题了再往正式项目里搬。这个“先验证再迁移”的习惯帮我省了很多返工。3.2 依赖引入的两种方式如果你用的是 Maven核心依赖通常是一个 starter类似下面这样具体 artifactId 以你选的版本为准dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency注意这里我写的是“类似”因为不同版本、不同模型提供方的 starter 命名规则不一样。有的版本里模型 starter 的命名还经历过调整。所以你在引入时一定要对着官方文档的依赖坐标抄别凭记忆写。如果你用的是 Gradle逻辑一样只是写法换成implementation。另外很多团队会用到 BOMBill of Materials来统一管理 Spring AI 相关依赖的版本这样你引多个模块时不用每个都写版本号避免版本冲突。这个做法在依赖较多的项目里强烈推荐。3.3 配置文件怎么写才不容易出错配置这块是新手最容易翻车的地方。核心是两件事一是 API 密钥二是模型名称和基础地址。密钥绝对不能硬编码在代码里也不建议直接明文写在application.yml里提交到仓库。常见做法是通过环境变量注入配置里用占位符引用spring: ai: openai: api-key: ${AI_API_KEY} chat: options: model: your-model-name temperature: 0.7这里temperature是控制输出随机性的参数值越低越确定、越保守值越高越发散、越有创造性。做事实性问答、数据提取这类任务我一般调到 0.2 到 0.3做创意文案、头脑风暴可以到 0.8 以上。这个参数不是随便填的它直接影响你线上效果后面调优时会反复用到。注意配置项的前缀在不同版本里可能不同比如有的版本是spring.ai.openai有的调整过层级。写之前先确认你版本的配置元数据或者干脆在 IDE 里看自动补全提示能补出来的才是对的。4. 核心实操写出第一个能跑的 AI 调用4.1 注入 ChatClient 的两种姿势第一种是自动配置直接给你一个ChatClient.Builder你在自己的配置类里基于它构建Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一个严谨的 Java 技术助手回答要简洁准确。) .build(); } }这里defaultSystem设的是默认系统提示词相当于给这个 ChatClient 定了个基调。所有通过它发起的调用都会带上这段系统消息。这样做的好处是业务代码里不用每次都重复写人设保持一致性。第二种是直接在 Service 里注入ChatClient.Builder每次调用时临时构建。这种方式适合不同业务场景需要不同人设的情况。我个人更推荐第一种把 ChatClient 作为单例 Bean 管理配置集中、便于统一调整。4.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(question)设置用户消息call()发起同步调用content()取出文本内容。整个链路读起来就像自然语言这也是 Spring AI 设计上比较讨喜的地方。但这里有个细节要注意content()拿到的是纯文本如果你需要拿到完整的响应对象比如要读 token 用量、finish reason 这些元信息应该用.chatResponse()而不是.content()。很多做成本统计的团队会需要后者。4.3 用 Prompt 模板做参数化提问实际业务里问题往往不是一句固定的话而是带变量的模板。Spring AI 支持用占位符组织 PromptString answer chatClient.prompt() .user(u - u.text(请用一句话解释什么是 {topic}面向 {audience}。) .param(topic, 依赖注入) .param(audience, 刚学 Java 的新手)) .call() .content();这种模板化写法把“提示词”和“业务数据”解耦了。提示词可以由运营或产品同学维护在配置里代码只负责填参数。我在项目里会把常用提示词抽到配置文件或数据库改文案不用重新发版这个实践非常实用。4.4 流式输出让等待不再难熬同步调用有个体验问题模型生成整段回答可能要好几秒用户界面一直转圈。流式输出能让内容一个字一个字往外蹦体验好很多。Spring AI 对流的支持是返回一个FluxStringFluxString stream chatClient.prompt() .user(讲讲 Spring Boot 自动配置的原理) .stream() .content(); stream.subscribe(chunk - System.out.print(chunk));如果你项目里用的是 WebFlux可以直接把 Flux 返回给前端做 SSE。如果是传统 MVC需要配合 SseEmitter 做适配。这里要注意线程模型流式回调可能发生在非请求线程上涉及 ThreadLocal 的场景要特别小心我在这上面排查过一次上下文丢失的问题。5. 参数调优与提示词工程决定效果的关键5.1 几个必须理解的模型参数除了前面说的 temperature还有几个参数值得关注。maxTokens限制单次响应的最大长度设太小会被截断设太大浪费成本topP是另一种采样控制通常和 temperature 二选一调frequencyPenalty和presencePenalty用来抑制重复内容。这些参数在 Spring AI 里通过ChatOptions设置ChatOptions options ChatOptions.builder() .temperature(0.3) .maxTokens(800) .build(); chatClient.prompt() .user(提取这段文本里的所有日期) .options(options) .call() .content();我的经验是不要一次性把所有参数都调一遍那样你根本不知道是哪个参数起了作用。每次只动一个观察效果变化记录下来。做多了你会对每个参数的手感有直觉。5.2 系统提示词的写法直接决定下限系统提示词是性价比最高的优化手段。同样一个问题系统提示词写得好不好输出质量能差一个档次。我总结了几条实用原则明确角色、明确输出格式、明确边界。比如做数据提取我会写“你是一个数据提取助手只输出 JSON不要任何解释文字字段缺失时用 null”。这样模型就不会给你加一堆“好的以下是提取结果”的废话。还有一个技巧是给例子few-shot。在系统提示词里放一两个输入输出示例模型对格式的遵循度会明显提升。这在做结构化输出时特别管用。5.3 结构化输出让模型返回能直接用的对象让模型返回 JSON 字符串再手动解析容易因为格式问题翻车。Spring AI 提供了把响应直接映射成 Java 对象的能力record CityInfo(String name, String country, long population) {} CityInfo info chatClient.prompt() .user(介绍一下东京) .call() .entity(CityInfo.class);底层它会引导模型按目标结构输出并做反序列化。但要注意这个能力依赖模型对结构化输出的支持程度不是所有模型都稳。我实测下来复杂嵌套结构偶尔还是会解析失败所以生产环境一定要加兜底解析失败时重试或降级到文本解析。6. 常见问题与排查实录6.1 启动就报错Bean 找不到或配置不生效最常见的原因是依赖没引对或者配置前缀写错。排查顺序是先确认 starter 依赖在不在再看配置文件里的前缀能不能被 IDE 识别识别不了基本就是前缀错了最后看密钥环境变量有没有真正注入进去。我遇到过一次是环境变量名拼错了一个字母找了半小时。6.2 调用返回 401 或鉴权失败八成是密钥问题要么没配、要么配错、要么密钥本身失效或额度用完。建议在启动时加一段日志打印密钥的前几位和后几位中间打码确认加载的是预期的那把。这个习惯能快速定位“配置没生效”类问题。6.3 响应被截断或内容不完整先看maxTokens是不是设小了。如果没设可能是模型默认上限较低。另外流式场景下如果前端没正确处理结束信号也会看起来像截断。排查时先用同步调用验证排除流式处理的问题。6.4 提示词被判定违规导致调用失败有时候你会收到类似“prompt 被标记为可能违规”的返回。这种情况通常是提示词里包含了容易被误判的内容或者触发了模型侧的安全策略。处理思路是检查提示词里有没有歧义表达调整措辞必要时对用户输入做前置过滤。这类问题不是 Spring AI 层面的而是模型服务侧的策略理解这一点能少走弯路。问题现象常见原因排查方向启动报 Bean 缺失依赖未引入或版本不匹配检查 starter 坐标与版本401 鉴权失败密钥未注入或失效打印密钥片段确认加载响应被截断maxTokens 过小调大参数并复测结构化解析失败模型输出格式不稳加重试与降级逻辑提示词被拦截措辞触发安全策略调整措辞并前置过滤6.5 几个我踩过的坑第一个坑是版本升级导致 API 变化。有次我把 Spring AI 升了个小版本ChatClient的构建方式就变了编译直接挂。教训是升级前先看变更日志别盲目升。第二个坑是把 ChatClient 当成线程安全的万能对象到处传。它本身设计上是可复用的但如果你在构建时绑定了请求级别的上下文就要小心串数据。我的做法是默认配置的 ChatClient 做成单例请求相关的动态内容通过参数传不塞进 Builder。第三个坑是忽略超时设置。模型调用偶尔会慢没有超时控制的话线程会被长时间占用高并发下容易拖垮服务。一定要给底层 HTTP 客户端配合理的连接和读取超时。7. 从 Demo 到生产还差什么跑通第一个调用只是起点。要真正上线还得考虑几件事一是重试与熔断模型服务偶发失败很正常要有退避重试二是成本控制记录每次调用的 token 用量设置预算告警三是可观测性把请求耗时、成功率、失败原因打点上报四是提示词版本管理把提示词当配置来管能回滚、能灰度。我个人的体会是Spring AI 帮你解决了“怎么调”的问题但“调得好、调得稳、调得省”是工程问题得靠上面这些配套。先把 Demo 跑通建立信心再一步步把这些补上节奏会比较舒服。如果你现在正准备在项目里接第一个 AI 功能建议就从这篇文章里的最小示例开始跑通了再往上加东西别一上来就搞复杂架构。
返回列表