news 2026/10/17 7:42:35

Spring AI 实战指南:Java 后端如何快速接入大模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 实战指南:Java 后端如何快速接入大模型

Java 圈这两年被大模型搅得挺热闹。一边是 Python 生态天天有新框架冒出来,另一边是 Spring 这棵常青树一直在观望,直到 Spring AI 项目出现。先别把它当成一个“套壳 SDK”,Spring AI 的核心目标很直接:让 Java 开发者用最熟悉的方式接入大模型——依赖注入、自动配置、模板化调用,能少写一行是一行。这篇文章我会从一个实际跑通过的 Demo 入手,把 Spring AI 的入门路线、核心 API、常见坑一次说清,适合正在做 Java 后端、想快速把 LLM 能力集成进现有系统的人。

1. Spring AI 到底解决了什么问题——先别急着写代码

1.1 Java 生态接入大模型的三个大坑

在没有 Spring AI 的时候,Java 接大模型基本是这么个流程:先对着文档拼 HTTP 请求,把 JSON 格式的消息体手工构造出来,再处理鉴权 Header,然后拿到返回结果后写一堆 JSON 解析逻辑。这还只是最简单的“发一句、回一句”场景。

稍微加点复杂度就难受了。比如你想实现多轮对话,那得自己维护会话历史,每次把历史消息拼进请求体;想做流式打字机效果,得对接 SSE 协议,手动处理数据帧;想让模型调用你自己的业务方法,就要实现一套工具调用的协议解析逻辑。这些工作不是不能做,但每次换个模型服务商、换一个接口版本,代码可能就要大改一遍。

Spring AI 的目标就是把这堆脏活统一掉。它借鉴了 Spring 生态一贯的思路——把可变的部分交给配置,把复杂的技术细节封装在框架层,让开发者的业务代码只关心“用户问什么”和“程序答什么”这两件事。

1.2 Spring AI 的核心设计:一切皆可注入

如果你用过 Spring Boot,那 Spring AI 的上手成本几乎为零。它把模型调用抽象成几个核心接口:ChatModel、ChatClient、StreamingChatModel、EmbeddingModel等。这些接口由框架的自动配置模块负责实例化,你只需要在配置文件里写上 API Key 和模型地址。

实际写代码的时候,你最常打交道的其实是ChatClient。它有点类似RestTemplate之于 HTTP 调用,是一个高度封装的门面类,支持同步调用、流式调用、结构化输出、工具调用等常见玩法。之前写过 JdbcTemplate 的人应该能秒懂这种模式。

Spring AI 还做了一件挺关键的事:统一了模型接口。不管底层接的是哪家服务商,对业务代码来说,你操作的始终是同一个 API。哪天想从一家模型换成另一家,改配置比改代码多。这个设计在国内的落地场景里尤其好用,因为不同平台的接口细节差异本来就大。

1.3 和其他方案的简单对比

很多人在选型时会纠结一个问题:直接用 HTTP 客户端不行吗?为什么非要引入一个框架?我的看法是分场景。如果只是临时写个小脚本、跑个测试,直接调接口完全没问题。但如果是放进一个长期维护、多人协作的工程里,框架带来的收益就很明显了。

方案学习成本与 Spring 生态整合流式/结构化支持长期维护成本
自己封装 HTTP 调用低一般,需要手写需自己实现高,接口一变就要改
LangChain4j中中,独立生态支持中,API 变化较快
Spring AI低(Spring 开发者几乎零门槛)极好,原生自动配置完整支持低,跟随 Spring 版本演进

2. 项目初始化与依赖配置——先把环境跑通

2.1 环境准备:JDK 与构建工具

Spring AI 1.0.0 GA 要求 JDK 17 及以上,所以如果你的项目还停留在 JDK 8,先别急着引入依赖,升级 JDK 是第一步。构建工具我用的 Maven,Gradle 也没问题,只是坐标写法略有差异。

Spring Boot 版本建议直接用 3.3.x 以上的稳定版本。Spring AI 依赖 Spring Boot 3.x 的自动配置机制,老版本 Spring Boot 2.x 是带不动的。建议新建一个干净的项目来做 demo,避免和老项目里的类冲突。

2.2 引入依赖:BOM 与 Starter

Spring AI 的依赖坐标不同版本变化过几次。在我用的 1.0.0 GA 版本里,Maven 项目需要在dependencyManagement里先引入 BOM,再加对应的 starter,这样才能保证各子模块版本一致。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> </parent> <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> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> </dependencies>

这里有个容易踩的坑:网上不少老教程写的是spring-ai-openai-spring-boot-starter这种坐标。在你的 1.0.0 项目里完全可能拉不下来,或者拉到一堆传递依赖冲突。遇到这种问题,去官方文档查当前版本的 artifactId 最靠谱。

2.3 配置文件:API Key 与基础参数

接下来在application.yml里配置模型接入信息。我用的是 Spring AI 内置的默认前缀spring.ai.openai,这只是框架的默认命名,不代表只能接某一家服务商。目前主流的大模型平台普遍支持同一套标准的 HTTP 接口协议,你只需要把 base-url 和 api-key 换成自己实际用的就行。

spring: application: name: spring-ai-demo ai: openai: api-key: ${LLM_API_KEY} base-url: ${LLM_BASE_URL} chat: options: model: gpt-4o-mini temperature: 0.7

API Key 不要硬编码进配置文件,用环境变量注入是更稳妥的做法。base-url则要填你所用模型服务的完整地址前缀,不带具体路径。

注意:temperature参数控制的是回答的随机性。数字越高越有创造性,越低越稳定。做分类、抽取类任务建议调到 0.2 以下,做文案生成可以保持 0.7 左右。

2.4 第一个能跑的最小 Demo

配置完成,写一个最小接口验证链路通不通。

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @PostMapping("/chat") public String chat(@RequestBody String message) { return chatClient.prompt() .user(message) .call() .content(); } }

启动项目后用 curl 测一下:

curl -X POST http://localhost:8080/chat \ -H "Content-Type: application/json" \ -d '"你好,介绍一下你自己"'

如果配置正确,你会收到模型返回的一长串自我介绍。到这里,Java 接入大模型的第一步就算真正迈出去了。

3. ChatModel 与 ChatClient——核心 API 的使用细节

3.1 从底层到门面:理解两个核心接口

ChatModel是底层接口,只负责把Prompt发给模型并返回ChatResponse。ChatClient则是面向日常开发的更高层封装,它内部持有ChatModel,提供了链式调用的 DSL 风格 API。

大多数场景我都推荐直接用ChatClient,因为它把 prompt 的拼装、参数的设置、输出结构的转换都收进了一套直观的链式语法。而且 Spring AI 官方也把ChatClient作为未来演进的重点,很多新特性都优先加在这层。

当然,如果你需要非常细粒度的控制,比如自定义重试策略、定制请求头,那还是可以绕过ChatClient直接操作ChatModel。不过这是少数情况,入门阶段先熟练ChatClient就够用了。

3.2 Prompt 的重心:System 与 User 消息

最近几年做 AI 应用有个共识:回答质量的关键往往不在模型,而在 Prompt 设计。Spring AI 里写提示词也很直观,最基础的是system和user两段。

String answer = chatClient.prompt() .system("你是一名资深Java架构师,回答问题时必须结合Spring生态的实践经验。") .user("如何设计一个高并发的消息推送系统?") .call() .content();

system消息用来定义角色和约束规则,user消息才是用户的真实提问。这两者的区别一定要分清楚,因为很多所谓“提示词注入”的问题,本质上就是把不可信的用户输入放进了 system 消息。

实践中我还会再加参数控制:

String answer = chatClient.prompt() .system("你是一名严谨的技术编辑,输出内容需要结构清晰。") .user("把这段产品介绍改写成三段式的微信公众号文章。") .options(ChatOptions.builder() .temperature(0.8) .maxTokens(800) .build()) .call() .content();

maxTokens限制的是输出长度上限,而不是输入长度。这个参数对控制成本很重要,下面讲 Cost 的时候会再展开。

3.3 流式输出:打字机效果的实现

如果只是同步等待完整回答,对用户来说会比较呆板。Chat 类产品通常希望模型一边生成一边把文字推送出来,这在 Spring AI 里用stream()即可。

Flux<String> stream = chatClient.prompt() .user("给我讲一个程序员过年的笑话") .stream() .content(); stream.doOnNext(System.out::print).subscribe();

后端接口想要把流式输出透传给前端,可以配合 WebFlux 写一个返回Flux<String>的接口,前端用 SSE 或者 fetch stream 读取。响应式编程对刚接触的人可能有点门槛,但基本套路就是:把业务逻辑当作一个数据流,在流上做订阅和处理。

我遇到过不少朋友在这里卡住——没有 subscribe 就没有输出。Flux 是冷流,不订阅就不会执行。调试的时候先.blockLast()也能看到完整内容,但线上环境最好不要用阻塞方式。

3.4 多轮对话与上下文管理

模型本身没有记忆,所谓“多轮对话”就是你每次都要把历史消息一起发给它。Spring AI 里可以通过messages方法手动拼出完整的历史。

List<Message> history = new ArrayList<>(); history.add(new UserMessage("我叫小明")); history.add(new AssistantMessage("你好,小明!很高兴认识你。")); history.add(new UserMessage("那我叫什么名字?")); String answer = chatClient.prompt() .messages(history) .call() .content();

这段代码跑出来的回答应该是“你叫小明”。如果你不传历史消息,模型大概率会回答“我不知道”。这就是上下文管理的意义所在。

更完整的方案是在应用层维护一个会话窗口,比如按conversationId存最近 N 轮消息,超出的部分截断。真实项目里一般不会把所有历史无限发给模型,因为 Token 数量会膨胀,成本会失控。

4. 结构化输出与函数调用——让模型真正能用起来

4.1 结构化输出:从自由文本到 Java 对象

开发中我们经常不是要一段散文,而是要从模型返回里拿到姓名、地点、金额这类字段,直接落库或者参与逻辑判断。最笨的方法是把结果当字符串解析,但模型输出不稳定,正则写到最后会让人崩溃。

Spring AI 提供了entity()方法,直接把模型输出映射成 Java 对象。比如我有一个 record:

public record PersonInfo(String name, String city, String job) {}

然后这样调用:

PersonInfo person = chatClient.prompt() .user("从这句话里提取结构化信息:张三住在北京,目前担任产品经理。") .call() .entity(PersonInfo.class); System.out.println(person.name()); System.out.println(person.city()); System.out.println(person.job());

它的原理是框架自动把 Java 类型描述成 JSON Schema 发给模型,让模型严格按照这个结构返回 JSON,再反序列化回对象。这个能力表面上很普通,实际用起来非常香。以前做一个“意图识别 + 信息抽取”接口,我至少要写几十行解析逻辑,现在一个 record 定义就解决了。

4.2 常见结构化输出的坑

record 字段名最好不要用太诡异的名字,模型不一定能理解。字段总是写中文名也行,但建议加注释或者让字段名本身足够语义化。

另外,一定要设置较低的 temperature,否则模型输出会“飘”,偶尔多给你一个字段,反序列化就会报错。遇到这种情况,可以把temperature调到 0,再用.call().entity()强制约束。

还有一个容易忽略的点:模型返回的 JSON 可能会携带 Markdown 代码块标记。Spring AI 的反序列化通常能自动处理干净,但我遇到过极端情况,偶尔会残留开头的老八股。解决办法是自己在系统提示里明确要求“只输出 JSON,不要其他内容”。

4.3 Function Calling:让模型调用你的方法

结构化输出解决的是“让模型按格式说话”,但很多时候我们需要“让模型按需做事”。最典型的场景是:用户问“北京天气怎么样”,模型其实不知道天气,但它可以调用你写好的天气查询函数,再把结果组织成自然语言回答。

Spring AI 对这类场景的支持叫 Tool Calling,也叫 Function Calling。先定义一个普通的方法,再注册成工具。

public record WeatherRequest(String city) {} public record WeatherResponse(String condition, int temperature) {} @Bean public ToolCallback weatherTool() { return new MethodToolCallback( "get_weather", "查询指定城市的实时天气,输入城市名,输出天气状况和温度。", new Function<WeatherRequest, WeatherResponse>() { @Override public WeatherResponse apply(WeatherRequest request) { // 这里可以对接真实的天气服务 return new WeatherResponse("晴", 25); } }); }

然后在 prompt 里挂载这个工具:

String answer = chatClient.prompt() .user("北京今天天气怎么样?") .tools(weatherTool()) .call() .content();

模型在回答前会先判断“这个问题需要调用 get_weather”,然后向框架发起工具调用,框架执行你的apply方法后,把结果继续交给模型生成最终回答。整个流程对调用方是透明的。

4.4 工具调用失败的排查思路

做 Function Calling 最容易出的问题是模型“不按套路调用工具”。排查时先检查两件事:一是工具描述是否清晰,里面的字段说明是否足够;二是参数对象和返回值对象的类型是否简单,过于复杂的嵌套类型会干扰模型的理解。

另一个经验是,不要注册太多无关工具。工具描述会让模型产生误判,工具越少,选择越准确。我见过一个项目把十几个工具都注册上去,模型经常选错,后来精简成三个核心工具,准确率立刻上来了。

提示:工具描述中不要写“这是内部使用”“仅用于测试”之类的文字。描述得好不好,直接影响模型的调用判断,这是我在实际项目中反复验证过的。

5. 实操中的常见问题与排查实录

5.1 常见报错速查表

这个表是我自己项目中遇到过的问题汇总,不保证覆盖所有情况,但覆盖面已经不小。

错误现象可能原因处理方式
401 UnauthorizedAPI Key 错误或未加载检查环境变量、配置文件占位符
Connection refused / timeoutbase-url 错误或网络不通确认服务地址可达、检查超时配置
400 Bad Request模型名拼写错误或参数越界对照服务商文档核对模型名
JSON parse error模型输出不满足结构调低 temperature、启用 entity 结构化
context length exceeded历史消息过长截断或压缩历史,控制 Token
Tool call failed工具描述不清晰或类型不匹配简化参数结构,优化描述

最频繁出现的是 401 和 connection timeout,绝大多数是配置问题而不是代码问题。遇到 401,先确认环境变量是不是真的传进了启动进程;遇到超时,先把服务地址复制到浏览器或 Postman 里试一遍,排除网络因素再回来查代码。

5.2 超时与重试配置

大模型接口普遍比普通 HTTP 接口慢,默认超时通常不够用。Spring AI 支持自定义客户端超时时间,在配置文件中增加如下内容:

spring: ai: client: connect-timeout: 10s read-timeout: 60s

connect-timeout是建立连接的超时,read-timeout是从连接建立到收到完整返回的超时。我自己习惯把 read-timeout 调到 60 秒以上,否则流式输出稍长一点就会被客户端断开。

重试机制要谨慎。如果是网络抖动,重试一次两次没问题;但如果模型本身报错,重试只会烧更多 Token,成本翻倍。建议对“任务已提交但响应超时”这类场景做幂等处理,业务侧能接受重复调用才加重试。

5.3 Token 成本控制与估算

Token 是成本的直接来源,很多人在 demo 阶段没什么感觉,一上生产就傻眼。一个几百字的 system prompt,乘以每次请求的用户数,很快就会变成一笔不小的开销。

控制成本的几个手段:第一,maxTokens设上限,防止模型生成冗长回答;第二,精简 system prompt,去掉废话;第三,历史消息做窗口化处理,不要无限累积;第四,对确定性要求高的接口,考虑使用更便宜的小参数模型。

实际的 Token 计费一般按输入和输出分开算,不同模型价格差距很大。上线前建议用压测脚本跑一轮真实业务场景,估算单次请求的平均 Token 消耗,再按预期调用量算一下月度成本,决策就有据可依了。

5.4 版本兼容:坐标、行为、Deprecated

Spring AI 从 0.x 到 1.0.0 经历了不小的 API 变化。我写这一节想强调一个经验:如果你在百度上搜到一篇很老的教程,正文里出现了ChatClientBuilder、OpenAiChatClient之类的类名,那它基本是 0.8 或 0.9 时代的代码,直接照抄大概率编译不过。

1.0.0 之后,官方推荐的入门方式就是注入ChatClient.Builder,然后通过链式 API 构建请求。老的RestTemplate式写法很多已经标记为 Deprecated。升级版本时,别只看编译错误,还要注意行为变化,比如工具调用从自动注册改成了显式tools()声明,这属于语义层面的变化。

6. 从 Demo 到生产,我的一点经验总结

6.1 一个最小可用的项目形态

Demo 能跑起来是第一步,真的要放进生产项目,我建议至少包含这么几个模块:模型调用层(负责 ChatClient 封装)、Prompt 模板层(集中管理提示词)、会话存储层(维护多轮历史)、审计日志层(记录请求响应和 Token 消耗)。不需要做成微服务,单模块内部分层就够用。

一个典型的结构差不多是这样:

src/main/java/com/example/ai ├── controller/ChatController.java ├── service/ChatService.java ├── tool/ToolConfig.java ├── record/PersonInfo.java └── config/AiConfig.java

6.2 生产环境的三件小事

日志是最容易被忽略的。模型回答不可控,线上出了问题如果看不到当时的 prompt 和返回结果,排查会非常痛苦。我习惯在调用入口打结构化日志,记录用户输入、模型输出、Token 用量和耗时,这几行日志往往比监控系统更先帮你定位问题。

限流和降级也是必须做的。大模型接口的响应时间天然不稳定,突发流量来的时候,该排队排队,该拒绝拒绝。给模型调用做一个简单的熔断机制,比如连续失败达到阈值就降级为预设的本地回答,能有效避免用户侧体验雪崩。

6.3 关于 Spring AI 的一句话心得

我不太想用“框架很强大”这种空话收尾。说点实在的:如果你已经是 Spring 开发者,Spring AI 的入门极快,真正花时间的地方不在 API,而在“怎么把模型能力嵌进业务逻辑”。我在调试 Function Calling 时来回折腾了挺久,最后发现关键不是代码,而是把工具描述写得够清楚。AI 应用的开发方式确实和传统后端不一样,它更像是在教一个聪明但没常识的新同事干活,而你写的每一段 prompt、每一个工具描述,都是在帮它补常识。大概这就是 Java 开发者接入大模型最需要转变的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 7:41:33

深度学习水果识别毕设全攻略:从图像分类到Web部署完整实践

1. 选题拆解&#xff1a;为什么“水果识别”是毕业设计的黄金题目1.1 选题定位与技术含量如果你正在纠结毕业设计做什么&#xff0c;我强烈建议认真考虑图像分类方向&#xff0c;而“水果识别”又是其中性价比最高的题目之一。原因很简单&#xff1a;它技术覆盖面完整、结果可视…

作者头像 李华
网站建设 2026/10/17 7:41:20

多模态大模型VLM从架构设计到落地部署的完整实践指南

做多模态大模型&#xff08;VLM&#xff09;这几年&#xff0c;我最大的感受是&#xff1a;图文理解这件事&#xff0c;外行看是"让AI既能看又能说"&#xff0c;内行看其实是"怎么把两种完全不同的信息塞进同一个数学空间"。VLM&#xff08;Vision-Languag…

作者头像 李华
网站建设 2026/10/17 7:41:11

RPA+大模型:打造外部群聊专家级自动应答系统

1. 从一线需求说起&#xff1a;为什么群聊回应也能自动到“专家级”做运营和客户成功的朋友&#xff0c;大概率都经历过这种状态&#xff1a;手机里挂着四五个外部群&#xff0c;每个群都在问问题&#xff0c;有的问报价&#xff0c;有的问故障排查&#xff0c;有的纯粹是闲聊。…

作者头像 李华
网站建设 2026/10/17 7:40:12

Flink状态TTL与State Backend清理机制:从配置到生产实践

1. 从一次状态爆炸说起&#xff1a;为什么TTL和State Backend是Flink绕不开的坎刚接触Flink那会儿&#xff0c;我写过一段看起来特别“优雅”的代码&#xff1a;用ValueState存用户最近一次登录的设备指纹&#xff0c;想着反正状态后端会自动管理&#xff0c;不用操心清理。结果…

作者头像 李华
网站建设 2026/10/17 7:40:10

双Agent架构实战:从单Agent瓶颈到企业级数字劳动力落地

做企业级智能体有一段时间了&#xff0c;最大的体感是&#xff1a;很多团队卡住不是因为模型不够聪明&#xff0c;而是架构没跟上。把智能体接到对话框里做一个问答助手&#xff0c;这事儿现在已经不太稀奇了&#xff1b;难的是让它真得像一名员工一样&#xff0c;接住一个目标…

作者头像 李华
网站建设 2026/10/17 7:39:49

SSM家庭健康管理系统:临床级指标建模与动态预警实现

简介&#xff1a;这是一套基于SSM&#xff08;SpringSpringMVCMyBatis&#xff09;框架开发的家庭健康管理系统实战项目&#xff0c;面向Java初学者与Web开发入门者&#xff0c;聚焦家庭场景下的健康指标数字化管理需求。系统涵盖家庭管理、成员档案维护、健康数据上报、指标动…

作者头像 李华