1. 为什么会想用SpringAI先把方向理清楚SpringAI简单说就是Spring生态里的“大模型接入层”。它把对话生成、文本向量化、向量检索、工具调用这些能力统一封装成Spring风格的API让Java后端不用再关心每个模型服务商各自的HTTP细节业务代码只需要依赖几个稳定的抽象接口。这篇文章我会从为什么选它讲起把核心API、配置项、RAG落地、工具调用以及我实际踩过的坑都过一遍。如果你正在Spring Boot项目里接大模型或者准备接但还在观望这篇应该能帮你省下不少时间。1.1 没有SpringAI的时候Java接大模型有多麻烦早先项目要接大模型我基本上都是直接对着模型服务商的文档写HTTP客户端。每个厂商都有自己的一套有的用Bearer Token鉴权有的要自定义Header有的返回JSON里结果放在某个固定字段有的放在另一个字段有的支持流式SSE有的流式格式还不一样。这还只是请求响应层面的差异。更麻烦的是工程层面的重复劳动。超时设置、连接池管理、错误重试、token计数、上下文拼接这些逻辑一旦散落在业务代码里后面就变成谁都不敢碰的胶水代码。我当时在一个后端系统里同时对接了商业API和本地推理服务于是出现了两个结构几乎一样但字段名完全不同的Service类。后来需求变了要换成另一家模型改动量几乎等于把接入层重写一遍。这个问题和早期Java连接数据库很像。在没有JDBC之前每个数据库都有自己的客户端协议业务代码和具体数据库强绑定。后来有了JDBC这套统一规范才让“换数据库不改业务代码”成为可能。SpringAI做的事情就是大模型领域的JDBC把各家模型服务的差异挡在标准接口后面。1.2 SpringAI到底做了哪几层抽象SpringAI并不是一个简单的HTTP工具类它从上到下分了几层最底层是ChatModel和EmbeddingModel这样的模型接口分别负责文本生成和文本向量化往上一层是VectorStore用来管理向量数据再往上是ChatClient这是面向业务代码的主要入口旁边还有Advisor机制可以在每次模型调用前后挂增强逻辑比如自动拼历史消息、自动做知识库检索。这种分层最直接的好处是业务代码依赖抽象而不是实现。你在Controller里写的是ChatClient拿着用户问题去调用具体背后是商业API还是本地模型是哪个厂商的模型都可以通过配置切换。SpringAI的自动配置会扫描spring.ai开头的配置项创建对应的实现Bean你只需要在构造函数里声明依赖就行。另外它还覆盖了流式响应。SpringAI底层基于Reactor所以流式调用返回的是Flux在WebFlux接口里可以直接返回给前端不需要自己解析SSE协议。这个点对做聊天类应用特别重要后面实操部分我会单独展开。1.3 什么样的项目适合用SpringAI先说结论如果你已经在一个Spring Boot项目里想给某个模块增加大模型能力SpringAI是效率很高的一条路。因为它的依赖注入、配置管理、异常处理都长在Spring体系内团队不用额外引入一套陌生的通讯层项目里的人都能很快接上手。反过来如果只是一个一次性脚本或者纯粹做算法验证那直接用HTTP客户端调一下接口就够了没必要引入整个框架。如果你的核心诉求是每个请求都要做底层级别的定制比如自定义路由、自定义鉴权转发那SpringAI的抽象反而可能让你觉得束手束脚。还有个容易被忽略的判断标准团队是否熟悉Spring生态。如果团队成员本来就是Spring背景SpringAI的学习成本很低几天就能跑通完整链路。如果团队全是Python背景硬要用Java技术栈做AI应用那没必要为了SpringAI而SpringAI。选型归根到底看的是“这套抽象有没有帮你省事”而不是它听起来酷不酷。2. 核心概念与常用配置动手前先记住这几个API概念这东西不弄清楚就直接抄代码出问题时会非常难受。我见过很多人从网上复制一个ChatClient示例跑不起来就去改配置改半天也不知道问题出在Prompt、Message还是ChatModel上。所以动手前先花十分钟把这几个对象的关系理清楚后面会顺很多。2.1 想不迷糊先认识这五个类第一个是ChatModel同步模型接口。它接受Prompt返回ChatResponse。在实际集成时你一般不会直接new它而是由Spring Boot自动配置注入一个实现Bean。第二个是StreamingChatModel提供流式调用返回Flux 。这个接口在做打字机效果时用得多。第三个是ChatClient业务层主要用的门面。它支持链式调用比如chatClient.prompt().user(你好).call().content()写起来像一段声明式代码。同时它还能挂Advisor和工具这是直接操作ChatModel不具备的便利。第四个是Prompt代表一次完整的模型请求里面可以包含多条Message。第五个是Message表示单条消息实际类型有SystemMessage、UserMessage、AssistantMessage对应聊天里的系统设定、用户输入和助手回复。把这几个对象理清楚后再回头去读官方示例就不晕了ChatClient负责组装请求Prompt里面放MessageChatModel负责真正向模型服务发起调用返回的ChatResponse再解析出文本。2.2 配置项逐个拆开说SpringAI的模型配置集中在application.yml里。下面是一个典型配置我用model-provider指代你实际使用的模型服务商模块前缀因为不同厂商的starter模块前缀会不一样spring.application.nameai-demo server.port8080 spring.ai.model-provider.api-key${AI_API_KEY:sk-xxx} spring.ai.model-provider.base-url${AI_BASE_URL:https://api.example.com/v1} spring.ai.model-provider.chat.options.model某模型标识 spring.ai.model-provider.chat.options.temperature0.7 spring.ai.model-provider.chat.options.max-tokens800 spring.ai.model-provider.chat.options.connect-timeout30s spring.ai.model-provider.chat.options.read-timeout120s这里我把具体厂商前缀省略了原因前面也说过SpringAI按厂商分模块配置前缀会跟着变。但需要理解的配置项是通用的配置项作用我的经验建议api-key请求鉴权用环境变量注入不要写死在代码或仓库里base-url模型服务地址商业API填官方地址本地推理引擎填本机端口注意防火墙model模型标识必须和服务商文档一字不差日期版模型也要带上后缀temperature采样随机性客服、结构化输出用0.2到0.4创意写作可以到0.8max-tokens最大输出长度按需求给太大容易慢太小会被截断read-timeout读取超时大模型响应慢是常态建议至少90秒以上别用默认几秒这里特别提醒一句api-key一定要走环境变量或配置中心不要写在application.yml里提交到代码仓库。我见过不止一次因为密钥泄露导致服务被刷爆账单的情况这个习惯从第一天就要养成。2.3 同步、异步、流式三种调用方式怎么选SpringAI的ChatClient同时提供call()和stream()两条路径。call()是同步阻塞发起请求后一直等到完整结果返回适合内部模块调用、批处理任务stream()返回Flux适合聊天页面的流式输出效果。选型经验是内部接口优先用同步逻辑简单也容易排查问题面向用户的聊天接口尽量用流式尤其生成内容较长时流式能大幅降低用户的等待感。另外要注意Flux是冷流只有被订阅后才会真正执行模型调用。你在Controller里直接把Flux返回给Spring WebFlux处理就行不要在Service层提前订阅否则可能把一次流式响应变成两次调用。用一个生活化的类比同步调用像打电话对方说完整段话你才能挂断流式调用像听语音直播边播边接收体感完全不同。做聊天产品这个体验差异非常明显。2.4 工具调用的核心机制从方法名到模型决策工具调用是SpringAI里最容易让人误解的部分。表面上看起来像“给模型装了个函数”实际上流程是你把一组方法签名和描述发给模型模型根据用户问题判断要不要调用某个方法、参数填什么然后以结构化JSON返回调用意图SpringAI在本地真正执行你的方法再把执行结果以Assistant消息回传给模型模型基于结果生成最终的自然语言答案。所以关键点有两个。第一方法描述决定了模型能不能正确判断调用时机参数说明越清楚越好最好连单位、取值、常见值都写上。第二方法本身是真实执行的所以要做好幂等设计别让某个有副作用的方法被模型反复调用。这里先建立这个认知模型并没有真的去查天气或查数据库它只是“决定”去查真正查的动作发生在你的进程里。3. 实操过程从Hello World到RAG与工具调用概念讲过一遍下面从头搭一个最小可运行的项目。我按实际操作顺序写每一步都是可以直接落地的代码。建议你边看边建一个Spring Boot工程跟做比只看不敲效果好得多。3.1 建一个能跑的最小工程创建一个普通的Spring Boot项目即可不用任何额外模板。首先在pom.xml里引入SpringAI的BOM再引入对应模型服务商的starterdependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后在dependencies里加具体的starter依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-xxx/artifactId /dependencyxxx换成你实际使用的模型厂商模块。SpringAI的starter命名规律很统一基本就是spring-ai-starter-model加厂商名。BOM版本我建议直接用一个已发布的GA版本不要用M系列快照来做业务开发否则很可能踩到API迁移的坑。写完依赖后启动类不需要额外修改。Spring Boot自动配置会根据application.yml里的配置项把ChatModel、ChatClient.Builder这些Bean装配好。想验证是否装配成功在任意测试类里注入ChatClient.Builder写一个最简单的调用就可以了。3.2 写下第一个对话接口我习惯在Controller里注入ChatClient.Builder而不是直接注入ChatModel。原因有两个一是构建器可以按场景创建不同的ChatClient实例不同模块可以使用各自的系统提示词二是后续要加Advisor、工具时链式API改起来很顺手。RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } GetMapping(/sync) public String sync(RequestParam String question) { return chatClient.prompt().user(question).call().content(); } GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String question) { return chatClient.prompt().user(question).stream().content(); } }这段代码里/api/chat/sync会阻塞等待模型返回完整文本/api/chat/stream返回Flux以SSE格式推给前端。Flux 即使是在Spring MVC项目里也能正常使用关键是produces要写成TEXT_EVENT_STREAM_VALUE。跑通这一步你的Spring Boot应用就具备和大模型对话的能力了。接下来要做的才是真正能产生业务价值的部分。3.3 让回答带上你的业务知识直接让模型回答用户问题它回答不了内部业务比如“某个订单现在什么状态”“公司对账周期是几号”。RAG是解决这类问题的常用方案流程可以概括为文档切分、向量化入库、查询时检索、把检索结果拼进上下文。SpringAI用VectorStore和EmbeddingModel把流程简化了很多。先用内存向量库做一个可用DemoConfiguration public class RagConfig { Bean VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); } }然后准备一段业务知识转成Document后加入向量库Service public class KnowledgeService { private final VectorStore vectorStore; public KnowledgeService(VectorStore vectorStore) { this.vectorStore vectorStore; } PostConstruct public void load() { vectorStore.add(List.of( new Document(公司产品支持7天无理由退货但定制类商品除外。), new Document(对账周期为每月1号到5号之后生成电子账单。) )); } }调用时可以用SpringAI提供的QuestionAnswerAdvisor来自动完成“检索到拼上下文到调用模型”这一串动作Advisor advisor new QuestionAnswerAdvisor(vectorStore); String answer chatClient.prompt() .user(question) .advisors(advisor) .call() .content();这样用户问“退货政策是什么”系统会先从向量库里找到相关文档片段拼进Prompt后再交给模型生成答案回答变得有依据。需要注意SimpleVectorStore只存在内存里适合原型演示生产环境要换独立向量库但接口不变换依赖和配置就行。这里有个实操细节文档切分会影响检索效果。我一般按段落切分或者按大概500字一个块、块与块之间重叠50字左右的方式切避免一个完整语义被拦腰切断。你可以在加载文档前先做切分再调用vectorStore.add。3.4 用工具调用帮模型“动手”RAG适合静态知识但用户如果问“某个城市现在天气怎么样适合穿什么衣服”模型没有实时天气数据这时候就要靠工具调用。先写一个天气查询服务Service public class WeatherService { Tool(description 根据城市名查询当前天气) public String getWeather(ToolParam(description 城市名例如滨海市) String city) { // 这里在真实项目里会去调用外部天气服务的HTTP接口 return 当前晴天气温28摄氏度风力2级适合穿短袖。; } }然后在ChatClient里把这个工具挂上去String answer chatClient.prompt() .user(滨海市现在适合穿什么衣服) .tools(weatherService) .call() .content();当模型发现用户问题涉及天气时它会在响应中生成调用天气方法的结构化指令SpringAI反射执行方法把返回文本回传给模型最后由模型结合天气结果给出穿搭建议。实操里最容易出问题的是参数名丢失。Java编译时如果不保留参数名反射拿到的可能变成arg0这类名字模型不知道该传什么。解决办法是给参数加上ToolParam描述或者在编译时开启-parameters参数。这个细节我踩过一次卡了大半天。3.5 多轮会话的两种做法ChatClient本身不记状态每次调用都是独立请求。要做多轮对话必须把历史消息显式带过去。第一种做法是自己组装PromptListMessage messages List.of( new SystemMessage(你是一个订单客服助手。), new UserMessage(我的订单号是A001), new AssistantMessage(订单收到请问想了解什么), new UserMessage(帮我查一下这个订单的物流) ); Prompt prompt new Prompt(messages); String answer chatModel.call(prompt).getResult().getOutput().getText();第二种做法是让Advisor来管理历史消息。SpringAI提供了MessageChatMemoryAdvisor配合ChatMemory实现类使用可以在每次调用时自动追加历史并限制保留窗口。对于业务模块来说第二种方式更省心但需要初始化一个内存版或持久化的ChatMemory实现。不管用哪种方式历史消息都会消耗token轮数越多开销越大。所以窗口长度必须控住不能无限堆积。4. 常见问题与排查技巧实录配置和代码本身不算难难的是遇到奇怪报错后怎么快速定位。这节把我积累的排查经验整理一下。其实很多问题都集中在几个方面配置项没对上、模型标识写错、上下文没维护、版本差异导致API对不上。4.1 高频报错速查表报错或现象常见原因排查方向401 Unauthorized密钥错误、未配置或额度不足先确认环境变量值再用裸HTTP工具直接测同一个服务地址400 Bad Request提示未知模型model标识不对对照服务商文档注意模型名可能带日期或版本后缀连接超时或Connection reset服务地址不可达、本地引擎未启动先用curl直接测试base-url再检查进程和日志返回内容被截断max-tokens不够适当调大同时在用户端提示内容长度限制工具从没被调用方法描述含糊、Bean没注册、tools参数没传检查容器里是否存在对应Bean方法描述写清楚调用场景工具调用后报参数缺失参数名在编译时丢失加ToolParam描述或开启-parameters编译选项中文乱码编码不一致或SSE解析方式不对统一UTF-8前端按text/event-stream解析向量检索结果不相关切分不合理、topK太小、相似度阈值太高调整切分长度和检索参数抽样打印检索结果验证这里最想强调的是“先用裸HTTP工具直接测同一个服务地址”。如果模型服务本身是通的再回头看SpringAI侧配置问题范围能缩小一半。这个顺序能省下大量排查时间。4.2 流式接口常见的两个“假故障”第一个假故障是页面等很久才一次性吐出一整段话根本没有打字机效果。大多数原因不是前端问题而是Service内部用了call()而不是stream()或者Controller返回类型写成了String。检查顺序很简单先看Controller返回类型是不是Flux再看Service链路里有没有把stream()换成call()。第二个假故障是流式接口能出内容但多轮对话后模型“失忆”。这个往往不是模型问题而是每次请求没有携带历史消息ChatMemory配置缺失导致上下文没有维护。加上MessageChatMemoryAdvisor后这个问题通常会消失。还有一个很隐蔽的坑Flux的冷流特性。如果你在某个地方对同一个Flux调用了blockLast()或collectList()做调试又在Controller里把它返回给前端等于触发了一次完整请求后再触发第二次模型会被调用两次。排查这类问题关键是搞清楚同一个Flux被订阅了几次。4.3 上下文越长越贵怎么控制成本大模型API按token计费多轮对话里每条历史都会算进输入成本会随对话轮数快速增长。控制成本通常分三步一是设置窗口只保留最近N条消息二是做摘要压缩把前面整段对话凝结成一小段关键信息三是在策略层面把固定业务事实放进系统Prompt别让它随对话历史无限膨胀。估算token时中文通常一个字可能折算一个多token英文大约三个字符算一个token具体以模型的Tokenizer为准。不要凭感觉觉得“没聊几句不贵”用户一旦聊到20轮以上输入会膨胀得很快。建议在调用日志里记录prompt_tokens和completion_tokens定期分析这样才能知道钱到底花在哪。4.4 版本更新带来的API漂移SpringAI版本迭代速度很快API变化也比较明显。早期版本里一些构造器用法到GA版本可能已经改成了Builder模式starter的groupId和artifactId也调整过。最大的坑是参考别人文章时他用的版本和你用的版本不一样代码自然对不上。我现在遇到编译报错第一反应不是改代码而是先确认依赖版本。看完pom里spring-ai-bom的实际版本再去翻对应版本的官方文档和迁移说明。不要看到报错就复制搜索到的代码很多时候是版本差异导致的伪报错。另一个小建议在pom里统一用dependencyManagement管理BOM版本这样升级只改一处相关starter版本能保持一致避免不同模块间版本错位。5. 收尾之前分享几点个人体会技术内容聊到这里基本可以落地了最后写几句我实际用下来的感受。第一次把SpringAI接进已有后端项目时最大的感受是接入成本被压得很低。以前手动写接入层要考虑重试、超时、流式解析、token统计少说也得折腾一两周现在半个小时内就能跑通一个能对话的Demo。但这并不意味着万事大吉底层逻辑被自动配置隐藏之后出问题时反而更考验对Spring Boot和Reactor机制的熟悉程度不然连错误日志都不太好定位。我的建议是先跑通最小链路再逐步加RAG和工具调用。别一上来就上生产级向量库先用内存版把业务效果验证出来确认有价值后再考虑数据持久化和权限隔离。工具调用也一样先拿一个无副作用的查询函数跑通再设计复杂流程。最后分享一个小技巧给正在踩坑的人无论用哪个模型服务商把模型标识、api-key、base-url全部抽到环境变量或配置中心。SpringAI的自动配置价值就在这里将来换模型供应商时只需改配置业务代码可以保持不变。遇到奇怪问题时先用裸HTTP工具直接打一次原服务地址和参数确认模型侧正常后再回来看SpringAI按这个顺序排查能省下非常多时间。