news 2026/10/23 6:53:54

Spring AI 实战:从原生 API 到框架接入的工程化迁移指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 实战:从原生 API 到框架接入的工程化迁移指南

接手一个 Spring Boot 老项目时,需求是把大模型能力接进来做智能问答和文档摘要。一开始大家想的是直接调 HTTP 接口,毕竟业务代码里拼几个 JSON 参数看起来也不难。真正动手才发现,鉴权、超时、重试、流式输出、上下文管理这些杂事会占掉大半开发时间。后来把 Spring AI 框架引入进来,才发现原来框架接入大模型的正确姿势不是“少写代码”,而是“把工程复杂度交给框架”,这篇就把我实际接入的思路和过程完整梳理一遍,供正在纠结框架接入和原生调用的同学参考。

1. 为什么我选择 Spring AI:从原始 API 调用的迁移之路

1.1 直接调用 API 的痛点:大量样板代码

先说说我最初用原生方式调大模型接口的体验。每接一个大模型厂商,都要写一份独立的 HTTP Client 封装:签名要按文档算、请求体要根据模型版本调整参数名、返回结果要自己解析嵌套的 JSON,遇到流式返回还要处理 SSE 事件流。

这还不算完。业务上需要把用户多轮对话拼成完整消息列表,需要处理 token 超长时的截断策略,需要在模型临时不可用时做降级。这些逻辑一旦散落在 Service 层里,项目很快就变成一堆不可维护的胶水代码。

我试过的做法是写一个LlmClient工具类,统一封装对话接口。刚开始觉得挺清爽,但随着接入的模型越来越多,问题逐渐暴露:不同模型的超时参数、温度范围、上下文长度上限完全不一样,工具类里的 switch-case 越来越多,每次新增一个模型就要改动现有代码,维护成本直线上升。

1.2 Spring AI 解决的是一整套工程问题

Spring AI 给我的第一印象是:它把大模型接入变成了一个 Spring 风格的工程问题,而不是单纯的 API 调用问题。框架底层替我们处理了模型切换、请求重试、结构化输出、对话记忆这些通用能力,业务代码只需要面向统一接口编程。

举个最直观的例子。原生方式下,不同厂商的对话补全请求体格式千差万别,有的叫messages,有的叫prompt,有的用temperature做随机度控制,有的用top_p。而 Spring AI 把这些差异完全隐藏掉了,业务侧只认一个ChatClient接口,传入消息、拿到响应,至于底层走的是哪个厂商的哪个模型,对上层完全透明。

另外它还继承了 Spring Boot 的自动配置能力。引入对应依赖后,在application.yml里配置好模型凭证,一个可用的聊天客户端就能直接注入到任何 Bean 中。这种“零配置即可用、按需定制”的体验,让团队里不熟悉大模型细节的后端同学也能快速上手。

1.3 更适合哪些项目场景

经过一段时间的实战,我总结出 Spring AI 特别适合的场景:

  • 已经深度使用 Spring Boot 作为主框架的项目。技术栈统一,学习成本低。
  • 需要对接多个模型厂商,且未来可能随时替换或增加模型。框架层的统一抽象让切换成本变得极低。
  • 业务上需要模型输出结构化数据,直接绑定到 Java 对象,而不是用字符串后处理。
  • 需要一个相对正式的 AI 功能接入方案,而不是临时写脚本调 API 的方式。

反过来,如果你的需求只是写一个一次性脚本跑一个 Prompt,或者对包体积极度敏感,那直接用原生 SDK 可能更轻量。Spring AI 的优势在工程化场景,不在纯脚本场景。

2. Spring AI 的核心抽象:一切皆可插拔的统一接口

2.1 ChatClient:最常用的门面

在 Spring AI 里,业务打交道最多的就是ChatClient。它可以理解成一个大模型对话的客户端门面,封装了 Prompt 构造、模型调用、响应解析的全过程。

看一个我实际项目中的写法:

@RestController public class ChatController { @Autowired private ChatClient chatClient; @PostMapping("/chat") public String chat(@RequestBody String message) { return chatClient.call(message); } }

就这么几行代码,一个能对话的 HTTP 接口就跑起来了。chatClient.call()会自动帮我处理模型请求、超时重试、结果整理这些事。如果想要更多控制,比如自定义系统提示词,可以这样做:

String response = chatClient.call(new Prompt( "你是一名经验丰富的Java工程师", message ));

这里Prompt对象就是 Spring AI 对“一次模型对话请求”的封装,除了systemText和userText,还可以带历史消息、模型参数、工具定义。所有信息最后会被框架组装成对应厂商兼容的请求格式,发送到模型端。

2.2 模型的统一交换:更换厂商只需改一处配置

Spring AI 的模型接入采用 SPI 机制,每个厂商都有自己的 AutoConfiguration。这意味着我在代码里基本不需要关心具体模型,只需要在配置文件中切换。

假设我先接入了某 AI 服务商的 A 模型:

spring: ai: model: provider: provider-a api-key: ${LLM_API_KEY} model: model-a

业务代码不需要任何改动。如果想要换成 B 模型平台,只要换掉依赖包并修改配置:

# 换成另一个厂商 spring: ai: model: provider: provider-b api-key: ${LLM_API_KEY_B} model: model-b

这种无感切换在实际项目里太有用了。比如某个模型线上负载过高时,我在配置中心动态切换模型端点就能完成迁移,不用重新发版。框架负责把不同厂商的响应差异统一成自己的数据模型,业务层完全无感知。

2.3 提示模板与输出解析器

大模型应用很少只传一个固定字符串,经常需要把用户输入动态拼接到一个模板里。Spring AI 提供了提示模板机制,类似 Java 里的MessageFormat:

String template = "用户提问:{question}\n请基于产品文档回答问题,要求简洁准确。"; String prompt = PromptTemplate.format(template, Map.of("question", userQuestion)); String answer = chatClient.call(prompt);

输出解析器则解决了另一个大痛点。模型输出的是自然语言文本,很多时候我们希望直接得到结构化结果。Spring AI 的BeanOutputConverter可以定义目标 Java 类,然后自动把模型输出映射为对象:

public record ProductInfo(String name, String category, Double price) {} BeanOutputConverter<ProductInfo> converter = new BeanOutputConverter<>(ProductInfo.class); String content = """ 请从描述中抽取商品信息: 这是一款无线机械键盘,属于外设配件,价格为399元。 """; // 拿到模型输出后自动转换为对象 ProductInfo info = converter.convert(chatClient.call(content));

这个能力让我省了大量 JSON 解析和后处理的代码。它内部通过与模型协商输出格式、再把结果反序列化为对象来实现,保证了结构的稳定性。

3. 实战接入:从零跑通一个最小 LLM Demo

3.1 工程搭建与依赖引入

我按照新项目的方式演示整个接入过程。首先创建 Spring Boot 工程,用的是 3.x 版本,JDK 17。Spring AI 目前对 Spring Boot 3.x 的支持最稳定,建议直接使用。

在pom.xml里添加依赖:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>1.0.0</version> </dependency>

目前 Spring AI 还在快速迭代中,如果使用的是 Maven 中央仓库没同步的版本,可能需要配置里程碑仓库。我的做法是直接使用正式发版版本,避免仓库配置带来的额外复杂度。

3.2 配置多渠道模型连接

接下来在配置文件中配置模型连接凭证。以最常用的方式演示:

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

这里我用openai配置块做示例,实际接入其他厂商时结构类似,只是 key 名略有不同。通过${OPENAI_API_KEY}引用环境变量,避免把密钥写进代码仓库,这一点在真实项目中尤为重要。

如果你同时接入了多家模型厂商,Spring AI 也允许为不同的ChatClientBean 指定不同的模型配置。可以通过@Qualifier注解来区分使用哪个客户端,让一个项目里同时存在多个不同模型的对话通道。

3.3 编写第一个聊天接口

有了依赖和配置,接下来就是写业务入口。为了演示方便,我直接写一个简单的 Controller:

@RestController @RequestMapping("/api/ai") public class AiChatController { private final ChatClient chatClient; public AiChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @PostMapping("/talk") public String talk(@RequestBody String question) { return chatClient.call(question); } }

这里我使用的是ChatClient.Builder来创建客户端,而不是直接注入ChatClient。这种写法更灵活,可以在构建时设置默认参数、附加系统提示等。

比如在构建阶段加入系统角色:

this.chatClient = builder .defaultSystem("你是一个耐心、细心的技术客服,回答问题时先给出结论,再补充细节。") .build();

这样后续所有通过该客户端发起的对话,都会自动附带这条系统提示,不用每次调用都传一遍。

3.4 服务启动与验证

配置完成后启动应用,用 curl 验证接口:

curl -X POST http://localhost:8080/api/ai/talk \ -H "Content-Type: text/plain" \ -d "用一句话解释什么是微服务架构"

正常的话几秒内就会返回模型生成的内容。我实际测试时,从请求发出到拿到完整响应,耗时在 1~3 秒之间,取决于模型端负载和网络状况。

如果你觉得等待时间太长,可以考虑把接口改成流式返回,这样边生成边输出,用户感知到的响应速度会快很多。这个后面会单独讲。

4. 让 Demo 变成真正的功能:记忆、工具调用与流式输出

4.1 对话记忆的实现思路

如果不做额外处理,每次调用chatClient.call()都是一次独立的对话。用户问“我今天吃了苹果”,下一句问“那明天能吃什么?”模型根本不知道之前说了什么。

要实现多轮对话记忆,最直接的做法是把历史消息一起传进去。Spring AI 的Prompt支持消息列表,可以把系统提示、历史问答、当前问题一次性组装进去:

List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个健康饮食顾问")); messages.add(new UserMessage("我今天吃了苹果")); messages.add(new AssistantMessage("苹果是不错的水果,含有丰富的膳食纤维。")); messages.add(new UserMessage("那明天能吃什么?")); String answer = chatClient.call(new Prompt(messages));

不过手写历史消息管理很容易出错,而且消息越长,token 消耗越大。实际项目中我更推荐把会话历史存到 Redis 或数据库,每次请求时截取最近几轮历史再传给模型。

还有一种更优雅的做法:使用 Spring AI 提供的ChatMemory接口和MessageWindowChatMemory实现,它可以按会话 ID 自动维护消息历史窗口,超出窗口大小的老消息会被自动丢弃,从而控制 token 消耗。

ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(10) .build(); ChatClient memoryClient = ChatClient.builder(chatModel) .defaultChatMemory(chatMemory) .build();

随后每次调用时传一个ConversationId,框架就能自动关联历史。这个能力在多轮问答场景非常实用。

4.2 工具调用:让大模型学会“用工具”

工具调用是 Spring AI 里最有价值的能力之一。它的核心思想是:模型在生成回答之前,可以根据需要调用你预先定义好的函数,把函数返回结果作为上下文再继续生成。

我用一个查询天气的例子解释。先定义一个工具方法:

public class WeatherTools { @Bean @Description("查询指定城市的当前温度") public Function<String, String> currentWeather() { return city -> { // 这里模拟查询结果,真实项目中可接入天气API if ("北京".equals(city)) { return "当前温度 28 摄氏度,多云"; } return "当前温度 25 摄氏度,晴"; }; } }

注册这个 Bean 之后,在构建ChatClient时启用工具:

this.chatClient = builder .defaultTools("currentWeather") .build();

当用户问“北京今天天气怎么样”,模型会意识到自己不知道天气数据,于是触发currentWeather函数调用,拿到返回结果后组织成一段自然的回答。整个过程用户无感知,就像模型天生知道实时天气一样。

这种能力的应用场景非常广:查询订单状态、计算数学表达式、获取数据库数据、调用内部 RPC 接口等等。把工具调用和 Spring 的依赖注入机制结合起来,大模型就不再是只能聊天的话痨,而是能真正“动手做事”的智能体。

4.3 流式输出:打造打字机效果的对话体验

前面演示的都是等待完整结果返回后一次性输出。但在对话场景中,模型生成一段文字可能需要十几秒,用户面对白屏等待会非常焦虑。流式输出是解决这个问题的标准方案。

Spring AI 提供stream方法,配合 Spring WebFlux 的Flux,可以做到边生成边推送:

@PostMapping(value = "/talk/stream", produces = "text/plain;charset=utf-8") public Flux<String> talkStream(@RequestBody String question) { return this.chatClient.prompt() .user(question) .stream() .content(); }

前端通过 EventSource 或 fetch 流式读取,就能实现类似打字机的逐字输出效果。实测下来,即使整体耗时较长,用户看到第一个字的时间通常不到 1 秒,体验提升非常明显。

特别提醒一句:使用流式接口时,要注意确认响应编码是 UTF-8,避免中文乱码。另外流式输出不适合所有接口,如果对响应有强校验需求的场景,比如要解析成结构化对象,最好还是使用完整输出。

4.4 结构化输出:让结果直接变成 Java 对象

结构化输出在真实项目中的价值被很多人低估了。没有结构化输出时,模型返回一段 JSON 文本,你得自己解析,还要处理模型偶尔多输出几个字、少个括号的意外情况。

前面已经提到用BeanOutputConverter做转换,这里我再补充一个更贴近业务实际的例子。假设系统需要从客户评价中提取满意度评分和反馈要点:

public record ReviewAnalysis( int score, List<String> highlights, String suggestion ) {}

调用时:

String userText = "刚收到的键盘手感不错,但是蓝牙连接偶尔会断,希望改进。"; String prompt = """ 分析以下客户评价,返回JSON格式结果: %s """.formatted(userText); ReviewAnalysis analysis = converter.convert(chatClient.call(prompt)); System.out.println("评分:" + analysis.score()); System.out.println("亮点:" + analysis.highlights()); System.out.println("改进建议:" + analysis.suggestion());

模型会严格按照预定义结构输出,框架负责将结果反序列化为对象。这样后续无论是存储到数据库,还是对接报表系统,都直接操作 Java 对象,代码干净利落。

5. 文档里没写的坑:我的踩坑记录与调优建议

5.1 模型输出编码与响应超时的处理

第一次上线时碰到的第一个坑是中文乱码。模型返回的内容偶尔会出现编码异常,排查了半天,发现是 HTTP 响应默认没有指定字符集,而模型端的输出在某种特殊场景下会被错误解码。

解决方法是在调用接口的配置中显式声明 UTF-8 字符集,并且在 Controller 层统一设置produces = "application/json;charset=utf-8"。另外,在ChatClient构建时设置较宽的超时时间也很关键,复杂 Prompt 的响应时间波动比想象中更大。

我建议超时时间设置在 30 秒以上,同时配合框架的重试机制。但要注意:不是所有请求都适合重试,比如用户支付类操作,重试可能导致重复执行。Spring AI 允许针对不同客户端设置不同的重试策略,这一点需要按业务场景区分。

5.2 对话上下文长度的管理

大模型的上下文窗口是有限的,即便现在很多模型号称支持几十万 token,实际使用时过长的上下文不仅消耗 token,还会显著增加响应延迟和报错概率。

实际项目中我采取的策略是:固定保留最近 N 轮对话,同时在每轮消息前记录时间戳,超过一定时长的对话自动清空重新开始。这样既能维持对话的连贯性,又不会让上下文无限膨胀。

更精细的做法是使用摘要压缩:当对话历史超过阈值时,先用模型对历史做一次总结,把总结作为新的系统提示,之后继续对话。这种方法在某些知识问答场景里效果非常好,能实现近乎无限的“记忆”。

5.3 成本控制与 token 使用优化

大模型接入之后,成本控制就成了一个绕不开的话题。我见过不只一个团队,上线第一个月费用账单让人倒吸一口冷气。

几点亲测有效的省钱经验:

  • 给不同业务配置不同模型。简单分类任务用轻量小模型,复杂创作任务才用大模型。
  • 合理设置temperature等采样参数。过高的随机度会降低回答稳定性,也可能让模型反复重试生成,浪费 token。
  • 在代码层面引入响应缓存。同一类问题如果答案相似,可以把结果缓存到 Redis,设置合理过期时间,能省钱一大截。
  • 监控每次调用的 token 消耗。Spring AI 的响应对象里带有 token 使用统计,建议统一记录到日志或监控系统,这样成本异常时可以及时定位到具体业务线。

5.4 模型版本与框架版本的兼容性

最后一个坑是最容易让人抓狂的:模型端接口升级之后,框架版本没跟上,导致响应格式解析发生异常。Spring AI 版本迭代速度非常快,不同版本间的配置项和 API 变化也比较大。

我的建议是:选定一个经过验证的版本组合后,在配置中心锁定版本号,不要频繁升级;升级时留意官方发布的变更说明,特别是配置项和自动配置类名的变化。

我在本地维护了一个版本矩阵文件,记录差异框架版本对应的工作模型组合和典型配置项,每次升级前对照检查一遍,能省掉不少排查时间。

至于那些网上流传的“升级后配置项改名字”之类的细节,最好的办法是直接跑一遍集成测试,覆盖聊天、流式、工具调用、结构化输出这几条主链路,通过后再发布,比任何文档都可靠。

最后再说两句

从原生 API 调用迁到 Spring AI 的过程,让我真切感受到框架接入的价值不在“少写几行代码”,而在工程规范和可维护性。尤其当项目里同时存在多个模型、多套对话场景时,统一的抽象层能极大降低维护成本。当然 Spring AI 还在快速演进,最新版本体验中也不是完全没有小毛病。但整体上它正在把大模型接入做成一件普通后端工程师也能轻松掌握的日常事务。如果你已经在用 Spring Boot 开发业务系统,尝试用它接一下大模型,是一种非常平滑的演进方式。

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

Redis持久化全攻略:RDB、AOF与混合持久化实战解析

很多团队把 Redis 当一个大号的 HashMap 用&#xff0c;平时只往里塞数据&#xff0c;从没想过它也会“失忆”。我印象特别深的一次事故&#xff1a;线上活动刚结束&#xff0c;运营正在跑对账脚本&#xff0c;突然 Redis 进程挂掉&#xff0c;等拉起来&#xff0c;热榜、库存缓…

作者头像 李华
网站建设 2026/10/23 6:51:08

运算符详解:从算术到位运算的陷阱与工程实践

我们接着上一章的数据类型往下聊&#xff0c;这次把“运算符”一次说透。作为编程里最基础的零件&#xff0c;运算符看起来没什么含量&#xff0c;加减乘除谁不会&#xff1f;但实际写代码的时候&#xff0c;坑全藏在细节里&#xff1a;除法的取整方向、短路求值的副作用、位运…

作者头像 李华
网站建设 2026/10/23 6:50:54

二层与三层网络架构的部署区别与选型实战指南

前阵子有个朋友找我咨询&#xff0c;他们公司办公室扩张&#xff0c;电脑加到了两百多台&#xff0c;原来的网络越来越卡&#xff0c;部门之间互访也总出问题。我去现场一看&#xff0c;典型的扁平化二层网络&#xff1a;一堆二层交换机堆叠&#xff0c;网关全在出口路由器上&a…

作者头像 李华
网站建设 2026/10/23 6:50:51

WMI底层原理与实战:CIM模型、Provider机制与安全调试

简介&#xff1a;本资源是微软官方发布的Windows Management Instrumentation&#xff08;WMI&#xff09;英文开发文档PDF合集&#xff0c;面向Windows系统管理员、IT运维工程师及C/C/C#/PowerShell开发者&#xff0c;用于深入理解WMI底层架构与自动化管理能力。文档覆盖WMI核…

作者头像 李华
网站建设 2026/10/23 6:49:13

IEEE IV 2025启示:自动驾驶研究热点转向决策智能与安全落地

IEEE IV 2025的会议议程公布的时候&#xff0c;我专门花了一个晚上把录用的论文列表从头到尾过了一遍&#xff0c;边看边感慨&#xff1a;这几年自动驾驶的研究热点&#xff0c;已经从"能不能识别出障碍物"全面转向"能不能在开放场景里安全、高效、可解释地做决…

作者头像 李华
网站建设 2026/10/23 6:49:09

BitNet b1.58深度解析:三值权重如何重塑大模型推理

如果你听说一个大模型的全部权重只能用 -1、0、1 三个数来表示&#xff0c;同时它的任务效果还能对齐全精度的 LLaMA 基线&#xff0c;你大概率会先怀疑&#xff1a;这是标题党&#xff0c;还是压缩算法的魔法&#xff1f;这不是魔法。2024 年 10 月微软研究院公开的 BitNet b1…

作者头像 李华