☰
Spring AI实战:Java开发者构建大模型应用的核心指南
2026/11/4 8:22:00 网站建设 项目流程

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.0

SPRING_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应用开发的面试场景了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询