1. 先聊聊Spring AI为什么我建议你用ChatClient后端应用接入大模型早几年还得自己拼HTTP请求、处理鉴权、管理会话状态。现在Spring AI把这个活儿收敛成了配置文件和一行链式调用。最近我用ChatClient搭了个Demo把application.yml填好几十行代码就能和大模型对话。这篇记录我从零跑通的过程重点拆解ChatClient那四步链式调用顺便把配置、参数、常见坑一次说清楚。Spring AI本身就是Spring生态里的一个集成层目标是把“调用大模型”这件事包装成Spring开发者熟悉的样子。你可以把它理解成一套针对AI服务的“Starter”引入依赖后框架自动帮你创建好底层客户端你只需要在yml里写几个配置项然后注入ChatClient就能聊。对于从零开始接触AI集成的后端开发者来说这种体验比直接对接HTTP接口友好太多。适合看这篇内容的人有两类。一类是Spring Boot玩家已经会写Controller和Service想快速把大模型能力塞进自己的应用里比如做文本摘要、智能客服、内容生成。另一类是刚接触AI但不想被各种SDK绑住的人想通过Spring AI统一对接多家模型服务至少先把这一套标准玩法跑通。这篇不讲那些虚头巴脑的架构就从配yml、写代码、踩坑三个角度把链路理顺。2. 环境准备从空项目到第一次对话2.1 依赖引入BOM版本管理是关键我建议直接用一个干净的Spring Boot 3.x项目起步Java版本选17或21都行太低的版本会跟Spring AI的字节码要求冲突。项目创建好之后第一件事是把Spring AI的BOM导入到dependencyManagement里这样整个项目的AI相关依赖版本都能统一对齐。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement接下来添加具体的starter。不同版本的Spring AI对starter命名的调整比较频繁早期版本可能叫spring-ai-openai-spring-boot-starter后来的版本又改成了spring-ai-starter-model-chat这种更通用的名字。我给项目的建议是先去官方文档确认当前版本对应哪个artifactId再照着填。依赖这个事别凭记忆写我吃过亏写错一个名字启动直接报NoClassDefFoundError。如果你只想最小化跑通不需要纠结选哪个模型厂商先找一个兼容标准HTTP接口的服务商把starter加进去剩下的交给自动配置。2.2 application.yml里真正要关心的几个项很多人一开始被“配好yml就能聊”这句话带偏以为只填一个api-key就完事了。实际上yml里最关键的不是key而是base-url和model。base-url是服务端地址model是你要用的模型ID这两个错了后面所有调用都会翻车。以下是我在本地一个Demo里用的配置你换成自己的实际服务商参数就行spring: ai: model: chat: api-key: ${AI_API_KEY} base-url: https://api.example.com/v1 options: model: demo-model temperature: 0.7这里用环境变量${AI_API_KEY}来注入密钥而不是把真实key写在yml里提交到仓库。这个习惯一定要养成。很多同学本地跑通了一上传Git仓库就把key带出去了第二天收到账单才追悔莫及。2.3 用Controller验证配置是否生效配置写完最直接的验证方式就是写一个极简接口。先不要急着上复杂逻辑把链路打通最重要。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() .user(message) .call() .content(); } }启动项目后访问/chat?message你好如果配置没问题几秒内就能收到模型返回的文本。这一步跑通了说明Spring AI的自动装配、网络连接、模型鉴权全部正常后面再研究链式调用就有底了。3. ChatClient四步链式调用一步步拆开看3.1 第一步构建ChatClient实例ChatClient不是直接用new来创建的而是通过Builder构建。为什么Spring AI不直接让你new一个实现类因为ChatClient本身是一套抽象接口底层要对接不同的模型服务、不同的请求参数、不同的响应策略。用Builder模式框架可以在构建过程中把默认选项、超时时间、拦截器等基础设施都悄悄塞进去你拿到手的就是一个配置完整、可以直接用的客户端。最简单的构建方式有两种。第一种是像我前面写的注入ChatClient.Builder再调用build()这是Spring Boot自动配置给你的不需要自己new。第二种是手动指定一个ChatModel再构建ChatModel chatModel ...; // 由Spring容器管理 ChatClient chatClient ChatClient.builder(chatModel).build();实际项目中大部分场景用第一种就够了。如果你有多个模型服务商要切换可以创建多个ChatClient Bean每个指向不同的base-url用Qualifier区分。3.2 第二步从prompt()开始组装消息组装消息的入口是prompt()方法这一步告诉你“我要开始构造一次对话请求了”。紧接着可以连续调用system和user来补充角色信息。chatClient.prompt() .system(你是一个耐心、友善的技术助手回答问题时尽量给出代码示例。) .user(请用三句话解释什么是依赖注入)system消息对应大模型里的系统角色用来设定人设、行为准则、输出格式。user消息是你的真实提问。先system后user是推荐的顺序因为系统提示词需要在大模型处理用户内容之前就生效这是各家模型API都遵循的约定。这里有一个容易被忽略的细节system和user方法不仅能传字符串还能传一个lambda表达式通过Spec来构建更丰富的消息内容。比如chatClient.prompt() .system(s - s.text(你是一个翻译助手只输出译文不要多余解释。)) .user(u - u.text(把这句话翻译成英文我爱编程))3.3 第三步调用call()等待模型响应消息组装完成后调用call()发起同步请求。这一步会阻塞当前线程等到大模型服务返回完整响应后才继续往下走。如果是用户点击按钮后实时等待答案的场景同步调用体验还行但如果你的接口本身吞吐量很高就需要注意响应时长后面我会提到怎么切到流式模式。call()的返回值是ChatResponse这个对象里包含结果列表、使用token数、响应元数据等信息。不要试图从ChatResponse里直接拿字符串而是继续走第四步。3.4 第四步用content()提取最终文本拿到ChatResponse之后调用content()方法获取模型生成的文本内容。这一步把响应对象里最核心的“回答文本”抽出来包装成普通的String方便你直接返回给前端或者存进数据库。完整的链式调用看起来就是一行String answer chatClient.prompt() .system(你是一个技术专家) .user(什么是CDN) .call() .content();把四步拆开看就是prompt()、system()、user()、call()再加上最终的content()取值。这套设计的好处是每一步返回的都是同一个链式对象你可以按需增删角色消息不会像传统HTTP调用那样为了拼一个请求体写成一大坨JSON。3.5 完整示例一个可复用的Service把上面的四步封装成一个Service方法是最常见的用法。Service public class AiChatService { private final ChatClient chatClient; public AiChatService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String ask(String question) { return chatClient.prompt() .system(你是一个可靠的Java开发助手回答要简洁、有代码示例。) .user(question) .call() .content(); } }这个Service再配合Controller一个可运行的聊天接口就出来了。整个过程没有手动拼HTTP请求没有写JSON解析逻辑也没有处理鉴权靠的就是Spring AI的自动配置和ChatClient的链式API。4. 玩明白链式调用参数控制、流式输出和多轮对话4.1 通过Options控制大模型行为就算一模一样的问题大模型也可能给出不同的答案这就是temperature参数在起作用。temperature越高回答越随机越低回答越稳定。做客服、做摘要我会倾向调低到0.2~0.3做创意文案可以放到0.8以上。你可以在yml里配置全局默认值也可以在创建ChatClient时用Builder设置默认选项ChatClient chatClient ChatClient.builder(chatModel) .defaultOptions(ChatOptions.builder() .temperature(0.2) .maxTokens(500) .build()) .build();这样设置的好处是这个ChatClient发出的所有请求都默认带上这些参数你不需要在每次prompt里重复配置。如果某个特定请求想要临时调整还可以在链式调用里继续叠加options。4.2 把call换成stream实现打字机效果同步call()有个明显的短板如果模型生成长文本用户可能要等十几秒才看到完整回答。现在很多AI应用做的是流式输出也就是大模型边生成边推送用户看到的是“一个字一个字蹦出来”的效果。Spring AI把这套逻辑封装成了stream()方法配合响应式Flux代码结构跟同步版本几乎一样FluxString stream chatClient.prompt() .system(你是一个翻译助手) .user(翻译你好世界) .stream() .content();在WebFlux环境中你可以直接把这个Flux返回给前端。注意使用stream()时不要用普通Spring MVC的阻塞式接口否则流式效果会大打折扣。流式模式对网络稳定性要求更高因为连接会持续更长时间服务端需要配置更合理的超时时间。4.3 多轮对话怎么组织ChatClient本身是无状态的也就是说它不会记住你上一次问了什么。想要实现多轮对话你需要把历史消息一起传给模型。一个比较朴素但能用的做法是把之前的对话拼进user消息里String fullContext 之前的对话 用户你好 助手你好有什么可以帮你 用户帮我推荐一本书 助手... 现在用户的问题%s .formatted(question);如果你嫌拼字符串太粗糙可以改用Message对象列表把SystemMessage、UserMessage、AssistantMessage组装好再创建Prompt发送。对于复杂的对话类应用建议使用专门的内存管理或向量库来保存历史会话不是ChatClient这层要考虑的事情。5. 踩坑实录常见问题与排查方法5.1 问题速查表我把实际运行中遇到的高频问题整理成了表格方便你对照排查。现象可能原因解决方案启动报错找不到ChatClient.Builder没引入对应starter或者BOM没有生效检查依赖mvn dependency:tree查看实际依赖启动失败提示缺少ChatModel Beanspring.ai.model.chat相关配置项没写或写错了属性名核对yml中的配置确认key和model都存在请求返回401api-key不正确或环境变量没有加载确认环境变量名是否和yml占位符一致请求返回404base-url路径不对检查base-url是否包含/v1按服务商文档修正请求返回400提示模型不存在model名称填错去服务商控制台复制准确的模型ID调用时报连接超时网络无法访问API服务确认网络策略、确认地址可ping通适当调大超时时间5.2 版本差异引发的配置属性变化Spring AI迭代速度非常快不同版本之间配置属性经常改名。同样是api-key早期可能是spring.ai.openai.api-key后来改成spring.ai.model.chat.api-key再往后又可能嵌入到options里。我在本地跑的时候就因为照着旧教程填配置启动时提示属性无法识别但日志又不会直接告诉你是哪个字段错了排查起来很费时间。解决办法只有一个打开你当前版本的官方文档或者直接看jar包里的ConfigurationProperties类。用IDE打开spring-ai-autoconfigure相关的jar搜索ConfigurationProperties注解就能看到当前生效的属性前缀和字段名。这个方法比在网上翻各种过时教程高效得多。5.3 内容返回为空但不报错有一种很隐蔽的情况是调用成功status是200content()却返回空字符串。我遇到过一次原因是system提示词里写了“直接输出结果不要任何前缀”模型可能真的只生成了换行符或者空白字符拿出来的content()自然就是空的。排查思路是先把响应对象完整打印出来看看ChatResponse里的原始内容到底是什么。如果原始输出里确实有空值大概率是参数配置问题比如maxTokens设置太小模型还没生成有效内容就被截断了。遇到这种情况可以把maxTokens适当调大或者简化提示词试试。6. 最后说点实在的经验这套链路跑通之后我最大的感受是Spring AI把接入大模型的复杂度压到了很低但并没有把所有细节都藏起来。很多东西最终还是要理解模型API本身比如system消息、temperature、流式输出、token限制这些概念不管用哪个SDK都必须掌握。有一个习惯我想强烈推荐类里注入ChatClient时不要直接new也不要每个方法都重新build一次。在构造器里统一构建一个ChatClient实例全局复用。因为ChatClient内部会持有连接池、超时控制、响应策略等资源频繁创建会造成不必要的开销。再分享一个小技巧。生产环境用真实模型之前先写一个MockChatModel或者本地假接口把ChatClient链式调用的日志打开确认请求体结构符合预期。我第二次调通只用了几分钟靠的就是这一步把问题隔离在模型服务之前省去看大量响应日志的时间。Spring AI目前还在快速演进ChatClient的API以后可能会继续调整。但核心思路不会变约定优于配置、自动化装配、链式调用。把这套基础打牢不管后面模型服务商怎么换你都只需要改yml和一两行依赖而已。