我最早产生接入 SpringAI 的想法是在一个给内部系统加「智能问答助手」的项目里。当时团队第一反应是直接调大模型平台的 HTTP 接口自己写 HTTP 客户端、自己拼 JSON 请求体、自己解析流式返回、自己管理上下文轮次还要根据不同的模型服务商各写一套适配代码。一通折腾下来代码倒是能跑但只要一换模型平台整个调用层就得推倒重来。后来换成 SpringAI 重新梳理了一遍整个接入过程从「写胶水代码」变成了「写配置和业务方法」这才是 Java 生态里接入大模型该有的姿势。这篇文章我会从依赖接入、配置项、核心 API、结构化输出、工具调用这几个方面展开最后单独讲几个我实际落地时踩过的坑。内容面向已经在用 Spring Boot、想给项目加 AI 能力的开发同学也适合正在做技术选型调研的团队参考。看完之后你能自己搭出一个最小可用的 SpringAI 工程并且知道后续往生产环境推的时候要提前防哪些问题。1. 没有 SpringAI 之前Java 项目接大模型到底有多别扭1.1 传统 HTTP 调用方式的样板代码困境很多 Java 开发者最开始接触大模型接口走的路子都差不多用 OkHttp 或者 HttpClient 拼一个 POST 请求header 里带鉴权信息body 是模型要求的消息数组然后通过回调或execute()拿到响应再手动从嵌套 JSON 里一层层取值。这个流程单看一次调用工作量不算大但一旦你的业务场景复杂起来问题立刻暴露出来。拿多轮对话举例。你要自己维护一个历史消息列表每次请求前把历史压入 body响应后再把 assistant 消息塞回去如果中间插了工具调用还要处理模型返回的 tool_calls 字段再继续把工具结果发过去。这一套链路下来光状态管理就够写几百行。而且不同平台之间的鉴权方式、路径前缀、参数命名、流式格式都可能不一样今天接 A 平台明天换 B 平台适配层的改动量几乎是重写一遍。1.2 SpringAI 的抽象方式模型即 Spring BeanSpringAI 的思路很直接把大模型接入当成一个普通的 Spring Boot starter 来处理。框架替你封装了 HTTP 通信、鉴权、请求构建、响应解析、流式处理、工具调用等底层细节你只需要面向ChatModel、ChatClient、Prompt、Message这几个核心抽象编程。打个比方这就像你平时用 MyBatis 操作数据库一样。JDBC 你也能写但没人会在项目里直接去处理 Connection、PreparedStatement、ResultSet 的每一个细节。SpringAI 对模型做的就是 MyBatis 对 JDBC 做的这件事把样板逻辑收敛到框架内部让业务代码只关心业务本身。ChatModel是最底层的模型接口负责执行一次对话调用对应早期的RestTemplateChatClient是更上层的流式 API支持链式调用和强大的 prompt 组装能力这也是目前推荐的主用入口。再加上Prompt、SystemMessage、UserMessage、AssistantMessage这套消息模型SpringAI 把大模型交互里最常见的几种场景都覆盖了。1.3 这套框架适合什么场景不适合什么场景从我这几个月的实践来看SpringAI 最擅长的是几类业务聊天/客服助手多轮对话、system prompt 约束、上下文维护都有现成 API。结构化数据提取让模型返回 JSON直接转成 Java 对象。工具调用/函数调用通过Tool注解把业务方法暴露给模型。知识库问答的起步阶段配合向量库和EmbeddingModel做最基础的 RAG。但如果你打算直接拿它做复杂的自主规划智能体或者模型要连续执行几十步任务才能出结果那 SpringAI 目前更适合当「底座」而不是「全自动大脑」。它把模型调用这一层做得足够稳但 Agent 的编排逻辑、记忆存储、任务拆解这些还是得你自己写。认清这一点能帮你少走不少弯路。2. 依赖接入和环境配置先把最简单的对话跑通2.1 版本选择这件事比你想的重要SpringAI 的版本迭代非常快不同版本之间的 API 变化也很大。如果你在搜索引擎里找教程很容易看到旧版本写法抄进去发现编译不过。我个人的建议是直接跟着当前稳定版走不要因为看过老教程就死守旧版本。现在接入 Spring 生态的新框架版本管理有一个相对固定的思路项目用 Maven 的话在dependencyManagement里先引入 SpringAI 的 BOM再引入对应的 starter避免直接写各种子模块版本号。这样升级的时候只改 BOM 版本就行。Java 版本方面Spring Boot 3.x 需要 JDK 17 或更高SpringAI 也一样所以老项目要接入前先确认基础环境已经升级。2.2 pom.xml 里的最小依赖长什么样以我最近搭的一个内部 Demo 为例父工程里的依赖管理大概是这样的dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0-M6/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后在具体模块里引入模型客户端 starter。这里要注意SpringAI 支持多家模型平台starter 的 artifactId 会随平台不同而变化。我为了演示通用接入流程使用了占位名称实际项目里请根据你选择的平台参考官方文档找到对应的 starter 模块名。dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-platform/artifactId /dependency /dependenciesplatform替换成你实际使用的模型平台模块名即可。之所以推荐这种写法是因为 SpringAI 把不同平台的差异全部收敛在 starter 内部业务代码层面你接触到的ChatClient、ChatModel等类型是统一的。这意味着你将来换平台改依赖和配置就能切走业务代码不用大改。2.3 application.yml 里的核心配置项SpringAI 的自动配置会读取spring.ai前缀下的属性。配一个模型连接最核心的就是三样接口地址、密钥、模型名称。spring: ai: platform: base-url: ${AI_BASE_URL} api-key: ${AI_API_KEY} chat: options: model: ${AI_MODEL_NAME} temperature: 0.7我一直用环境变量的方式注入这些敏感配置而不是直接写死在 yml 里。原因很朴素代码仓库会流转到不同开发者手里api-key 一旦进了 Git 历史后患无穷。base-url这里也多说一句不同平台的接入地址差异很大有的平台要求走它自有的端点有的平台提供兼容主流协议的端点用后者的话你甚至不需要改代码只改base-url和api-key就能切模型。2.4 写一个最朴素的 Controller 验证链路依赖和配置就绪之后写一个最基础的接口来验证整条链路是否通。SpringAI 的自动配置会把ChatClient、ChatModel这些 Bean 注册到容器里你直接在 Controller 里注入就行。RestController RequestMapping(/ai) public class AIController { private final ChatClient chatClient; public AIController(ChatClient.Builder chatClientBuilder) { this.chatClient chatClientBuilder.build(); } GetMapping(/chat) public String chat(RequestParam(defaultValue 介绍一下你自己) String message) { String response chatClient.prompt(message) .call() .content(); System.out.println(response); return response; } }跑起来之后浏览器访问/ai/chat看到模型正常回复接入就算完成了。很多第一次用的人在这里会愣一下怎么连组建请求的代码都不用写确实不用写。chatClient.prompt(message)内部已经按照消息协议帮你把UserMessage组装好call().content()负责发请求并取出文本结果。这里有个小细节要注意ChatClient是通过ChatClient.Builder构建出来的不要自己去new否则拿不到自动配置里的模型参数。3. ChatClient 的消息体系从单轮到多轮再到流式3.1 调用参数对输出质量的影响在继续写业务之前我想先聊几个直接影响输出效果的参数。这些参数配置在spring.ai.platform.chat.options下也可以在每次请求时通过ChatOptions临时覆盖。我用一张表来总结它们的用途参数作用我的实践建议temperature控制随机性数值越高回答越发散客服/知识库问答这类要精确的场景调到 0.2-0.4文案创意场景可以到 0.8 左右max-tokens限制本次回复的最大 token 数不要设太小否则长文本会被截断但设得太大又会增加成本和延迟top-p核采样控制候选词集合范围一般保持默认就可以和 temperature 二选一调整即可model指定模型标识符开发环境用轻量模型生产环境用强模型用配置切换timeout请求超时时间流式场景尤其注意模型生成慢超时阈值要给足我在项目里最常用的一套组合是精确型任务temperature0.2、max-tokens1024创意型任务temperature0.8、max-tokens2048。这些参数直接决定了回答的「性格」建议团队内部形成一套参数约定避免每个人凭感觉乱调。3.2 用 SystemMessage 约束场景一个 SQL 生成小助手只做一个「你说一句我回一句」的聊天接口没什么意思真实业务里最常用到的是 System Prompt。SystemMessage 用于设定模型的身份和行为规则这也是所有对话任务的基座。我做过一个内部小工具让大模型根据表结构生成 SQL 查询语句。业务人员不需要懂数据库只要用自然语言描述需求系统返回可执行的 SQL。这个功能的 System Prompt 很能说明问题GetMapping(/sql) public String generateSql(RequestParam String requirement) { String systemPrompt 你是一名数据库专家。用户会给你一个业务需求请生成对应的 SQL 查询语句。 约束 1. 只能返回 SQL 和简短说明不要返回多余内容。 2. 优先使用索引字段。 3. 如果需求不明确先回复需要补充的条件。 现有表结构 create table order_info ( id bigint primary key, user_id bigint, goods_name varchar(128), order_amount decimal(10,2), create_time datetime ); ; return chatClient.prompt() .system(systemPrompt) .user(requirement) .call() .content(); }看到没有system(...)和user(...)是分开设置的SpringAI 会把这些消息拼装成正确的消息序列发送给模型。这条链路里 System Prompt 是整个回答质量的锚点大部分「模型回答不靠谱」的问题根源都是 System Prompt 约束不够清晰而不是模型本身不行。3.3 多轮对话上下文怎么维护聊天类业务的核心难点是上下文维护。SpringAI 的底层消息模型里SystemMessage、UserMessage、AssistantMessage都实现了Message接口多轮对话本质上就是往消息列表里追加内容。Component public class ChatService { private final ChatModel chatModel; public ChatService(ChatModel chatModel) { this.chatModel chatModel; } public String chatWithHistory(String userId, String userInput) { // 实际项目请用 Redis 等外部存储这里用内存容器演示 ListMessage history HistoryStore.get(userId); history.add(new UserMessage(userInput)); ChatResponse response chatModel.call(new Prompt(history)); String assistantText response.getResult().getOutput().getText(); history.add(new AssistantMessage(assistantText)); HistoryStore.save(userId, history); return assistantText; } }这里最重要的一个经验每次请求都必须把完整历史消息列表发给模型。模型本身没有记忆它只能根据你提供的全部消息来猜测上下文。所以消息越多token 成本越高请求越慢。实战里要在性能和成本之间做平衡一般做法是只保留最近 N 轮对话或者把过长的历史做摘要压缩。还有个小坑在多轮对话中如果某次模型回复失败或者中断你最好把这条失败的 assistant 消息也从历史里清掉否则下次请求时模型会看到一个「半截话」容易影响回答逻辑。3.4 流式输出Flux 才是聊天场景的正确姿势前面讲的都是同步调用也就是等模型把完整内容生成完一次性返回。这种方式在生成较长的回答时体验很差用户会盯着页面上一个转圈好几秒。真实世界的 AI 应用几乎都是流式输出一个字一个字往外蹦。SpringAI 对流式的支持做得比较优雅核心 API 返回的是 Reactive Streams 的FluxString。如果你的接口在 Spring WebFlux 环境下可以直接把Flux作为响应体返回如果是 Spring MVC 环境也能借助 SseEmitter 把数据推给前端。GetMapping(value /stream, produces text/event-stream) public FluxString streamChat(RequestParam String message) { return chatClient.prompt(message) .stream() .content(); }stream()之后拿到的FluxString通过produces text/event-stream声明为 SSE 接口后浏览器 EventSource 或前端 fetch API 就能实时消费。这一步出来之后产品体验会有一个质的提升。多提一句流式接口的线程模型和同步接口完全不同同步调用会一直占着一个 Tomcat 工作线程而流式返回是异步处理的不会长时间占用 Web 容器线程对高并发场景友好得多。4. 结构化输出让模型直接返回可用的 Java 对象4.1 从「解析 JSON 的痛」到 BeanOutputConverter让大模型输出 JSON 是一个很常见的需求从简历里提取候选人信息、从聊天记录里抽取工单内容、判断用户情绪并给出分类标签等等。但之前自己处理这个过程非常烦模型偶尔会在 JSON 前后加一段解释文字你的JSONObject.parseObject直接崩有时候字段名和预期的对不上又要写复杂的类型适配。SpringAI 提供了一个工具类BeanOutputConverter可以把模型的输出约束并转换成任意 POJO/record 类型。它的实现思路是把你的目标类型结构写进 prompt让模型只返回合规 JSON然后在拿到响应文本后用框架自带的转换逻辑反序列化成 Java 对象。4.2 一个完整的结构化输出例子我用一个课程推荐场景来演示。假设有一个 Agent 需要根据用户的学习需求返回推荐课程列表课程对象包含名称、时长和学习路线关键词public record Course( String courseName, Integer durationDays, ListString skillPoints ) {}接口代码如下GetMapping(/course/recommend) public Course recommendCourse(RequestParam String goal) { BeanOutputConverterCourse converter new BeanOutputConverter(Course.class); String response chatClient.prompt() .system(converter.getSystemInstruction()) .user(我想学 SpringAI目标 goal) .call() .content(); // 把模型返回的文本转成 Course 对象 return converter.convert(response); }converter.getSystemInstruction()会自动生成一段「请严格按照 JSON 格式返回」的约束说明然后把 Java 类型字段信息告诉模型。converter.convert(response)则负责把返回的 JSON 文本解析成Course实例。整个过程没有手写任何JSONObject代码。4.3 结构化输出不是银弹该校验还得校验我在实际用了一段时间之后发现结构化输出有几个隐藏问题需要自己留意模型偶尔还是会返回带说明文字的文本比如先写一句「好的这是您需要的课程信息」然后才是 JSON。遇到这种情况converter.convert()会失败。需要在转换前对文本做一次预处理或者捕获异常后重试一次。模型的 JSON 字段类型可能与声明不一致比如你的 record 里是Integer它给你返回字符串5。框架会尽力转换但复杂的嵌套类型还是建议手动校验。结构化输出会多消耗一些 token因为它要把类型结构写进 System Prompt这在 token 计费模式下是有成本的。这些限制说明了同一件事把 AI 的输出当成普通程序输入来对待。转换前校验、转换后兜底、失败时重试这三个习惯养成之后结构化输出在工程上才算是真正稳了。5. 工具调用让模型学会调用你的 Java 方法5.1 为什么需要 Tool Calling模型本身是「离线大脑」它知道很多通用知识但它不知道我们系统里的实时状态订单号对应的物流进度、某个商品当前的库存、用户最近的消费记录、数据库里某张表的结构……这些都藏在你的业务系统里。要做到「问模型一个问题它自己去查系统再回答」靠常规聊天接口做不到。Tool Calling 就是为了解决这个问题而生的模型在回答过程中可以决定调用一个你注册好的函数拿到函数返回值之后再组织成最终回答。5.2 用 Tool 注解暴露方法SpringAI 注册业务方法的方式非常简单在方法上打Tool注解即可Component public class OrderTools { Tool(description 根据订单号查询订单当前状态订单号以A开头) public String getOrderStatus(String orderId) { // 这里模拟查询后端订单服务的逻辑实际项目可注入 Service 调用 if (A1001.equals(orderId)) { return 已发货预计后天到达; } if (A1002.equals(orderId)) { return 正在打包暂未出库; } return 未找到该订单; } }关键就在这个description上。别小看这段描述它决定了模型在什么时机、用什么参数来调用这个方法。描述写得越具体参数含义越明确模型的调用准确率越高。如果你写的是「查询订单状态」模型很可能在你只提供用户ID的时候也尝试调用然后传入一个错误参数。有了工具类之后在ChatClient构建时把它关联进去RestController RequestMapping(/ai) public class ChatController { GetMapping(/tool) public String chatWithTool(RequestParam String message) { var chatClient ChatClient.builder(chatModel) .defaultTools(new OrderTools()) .build(); return chatClient.prompt(message) .call() .content(); } }请求进来后模型先判断当前问题是否需要工具如果需要SpringAI 会自动完成「模型返回工具调用请求 → 框架反射执行你的 Java 方法 → 把方法结果回传给模型 → 模型基于结果生成最终回答」的闭环。整个过程对业务方法来说就是一个普通的方法调用但对使用者来说模型「学会」了查询你系统的业务状态。5.3 工具调用链路的理解与排查工具调用是 SpringAI 里最推荐优先掌握的扩展能力因为它的收益非常直接。但调试的时候会有一个常见的理解偏差你以为程序会先执行工具方法再让模型看结果实际上模型的决策顺序是「先判断调用什么工具、传什么参数再执行然后再继续生成」。如果你的工具方法返回内容格式很复杂比如一大段 JSON模型可能理解不了所以工具方法的返回值应该尽量是简短自然语言的报告而不是原始数据结构。排查工具调用问题时我建议把每次请求的完整日志打开。SpringAI 在响应对象里会保留工具调用的中间信息看日志能确认模型到底选了哪个工具、传了什么参数多数「工具没生效」的案例最后都查到这里。6. 实战落地才会遇到的几类坑6.1 依赖拉取与网络受限环境国内团队接 SpringAI 时第一个坎往往不是代码而是依赖下不下来。Spring 的里程碑版本仓库比较特殊如果你的构建服务器网络受限会卡在坐标解析阶段。实际项目里我一般两个方案并行在 Maven 配置里加仓库镜像或者在本地准备好依赖后导出到公司内部仓库再统一使用。这个问题看着小却容易在团队协作时挡住所有人最好提前处理。6.2 流式接口和线程模型带来的隐患用流式接口后很多人以为只要把 Controller 返回类型改成Flux就完事了。其实这里面有一个容易被忽略的隐患如果业务代码在Flux生成过程中调用了阻塞式 IO比如同步查数据库、同步调外部 API会阻塞底层的事件循环线程。我排查过一个线上偶发卡顿的问题最后发现是工具方法里用了一个阻塞式 HTTP 客户端把整个流式链路拖住了。经验有两条流式链路里的工具方法尽量用异步写法没有把握的话先给回调链路上配置单独的调度线程池。压测跑过之后再考虑优化到极致。6.3 限流、重试与超时模型接口不像数据库接口数据库出问题通常很快就能感知模型接口出问题症状要隐蔽得多。限流时它不一定会报错而是长时间没有返回超时设短了生成一份长报告会被频繁打断重试机制写不好短时间高频重试反而触发更严格的限流。我现在的做法是给模型调用统一包一层带重试和熔断的服务。重试策略用指数退避初次等待 500 毫秒倍率 2最多 3 次。超时时间根据场景区分纯聊天设置 30 秒生成长文档设置 60 到 90 秒。这些参数写死在代码里不如放在配置中心方便线上动态调整。Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultOptions(ChatOptions.builder() .temperature(0.7) .build()) .build(); }6.4 日志安全API Key 和用户敏感信息最后一个坑更多是意识和习惯问题。Spring 的自动配置在启动阶段会解析并读取配置项如果日志级别调到 DEBUG某些框架会打印出完整的配置绑定信息API Key 可能就这么泄露到日志里。我的处理方式是配置文件里的密钥一律使用环境变量占位并确保日志框架中标记的敏感字段不会被输出在业务日志里也只记录模型返回的文本摘要不带完整 Prompt很多企业内部数据不适合作为提示词发送出去。这一步做不做上线后面对审计时的压力完全不同。宁可现在多花几分钟设计脱敏策略不要在出事之后补。最后聊点实在的接入 SpringAI 这几个月我最深的感受是它没有试图让你「忘记」大模型的复杂性而是把复杂性挡在了合理的边界之外。Java 团队原本要花两周时间写的模型接入层现在一个周末就能跑通剩下的大部分精力可以用在真正的业务逻辑和产品体验上。如果你正在选型我的建议是先别急着上各种重型 AI 框架把 SpringAI 的基础链路、结构化输出、工具调用这三板斧吃透已经能覆盖大多数真实业务场景。最后留一个小技巧把模型平台相关的配置全部收敛到配置中心通过不同环境profile切换模型名称开发环境用轻量模型控制成本生产环境用能力更强的模型保证质量这一套组合在多个项目里都跑得很稳。