1. Spring AI到底解决了Java开发者的什么痛点
先说我自己的经历。去年我接到一个活儿,要在现有的Spring Boot系统里接入大模型能力,让用户能用自然语言查订单、问库存。当时第一反应是:直接拿HttpClient调OpenAI接口不就完事了?结果真上手才发现,事情没那么简单。
先是API鉴权、请求封装这些基础工作,倒还好办,无非是写个RestTemplate的封装类。真正恶心的是上下文管理——我得自己维护一个会话列表,每次请求把历史消息全塞进去,还要控制token长度,不然跑几次就超限了。接着是结构化输出,模型返回的是JSON字符串,我得自己解析、强转、处理各种格式错误。最崩溃的是换模型,项目中途客户要求换成国产模型,结果发现不同厂商的API格式不完全一样,又要改一遍对接代码。
这时候我才认真研究Spring AI。它是Spring官方出的AI框架,目标就是让Java开发者用一套统一的API对接不同的大模型。你可以把它理解成JDBC那套思路:JDBC统一了数据库访问,Spring AI统一了模型调用、Prompt管理、上下文记忆、结构化输出、RAG这些AI应用开发的常见环节。
Spring AI的定位非常明确:不是再包一层SDK,而是提供一套完整的AI应用开发框架。它把“对话”抽象成ChatClient,把“提示词”抽象成Prompt和Message,把“上下文”抽象成ChatMemory,把“知识库”抽象成VectorStore。你写的业务代码只需要面向这些抽象接口,底层用哪家模型、怎么调用,框架替你搞定。
这篇文章我带你从零构建一个Java AI应用,不整虚的,直接上代码。我会把环境准备、核心概念、Demo实操、踩坑经验全讲透。适合两类人:一是想在Spring Boot项目里快速接入AI能力的Java工程师,二是面试被问到“你们项目怎么接入大模型”时不想只会背八股的同学。
2. Spring AI 1.0版本该怎么选:依赖坐标和版本坑
2.1 版本演进到现在,别再去看老教程了
Spring AI从2023年底开始孵化,经历了0.8.x的里程碑版本、1.0.0-M系列预发布,到2025年终于发布了1.0 GA版本。现在网上搜到的教程有相当一部分还在讲0.8.x的API,很多写法已经过时了。
我写这篇文章的时候,稳定版本是1.0.x,而2.0版本官方已经宣布在路上了。我的建议很直接:直接用1.0.x系列的正式版,别用M系列,更别用0.8.x。版本选择这件事最坑的地方在于,Spring AI的API变化跨度很大,特别是ChatClient的构建方式,0.8和1.0之间改动非常明显。
如果你拿0.8.x的代码跑在1.0上,大概率会在ChatClient.builder()这一步直接编译报错。所以我下面给的代码全部基于Spring Boot 3.4.x + Spring AI 1.0.x组合,你照着写就能跑通。
2.2 Maven依赖到底该加哪个
Spring AI的依赖设计是分模型的,每个模型厂商一个Starter。你需要什么模型,就加对应的依赖,不会一股脑全给你塞进来。我用的是OpenAI兼容接口,依赖如下:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>1.0.0</version> </dependency>如果你用的是阿里云的通义千问,那应该加的是另一个Starter:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-qwen</artifactId> <version>1.0.0</version> </dependency>注意,如果用到后续讲的RAG和向量库功能,光加模型Starter不够,还需要spring-ai-starter-vector-store-redis这类向量库依赖。我建议初始学习阶段只加模型Starter,先把对话跑通,再加其他东西。一口吃不成胖子,Spring AI的模块太多,一次加太多依赖排查问题的时候非常痛苦。
由于Spring AI的部分依赖在中央仓库之外,你需要在Maven的settings.xml里配置Spring的仓库:
<repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository>不过我实测1.0 GA以后,大部分Starter已经同步到Maven Central了,这一步大概率可以跳过。但保险起见,还是建议你配置上,免得构建的时候报找不到依赖。这个坑我踩过,卡了我半天,最后发现是仓库没配全。
2.3 application.yml里的模型配置
配好依赖后,在application.yml里配置模型的基础信息。以OpenAI兼容接口为例:
spring: ai: model: api-key: sk-xxxxxx base-url: https://api.openai.com openai: chat: options: model: gpt-4o-mini temperature: 0.7如果你是接国内模型服务(比如通义千问),base-url和api-key换成对应的就行。这里有个关键点:Spring AI的所有配置项都有默认值,但不配置api-key应用根本起不来。所以第一步一定是先把key配好,不然你会在启动日志里看到一个非常抽象的ApiKey相关的异常。
3. ChatClient才是核心:理解Prompt、Message和对话记忆
3.1 ChatClient的设计哲学
用过Spring的人都知道JdbcTemplate,ChatClient就是AI世界的JdbcTemplate。它把模型调用、消息组装、参数配置这些细节全部封装起来,你只需要关心“我要跟模型说什么”。
在Spring AI 1.0中,ChatClient的创建方式是Builder模式:
@Configuration public class AiConfig { @Bean ChatClient chatClient(ChatClient.Builder builder) { return builder.build(); } }ChatClient.Builder由Spring Boot自动装配注入,它内部已经读好了你在application.yml里配置的模型信息。你拿到Builder之后可以直接build出一个默认的ChatClient,也可以在Builder阶段统一配置默认Prompt、默认参数。
这里有一个非常重要的设计细节:Builder阶段配置的Prompt会成为全局默认提示词,而每次调用时传入的Prompt会覆盖默认值。这个机制非常实用,比如你想让所有AI接口都遵守“你是一个专业的Java开发助手”这个角色设定,直接在Builder里配一次就够了。
3.2 Prompt的结构:System、User、Advisor三层
Spring AI把一次请求的Prompt拆成了多个层次,这一点和直接调API的体验完全不同。一个完整的ChatClient调用长这样:
String answer = chatClient.prompt() .system("你是订单系统的智能客服,回答要简洁专业。") .user("帮我查一下订单3001的物流状态") .call() .content();system()里放的是系统角色设定,user()里放用户的实际问题。框架会自动把这两部分按模型要求的格式拼装成完整的请求体。
在1.0版本中,Prompt体系还引入了Advisor概念,相当于请求拦截器。最常用的两个Advisor是:
MessageChatMemoryAdvisor:自动管理对话历史,让你实现“多轮记忆”变得非常简单。SimpleLoggerAdvisor:打印每次请求和响应的完整信息,调试神器。
我强烈建议你一上来就引入SimpleLoggerAdvisor,因为大模型应用出问题的时候,你根本不知道模型返回了什么、Prompt被拼成了什么样。有了请求日志,定位问题速度快一个数量级。
配置方式也简单,在Builder阶段加就行:
@Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); }3.3 会话记忆不是默认开启的
很多从ChatGPT网页端转过来的人会默认认为API天然支持多轮对话,这是最大的误解。直接调API时,你每次请求都是全新的无状态调用,模型不记得你上句话说了什么。Spring AI默认也是无状态的,需要你显式配置ChatMemory。
1.0版本内置了InMemoryChatMemory,适合单机场景。如果要支持分布式部署,需要换成Redis实现。
@Bean ChatMemory chatMemory() { return new InMemoryChatMemory(); }然后在构建ChatClient时加上MessageChatMemoryAdvisor:
ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();这里有个特别容易翻车的点:MessageChatMemoryAdvisor需要指定会话ID才能区分不同用户。如果你不传conversationId,所有请求共享同一份上下文,那真是修罗场——用户A的问题会被用户B看到。正确传法如下:
String answer = chatClient.prompt() .advisors(advisorSpec -> advisorSpec .param("chat_memory_conversation_id", userId) .param("chat_memory_response_size", 20)) .user("我的订单到哪了") .call() .content();chat_memory_response_size表示返回时携带多少条历史消息作为上下文,这个值不是越大越好,API有token限制,设置太大直接报上下文超限。
4. 实操:从零构建一个带记忆的智能问答接口
4.1 创建项目和目录结构
我用Spring Initializr创建一个Spring Boot 3.4.x项目,包名就用com.example.ai。项目结构按功能模块划分:
src/main/java/com/example/ai/ ├── AiApplication.java ├── config/ │ └── AiConfig.java ├── controller/ │ └── ChatController.java └── service/ └── ChatService.java这个结构没有过度设计,就是Spring Boot的标准三层,但为后面扩展RAG和Agent留好了位置。别一上来整controller/service/repository/mapper全套,AI应用的核心在Service层。
4.2 配置类:构建ChatClient实例
@Configuration public class AiConfig { @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } @Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), new SimpleLoggerAdvisor() ) .defaultSystem("你是一位Java技术专家,擅长解答Spring Boot和AI应用开发相关问题。回答时先用一句话概括结论,再展开细节。") .build(); } }defaultSystem这段是我建议大家都写上的,因为模型默认回答风格比较“泛”,给定角色和回答约束后,回复质量会有肉眼可见的提升。这算是我在实际项目中验证过的经验,比在每次调用时都传system prompt要省事得多。
4.3 Service层:处理业务逻辑
@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient chatClient) { this.chatClient = chatClient; } public String chat(String userId, String message) { return chatClient.prompt() .advisors(advisorSpec -> advisorSpec .param("chat_memory_conversation_id", userId) .param("chat_memory_response_size", 10)) .user(message) .call() .content(); } public Flux<String> streamChat(String userId, String message) { return chatClient.prompt() .advisors(advisorSpec -> advisorSpec .param("chat_memory_conversation_id", userId) .param("chat_memory_response_size", 10)) .user(message) .stream() .content(); } }注意看streamChat这个方法的返回值是Flux<String>,这是Spring WebFlux的响应式类型。流式输出的体验和ChatGPT网页端是一致的,字一个个往外蹦。REST接口里用响应式类型完全没问题,Spring MVC从5.3开始就支持。
4.4 Controller层:暴露REST接口
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @GetMapping("/sync") public String syncChat(@RequestParam String userId, @RequestParam String message) { return chatService.chat(userId, message); } @GetMapping("/stream") public Flux<String> streamChat(@RequestParam String userId, @RequestParam String message) { return chatService.streamChat(userId, message); } }到这里,第一个Java AI应用已经跑通了。启动项目后访问http://localhost:8080/api/chat/sync?userId=u1&message=什么是ChatClient,你会看到模型返回的完整回答。再追问一句“和JdbcTemplate有什么关系”,你会发现它记得你刚才问过ChatClient——这就是多轮记忆生效了。
4.5 用curl验证流式接口
/api/chat/stream返回的是text/event-stream格式,用curl验证最方便:
curl -N http://localhost:8080/api/chat/stream?userId=u1\&message=用一句话解释Spring%20AI-N参数是关键,不加的话curl会缓冲整个响应,你就看不到打字机效果了。如果返回的是一大段JSON而非纯文本,检查一下Controller有没有加produces = MediaType.TEXT_EVENT_STREAM_VALUE,这个是SSE流式响应的标准写法。我最初就漏了这个,浏览器端怎么都接不到流式数据。
5. 升级你的AI应用:结构化输出、函数调用和RAG
5.1 结构化输出:让模型乖乖返回JSON对象
写AI应用最难受的事情之一,是拿到的回答是一大段自然语言,你得自己用正则或者字符串截取去提取关键信息。Spring AI提供了一套BeanOutputConverter机制,让你的返回值直接变成Java对象。
比如我要让模型从用户输入中提取订单信息:
public record OrderInfo(String orderId, String customerName, List<String> items, BigDecimal totalAmount) {} public OrderInfo extractOrderInfo(String userInput) { BeanOutputConverter<OrderInfo> converter = new BeanOutputConverter<>(OrderInfo.class); String prompt = """ 从用户输入中提取订单信息,包含订单号、客户姓名、商品列表、总金额。 如果某个字段不存在,使用null。 %s """.formatted(converter.getInstructions()); String json = chatClient.prompt() .system(prompt) .user(userInput) .call() .content(); return converter.convert(json); }这里的关键是converter.getInstructions()。它会自动生成一段描述目标JSON结构的提示词,告诉模型“你应该返回这种格式”。模型返回的JSON字符串,再由converter.convert()反序列化成对象。这个机制比我最初自己手写JSON解析稳定太多了,因为模型偶尔会把字段名改掉、加多余换行,而BeanOutputConverter对这些边缘情况的容错率要高不少。
5.2 函数调用:让AI具备调用你业务代码的能力
有一个场景特别典型:用户问“库存够不够发明天要用的货”,模型本身不知道你的库存数据,但它能决定是否需要调用一个checkStock()函数。
Spring AI 1.0把函数调用做得很优雅。你只需要定义一个普通的Spring Bean方法,加上@Tool注解:
@Service public class InventoryService { @Tool("查询指定商品SKU的实时库存数量") public int checkStock(String skuCode) { // 这里对接真实的库存表或外部库存服务 return 123; } }然后在构建ChatClient时注册这个工具:
ChatClient chatClient = ChatClient.builder(chatModel) .defaultTools(new InventoryService()) .build();接下来神奇的事情发生了:当你问模型“SKU-1001库存还有多少”时,模型会先生成一次函数调用,Spring AI自动执行checkStock("SKU-1001"),把结果拼进上下文,再组织最终的回复给你。这个过程完全在框架内部完成,业务代码不需要感知。
这里面有个值得注意的细节:@Tool注解的description字段非常非常重要。大模型是靠这个描述来判断什么情况下该调用这个函数的。描述越具体越好,最好包含触发条件和参数含义。我见过太多人忽略这个字段,导致模型在错误的情况下乱调用工具。
5.3 RAG:把企业私有知识塞进模型
RAG(检索增强生成)是现在AI应用落地最广的架构。原理和你在搜索引擎里搜完再总结差不多:先把你的文档切块、向量化存入向量数据库,用户提问时先检索最相关的片段,连同问题一起提交给模型,让模型基于这些片段作答。
Spring AI的RAG链路封装得挺完整,从Embedding到向量存储都有一套抽象。一个最简易的RAG实现大概四步:
第一步:构建向量存储
@Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); }SimpleVectorStore是内存版,适合做Demo。生产环境可以考虑把实现换成Redis或者PGVector。运行时遇到SimpleVectorStore初始化报错,多半是EmbeddingModel没配好,检查一下模型Starter的依赖有没有引入。
第二步:文档加载和切分
var reader = new PageableTxtDocumentReader("classpath:docs/faq.txt"); var documents = reader.read();Spring AI的PageableTxtDocumentReader能直接把resources目录下的文本文件读成文档对象。如果是PDF,可以用PageablePdfDocumentReader。
第三步:写入向量库
vectorStore.add(documents);这一步会调用EmbeddingModel把文档向量化。文档不长的话很快,但如果知识库很大,建议分批写入,避免一次性内存溢出,这是我被现实教育过的事情。
第四步:检索问答
String question = "订单超时了怎么办"; String context = vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(3).build() ).stream() .map(doc -> doc.getContent()) .collect(Collectors.joining("\n")); String answer = chatClient.prompt() .system("仅根据以下上下文回答问题,不要编造:\n" + context) .user(question) .call() .content();这里面的topK(3)表示取最相关的3段内容。这个参数直接影响回答质量和token消耗——取太少模型没有足够信息,取太多又容易引入噪声。我的实践是先定3,看回答质量再调整,没有一步到位的黄金值。
RAG方案的价值在于,模型不需要重新训练,就能回答你私有知识库里的问题。这在企业场景落地时非常受欢迎,因为训练模型太贵了,而且知识更新频繁,RAG天然适配这种动态变化。
5.4 简单Agent:让模型自己决定先干什么
热搜词里出现频率很高的“AI Agent”,其实在Spring AI里落地没有想象中那么玄乎。Agent的核心能力就是“模型自主决策调用什么工具”,上面说的函数调用机制就是Agent的骨架。你可以把多个@Tool方法注册进去,模型会根据用户问题自动选择调用哪些工具、以什么顺序调用。
比如同时注册checkStock(sku)和createOrder(userId, sku, count)两个工具,用户说要下单,模型就会先检查库存,再创建订单,整个过程不需要你写硬编码的判断逻辑。这就是最朴素的Agent实现。更复杂的Agent需要引入多轮推理、任务规划、子Agent编排,Spring AI从1.0开始也提供了相应的抽象支持,不过那是另一个大话题了,入门阶段先把工具调用玩熟,Agent的核心原理你也就掌握了一半。
6. 容器化部署:把AI应用打成Docker镜像
聊完功能,说部署。AI应用跑通之后不可能一直留在本地IDE里,打包镜像上服务器是躲不掉的环节。Spring Boot 3.x官方提供了layertools分层打包方案,Dockerfile写起来非常简洁:
FROM eclipse-temurin:17-jre AS builder WORKDIR /application ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} application.jar RUN java -Djarmode=tools -jar application.jar extract FROM eclipse-temurin:17-jre WORKDIR /application COPY --from=builder /application/dependencies/ ./ COPY --from=builder /application/spring-boot-loader/ ./ COPY --from=builder /application/snapshot-dependencies/ ./ COPY --from=builder /application/application/ ./ EXPOSE 8080 ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]这套分层镜像方案的核心优势是:依赖层、框架层、业务代码被拆成不同镜像层,以后每次发版只重新构建业务代码那一层,镜像推送体积和构建速度都会快很多。
构建镜像:
./mvnw clean package -DskipTests docker build -t spring-ai-demo:1.0.0 .启动时记得把API Key通过环境变量传进去,别把key写死在镜像里:
docker run -p 8080:8080 \ -e SPRING_AI_MODEL_API_KEY=sk-xxxxxx \ spring-ai-demo:1.0.0SPRING_AI_MODEL_API_KEY对应application.yml里的spring.ai.model.api-key配置项。Spring Boot的配置项天然支持环境变量覆盖,这也算Spring生态的老传统了,不需要额外写代码。
7. 实测下来必须知道的坑:给新手的排雷指南
7.1 api-key没配好,启动就报错
Spring AI启动时如果检测不到API Key,会直接抛异常终止应用。异常信息通常是这样的:
No ApiKey configured for OpenAI解决办法不复杂,就是在application.yml里配好:
spring: ai: model: api-key: ${OPENAI_API_KEY:sk-demo}建议不要在配置文件里硬编码真实Key,用环境变量方式注入。不然代码传到Git仓库,Key就跟着泄露了。我见过不止一次因为这个问题把公司的Key送去给全互联网共享的事故。
7.2 Spring Boot版本和Spring AI版本要匹配
Spring AI 1.0.0官方明确适配Spring Boot 3.4.x。如果你用第4版写的老项目,强行升级到Spring Boot 3.5或更高版本,可能出现自动装配失效,具体表现为ChatClient.Builder无法注入、启动报No qualifying bean。
遇到这个问题,先用Maven强制统一版本管理。在pom.xml中添加Spring AI BOM是一个稳妥做法:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>有了BOM之后,各Starter版本号可以省掉,由BOM统一对齐,能减少很多“版本冲突”带来的困扰。
7.3 大模型的温度参数不要随便调大
temperature控制模型输出的随机性,范围是0到2。0值意味着每次回答基本一致,接近1.0时回答更发散,更适合文案创作类任务。
但做企业应用,尤其是涉及结构化输出、数据查询、函数调用这些场景时,我真不建议你调高它。我测试过,temperature调成0.8以后,结构化输出偶尔会出现字段缺失甚至格式错误。在业务接口上我一般固定为0.1到0.3,只有面向C端聊天场景才敢往高了调。
7.4 在日志里看Prompt:排查问题的不二法门
最后说一个调试习惯。AI应用调试和传统应用调试最大的不同是,你看不到模型到底收到了什么。有时候你觉得问题出在代码,其实是Prompt没写清楚;有时候你觉得Prompt写得没问题,其实是上下文里串了别的用户的历史消息。
所以我强烈建议,所有项目从第一天起就引入SimpleLoggerAdvisor或者自己写一个Advisor打印请求和响应日志。我保存了线上的日志,可以分享一个真实的排查案例:用户反馈“AI答非所问”,查看日志后发现是chat_memory_conversation_id参数传了固定值,所有用户共享了同一份对话上下文。这种问题不靠日志排查,靠猜的话能猜到你怀疑人生。
7.5 关于大模型API的速率限制
还有一个容易忽略的点:大模型API服务普遍有速率限制,单位时间内的请求次数和token总量都有配额。用InMemoryChatMemory虽然方便,但会不断累积历史消息,token消耗逐轮上涨,很可能在某个深夜突然触发速率限制,接口直接报429。
建议在服务层加一个简单的滑动窗口限流,或者至少对chat_memory_response_size做限制,别放任上下文无限增长。我在生产环境是把历史消息截断到最近10条以内的,效果还不错。
结语:一点个人经验
从最开始的盲目“调API”,到后来系统学习Spring AI的抽象体系和扩展机制,最大的感受是:Spring AI确实把AI应用开发的复杂度降下来了一个台阶。尤其是ChatClient、结构化输出、函数调用和RAG这四个核心能力,覆盖了当前绝大多数业务场景的落地需求。
如果你正在学Java,或者所在团队想给老系统加AI能力,我的建议是不要纠结选哪家模型厂商,先用OpenAI兼容接口或国内模型跑通一个最小Demo,建立完整的链路感知,然后再去深入模型选型、提示词工程、知识库建设这些环节。把这个Demo做完,你对Spring AI的理解基本可以覆盖大部分Java AI应用开发的面试场景了。