最近接手了一个内部系统的小项目,需求特别直白:用户提问,系统调大模型回答。说实话看到这个需求我第一反应是“又要写一遍HTTP调用、封装请求体、处理流式响应、搞重试机制……”,这些零碎活我以前没少干,每个大模型厂商的SDK风格还不一样,换个模型就得动一层代码。这次我决定换条路,刚好团队里有人提到Spring家族新出的AI脚手架,再加上通义千问的3.5模型,组合起来确实有点东西——Spring AI + 千问3.5,真正做到了“一行代码接上大模型”,后面你会看到这一行是怎么来的,以及背后的门道。
这篇文章适合两类人看:一是Java后端、平时写Spring Boot,想给项目快速加AI能力但不想自己手搓底层调用的;二是已经用过OpenAI或各家SDK,但觉得代码耦合太深、想统一接入层的。我会把从环境准备、依赖引入、配置项含义,到多轮对话、流式输出、结构化返回、工具调用的完整链路都过一遍,过程中会穿插一些我实际踩过的坑,这些坑官方文档里基本不会写。
先说结论:Spring AI这个脚手架把大模型接入抽象成了几个聊天客户端接口,底层对接哪家模型由配置和依赖决定,业务代码只管ChatClient或ChatModel,所以“一行代码”不是噱头,是抽象层级设计带来的结果。但要用得顺手、不踩坑,你得明白它在你项目里到底都干了什么。
1. 为什么选Spring AI,而不是继续手写HTTP调用
1.1 手写调用的问题,我都替你们踩过
以前接大模型,最朴素的做法是拿RestTemplate或OkHttp对着API文档写请求。单个模型还好,一旦要切换模型或者同时接多家,就麻烦了:
- 每家模型的鉴权方式不一样,有的是Header里放Key,有的是自定义签名;
- 请求体结构不一样,OpenAI系是messages + model,国产厂商有时叫prompt、有时叫input;
- 流式响应格式更是五花八门,SSE解析逻辑得自己一遍遍调;
- 业务代码里夹杂着大量的JSON拼装、错误码映射,时间一长根本不敢动那块代码。
这些痛点本质上是“模型厂商SDK与业务代码强耦合”。你写的是业务功能,不是某个模型API的客户端,但实际代码里80%都在处理协议细节。
1.2 Spring AI的抽象思路
Spring AI做的事就是把“大模型”抽象成几句话:
- 面向开发者提供一个统一的聊天客户端接口(ChatClient / ChatModel);
- 模型差异通过“模型适配器”隔离,你引入哪个厂商的starter,它就装配哪个实例;
- 请求参数、输出解析、流式处理这些脏活全在框架层面消化掉。
类比一下就懂了:以前你接大模型,等于自己去菜市场买菜、洗菜、切菜、开火;用了Spring AI,相当于你直接面向菜单点菜,后厨谁炒的、用哪个灶台你根本不关心。业务代码只需要说“我要一份宫保鸡丁(问千问一个问题)”,剩下的交给框架。
这也解释了为什么标题说“一行代码”:因为最难的那部分已经被框架替你做了,你需要做的只是声明一个客户端、注入进去、调用方法。
2. 环境准备与最小依赖:版本卡的严,差一点就启不来
2.1 先说版本这件“小事”
Spring AI目前对版本高度敏感。我一开始图省事,随便找了一个可用的Spring Boot 3.2版本配Spring AI 0.8.1,结果启动直接报找不到自动配置类。后来才反应过来,Spring AI的版本和Spring Boot的版本有严格的对齐关系。
我实测下来比较稳的组合是:
| 组件 | 版本 | 备注 |
|---|---|---|
| JDK | 17+ | Spring Boot 3.x强制要求 |
| Spring Boot | 3.3.x 或 3.4.x | 3.2也能跑,但建议新一些 |
| Spring AI | 1.0.0-M系列 或 0.8.1+ | 里程碑版功能更全 |
| Maven | 3.6+ | 常规要求 |
| DashScope SDK | 跟随Spring AI管理 | 不需要单独手写版本 |
提示:Spring AI版本分M系列、RC系列、正式版,M系列代表里程碑版,API可能微调。生产环境建议用正式版或至少RC版,别追太新的M版。
2.2 引入依赖,注意还需要单独的Repository
使用Spring AI某个版本时,如果没配置专门的仓库,Maven会下载不到部分依赖。原因在于Spring AI在早期里程碑阶段发布到了Spring的里程碑仓库里。
我在pom.xml里这样配:
<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-qwen</artifactId> </dependency> </dependencies>引入之后,Maven会自动把通义千问的模型适配器、自动配置类和底层HTTP客户端都拉下来。不需要额外引DashScope的OpenAI兼容SDK,Spring AI已经封装好了。
2.3 配置项:API Key和模型名就够了
在application.yml里加这些:
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7注意,api-key建议走环境变量,不要硬编码到配置文件里。model配的是通义千问3.5系列里的具体模型标识,比如qwen-plus、qwen-turbo这类。你可以理解成“选哪个档位的模型”,plus更聪明但慢一些,turbo更快更便宜。
配置完成之后,整个“集成大模型”这件事就已经完成60%了,接下来就是写代码调用。
3. 一行代码背后的自动装配:到底谁在帮你干活
3.1 只用ChatClient,不关心底层是谁
当我写好配置、拉起项目后,真正写业务代码时发现,只需要注入一个接口:
@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt(message).call().content(); } }就这,一个能对话的大模型接口就已经上线了。如果用一句话概括这里发生的事:Spring AI的自动配置检测到你引入了qwen的starter,会自动往容器里放一个通义千问的ChatModel实现,然后通过ChatClient.Builder把统一的客户端构建好。你在代码里只看到ChatClient,完全没有出现“qwen”字样。
3.2 自动装配链路拆解
Spring AI的自动装配核心是ChatClientAutoConfiguration这块逻辑:
- 扫描classpath里有没有对应的模型starter依赖;
- 如果有,就读取
spring.ai.dashscope.*配置; - 构建一个
DashScopeChatModel实例; - 再把它包装成
ChatClient.Builder,注入到容器里。
所以当你在业务代码里用builder.build()时,拿到的其实是一个“所有外界差异都被屏蔽”的客户端。你传一个Prompt进去,它内部会帮你构造消息结构、携带模型参数、处理鉴权、发送请求、解析响应。
3.3 请求过程
我看了下实际运行时的调用轨迹,可以看到框架内部做了不少事情:
- 把
prompt("你好")转成Prompt对象,内部包含一条用户角色消息; - 读取全局默认的模型名和采样参数(temperature等);
- 填充到请求体里,发给DashScope兼容端点;
- 解析返回值,包装成
ChatResponse; - 最后通过
.content()拿到了回答文本。
这个流程里,业务代码只写了“用户说了什么”,至于“用什么模型、怎么对话、怎么解析”全被框架接管了。这就是“一行代码”的真相。
4. 从Demo到真实业务:多轮对话、流式输出与结构化解析
4.1 多轮对话:别再用String拼接历史消息了
很多人在没有框架时,多轮对话都是手动维护一个历史消息列表,每次请求前拼进去。Spring AI里直接用MemoryChatMemory:
@Bean ChatMemory chatMemory() { return new InMemoryChatMemory(); } @Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultSystem("你是一个乐于助人的智能助手,回答简洁准确。") .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); }调用时加一个会话ID,框架自动帮你记住上下文:
String answer = chatClient.prompt() .user("我叫张三") .advisors(a -> a.param("chatId", "session-001")) .call() .content(); String answer2 = chatClient.prompt() .user("我叫什么名字?") .advisors(a -> a.param("chatId", "session-001")) .call() .content();第二个问题会正确回答出“你叫张三”。它的原理是MessageChatMemoryAdvisor在每次请求前,根据chatId把历史消息从ChatMemory里捞出来塞进Prompt里,请求结束后再把本轮对话写回去。这个机制避免了手写历史队列以及并发下会话串号的问题。
4.2 流式输出:聊天体验的关键
Web应用里的AI回复通常是一字一字蹦出来的,不能等全部生成完一次性返回,否则首字延迟太高,体验很糟糕。Spring AI的流式调用我实测用Flux<String>非常顺手:
@GetMapping(value = "/chat/stream", produces = "text/plain;charset=UTF-8") public Flux<String> stream(@RequestParam String message) { return chatClient.prompt(message).stream().content(); }前端接到的是SSE流,每段就是一个增量token。Spring AI内部会自动帮我们处理流式响应格式的解析,不会让你去拆SSE事件。
提示:如果用了Spring Cloud Gateway之类的网关,记得配置缓冲关闭,否则流式响应会被网关先缓存完再一次性返回,流式就失效了。
4.3 结构化输出:让模型返回JSON而不是废话
对话接口偶尔用来给人看没问题,但业务系统很多场景是需要模型返回结构化数据,比如从用户描述里提取“日期、地点、人物”。以前我要写正则、要不断调prompt让模型“不要多解释”。Spring AI提供了entity方法,直接映射到POJO:
public record MeetingInfo(String date, String location, String participants) {} MeetingInfo info = chatClient.prompt() .user("帮我安排下周三下午三点在第二会议室开项目评审会,参会人是A和B") .call() .entity(MeetingInfo.class);框架会通过“结构化输出提示词”自动约束模型,返回的内容会被反序列化成对象。实测下来准确率比我手工构造JSON输出提示词高不少,因为框架层的提示词经过了专门调教。
5. 避坑实录:接入过程中我摔过的五个跟头
5.1 网络访问问题:时间超时和连接失败
大模型接口响应时间长,默认的HTTP超时设置往往不够。我第一次跑长问题时就遇到了SocketTimeoutException。解决方式是在配置里把超时调大:
spring: ai: dashscope: connect-timeout: 30s read-timeout: 300s如果是把应用部署在某些网络受限的环境里,连接DashScope的域名不通,排查方法很简单:先curl一下模型服务的端点,能通再跑应用,省得在框架层瞎猜。
5.2 模型版本标识对不上
通义千问3.5系列里,不同能力档位对应的模型标识不一样。写错模型名会报InvalidParameter或找不到模型。我刚开始用qwen3.5-max测试,结果发现这个标识在对应环境里并不存在,改成qwen-plus后一切正常。接入前建议先确认你账户里有效的模型标识,可以用DashScope控制台的模型列表或文档核对,别想当然。
5.3 依赖冲突:OkHttp和HTTPClient之争
Spring AI底层用的是RestClient或者特定的HTTP客户端,如果你项目中同时引入了老版本OkHttp或者其他HTTP库,偶尔会有版本冲突。典型报错是NoClassDefFoundError。我的处理方式是排掉传递依赖里冲突的版本,统一用Spring Boot管理的版本。
5.4 自动配置没生效:属性前缀写错
Spring AI历史版本里,通义千问的配置前缀可能是spring.ai.qwen,后来调整成spring.ai.dashscope。如果你按旧文章写配置,应用会启动成功,但实际用的是默认地址或者空白Key,请求直接401。遇到自动配置没生效的情况,先检查spring.factories或AutoConfiguration.imports文件里有没有相关配置类,再检查前缀是否匹配当前版本。
5.5 代理环境的特殊情况
有个某公司的同事跟我说他接入后一直超时,后来发现是公司网络出口需要走代理。给Java进程配上代理参数就好了:
java -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080 -jar app.jar这类环境问题容易伪装成框架问题,遇到连接类报错,先分清楚是网络问题、代理问题还是认证问题,一层层排除,别上来就改业务代码。
6. 进阶玩法:让千问3.5帮你干真正的活
6.1 函数调用(Function Calling):把模型接到你的系统上
大模型本身只有语言能力,它不知道你系统里的订单数据、用户信息。函数调用的思路是:你告诉模型有哪些工具可用,当用户的问题需要查数据时,模型不直接编答案,而是返回一个“调用某工具、参数是什么”的结构,你的代码执行工具拿到真实数据后,再把结果喂回给模型生成最终回答。
Spring AI里把这个抽象成了@Tool注解,用起来极其直观:
@Service public class OrderTools { @Tool(name = "getOrderStatus", description = "根据订单号查询订单状态") public String getOrderStatus(String orderId) { // 这里调用你的订单服务,返回真实状态 return "已发货"; } } @Bean ChatClient chatClient(ChatClient.Builder builder, OrderTools orderTools) { return builder .defaultTools(orderTools) .build(); }用户问“订单12345现在什么状态”,千问会先触发getOrderStatus("12345")拿到真实结果,再组织语言回答。整个过程无需我写解析逻辑,框架帮我们完成了工具的注册、参数提取和调用结果回填。
6.2 多模型切换:配置即切换
Spring AI的另一个价值是厂商无关。项目里如果把ChatModel作为依赖,将来要换模型,不需要改业务代码,只需要改依赖坐标和配置。这个设计在模型成本、效果对比、容灾备份这些场景下非常实用。
提示:生产环境建议做个简单的模型路由层,按需切换到不同模型,而不是把所有配置都写死在application.yml里,这样灰度切换或降级会容易很多。
6.3 结合Spring MVC的路由设计
把AI能力和业务系统结合时,我通常不会直接暴露一个裸的chat接口,而是做一个门面层。比如用户输入的原始文本先进业务系统做意图识别,判断是要走客服问答、订单查询还是闲聊,再分别调用不同的提示词模板和工具组合。Spring AI的ChatClient天然适合做这种门面,因为它本身就是轻量的、无状态的,可以按场景构建不同的客户端实例。
7. 最后再分享一个我自己很受用的技巧
项目真正上线之后,我发现一个细节值得特别注意:大模型的回复质量,很大程度上取决于你的System Prompt设计,而不是模型参数。
我习惯在ChatClient构建时配置defaultSystem,把业务背景、回答风格、边界条件都写清楚。比如客服场景我写“你是电商平台的售后服务助手,回答要友好简短,涉及退款问题时先引导用户提供订单号,不知道的信息不要编造”。这样做以后,用户满意度比不写System Prompt时高不少,而且能明显减少模型胡编乱造的情况。
另外,别忽略日志。Spring AI本身没有特别丰富的调用日志,我建议自己给ChatClient加一层包装或者至少打印请求和响应的摘要信息。真正出问题的时候,有日志和没日志排查效率差着好几倍。我用的是最简单的过滤器思路,在调用前后记录模型名、输入长度、输出长度、耗时,对线上问题定位帮助极大。
回头再看这个项目,最大的感受是:集成大模型这件事,技术门槛已经被框架压得很低了。真正拉开差距的,是你懂不懂模型参数背后的含义、懂不懂上下文管理的机制、懂不懂工具调用怎么和业务系统结合。希望这篇文章能帮你少走几步弯路,尤其是版本和配置那一块,别像我一开始那样,光启动就折腾掉半天。