
1. 为什么 Java 团队需要 Spring AI Alibaba大概从 2024 年开始Java 后端接大模型这件事变得不再只是 Demo 层面的玩具。很多人最初接触 AI 编程时第一反应是直接用 HTTP 调用各家模型厂商的 SDK写个 RestTemplate 或者普通的 HTTP Client 去调接口也能跑通。但一旦真正进入业务开发你会发现这条路走起来很快就会有一堆重复劳动对话上下文的维护、Prompt 模板的复用、模型切换的适配、多轮会话的状态管理、流式输出的处理还有后面必然要面对的工具调用和 Agent 编排。Spring AI 这个项目的思路本质上就是把跟大模型打交道这件事抽象成一套类似 Spring Data 的编程范式——你不需要关心具体模型厂商的 HTTP 协议差异只需要面向统一的 ChatClient 接口编程就像写 Spring 的时候面向 JdbcTemplate 一样自然。Spring AI Alibaba 则是这条路线上的国内落地版。它把 Spring AI 的标准 API 接口和阿里云的通义千问Qwen系列大模型打通同时还附带了一系列国内开发环境下非常实用的扩展能力。我身边不少团队选它理由很简单国内访问稳定、文档中文友好、模型调用成本和合规问题更容易处理而且和 Spring Boot 版本兼容性做得比较到位。这篇文章不是官方文档的复读机而是把我自己从零搭起来、跑通、踩坑到落地的完整过程整理出来。无论是刚接触 Spring AI 的新手还是已经做过某些 AI 项目但被各家 SDK 搞烦了的老手这篇文章都能让你少走几个月的弯路。2. 初始依赖搭建版本匹配和第一行代码2.1 不要自己去猜版本号Spring AI Alibaba 和 Spring Boot 的版本绑定关系非常严格这几乎是所有新手踩的第一个坑。如果你用 Spring Boot 3.4.x但是随手引入了 Spring AI Alibaba 的 1.0.0-M2 旧版本编译期大概率能过运行期各种 AbstractMethodError 和 ClassNotFound 就会教做人。我的建议是直接用 Spring Initializr 生成项目骨架时勾选 Spring Web 和 Spring AI Alibaba 依赖。如果项目已经存在需要手动加依赖用下面的版本组合以我最终稳定的版本为例parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.5/version relativeParent/ /parent dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0.2/version /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-bom/artifactId version1.0.0.2/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency /dependencies repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshots enabledfalse/enabled /snapshots /repository /repositories这里加 WebFlux 的原因后面会细说先记住结论如果你要做流式对话WebFlux 比同步 Web MVC 省太多事。2.2 从配置文件到第一个可运行的 Bean依赖引入以后只需要在 application.yml 里配置好 API Key 和模型名称spring: ai: alibaba: tongyi: api-key: ${DASHSCOPE_API_KEY} model: qwen-plus chat: client: enabled: true这里用的是 DashScope 的 API Key不是阿里云 AccessKey很多人搞混。DashScope 可以在阿里云百炼平台直接申请有免费额度测试完全够用。然后写一个几乎是最简单的测试接口RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder chatClientBuilder) { this.chatClient chatClientBuilder.build(); } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt(message) .call() .content(); } }启动项目浏览器打开http://localhost:8080/chat?message你好正常情况下几秒钟就能拿到大模型的回复。到这里一个最小可用的 Spring AI Alibaba 项目就落地了。但注意这仅仅是能跑离能上生产还有非常远的距离。后面每一个环节我都踩过不同的坑。3. 结构化输出让模型返回 JSON 而不是废话3.1 为什么要结构化输出实际业务开发中让大模型直接返回一段自由文本基本等于没有完成需求。比如你要做一个简历解析工具模型回答里带着各种好的我来帮你分析这份简历……这种前缀后端 JSON 解析直接报错只能靠正则硬切切完还容易切歪。Spring AI 的 Structured Output 机制就是解决这个问题的。它支持三种模式JSON 模式、Map 模式、Bean 模式。Bean 模式最常用直接定义 Java POJO让模型严格按字段返回。3.2 用 Bean 模式解析简历信息的完整示例先定义实体类public record ResumeInfo( JsonPropertyDescription(候选人姓名) String name, JsonPropertyDescription(工作年限数字单位年) Integer experienceYears, JsonPropertyDescription(技能列表每一项是独立的技能名) ListString skills, JsonPropertyDescription(最近一家公司名称) String lastCompany ) {}然后写接口PostMapping(/resume/parse) public ResumeInfo parseResume(RequestBody String resumeText) { ChatClient chatClient this.chatClient.mutate() .defaultSystem(你是一位资深的HR助理请严格提取信息不要输出任何多余内容。) .build(); return chatClient.prompt() .user(请解析以下简历内容 resumeText) .call() .entity(ResumeInfo.class); }这里的关键是entity(ResumeInfo.class)方法。Spring AI 内部会调用模型支持的 JSON 输出能力然后自动把 JSON 反序列化成你的 POJO。实际跑下来Qwen 系的模型对这种结构化输出的支持稳定度很不错基本没有出现过字段错乱。3.3 常见坑模型偶尔会不听话即使加了JsonPropertyDescription和 System Prompt 双重约束长文本场景下模型仍然有极低概率会在 JSON 外面包一层 markdown 代码块比如json {...}这个问题在 Spring AI 底层其实有 Cleanup 机制但我在 1.0.0.2 版本实测遇到过一次漏网之鱼。解决办法是不要依赖框架兜底自己在返回前做一层校验 java public T T safeParse(String text, ClassT clazz) { String cleaned text.replaceAll(^json\\s*|$, ).trim(); try { return objectMapper.readValue(cleaned, clazz); } catch (JsonProcessingException e) { int start cleaned.indexOf({); int end cleaned.lastIndexOf(}) 1; return objectMapper.readValue(cleaned.substring(start, end), clazz); } }虽然丑但有效。处理这种边缘情况的能力往往才是线上稳定性的分水岭。4. 流式输出从等 10 秒到打字机效果4.1 为什么流式输出会卡住我第一个流式输出的项目用的是 Spring MVC 同步接口前端那边用 fetch 请求后端直接返回FluxString结果前端迟迟收不到数据。排查了很久发现问题出在MVC 的线程模型Spring MVC 的同步请求不会完全释放 Servlet 线程大模型的流式响应是长连接一个流式请求占住线程好几个秒这在并发稍高的场景下就是事故。所以前面我特意加了 webflux 依赖。Spring AI 的流式响应推荐在 WebFlux 环境下使用4.2 基于 WebFlux 的流式接口GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String message) { return chatClient.prompt(message) .stream() .content(); }前端用 EventSource 或者 fetch 的 ReadableStream 来接const response await fetch(/chat/stream?message${encodeURIComponent(msg)}); const reader response.body.getReader(); const decoder new TextDecoder(); while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); // 把 chunk 追加到界面展示区 }TEXT_EVENT_STREAM 格式的数据前端可以直接按data:开头的行去解析体验就是打字机效果。4.3 一个关键细节超时配置流式接口很容易忽略超时设置。大模型生成长文时单次响应超过 60 秒很正常而默认的 WebClient 超时可能只有 30 秒经常出现用户看到一半连接断了。所以在配置类里要显式把超时时间拉长Bean public WebClient.Builder webClientBuilder() { return WebClient.builder() .baseUrl(https://dashscope.aliyuncs.com/api/v1/) .requestFactory(() - { ReactorClientHttpConnectorFactory factory new ReactorClientHttpConnectorFactory(); HttpClient client HttpClient.create() .responseTimeout(Duration.ofMinutes(3)); factory.setHttpClient(client); return factory; }); }这里有一个经验流式输出的超时配置要和全局 WebClient 分开设置。如果你在项目里已经定义过一个默认 WebClientSpring AI 内部的配置可能会和你冲突。我的做法是新建一个独立的Qualifier(aiWebClient)Bean 单独给 AI 相关调用使用彻底隔离。5. Admin 控制台Spring AI Alibaba Admin 的价值5.1 为什么需要一个 Admin 控制台看到热搜里有spring ai alibaba admin这个词说明不少人在关注这个方向。其实官方的 Spring AI Alibaba 并不默认带一个图形化 Admin 界面但延展生态里存在spring-ai-alibaba-admin相关的扩展模块它的核心价值在于让你在开发联调阶段能看到整个 AI 调用的链路明细相当于给 AI 后端装了一个监控仪表盘。如果你在项目里引入了dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-admin/artifactId version${spring-ai-alibaba.version}/version /dependency启动后在配置的 admin 端口就能看到每次对话的 Prompt 内容和完整响应Token 消耗统计各模型的调用次数与平均延迟最近一段时间的错误日志5.2 Admin 支持的业务场景范围这个 Admin 模块比较适合两种场景一种是团队内部调试。我开发阶段就靠它来观察 Prompt 实际发出去的内容到底是什么样。很多时候我们以为自己在代码里拼的是xx 格式的 Prompt实际因为模板渲染、参数转义等问题真正发给模型的内容早就变了样。在 Admin 面板里看一眼真实请求很多问题瞬间就明白了。另一种是小规模生成式 AI 应用的可观测性建设。如果你的系统已经接了多个供应商的模型比如 Qwen、DeepSeek、Kimi想在统一面板里看所有模型的消耗和延迟这个控制台能省掉你自己写日志采集和聚合的工作量。5.3 需要注意的支持范围限制需要强调的是Spring AI Alibaba Admin 目前并不是 Spring AI Alibaba 官方发行版的默认组件它更多是社区和生态侧的延展。如果你在生产环境使用一定要先确认它对你所选择的 Spring Boot 版本、Spring AI 版本的兼容性尤其是调用日志持久化这个能力不同小版本的实现差异很大。我在 1.0.0.2 版本实测时Admin 控制台能正常展示实时数据但历史记录模块偶尔会出现时间排序错乱的问题。所以结论是开发调试环境强烈推荐用生产环境谨慎评估后再上。生产环境如果只需要基础监控用 Spring Boot Actuator 加上自建 Prometheus 指标稳定性会更好。6. 多轮会话管理从无状态到有上下文6.1 最朴素的做法和它的硬伤最简单的多轮对话实现是把所有历史消息都塞到 user prompt 里让客户端每次都携带完整上下文。这在小 Demo 里可行但有两个致命问题token 消耗线性增长以及模型收到垃圾上下文越多越容易回答跑偏。Spring AI 提供了一套 ChatMemory 和 ConversationId 机制来解决这个问题。6.2 用 ChatMemory 实现会话隔离首先在配置类里注册一个内存版的聊天记忆Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); }在接口里显式指定会话 IDPostMapping(/chat/session) public String chatWithSession(RequestParam String sessionId, RequestBody String message) { return chatClient.prompt() .user(message) .chatMemory(chatMemory) .conversationId(sessionId) .call() .content(); }用同样的sessionId发消息模型就能记住前文的对话内容用不同的sessionId自动互相隔离。非常适合用户 A 和用户 B 各自独立的客服机器人这种场景。6.3 内存容量规划和 Redis 扩展InMemoryChatMemory的瓶颈非常明显内存里存不了多少会话而且应用重启后数据全丢。生产环境建议换成 Redis 实现。Spring AI 官方有 RedisChatMemory但依赖了spring-ai-redis需要额外引入dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-redis/artifactId /dependency然后配置Bean public ChatMemory redisChatMemory(RedisTemplateString, Object redisTemplate) { return new RedisChatMemory(redisTemplate); }这里我是直接用 Spring Data Redis 自带的 RedisTemplate不改原有 Redis 序列化配置的话会出现 Java 序列化对象存进去取出来反序列化失败的问题。建议单独建一个泛型为String, Object的 RedisTemplate Bean并指定使用 GenericJackson2JsonRedisSerializer避免踩序列化坑。6.4 上下文窗口溢出后的策略Qwen 系列的上下文窗口比较大qwen-plus 是 131K token但真当业务对话足够长时仍然会到达上限。我的建议是不要指望框架帮你自动截断Spring AI 目前的上下文裁剪策略偏简单生产环境更需要自己控制。常用策略有两种滑动窗口截断和摘要会话压缩。滑动窗口截断就是自己维护一个最近 N 条消息的列表超出部分从 ChatMemory 里删除摘要会话压缩是当消息数超过阈值时让模型用几句话总结前面的内容再把摘要作为新的系统提示词。我目前采用的是最近 20 轮 定期摘要的组合策略。每 50 轮对话触发一次摘要生成新摘要覆盖旧摘要既保证 Token 消耗可控又能做到对前文关键信息的有损但不离谱的保留。这个方案在成本、效果和代码复杂度上比较均衡也推荐你按这个方向设计。7. 工具调用让大模型真正会做事7.1 从只会聊天到可操作业务如果大模型只能返回文本那它的上限就是聊天机器人。要让模型能查数据库、调下单接口、查天气、算库存就得靠 Function Calling工具调用。Spring AI 里实现方式非常优雅。你只需要定义普通 Java Bean 方法加上Tool注解Component public class OrderTools { Tool(name queryOrderStatus, description 根据订单号查询订单的当前状态) public String queryOrderStatus(JsonPropertyDescription(订单号) String orderId) { // 伪代码这里查数据库或调内部接口 return 订单 orderId 当前状态已发货预计3天内送达; } Tool(name cancelOrder, description 根据订单号取消未发货的订单) public String cancelOrder(JsonPropertyDescription(订单号) String orderId) { return 订单 orderId 已成功取消; } }在 ChatClient 构建时注册工具ChatClient chatClient chatClientBuilder .defaultTools(queryOrderStatus, cancelOrder) .build();当用户问我的订单 20250101 现在什么状态大模型内部会决策调用queryOrderStatus工具拿到结果后再整合成自然语言回复给你。整个过程在外表看毫无感知体验很好。7.2 工具调用失败时的兜底机制工具调用另一个必须考虑的点是失败兜底。我在生产环境遇到过的典型情况订单系统接口超时工具内部抛出异常但大模型层可能把这个异常当成响应内容直接告诉你订单状态是java.lang.RuntimeException: timeout。解决思路是给工具方法加一个可控的异常返回约定Tool(name queryOrderStatus, description 根据订单号查询订单的当前状态) public String queryOrderStatus(String orderId) { try { String result orderQueryService.query(orderId); return 查询成功 result; } catch (Exception e) { return 查询失败失败原因 e.getMessage(); } }要让模型能在失败时对用户说不好意思订单查询服务暂时不可用,而不是原文输出一段异常堆栈。7.3 哪些工具适合暴露给模型这是我觉得最需要谨慎的地方。千万别把所有内部接口都注册为工具。最常见的反面教材是有人把删除用户的功能直接暴露成了工具。大模型本身没有恶意的但它是概率模型你不知道哪一次用户输入会诱导它错误地调用删除操作。我建议工具暴露要严格遵循这几个原则只暴露只读查询类和高度幂等的操作敏感操作必须二次确认。工具描述写得越具体越好模型对模糊的描述会产生不可控的调用判断。工具数量控制在 10 个以内超过的话模型的选择准确率会明显下降。如果确实需要暴露写操作务必要在工具方法内部增加一层会话级权限校验确认当前用户的身份和操作权限绝不能依赖大模型去做鉴权判断。8. 典型业务落地智能客服的工作流示例这里我分享一下我们团队实际落地的一个智能客服辅助系统的完整链路把这个教程里的核心模块串在一起发挥作用。8.1 整体架构整个系统的核心是一个AIService它串联 ChatClient、ChatMemory、具体业务 Tools 以及一层 RAG知识库检索Service public class CustomerServiceAiService { private final ChatClient chatClient; private final ChatMemory chatMemory; private final FaqRetriever faqRetriever; public CustomerServiceAiService( ChatClient.Builder chatClientBuilder, ChatMemory chatMemory, FaqRetriever faqRetriever) { this.chatMemory chatMemory; this.faqRetriever faqRetriever; this.chatClient chatClientBuilder .defaultSystem(你是某电商平台的AI客服回答必须基于提供的资料和工具结果不要编造信息。) .defaultTools(queryOrderStatus) .build(); } public FluxString chat(String sessionId, String userMsg) { String faqContext faqRetriever.search(userMsg); return chatClient.prompt() .system(以下是匹配到的知识库内容可作为参考\n faqContext) .user(userMsg) .chatMemory(chatMemory) .conversationId(sessionId) .stream() .content(); } }8.2 链路各环节的职责拆解这个服务里出现了两个 AI 相关模块职责需要分清faqRetriever.search()走的是向量检索先把用户的问题转成向量再到向量数据库我们用的是阿里云向量检索服务里找最相近的 FAQ 片段找到后作为参考上下文塞给模型。queryOrderStatus工具处理实时数据类问题例如订单状态、物流进度。这些信息不在知识库里必须实时查。为什么要拆成两路因为知识库适合那些答案相对固定的问题退换货政策、发货时效、优惠券使用规则而工具调用适合每次答案都可能变化的问题我的订单到哪了退款到账了吗。两者互补用户体验差异很大。8.3 关于延迟和成本的控制智能客服还有两个容易被忽视的非功能性指标延迟和成本。实测下来qwen-plus单次问答的调用延迟在接入 RAG 之后会从 1 秒涨到 3 秒左右因为向量检索本身需要时间。如果要控制整体延迟到 2 秒以内有两个实用方法一是给faqRetriever加上缓存相同问题的检索结果直接存缓存 30 分钟二是用qwen-turbo做常规问题响应只有当你检测到用户情绪比较糟糕或者问题确实复杂时再切换qwen-plus这个策略能把成本降到原来的三分之一左右。模型切换在 Spring AI 里不需要重启应用只需要在 ChatClient 构建时动态传入 Model 对象即可这算是框架设计得比较好的一点。9. 实测踩坑记录版本兼容、模型幻觉与架构反思9.1 问题一spring-ai-alibaba 依赖里的包冲突这是最高频的问题。项目里如果同时存在org.springframework.ai:spring-ai-core和spring-ai-alibaba-starter两个 jar 包里的部分类路径可能产生冲突导致奇怪的 BeanDefinitionStoreException 或NoClassDefFoundError。我的经验是只引入spring-ai-alibaba-starter不要单独引入spring-ai-core。Starter 内部已经传递依赖了核心包多引反而容易混乱。如果你确实需要单独指定某个 Spring AI 模块的版本使用 BOM 统一管理不要自己写死版本号。9.2 问题二模型对业务数据的幻觉系统接入知识库以后一个更隐蔽的问题浮现了模型在找不到答案的时候很有礼貌地编造答案。比如知识库里没有保价政策相关内容模型却一本正经地告诉用户本品支持7天保价。缓解这个问题的办法我总结了三板斧System Prompt 中强调如果给定资料中没有答案请直接说不知道。设置一个回答置信度当知识库检索的相似度得分低于 0.6 时不让大模型直接回答而是返回该问题需要转人工。对模型输出做一轮关键信息校验针对具体业务场景提取实体再回知识库验证一遍。这一步不是必须的但对高稳定要求的场景非常有效。9.3 问题三架构层面的反思最后想聊一个架构层面的体会这也是我在多个项目之间切换后的实际感受。Spring AI Alibaba 真正带来价值的不是造了一个新轮子而是把模型供应商差异这个复杂度封装在框架内部。团队里的 Java 工程师不需要重新学 Python不需要懂 prompt engineering 太多细节就能写出可维护的 AI 应用代码。但反过来也要警惕框架的封装越高级底层黑盒就越大。一旦遇到问题如果你连Interceptor 机制、Message 结构、ChatClient 内部执行链都不理解调试起来会非常痛苦。所以我会建议初学者在跑通 Demo 之后留出至少半天时间看一下 Spring AI 的源码结构尤其是ChatClient内部的Prompt - Messages - Model - StructuredOutput这条执行链路把这层逻辑理清楚了后面排查问题基本就是降维打击。我个人的实际使用感受是Spring AI Alibaba 目前的版本已经具备了在生产环境小规模使用的条件但距离无脑上生产还有一定距离。选不选它取决于你的团队是否已经深度使用 Spring Boot 生态。如果答案都是肯定的那它无疑是 Java 后端接入大模型最平滑的一条路。如果你本来就是 Python 技术栈那就没必要硬套这个框架。做技术选型最重要的是服务现有团队的实际约束和项目目标而不是追着热点跑。希望这篇文章能给你足够的信息来判断这条路是否适合自己。