最近接了个内部系统改造的活儿,要把大模型对话能力嵌进现有 Spring Boot 微服务里。动手前我调研了一圈,最后选了 Spring AI Alibaba,可以说这个选择直接把我从“手写 HTTP 调用 + JSON 解析地狱”里救了出来。如果你也是 Java 技术栈,打算把通义系列模型的对话、绘图、语音、知识库问答能力集成到项目里,那么这篇教程基本覆盖了你从零到生产可用所需要的全部关键步骤。我会从环境准备、第一个对话、流式输出、函数调用,一直讲到我实际踩过的坑和排查思路,全程是能直接抄作业的代码和配置。
1. 先搞清楚 Spring AI Alibaba 是什么,避免用错方向
1.1 一句话定位
Spring AI Alibaba 是构建在 Spring AI 抽象之上的模型接入实现,它的核心价值是把大模型能力以 Spring 标准 API 的形式暴露给开发者。换句话说,它是一个 Starter 加一堆适配器,你只要在 pom 里加上依赖,在 application.yml 里配好模型服务商的密钥,就可以像注入普通 Bean 一样注入 ChatClient、ImageModel、AudioSpeechModel 这些对象,然后调用方法拿到结果。
它解决的痛点非常具体:Java 开发者接大模型时,绕不开鉴权、网络、重试、Token 计费、流式传输、JSON Schema 这类跟业务无关的脏活。直接用模型服务商 SDK 也不是不行,但一换服务商,整个调用层基本要重写。而基于 Spring AI Alibaba,你的业务代码面向抽象接口,底层换了模型,上层代码通常不用动。
1.2 和直接用模型服务商 SDK 的区别
我给团队内部讲选型时列过一张对比表,今天也贴出来:
| 对比维度 | 直接用 SDK | 使用 Spring AI Alibaba |
|---|---|---|
| 业务代码占比 | 约 20%,剩下都在处理网络和序列化 | 约 80%,专注 Prompt 和结果处理 |
| 换模型服务商成本 | 高,调用层推倒重来 | 低,多数场景改配置即可 |
| 调试手段 | 自己封装日志 | 继承 Spring Boot 配置体系和 Actuator |
| 与 Spring 生态整合 | 需要在 Service 层自己做封装 | 原生支持 Bean 注入、AOP、属性绑定 |
| 学习门槛 | 看服务商文档 | 看 Spring 编程模型即可 |
这里我不展开高深理论,只讲一个最直观的体验:用 SDK 时,我想打个日志看请求参数和响应耗时,得在拦截器里写一大堆代码;用 Spring AI Alibaba 之后,它天然融入 Spring 的配置体系和生命周期管理,很多观测能力顺手就有。
1.3 版本与生态边界
Spring AI Alibaba 1.x 基于 Spring AI 1.0,我的建议是 Spring Boot 用 3.4.x 或 3.5.x,JDK 用 17 或 21。还在用 Spring Boot 2.7 的老项目,别想着直接加依赖,得先把 Spring Boot 升上去,这一步牵扯的兼容问题后面第五节我会专门聊。
另外要注意,Spring AI Alibaba 不是“大模型全家桶”,它目前聚焦在通义系列模型以及与 Spring AI 生态相关的向量数据库、函数调用等能力上。如果项目里还要接别的模型服务商,Spring AI 本身也有对应的适配模块,只是不在 Alibaba 这个 Starter 的范围内,这是很多刚上手的人容易搞混的地方。
2. 动手前准备:版本、依赖、密钥一个都不能少
2.1 确认 JDK 与 Spring Boot 基线
我见过太多从网上复制依赖却起不来的情况,问题往往不在代码,而在版本基线。Spring AI Alibaba 1.0.0 发布后,对 Spring Boot 版本有明确的适配范围。我自己用 Spring Boot 3.4.5 + JDK 21 跑得很稳,用 3.5.x 也没问题。如果你用 Spring Boot 4.x,需要选择支持该版本的 Spring AI Alibaba 后续版本,而不是拿 1.0.0 硬试。
检查基线可以用最简单的办法:项目里先只留 spring-boot-starter-parent 作为父工程,然后看启动日志中 Spring 版本和 Spring AI 内置版本是否匹配。只要出现版本冲突,第一屏的报错信息通常会直接给出冲突类名,千万别跳过看。
2.2 引入 BOM 和 Starter
依赖引入的核心在版本管理。Spring AI Alibaba 提供了自己的 BOM,用来统一管理它内部各个模块的版本。我建议在 dependencyManagement 里引入它,而不是在上百个依赖里手工写版本号。
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.4.5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后加最常用的 Starter:
<dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> </dependency>这个 Starter 会把对话模型、嵌入模型、图片模型、语音模型的基础依赖都带进来。如果后续只想用某个能力,比如只做聊天,可以换成 spring-ai-alibaba-chat-starter 之类的细分模块,减少无谓的依赖体积。不过第一次做 Demo,直接用全量 Starter 是最省心的。
2.3 配置模型服务商密钥和模型参数
配置文件是接入过程中最容易翻车的一环。注意不同版本的配置前缀有变化,1.0.0 版本常用的配置如下:
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7这里我把 api-key 用环境变量引用,而不是硬编码进配置,避免密钥泄漏到代码仓库。模型参数里的 temperature 控制随机性,做代码生成类任务我会调到 0.2 左右,做创意写作再调回 0.8,这个没有绝对标准,按场景试。
提示:如果你拿到的新版本默认配置前缀变成了 spring.ai.alibaba,说明官方调整了配置结构。最稳妥的做法是直接看对应版本官方文档的配置页,不要凭经验硬猜。
3. 5分钟跑通第一个对话:ChatClient 基础实战
3.1 注入 ChatClient 并发送请求
Spring AI Alibaba 的入口对象是 ChatClient,这是在 Spring AI 1.0 里主推的流式 API。先声明一个配置类,把 ChatClient.Builder 注入进来,然后构建我们的客户端:
@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个乐于助人的技术助手,回答尽量简洁准确。") .build(); } public String ask(String question) { return chatClient.prompt(question) .call() .content(); } }代码就这么点。defaultSystem 相当于给模型设定了一段系统提示词,每个请求都会带上。如果你希望每个请求单独指定系统提示,可以在 prompt 之后追加 .system(...) 覆盖。
3.2 验证调用成功的两个信号
写完调一下,控制台能看到两个关键信号:第一是请求日志里出现模型服务商返回的 Token 统计,第二是返回内容符合预期。如果返回空字符串,大概率是 Prompt 被系统提示词约束得太死,或者是 temperature 过低导致模型倾向于保守回答,排查方向别搞反。
第一次跑通之后,我建议顺手做一个简单的耗时记录。Spring AI 的同步调用通常 2 到 5 秒,如果你的接口动辄十几秒,先别怪模型慢,检查是不是网络代理、DNS 解析或者本地网关拦截了请求。
3.3 参数传递:让每次请求可定制
真实业务里不会只有一个固定模型参数。使用 ChatClient 的请求级配置,可以做到不同接口不同模型:
String result = chatClient.prompt("帮我写一封请假邮件") .options(DashScopeChatOptions.builder() .withModel("qwen-turbo") .withTemperature(0.4) .build()) .call() .content();qwen-turbo 适合快速、低成本的场景,qwen-plus 是均衡选择,qwen-max 适合复杂推理。把模型选择权从全局配置挪到请求级,是很多生产项目最终都会走的方向,因为不同业务场景对成本和效果的要求完全不同。
4. 让对话更像真实产品:流式输出与多轮记忆
4.1 流式输出:解决“响应太慢”的体验问题
同步调用虽然代码简单,但用户看到的是一个一直在转圈的按钮。换成流式输出后,模型生成到一半,界面就可以开始打字。Spring AI Alibaba 对 Spring AI 的流式支持是完整的,返回一个响应式流:
public Flux<String> askStream(String question) { return chatClient.prompt(question) .stream() .content(); }Controller 层直接返回 Flux,配合 WebFlux 的响应式协议,前端就能用标准 SSE 接收。如果你项目里还是 Spring MVC,也可以把 Flux 收集成 List 后一次性返回,但那就失去流式的意义了。
注意:流式输出一定要处理好连接断开的情况。前端页面刷新或主动取消请求时,Flux 的取消信号会自动传到下游,但你自己的业务逻辑里如果有状态清理动作,需要监听取消事件,否则会造成状态残留。
4.2 多轮记忆:记住上下文的正确姿势
大模型本身没有记忆,所有“记住”都是靠把历史消息重新发给模型实现的。在 Spring AI 里,ChatMemory 负责管理会话历史,InMemoryChatMemory 是入门最简单的实现:
ChatMemory chatMemory = new InMemoryChatMemory(); ChatClient chatClient = ChatClient.builder(chatModel) .defaultMemory(chatMemory) .build(); chatClient.prompt("我叫小李,我喜欢骑行").call().content(); chatClient.prompt("我叫什么名字?我的爱好是什么?").call().content();第二次提问时,模型能给出正确答案,因为 ChatClient 自动把前一轮的对话消息拼了进去。要控制记忆长度,可以用 WindowChatMemory 并设置窗口大小,比如只保留最近 10 条消息,避免请求体越来越长导致费用飙升。
4.3 记忆的副作用与隔离策略
多轮记忆用起来爽,坑也不少。多用户共用同一个 ChatClient 时,如果用的是一份默认记忆,A 用户的对话会被 B 用户看到,这在生产环境是严重事故。解决办法是按会话维度隔离记忆,在每次 prompt 前用 conversationId 指定记忆空间:
chatClient.prompt(question) .memory(conversationId) .call() .content();我实际项目的做法是,把 conversationId 存到用户会话或请求头里,每次调用都显式传进去。这样即使用户同时开两个窗口,对话上下文也不会串。
5. 把大模型接进业务:函数调用与结构化输出
5.1 函数调用:让模型帮你调 Java 方法
函数调用是落地最实用的一个能力。它的原理不神秘:你告诉模型有哪些函数可用,模型在回答时发现需要实时信息,就会在回复里请求调用某个函数,框架拿到请求后帮你执行 Java 方法,再把结果回传给模型生成最终答案。
举个例子,用户问“北京今天适合跑步吗”,模型如果不调用天气函数,给出的回答就是瞎编。我们定义一个天气查询函数:
@Bean @Description("查询指定城市的当天天气情况") public Function<WeatherRequest, WeatherResponse> currentWeather() { return req -> { // 这里替换为真实的天气服务调用 return new WeatherResponse(req.city(), "晴", 26); }; } public record WeatherRequest(String city) {} public record WeatherResponse(String city, String condition, int temperature) {}然后把这个函数注册到 ChatClient 上:
ChatClient chatClient = ChatClient.builder(chatModel) .defaultFunctions("currentWeather") .build();此时你再问“北京天气怎样”,模型会先生成函数调用参数,框架调完函数,再把结果组织成自然语言回答。关键细节是 @Description 一定要写清楚,它决定模型什么时候该用这个函数,描述越模糊,模型越容易乱调。
5.2 结构化输出:把回复解析成对象
直接拿 content 字符串让前端自己去解析 JSON,迟早会出事。Spring AI 提供了结构化输出能力,只要返回类型能匹配,就能把模型输出映射成对象:
record Recipe(String name, List<String> ingredients, int cookMinutes) {} Recipe recipe = chatClient.prompt("给我一个番茄炒蛋的家常做法,返回 JSON") .call() .entity(Recipe.class); System.out.println(recipe.name());这里背后的逻辑是框架根据目标类生成 JSON Schema 提示,引导模型按结构输出,再反序列化到对象。需要注意 record 类型是支持的,但字段别太复杂,嵌套层级一旦超过三层,模型输出稳定性会明显下降。
5.3 结构化输出失败的兜底策略
模型毕竟是概率输出,偶尔会返回残缺 JSON。我一般会在业务层加 try-catch,捕获解析异常后做一次“修复重试”:把模型原始文本重新丢给它,附加一句“请只输出合法 JSON”。这个笨办法在实际项目中救了我很多次。
如果重试还是失败,就降级返回固定错误信息,而不是把异常直接抛给前端。这种兜底逻辑应该写进公共的 API 封装里,而不是每个业务方法单独处理。
6. 多模态扩展:文生图、语音合成、私域 RAG
6.1 文生图:几行代码生成图片
Spring AI Alibaba 对图片生成也做了封装。注入 ImageModel 后,直接调用:
ImageResponse response = imageModel.call( new ImagePrompt("一只戴着头盔骑公路车的橘猫,插画风格") ); String imageUrl = response.getResult().getOutput().getUrl();生成的图片 URL 可以直接返回给前端。如果希望返回 Base64 而不是 URL,可以在请求参数里设置响应格式。我第一次跑通的时候挺感慨,过去想接这类能力,得读一整套图片服务商文档,现在代码量几乎可以忽略。
6.2 语音合成:让应用开口说话
语音合成的用法和图片生成对称。注入 AudioSpeechModel 后,传入文本和语音模型参数,返回音频字节流:
SpeechPrompt prompt = new SpeechPrompt( "欢迎使用 Spring AI Alibaba,已经可以播放语音了", SpeechOptions.builder() .withModel("cosyvoice-v1") .build() ); SpeechResponse response = audioSpeechModel.call(prompt); byte[] audio = response.getResult().getOutput().getAudio();拿到字节流后,可以写文件、走对象存储、或直接作为接口响应返回给前端播放。我在做语音播报类功能时,用这个接口替代了原本的人工录制,后续维护提示文案的改动成本大幅降低。
6.3 RAG 私域知识库:给模型“补课”
大模型的知识截止时间有限,私域资料它完全不了解。RAG 的思路是:先把你的文档拆成小块、转成向量存起来,用户提问时,先去向量库检索最相关的片段,再把这些片段和问题一起交给模型组织答案。这样回答就有了事实依据。
一个最小可用的流程如下:
// 1. 构建文档并切分 Document doc = new Document("产品使用手册的正文内容……"); var splitter = new TokenTextSplitter(); List<Document> chunks = splitter.apply(List.of(doc)); // 2. 向量化入库 vectorStore.add(chunks); // 3. 创建检索增强顾问 var advisor = new QuestionAnswerAdvisor(vectorStore); // 4. 带知识库回答 String answer = chatClient.prompt("这个产品支持导出哪些格式?") .advisors(advisor) .call() .content();实际生产环境,文档可能来自 PDF、Word、数据库,需要先用对应的 DocumentReader 读取,再处理切分、Embedding 和入库。这部分代码量并不大,真正花时间的是清洗数据和调整切分粒度。切得太碎,语义容易丢失;切得太长,检索命中率下降。我通常按 300 到 500 Token 切分,再留一点重叠。
6.4 向量库选型的取舍
开发调试阶段,用内存向量库最方便,重启即清空,不会污染数据。等到要上生产,再根据团队已有设施选:有 Redis 就用 Redis 向量检索,有专门向量库就接向量库。Spring AI Alibaba 支持几类常见方案,选型时可以优先考虑运维成本最低的那个,不要为了“技术先进”单独引入一个没人会维护的组件。
7. 常见问题与排查实录:我踩过的那些坑
7.1 版本冲突导致的 NoSuchMethodError
这个问题出现频率排第一。现象是启动正常,一调用就抛 NoSuchMethodError 或 ClassNotFoundError,指向的类名又是 Spring AI 核心包里的。原因是项目中 Spring AI 相关依赖的版本被某个传递依赖覆盖了,和 Spring AI Alibaba 内置版本不一致。
排查办法很简单,先用 Maven 依赖树看版本:
mvn dependency:tree -Dincludes=org.springframework.ai重点看 spring-ai-core、spring-ai-model 的版本。如果同时出现多个不同版本,就检查有没有别的库间接引入了 spring-ai-bom,或者手动强行指定了旧版本。删掉重复引入,统一由 spring-ai-alibaba-bom 管理版本,通常能解决。
7.2 401 Unauthorized 与密钥不生效
这类问题的表象都一样:请求被服务端拒绝。常见原因有两个。
第一个是 api-key 压根没读进去。如果你用了环境变量引用 ${DASHSCOPE_API_KEY},确认操作系统环境变量确实存在,别只在 IDE 里配了而部署环境没配。第二个是新版本配置前缀变了,你写的 spring.ai.dashscope.api-key 没被识别,密钥实际为空。这时可以在启动类里临时打印一下属性值确认。
还有一个易错点:如果密钥的前后残留空格,有些服务端不会自动 trim,直接在 yml 里复制粘贴时最容易带进去。用代码生成密钥再粘贴,能避开这个问题。
7.3 超时与限流:生产环境必调参数
上线后你会发现本地好好的,生产环境请求偶尔超时。默认超时时间对复杂模型推理来说可能不够,尤其是调用 qwen-max 这类大模型,长文本生成的耗时会长不少。Spring AI Alibaba 提供了超时配置项:
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} connect-timeout: 30s read-timeout: 60sconnect-timeout 管建连,read-timeout 管等待响应。长问答场景建议 read-timeout 拉长到 60 秒或更久,否则模型还没生成完,客户端就先超时断开了。限流方面,服务商会返回限流错误码,这时单纯加超时没用,要在业务层做重试退避,指数退避是个稳妥方案。
7.4 流式输出中断与结果为空
流式输出中断,很多情况不是服务端问题,而是中间网络代理对 SSE 连接做了缓冲或超时断开。如果你用了 Nginx 这类反向代理,需要关闭对 SSE 的缓冲,否则前端收不到增量内容,只能等所有内容生成完才一次性收到。
结果为空的情况,先查系统提示词是不是要求模型“如果不知道就不要回答”;再看是不是命中了内容安全过滤,模型直接输出了空回复。最后再考虑参数问题。诊断顺序从外到内,不建议一上来就改 temperature。
7.5 多轮记忆丢失
明明配置了 ChatMemory 并跑通了示例,第二天业务测试时发现上下文还是丢失。最常见的原因是 ChatClient 构建位置不对:每次请求都 new 了一个 ChatClient,而记忆绑定在 ChatClient 内部,相当于每轮对话都是新会话。记住,ChatMemory 和 ChatClient 都应该是长期存在的 Bean,不是请求级别的临时对象。
另外一个容易被忽略的点是异步线程。如果你在多线程环境下调用同一 ChatClient,要注意并发修改会话历史的线程安全问题,必要时按会话维度加锁或改用支持并发的记忆实现。
最后给你点实操建议
做第一个 Demo 的时候,别一上来就搞流式加函数调用加知识库全家桶,先用同步调用把配置、版本、密钥这三关打通,再逐步叠加。我见过有人折腾半天,最后发现仅仅是版本问题导致所有高级特性全废,这种排查成本特别高。
还有一个小技巧:启动参数加上 --debug 可以打出自动配置的详细决策过程,哪条配置被加载、哪条被忽略一目了然。Spring Boot 的机制配合合理的配置覆盖,能解决绝大多数“为什么我的配置没生效”的疑问。
我自己最大的体会是,接入大模型这件事正在从“AI工程师的专属活”变成“普通后端工程师的日常活”。Spring AI Alibaba 降低的不仅是编码量,更是整个团队尝试新技术的心理门槛。希望这份教程能让你少走几步弯路。