最近在给项目接入大模型的时候,发现不少同学一上来就抱着HTTP请求工具直接撸接口:手动拼消息体、手动处理鉴权、手动解析流式返回,还要考虑超时重试、上下文截断、模型切换这些琐碎事。等代码写起来,很快就发现自己在重复造轮子,而且业务代码被这些底层细节糊得没法看。Spring AI这个框架接入方案,就是冲着这个痛点来的,它把“程序对接大模型”这件事抽象成了一层统一的Spring风格接口,让你把模型服务当作一个普通Bean注入、调用和扩展。
这篇文章我会从框架接入的角度,把Spring AI解决什么问题、怎么配置环境、怎么写出第一个可用的对话Demo、再往上怎么做到结构化输出、多轮记忆和工具调用,最后分享我踩过的几个常见坑。适合那些已经在用Spring Boot写后端、想给系统加入大模型能力,又不想在模型厂商的API细节里反复横跳的开发者。
1. 为什么要把大模型接入收编进框架
1.1 自己拼HTTP调用,事情远没有想象中简单
如果你只是“需要在项目里调一次大模型”,直接发一个HTTP请求确实最快。但当一个项目里出现多个调用场景,比如闲聊对话、意图识别、内容摘要、结构化抽取,手动调用的缺点会立刻暴露。每个场景都要重复写鉴权、拼Prompt、解析Response的代码,而且各家模型服务的请求格式、超时策略、错误码风格还各不相同,换一家模型厂商就要动一遍底层代码。
更难受的是流式输出。大模型接口通常支持流式返回,好让用户看到打字机效果,而手动处理流式数据要维护缓冲区、处理帧边界、考虑连接断开。Spring AI里封装了Reactive的返回类型,我在代码里拿到的就是一个Flux,用响应式编程习惯去处理就行,不需要盯着底层字节流发呆。
还有一层是测试和替换成本。手写调用时,“模型接口”和“业务代码”是强耦合的,想引入一个测试替身或切换到另一个模型服务,改造成本很大。框架接入之后,底层模型变成了一个可以替换的Bean,业务代码面前只有一个统一的调用入口。
1.2 Spring AI的抽象思路:像写数据访问层一样写大模型
我刚接触Spring AI时最大的感受是:它本质上在用Spring解决“集成”问题的老套路,去处理大模型接入。核心是两层结构,高层给你一个门面对象,你只管传输入收返回值;底层才负责跟具体的模型服务通信。这种分层思路让人安心。
换个角度理解,这就像以前用Spring操作数据库时,不会直接去写JDBC驱动细节,而是用JdbcTemplate或JPA。Spring AI把“大模型”看成了一种数据源或者外部依赖,你用统一的Client去聊天、去生成结构化对象、去调用工具函数,具体背后的模型是云端服务还是本地部署,对业务代码透明。
官方划分了几个明确的职责模块,比如核心抽象定义了消息、Prompt等数据结构,模型模块负责跟具体服务通信,工具模块支持模型调用外部函数,记忆模块管理历史上下文。模块划得清楚,学习路径就很顺畅,你完全可以只用自己的那部分。
1.3 配置文件驱动的模型切换,是框架最实在的好处
框架接入之后,“换模型”经常简化成改几行配置。在配置文件里指定一个模型服务地址和模型名,业务代码几乎不用改动。这对于开发环境用本地模型、生产环境用云端模型这种常见工作流,简直是救命。
举一个我实际经历过的场景:开发阶段为了省钱,用一个小型本地模型把流程跑通;到了联调阶段,需要切换到云端的大参数模型看效果。手写调用的话,这会牵涉到改API地址、换返回格式解析逻辑、调整超时时间等一系列操作。用Spring AI,我把不同的模型配置放在不同的Profile下面,启动Spring Boot时指定环境即可。
这种能力也让灰度发布多了一种玩法:同一套代码,一部分流量走A模型,一部分走B模型,通过配置中心动态修改参数就能实现。这不太像一个“库”,更像一个模型接入的中间层。
2. 核心概念与依赖准备
2.1 先搞懂ChatClient、ChatModel和Prompt
在写代码之前,得先理解Spring AI里几个最容易搞混的对象。我在学习初期就吃过亏,以为有个“AI客户端”类就能通吃一切,结果文档里同时出现了好几个概念。
ChatClient是面向开发者的门面。它封装了Prompt拼接、参数配置、返回类型转换这些动作,提供同步调用和流式调用等便捷方法。大多数时候你只跟它打交道。
ChatModel是底层适配器接口,负责跟具体的大模型服务打交道。不同的模型厂商对应不同的实现类,但你几乎不会直接操作它。
Prompt对象包含了一组消息,通常包括系统消息和用户消息,还会带上模型参数,比如温度。所有发给模型的内容都会封装成Prompt。
响应对象里携带了模型返回的文本,以及Token使用统计等信息。理解了这几个概念,后面所有代码都围绕着“构造Prompt,调用Client,处理响应”这个套路展开。
2.2 引入依赖:版本对齐是关键
Spring AI的依赖跟其他Spring项目不太一样,它的一个版本会同时依赖特定版本的Spring Boot。如果你用Spring Boot 3.2配上了Spring AI 1.0,大概率会遇到兼容性问题。我的建议是直接沿用Spring Initializr生成的版本组合,别自己猜。
依赖结构是这样的:Spring AI提供了BOM包统一管版本,你需要引入一个“模型供应商的Spring Boot Starter”。我在项目中用的版本是Spring Boot 3.3.x配套的Spring AI 1.0.x,BOM引入后,再引入对应的Starter即可。
用Maven的话配置大概是这个样子:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> </parent> <properties> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-example</artifactId> </dependency> </dependencies>这里的example坐标需要替换成你实际使用的模型厂商对应的Starter。如果你是接入本地部署的模型服务,通常也有对应的Starter,或者你可以通过通用配置的方式指向本地的兼容接口。记住一个原则:跟着官方文档选版本,尤其注意兼容性矩阵。
2.3 最小化配置:密钥和模型名
引入依赖之后,首先要做的是在配置文件里声明模型服务的连接信息。最常改的几个配置项包括API地址、密钥和模型名。密钥一定不要直接写死在代码或者配置文件里,我习惯用环境变量占位符注入。
一份典型的配置文件长这样:
spring: ai: model: chat: base-url: ${LLM_BASE_URL:http://127.0.0.1:8081} api-key: ${LLM_API_KEY:local-dev-key} model: chat-pro-1 options: temperature: 0.7 max-tokens: 2048这里的关键点是base-url这个配置。开发阶段我经常把它指向本地服务,这样不需要消耗云端额度就能把链路调通。切到生产环境时,通过环境变量覆盖即可。配置项的变化会随着版本迭代有所调整,稳妥的做法是查看你当前版本对应的官方配置说明,但整体思路都是“地址加密钥加模型名”。
3. 实操:写一个能跑起来的对话服务
3.1 注入ChatClient并完成第一个对话
配置好依赖之后,代码侧的事情其实非常少。Spring AI会为你自动装配好ChatClient实例,你只需要把它注入到你的Service里面就行。注意,官方推荐注入ChatClient.Builder而不是直接注入ChatClient,这样可以按需定制默认行为。
一个最基础的Service长这样:
@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String chat(String userMessage) { return chatClient.call(userMessage); } }然后配一个Controller暴露接口:
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @PostMapping public String chat(@RequestBody String message) { return chatService.chat(message); } }启动应用之后,对着这个接口发一个POST请求,就能拿到模型返回的文本。这一步虽然简单,但已经把“程序接入大模型”这件事落地了。你没有手动处理HTTP客户端、没有解析JSON格式消息体、没有处理鉴权头,所有底层通信都让框架完成了。
3.2 加入系统提示词,让对话更可控
只做基础问答不够,实际业务里几乎都要设定角色行为和上下文。比如做一个客服助理,要在开头明确“你是某产品的客服助手,回复需简洁友好”。
Spring AI里实现这种设定非常直观,使用系统消息和Prompt模板。示例代码如下:
public String chatWithSystemPrompt(String userMessage) { return chatClient.prompt() .system(msg -> msg.text("你是某产品的客服助手。请用简洁友好的语气回答。")) .user(userMessage) .call() .content(); }你甚至可以把用户消息也用模板管理。我经常会定义一组Prompt模板,存放在配置里,方便运营人员修改措辞,而不需要动代码。这种生成方式比较利于协作。
3.3 流式输出,提升用户体验
聊天场景如果等模型全部生成完再返回,体验会很差,尤其长回答可能要等十几秒。Spring AI对流式输出的封装非常到位,我找到的方法是调用stream方法,拿到一个响应式的流。
public Flux<String> streamChat(String userMessage) { return chatClient.prompt() .user(userMessage) .stream() .content(); }用WebFlux或者SSE的方式推给前端,页面就能出现一个一个字的输出效果。这里要提醒一下,启用流式后,网关和负载均衡器的超时时间要调大,有些路由组件默认30秒就会断开连接,造成前端收到的流中断。我踩过一次坑,最后把读超时调到了5分钟,才稳定跑通长文本生成。
4. 进阶能力:从“能聊”到“能用”
4.1 让模型返回结构化对象,直接对接业务
文本对话只是基础能力。真正让大模型进入业务系统的杀手锏是让模型输出结构化数据。比如让模型从一个自然语言指令中提取“活动推荐列表”,然后直接映射成Java对象。
Spring AI提供了结构化输出能力,你可以把一个POJO类型传给调用方,框架会引导模型按JSON格式返回并完成反序列化。我常在项目里定义一个记录类:
public record ActivityRecommend( String title, String location, int durationHours, List<String> tags) {}然后这样调用:
public ActivityRecommend recommendActivity(String description) { return chatClient.prompt() .user("根据以下描述推荐一个活动:" + description) .call() .entity(ActivityRecommend.class); }这种方法极其实用,因为它让大模型变成了一个“理解自然语言并输出业务对象”的转换器。我把它用在工单分类、简历解析、地址标准化这些场景,效果比规则系统好太多。需要注意,结构化输出对模型能力有要求,小参数模型经常输出不合法JSON或漏字段。这时可以选择增大模型的上下文能力,或者在Prompt模板里给出JSON示例。
4.2 多轮对话与上下文记忆管理
如果你做的是连续对话,别忘了每次请求都是无状态的。开箱即用的状态支持需要自己选择保存方式,既有内存实现也有持久化实现。我的做法是把对话历史按会话ID保存下来,每次调用前取最近若干条消息拼进Prompt。
手动维护也不复杂,主要是控制历史轮数。模型上下文窗口有限,且越长的历史输入成本越高。我维护一个滑动窗口,只保留最近10轮,超过就丢弃更早的消息。
如果你使用的是Spring AI的记忆抽象,可以按会话ID绑定历史消息。这种设计对于REST风格的后端很友好,前端每次请求带着会话ID,后端自动加载该会话的上下文。
4.3 工具调用:让模型动手干活
工具调用是实现Agent能力的关键。以前模型只能“说”,现在它可以通过调用外部函数去查天气、算数学、查数据库,再把结果整合成回答。Spring AI把函数调用实现为方法的注册与自动完成。
我在Service中定义一个普通方法,加上工具注解:
@Tool(description = "查询指定城市的当前天气,输入为城市名") public String getCurrentWeather(String city) { // 这里调用实际天气服务 return "晴,26度,微风"; }然后让ChatClient知道这个工具存在,有两种方式,一种是给ChatClient配置默认工具,一种是在调用时指定。我习惯在构建ChatClient时注册工具方法,这样所有对话都自动具备调用能力。
工具调用的内部流程是:模型分析用户问题,觉得需要天气信息,就返回一个“调用天气函数”的指令;框架自动执行该方法,把结果回传给模型;模型再根据结果组织最终回答。整个过程对调用方是透明的,你从外面看,就是模型会突然“知道”今天的天气并回答你。
这个机制在生产上非常有用,我把它用于查询订单状态、计算运费、解析内部系统编码,可靠度比让模型凭空想象要高得多。
5. 常见问题与排查技巧实录
5.1 请求超时与连接池耗尽
在刚开始接入时,我本地并发测试总会间歇性报连接超时。排查下来发现,Spring Boot的默认连接池和HTTP客户端超时设置对Spring AI来说不够用。模型推理耗时通常好几秒,一旦并发上来,连接很容易被占满。
解决办法是调整底层HTTP客户端的连接池大小和超时时间。如果你的模型服务放在内网,建议把连接池调大一些,并设置较长的读超时。不同模型服务的响应速度和吞吐模型也不一样,需要通过压测找到合适的数值。
5.2 返回结果被截断或突然中断
模型中文字符集的问题通常表现为乱码,这个好解决,统一设置字符集为UTF-8即可。近距离接触过“结果突然断掉”的情况,一部分原因是最大Token数设置太小,比如默认256个Token只够撑一小段中文;另一部分是输入内容里包含了特殊字符,把后续文本截断了。
排查时先打开日志看原始响应,确认是网络问题还是内容生成长度问题。如果是长度,调大max-token参数;如果是截断,检查模型的输入文本是否包含过长的连续字符或者特殊标记符。
5.3 工具调用不生效或结果不正确
工具调用这个功能很酷,但它对模型的要求比较高。如果发现模型根本不调用注入了的工具,首先确认你用的模型是否支持工具调用能力,部分轻量模型并不支持;其次检查工具描述和参数描述是否清晰,模型是通过描述来决定何时调用、传什么参数的。
还会遇到调用结果和最终回答对不上的情况。我排查过一次,原因是工具方法内部抛异常,框架默认把异常信息当成结果回传给模型,模型又没有识别出那是错误信息,就顺着错误信息回答了。解决办法是在工具方法内部做好异常处理,返回明确的“查询失败”文本,模型会知道如何应对。
5.4 切换模型后效果下降
框架接入不同模型很方便,但换模型不是改个配置就万事大吉。不同模型对于Prompt格式的敏感程度差别很大,我的体感是换模型后需要重新调试系统提示词和结构化输出的字段说明,否则效果可能断崖式下跌。
比如说结构化输出,有的模型对JSON Schema敏感,有的模型对示例更敏感,你要在Prompt里同时提供描述和示例,还要确认底层是否真的走了JSON模式。如果返回了JSON但少字段,通常是模型能力不足,而不是配置问题。
5.5 密钥泄露和服务降级
最后提醒一个安全问题:API密钥千万不要提交进代码仓库。我见过有项目把密钥写进application.yml后直接推到公共仓库,几分钟内密钥就被扫描工具拿走盗刷。正确做法是使用环境变量或配置中心管理密钥,并对密钥做权限限制,只允许特定网段调用。
服务降级也一样重要。大模型服务一旦不可用,你的接口不能直接报500。我通常会在调用出错时返回一个友好提示,或者走一个简单的规则兜底逻辑,至少保证核心链路不受影响。
6. 我对Spring AI接入的一点实践经验
用的时间越久,越觉得Spring AI本质上解决的不只是“调用方便”,而是让大模型变成了Java生态中的一个标准组件。你不需要关心它是怎么连上的,只要围绕Prompt和响应去组织业务逻辑。这种抽象非常适合团队协作,因为读代码的人不需要熟悉模型供应商的SDK也能快速理解。
我在这类接入项目里的建议是先小步快跑。不要一上来就追求复杂的Agent和工具调用,先用一个最简单的对话接口跑通整条链路,再逐步加流式、结构化输出和工具调用。每次加一个能力,都要做回归测试,因为大模型的返回有随机性,没有足够测试,你很难知道哪些失败是模型问题、哪些是代码问题。
还有一点,做好日志和追踪。每次请求的Token消耗、耗时、模型返回质量这些指标很值得监控。我甚至在关键业务里把一部分请求的Prompt和响应落库,方便回溯分析模型表现。这样的数据积累比任何文档都更能说明问题。
Spring AI是个还在快速演进的项目,不同版本之间会有配置项和API的调整。我在升级时踩过几次坑,所以现在养成了先看变更记录再动手的习惯。如果你准备拿它做生产级项目,建议锁定版本,并在测试环境充分验证后再升级。框架接入的好处在于旧模型只需要改配置,但框架本身的升级还是要谨慎对待。