Spring AI 与 Spring Cloud Alibaba AI 实战:Java 接入大模型的统一抽象与落地
Spring 生态里做 AI 应用最近半年问得最多的问题就是两个一是 Spring AI 到底能干什么和直接调 SDK 有什么区别二是项目里已经有 Spring Cloud Alibaba 了再加一个 Spring Cloud Alibaba AI 是不是重复造轮子。这篇文章我从使用者的角度把这些事捋清楚并结合实际跑过的 Demo 讲怎么快速落地。我最早接触 Spring AI 的时候项目还在 0.x 版本接口变动频繁网上资料也少踩了不少坑。现在版本已经稳定很多和 Spring Boot 3.x 的集成也比较顺是时候认真聊聊它了。如果你是 Java 技术栈的开发者想在业务系统里接入大模型能力又不想被各家 SDK 绑死这篇文章基本能回答你八成的问题。1. 为什么 Java 生态需要自己的 AI 框架1.1 Java 接入大模型的传统困局做 Java 后端的人应该都有体会接入大模型最直接的方式就是调用各路 SDK 或者直接发 HTTP 请求。这种方式看似简单但真正放进业务系统里会出现一堆问题首先是各家模型厂商的 API 风格不同。有的用 Python 风格的关键字参数有的用 JSON Schema 声明工具调用有的流式返回格式都不一样。今天用某家的对话模型明天想换另一家改造成本不小。其次是业务代码里到处是字符串拼装的 Prompt什么版本管理、复用、动态拼参数全乱套。再一个是结构化输出的处理模型返回的是一段文本你要么靠正则拆要么让模型保证输出 JSON 再解析前者脆弱后者不稳定。如果只是写个 Demo 玩这些都不是问题。但要做企业级应用需要考虑统一接入层、可观测性、限流降级、模型切换等工程问题这些正是 Spring 这类框架擅长解决的。1.2 Spring AI 的核心理念统一抽象Spring AI 的思路和 Spring 生态一贯的做法完全一致设计一套统一抽象的 API屏蔽底层模型差异让开发者面向接口编程而不是面向某一家厂商的 SDK 编程。在 Spring AI 里你只需要面对ChatClient、EmbeddingModel、ChatModel这几个核心接口。底层到底是 OpenAI、通义、还是其他兼容模型通过配置切换就行。ChatClient chatClient ChatClient.builder(chatModel).build(); String response chatClient.prompt(一句话介绍你自己).call().getContent();这段代码不依赖任何具体厂商的类换模型厂商的时候改的是application.yml里的配置业务代码保持不动。这个设计思路和 JDBC 统一数据库访问、SLF4J 统一日志门面的逻辑是一脉相承的Java 开发者应该很熟悉。1.3 与其他 Java AI 框架的定位差异市面上不是没有其他 Java AI 项目但它们大多只做了 API 封装比如提供几个 HTTP 客户端类让你用 Java 方式调用模型接口。Spring AI 的野心更大它把 Prompt、AI 模型、结构化输出、向量存储、函数调用这些概念全部抽象成了框架层能力。这意味着它不仅在解决“怎么调用模型”的问题还在解决“怎么把 AI 能力融入现有 Spring 应用架构”的问题。配合Configuration的自动装配、Spring Boot 的 Starter 机制、Spring Cloud 的服务注册发现与限流熔断AI 能力可以像数据库连接池一样被当成基础设施来维护。2. Spring AI 环境准备与快速体验2.1 搭建最小可运行环境使用 Spring AI 之前先确认你的项目基础条件JDK 17Spring Boot 3.x 强制要求Maven 或 Gradle推荐 Maven资料更多一个可以调用的大模型 API KeySpring AI 官方支持 OpenAI、Azure OpenAI、Hugging Face、Ollama 等国内更常用通义千问通过 DashScope 兼容模式接入在pom.xml中加入依赖以 Spring Boot 3.2.x 为例parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version /parent properties spring-ai.version1.0.0-M4/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies如果你的模型服务兼容 OpenAI 协议配置非常简单spring: ai: openai: base-url: https://your-model-endpoint api-key: your-api-key chat: options: model: your-model-name temperature: 0.7这里有个容易踩坑的点如果你用了 DashScope 的 OpenAI 兼容模式base-url要填兼容端点api-key填 DashScope 的 Keymodel填对应模型名。2.2 第一个对话接口配置完成后写一个最快能跑的 DemoService public class AiChatService { private final ChatClient chatClient; public AiChatService(ChatClient.Builder chatClientBuilder) { this.chatClient chatClientBuilder .defaultSystem(你是一名资深Java架构师请用简洁、专业的方式回答技术问题。) .build(); } public String chat(String userMessage) { return chatClient.prompt(userMessage).call().getContent(); } }通过构造器注入ChatClient.Builder提前指定系统提示词这种写法比在每个方法里重复拼 System Prompt 要优雅得多。ChatClient.Builder本身由 Spring AI 自动配置生成你只需要在配置里填好模型信息。2.3 多模型切换的工程实践实际项目中很少只用一个模型。比如复杂推理用更强的模型简单分类用更快更便宜的模型。Spring AI 支持在代码中指定模型RestController public class ChatController { private final ChatModel chatModel; private final ChatModel fastChatModel; public ChatController( Qualifier(openAiChatModel) ChatModel chatModel, Qualifier(ollamaChatModel) ChatModel fastChatModel) { this.chatModel chatModel; this.fastChatModel fastChatModel; } GetMapping(/chat/fast) public String fastChat(RequestParam String message) { return fastChatModel.call(new Prompt(message)).getResult().getOutput().getContent(); } }前提是配置里同时声明了openai和ollama的连接信息。注意 Bean 的限定名是 Spring AI 框架约定好的不同 Starter 有不同的 Bean 名称建议先跑个接口把容器里所有ChatModel类型的 Bean 打出来看一眼再决定用哪个Qualifier。3. Spring AI 核心功能拆解与实战3.1 提示词模板告别字符串拼接写业务代码最忌讳的就是字符串拼接出一大段 Prompt里面混着用户输入、业务参数、历史记录读起来费劲维护起来更费劲。Spring AI 使用PromptTemplate解决这个问题它是基于字符串模板引擎实现的。public String generateReport(String quarter, String product) { String template 请根据以下数据生成季度报告 季度{quarter} 产品{product} 要求包含销售趋势分析、问题总结、下季度建议。 ; PromptTemplate promptTemplate new PromptTemplate(template); MapString, Object params Map.of( quarter, quarter, product, product ); Prompt prompt promptTemplate.create(params); return chatModel.call(prompt).getResult().getOutput().getContent(); }模板中{quarter}和{product}就是占位符。需要注意模板文件本身也是资源建议放到resources/prompts/目录下配合PromptTemplate的 classpath 加载方式方便后续统一修改。PromptTemplate promptTemplate new PromptTemplate( new ClassPathResource(prompts/report-template.st), Map.of(quarter, 2025-Q1, product, 企业版) );3.2 结构化输出让模型返回 Java 对象大模型返回的原始内容是字符串但业务系统需要的是对象。与其在代码里解析 JSON、处理格式异常不如让框架帮你做好反序列化。Spring AI 的BeanOutputConverter可以把模型输出直接转换成 POJOpublic record UserInfo(String name, int age, String city, ListString hobbies) {} public UserInfo extractUserInfo(String text) { BeanOutputConverterUserInfo converter new BeanOutputConverter(UserInfo.class); String template 从以下文本中提取用户信息并按照 JSON 格式返回 {input} 返回格式要求{format} ; PromptTemplate promptTemplate new PromptTemplate(template); MapString, Object params Map.of( input, text, format, converter.getFormat() ); Prompt prompt promptTemplate.create(params); ChatResponse response chatModel.call(prompt); String content response.getResult().getOutput().getContent(); return converter.convert(content); }原理并不神秘converter.getFormat()会生成一段指令告诉模型应该输出什么样的 JSON 结构。模型返回文本后convert方法做反序列化。这种方式依赖模型遵循指令的能力实测下来参数越少、字段描述越清晰成功率越高。如果模型偶尔返回带解释文字或 Markdown 代码块包裹的 JSON建议在convert外面捕获JsonParseException并写一个重试逻辑。另一种方法是把response.getResult().getOutput().getText()里的 Markdown 代码块剥掉再交给 Jackson 解析这个操作在线上很有用。3.3 函数调用让模型使用你的方法函数调用是当前大模型应用最实用的能力之一。场景比如用户问“帮我查一下订单 10086 的物流状态”如果你只是把问题丢给模型它根本不知道物流接口长什么样。通过函数调用你可以把某个业务方法“注册”给模型模型判断需要该工具时就会在响应中触发调用。在 Spring AI 中通过Tool注解实现Service public class LogisticsTools { Tool(description 根据订单号查询物流状态) public String getLogisticsStatus(String orderId) { // 这里调用真实业务接口 return 订单 orderId 当前状态运输中预计明天送达; } }然后在ChatClient上启用这个工具AiChatService(LogisticsTools logisticsTools, ChatClient.Builder builder) { this.chatClient builder .defaultTools(logisticsTools) .build(); }运行后会看到当用户询问物流相关问题时模型会自动决定调用getLogisticsStatus方法把返回的字符串组织进最终回复里。有个坑是工具注解的方法参数个数、类型要尽量简单最好能用基本类型和 String 表达复杂对象。因为模型侧是通过 JSON Schema 来理解参数的如果参数是一个复杂对象模型经常不知道怎么填。传递复杂对象时要么拆成多个简单参数要么在 description 里给出足够的字段说明和示例值。3.4 嵌入模型与向量检索入门写过语义搜索或者 RAG 的话对 embedding 不会陌生。Spring AI 抽象了EmbeddingModel接口配合向量数据库可以做知识库检索。public ListDouble embedText(String text) { EmbeddingResponse embeddingResponse embeddingModel.embedForResponse(List.of(text)); return embeddingResponse.getResult().getOutput(); }配合VectorStore接口Spring AI 支持很多种向量存储实现。这里最需要注意的是不同模型的 embedding 维度可能不一样如果你在测试环境换了 embedding 模型之前写入向量库的数据可能会被查询条件过滤掉或维度不匹配报错建议向量库的集合按模型名分区存放。4. Spring Cloud Alibaba AI 的差异化价值4.1 它到底是什么和 Spring AI 什么关系Spring Cloud Alibaba AI 是 Spring Cloud Alibaba 生态针对 AI 场景的扩展模块。它复用了 Spring AI 的底层抽象但针对国内常用的通义系列模型做了大量适配和增强。可以这么理解Spring AI 是“通用规范”Spring Cloud Alibaba AI 是“国内落地版”。它内置了通义千问、通义万相等系列的对接实现用起来比自己在 Spring AI 里配一个兼容端点舒服很多。依赖替换成dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0/version /dependency4.2 AIService接口即服务Spring Cloud Alibaba AI 最有特色的功能是AIService它允许你定义一个普通的 Java 接口框架自动生成实现。AIService public interface RepoAssistant { SystemPrompt(你是一个代码仓库助手负责分析代码问题。) String analyzeCode(String codeSnippet); UserPrompt( 请根据下面的对话历史理解用户意图并回答。 用户说{userMessage} ) String chat(String userMessage); }然后注入使用Service public class RepoService { private final RepoAssistant repoAssistant; public RepoService(RepoAssistant repoAssistant) { this.repoAssistant repoAssistant; } public String analyze(String code) { return repoAssistant.analyzeCode(code); } }这种方式比在每个 Service 方法里手动创建PromptTemplate简洁很多。注解上可以声明SystemPrompt、UserPrompt方法参数直接拼进模板。它的设计思想说实话很接近 Spring 的FeignClient把远程调用抽象成接口降低心智负担。4.3 DashScope 模型接入与 OutputParser接入 DashScope 系列模型时Spring Cloud Alibaba AI 提供了更细粒度的支持。配置示例spring: cloud: alibaba: ai: dashscope: api-key: your-dashscope-api-key chat: options: model: qwen-plus temperature: 0.85在结构化输出方面除了 Spring AI 的BeanOutputConverter它还提供了DashScopeOutputParser用于兼容 DashScope 特有的输出格式。如果遇到ConverterNotFoundException之类的报错多半是输出解析器和模型返回格式不匹配可以尝试切换成官方提供的响应解析器。4.4 集成时最容易踩的环境问题先说依赖冲突。Spring Cloud Alibaba AI 的 Starter 依赖了指定版本的 Spring AI如果你手动引入了spring-ai-bom版本有可能出现 NoSuchMethodError。建议直接依赖 Spring Cloud Alibaba 的 BOM 来统一管理版本。其次是模型名称。DashScope 平台上的模型名如qwen-plus、qwen-max和某些文档里写的不一致调用前确认一下控制台里开通的模型避免 404 或 ModelNotFound 错误。还有一点用户如果是在本地测试尽量不要走公网访问 DashScope而是看是否有内网或代理配置。项目上线前一定确认网络策略否则延迟会高得离谱超时重试还会白白消耗额度。5. 从入门到落地避坑与性能优化实录5.1 超时与重试的合理配置大模型接口延迟波动大简单的 HTTP 客户端默认超时 5 秒肯定不够用。建议配置spring: ai: openai: chat: options: timeout: 60s client: connect-timeout: 10s read-timeout: 120s重试策略要谨慎。模型接口返回 429 限流或 5xx 时可以重试但如果是 400 参数错误重试多少次都是浪费。5.2 Token 与上下文窗口的工程管控Token 是成本也是上下文窗口的限制。实际项目中你要对发给模型的内容做管控一是 Prompt 长度。把模板和用户输入分开计算预估 token 量比模型窗口上限留至少 20% 余量。二是历史消息。滑动窗口只保留最近 N 轮否则对话次数多了直接把上下文塞爆。public String chatWithHistory(String userMessage, ListMapString, String history) { ListMapString, String recentHistory history.stream() .skip(Math.max(0, history.size() - 10)) .toList(); String template 系统提示你是AI客服助手。 历史对话 {history} 用户当前消息{userMessage} ; PromptTemplate promptTemplate new PromptTemplate(template); Prompt prompt promptTemplate.create(Map.of( history, recentHistory.toString(), userMessage, userMessage )); return chatModel.call(prompt).getResult().getOutput().getContent(); }5.3 流式输出的正确打开方式实测中直接等完整响应在用户体验上非常糟糕一个看似简单的问题可能要等几十秒。Spring AI 支持流式响应public FluxString streamChat(String message) { return chatClient.prompt(message) .stream() .content(); }Controller 层配合text/event-stream返回前端用 SSE 接收。这里注意流式接口的响应体无法复用普通ChatResponse的解析逻辑如果要做工具调用和结构化输出流式场景要单独处理。5.4 常见问题排查速查表现象可能原因解决方式启动报NoSuchBeanDefinitionException: ChatModel没有引入对应 Starter 或配置缺失检查依赖和spring.ai.*配置调用返回 401API Key 错误或格式不对检查配置项确认模型平台要求的前缀返回内容带 Markdown 代码块模型未遵循纯 JSON 输出指令加后处理剥离代码块或优化 System Prompt工具调用没生效方法没有加Tool注解或工具类没注册检查defaultTools注册的文件确认方法为 public中文内容乱码或截断上下文窗口溢出或字符编码问题检查模型上下文长度缩短 Prompt响应速度极慢网络链路过长或模型本身延迟高检查网络环境考虑内网或专线接入结构化输出转换报错输出格式与 POJO 字段不一致优化字段描述增加示例必要时自定义 Converter值得单独说的是PromptTemplate里的特殊字符。模板文本中如果有{}的普通花括号比如 JSON 示例会被模板引擎误认为是占位符导致解析异常。解决办法是把示例用其他方式传入或者转义花括号。一些实在的使用心得我用 Spring AI 和 Spring Cloud Alibaba AI 跑过几个实际项目最大的感触是选择哪套框架不是关键关键是把你自己的业务边界弄清楚。模型终究是“外挂”你的业务逻辑、工具链、数据权限控制才是系统的核心。如果你所在的团队既有 Spring 技术栈熟悉度又有快速接国内模型的需求Spring Cloud Alibaba AI 很合适。如果你要对接多家模型想保留未来切换空间直接基于 Spring AI 做一层自己的抽象就好。最后分享一个小技巧开发阶段把spring.ai.chat.options.temperature调到 0.2 以下模型的输出会更稳定便于调试。等正式上线想做一些创意生成时再把温度调高。这能省下很多因为输出“太飘”而排查问题的时间。