1. 项目概述与整体思路
1.1 这到底是个什么东西
Spring AI,简单说就是 Spring 官方给 Java 开发者准备的一套 AI 应用开发脚手架。它把“接大模型”这件事从原来的苦力活变成了“配个 yml、写几行代码”就能跑起来的标准化流程。我第一次看到这个项目名字的时候第一反应是——又一个包装过的封装库?毕竟 Java 生态里各种 AI SDK 已经够多了,阿里有、百度有、各家模型厂商都有自己的 Java SDK,谁没事再多学一个?但真正用下来之后,我觉得它和那些厂商 SDK 有本质区别:它把“接大模型”抽象成了像 JdbcTemplate 一样的东西,你不需要关心底层 HTTP 调用、消息格式转换、流式响应的解析,只需要关心业务本身。
这个标题里最关键的两个信息:“配好 yml 就能聊”和“ChatClient 四步链式调用”。前者说的是配置层面,后者说的是编码层面。这两件事正好是大部分人刚接触 AI 编程时最头疼的两个点——配置繁琐、代码冗余。Spring AI 通过 Spring Boot 的自动配置机制把前一个问题解决掉,通过 ChatClient 的流式 API 设计把第二个问题解决掉,这也就是为什么我觉得这个项目值得写一篇完整文章来拆解。
1.2 适合谁看、能解决什么问题
如果你是以下这几类人,这篇文章应该能帮到你:
- 想把大模型能力集成到现有 Spring Boot 项目里的 Java 后端工程师
- 对各种模型供应商(通义千问、智谱、Ollama 本地模型等)感兴趣、但不想为每家都单独写对接代码的开发者
- 刚接触 AI 应用开发、对“模型部署、API Key、Token、上下文”这些概念还比较模糊的新手
这篇文章会从零开始,带你跑通一个最小可用的 Spring AI 工程,接着深入拆解 ChatClient 的链式调用每一环在做什么,最后补上我在实际项目中踩过的坑和排查思路。里面的代码我尽量给出完整可用的版本,配置也给出了不同模型供应商的对照,你可以直接抄作业。
1.3 我为什么选 Spring AI 而不是直接用模型厂商 SDK
打个比方,直接对接模型厂商 SDK 就像自己装修房子:你得自己买水泥、买砖头、找工人,每一步都得自己盯着。而用 Spring AI,相当于找了一个施工队,你只需要告诉他们“我想要个什么风格的客厅”,剩下的事他们去协调。具体到技术上,Spring AI 帮你承担了这几类重复劳动:
一是多厂商适配。你有没有想过,如果项目里今天用通义千问,明天想换成智谱,或者本地用 Ollama 跑开源模型做测试,你的业务代码要改多少?直接对接厂商 SDK,换一家就意味着重写一整套调用逻辑。Spring AI 把模型接口统一成了 ChatModel,各家模型的差异被封装在 starter 内部,换模型供应商的时候大部分场景只需要改 yml 配置。
二是结构化输出。大模型返回的是字符串,但业务里我们往往需要一个 JSON 对象。Spring AI 提供了一套输出解析机制,可以直接把模型返回映射成 POJO,省掉自己写 JSON 解析和错误处理的功夫。
三是与 Spring 生态的融合。你如果在用 Spring Boot、Spring Cloud,会发现 AI 相关的配置可以进 application.yml,AI 调用的结果可以像普通 Bean 一样注入,还能配合 Spring 的异步、缓存、重试机制,整个开发体验非常顺。
2. 环境准备与依赖引入
2.1 版本选择和基础要求
Spring AI 这个项目迭代非常快,版本差异也大,这是刚开始使用最容易掉坑的地方。我目前用的版本是 1.0.0 GA 之后的稳定版,对应的 Spring Boot 版本是 3.4.x。有一点必须提醒:Spring AI 早期版本(0.8.x、1.0.0-M系列)的 API 变化非常大,你在网上搜到的很多教程可能对应的是旧版本,跑起来直接编译报错。所以不管你看谁的教程,先确认版本,再动手写代码,这是第一原则。
用 Maven 的话,父工程加上版本管理:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.5</version> <relativePath/> </parent>然后 Spring AI 的 BOM(Bill of Materials)也需要引入,因为 Spring AI 的模块比较多,用 BOM 可以不用自己操心各模块的版本兼容性:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>JDK 版本建议 17 以上,因为 Spring Boot 3.x 本身就是基于 JDK 17 的。如果项目还在用 JDK 8,那估计得先把项目升级一下,这是个硬门槛。
2.2 核心依赖怎么选
Spring AI 的依赖是按模型供应商拆分的,每个供应商对应一个 starter。目前我接触到的有这些:
spring-ai-starter-model-openai:官方 OpenAI 接口spring-ai-starter-model-zhipuai:智谱 AIspring-ai-starter-model-tongyi:阿里通义千问spring-ai-starter-model-ollama:本地 Ollama 模型spring-ai-starter-model-qwen:阿里 Qwen 系列
如果你只有一个供应商的需求,直接引入对应 starter 就够了。如果你想在多个厂商之间灵活切换,可以都引进来,通过配置项激活具体的模型。不过我不建议一开始就贪多,先选定一个主用的模型供应商跑通,后续再扩展其他家。
以智谱 AI 为例,引入:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-zhipuai</artifactId> </dependency>以 Ollama 本地模型为例,引入:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency>还有一个情况要特别说明:如果你用的是 OpenAI 兼容接口的网关或者私有化部署(市面上很多中转平台都提供 /v1/chat/completions 兼容接口),那直接引入 OpenAI starter,再把 base-url 指向你自己的服务地址就可以了,这也是一个非常常见的使用姿势。
2.3 额外必装组件:Spring Web
Spring AI 本身不自带 Web 能力,如果你打算通过 HTTP 接口对外暴露聊天能力,还得引入 spring-boot-starter-web:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>哪怕你只是想做一个本地命令行 Demo,不引入 Web 也能跑,ChatModel 可以直接注入到普通 Service 里调用。但实际项目里 AI 能力基本都要做成接口给别人调,所以 Web 这个依赖几乎算是标配。
3. application.yml 配置详解
3.1 最简配置:Ollama 本地模型
先把最简单、不用花钱、也不用申请 API Key 的方案说清楚——Ollama 本地部署。这个方案特别适合学习阶段反复试验,因为你不用关心每调一次花了多少 token 钱,也不用担心把 Key 泄露到代码仓库里。
首先你需要在本机安装 Ollama,然后拉取一个模型,推荐从 qwen2.5 开始:
ollama pull qwen2.5:7b接着在 Spring Boot 项目的 application.yml 里配上:
spring: ai: ollama: base-url: http://localhost:11434 chat: model: qwen2.5:7b options: temperature: 0.7 top-k: 10这里解释几个配置项的用途:
base-url:Ollama 服务的地址,默认端口是 11434,如果你在远程服务器上装了 Ollama,把这个地址换成服务器的 IP 和端口就行。model:指定用哪个模型,要和ollama list里显示的名称一致。temperature:控制模型输出的随机性,值越小输出越稳定、越保守;值越大越有创造性。做客服问答建议 0.3 以下,做头脑风暴可以拉到 1.0。top-k:采样时考虑的候选 token 数,调小可以让输出更集中。
这只是最基础配置。Spring AI 的 Ollama 模块还支持更细粒度的参数,包括top-p、frequency-penalty、presence-penalty,但新手阶段不需要全配上,保持简单反而容易排查问题。
3.2 在线大模型配置:智谱、通义千问
如果你要用云厂商的模型,配置也大同小异,区别是要申请 API Key。以智谱 AI 为例:
spring: ai: zhipuai: api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-flash temperature: 0.7这里强烈建议用环境变量方式注入 API Key,而不是直接写到 yml 文件里。万一项目代码传到公共仓库,Key 就泄露了。用${ZHIPU_API_KEY}这种写法,Spring 会自动从系统环境变量里取值,安全得多。
以通义千问为例:
spring: ai: tongyi: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus你可能会问,这些配置我记不住怎么办?其实 Spring AI 的 starter 引入了配置元数据,你在 IDEA 里写 yml 的时候会有自动提示,Ctrl+Space 就能看到当前模块支持哪些配置项。这个功能在写配置的时候帮助很大,敢用 IDE 的提示,就基本不会配错。
3.3 超时与重试配置
真实生产环境里,大模型接口偶尔会慢,或者因为网络问题超时,这是很常见的事。Spring AI 提供了一套重试和超时配置,在spring.ai.retry下面:
spring: ai: retry: max-attempts: 3 backoff: initial-interval: 1000 multiplier: 2 max-interval: 10000这套配置的含义是:最多重试 3 次,第一次重试前等 1 秒,之后每次间隔翻倍,最长不超过 10 秒。这个设计我觉得比较合理,因为大模型服务经常是瞬时抖动,等一小会儿再试往往就成功了。
如果你希望某些场景不重试(比如用户的输入参数非法,重试多少次都没用),可以通过自定义RetryTemplate来定制,这个后面进阶部分再讲。
3.4 配置项太多?用 Actuator 检查实际生效的配置
配置写完之后,怎么确认配置真的生效了?Spring Boot Actuator 可以帮你:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>启动项目后访问/actuator/env,搜索spring.ai前缀的配置,就能看到当前生效的配置值。注意,暴露出来的 Key 是脱敏后的形式,不会显示完整内容。我在排查问题时经常用这个接口来看配置是不是被其他地方的配置覆盖了,比纯靠日志猜效率高很多。
4. ChatClient 四步链式调用拆解
4.1 第一步:构建 ChatClient
整个 Spring AI 编程模型的核心就是 ChatClient。你可以把它理解成一个“聊天机器人客户端”,所有与大模型的对话都通过它来完成。
最常用的构建方式是通过ChatClient.Builder:
@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } }这里有一个容易忽略的点:ChatClient.Builder是 Spring AI 自动配置好的 Bean,你直接注入到构造函数里就行,不需要自己 new。构建时如果什么参数都不传,它会自动根据你引入的模型 starter 和配置创建对应的 ChatModel,然后包装成 ChatClient。这意味着你换模型供应商的时候,这个 Bean 的构建代码完全不用变。
构建时也可以传入一些默认行为,比如设置默认的 system 指令:
this.chatClient = builder .defaultSystem("你是一个精通 Spring Boot 的编程助手,回答问题要简洁明确,必要时给出代码示例。") .build();这样后续每次调用对话,都会自动带上这段系统提示词,不用每次重复写。
4.2 第二步:prompt() 开启一次新的对话
第一步是把 ChatClient 准备好,第二步就是真正跟模型对话了。prompt()方法标志着一轮新对话的开始,它返回一个PromptRequestSpec对象,这个对象就是链式调用的“中继站”。
String answer = chatClient.prompt() .system("你是资深Java工程师,擅长代码审查。") .user("请帮我看看下面这段代码有什么问题:...") .call() .content();这个链式结构把一次完整的“问-答”过程拆成了四个环节,我拆开给你看:
prompt():新建一个 Prompt.system(...)和.user(...):组装消息.call():真正发送请求并等待响应.content():把响应的内容提取成字符串
为什么 Spring AI 要设计成这种链式调用的形式?因为这样可以保证每一步都只做一件事,调用链清晰可读,而且每一步都能在对应阶段加入额外参数。比如你需要在某次调用里临时改 temperature,不需要动全局配置,只需要在prompt()后面加一个.options(...)即可:
String answer = chatClient.prompt() .system("你是创意文案专家。") .user("写三句关于秋天的朋友圈文案。") .options(ChatOptions.builder() .temperature(0.9) .build()) .call() .content();这个能力非常重要,意味着你可以针对不同业务场景设置不同的模型参数,而不是全局一刀切。
4.3 第三步:组装消息内容(system、user、messages)
链式调用里最核心的是消息组装。这里要区分system和user这两个角色:
system:系统提示词,相当于给模型设定人设、行为边界、输出格式要求。user:用户的实际输入,相当于对话里人说的话。
一个常见误区是:把 system 理解成“系统的输入”,然后在里面写业务数据。其实 business 数据应该放在 user 消息里让模型理解,system 只应该放规则和约束。
举个例子,你要做个“商品评论情感分析”功能:
String analyze = chatClient.prompt() .system("你是一个电商评论分析助手。请判断评论的情感倾向,只回复『正面』『负面』『中性』三个词。") .user("商品到货很快,但包装破损了,客服处理态度也很差。") .call() .content();这样模型就知道自己的任务边界是分类,而不是跟用户闲聊。如果 system 里不设限制,模型可能会输出一堆解释,给你解析结果带来很多麻烦。
除了 system 和 user,.messages(...)方法还可以传入历史消息。这个在实现多轮对话时很有用,后面单独讲。
4.4 第四步:call() 触发调用并提取响应
组装完消息之后,.call()就是那个“拍板”的动作,它会阻塞等待大模型返回完整结果。.call()返回的是ChatResponse对象,里面包含完整的信息,包括模型返回的内容、token 消耗、finish 原因等。
如果直接用.content()就能拿到最核心的文本内容。但如果想要更多信息,比如 token 数,可以这样:
ChatResponse response = chatClient.prompt() .system("你是翻译助手。") .user("把『Hello World』翻译成中文。") .call(); String text = response.getResult().getOutput().getText(); long tokens = response.getResult().getMetadata().getUsage().getTotalTokens();这里有一个我在项目中频繁踩过的坑:如果你调用的模型不支持某种能力,或者响应被截断了,response.getResult().getOutput()可能返回 null,因此直接.content()在某些极端情况下会抛 NPE。防御性写法是拿到 response 之后先判空,或者捕获异常再处理,不要把.call().content()当成总是安全的写法。
还有,.call()是同步阻塞的,网络请求大模型一般需要几秒到几十秒。如果你在 Web 接口里直接调用,会导致请求线程一直占着,需要给接口设置合理的超时时间。如果需要非阻塞场景,Spring AI 也提供了.stream()方法做流式响应,后面专门讲。
4.5 完整示例:把四步串起来
我们把上面的步骤整合成一个完整的 REST API。先写 Service:
@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是敬业的技术助手,回答要准确、简洁,代码示例优先。") .build(); } public String chat(String userMessage) { return chatClient.prompt() .system("如果你不确定答案,请直接说『我不太确定』,不要编造。") .user(userMessage) .call() .content(); } }再写 Controller:
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @PostMapping public String chat(@RequestBody ChatRequest request) { return chatService.chat(request.message()); } }用 Postman 调一下,发一段话过去,很快就能收到模型回复。这套流程跑通之后,恭喜,你已经拥有了一个最简版的大模型应用服务。后面的进阶玩法都是在这个骨架上扩展出来的。
5. 进阶用法:流式输出与多轮对话
5.1 流式输出:让回复像打字机一样出现
大模型接口的响应往往需要好几秒甚至更久,如果一次性等全部内容返回,交互体验很糟糕。Streaming 是大模型应用里最常用的优化手段,Spring AI 也提供了一等公民级别支持。
流式调用的 API 设计跟同步调用非常像,只是把.call()换成.stream():
Flux<String> stream = chatClient.prompt() .system("你是天气预报助手。") .user("今天北京天气怎么样?") .stream() .content();这里的.stream()返回的是 Project Reactor 的Flux<String>,你可以在 WebFlux 环境中直接返回给前端,也可以自己订阅做逐字处理:
stream.subscribe(content -> { System.out.print(content); // 这里就是流式输出的内容片段 });实测下来,流式输出配合 SSE(Server-Sent Events)协议,前端可以实现打字机效果,这是目前我觉得大模型应用交互体验最好的方式。在 Spring MVC 环境里也可以用SseEmitter或者 WebFlux 来实现。
注意一点:流式接口拿到的 token 用量信息和普通调用不太一样,需要在ChatResponse里取 finish 原因,然后在流结束时统一汇总。不同模型厂商对流式响应里的 usage 支持程度也不一样,有的模型在流式响应里根本不返回 token 数,如果要统计成本就得自己在代码里估算。
5.2 多轮对话:把历史消息传进去
大模型本身是没有记忆的,它对上下文的理解完全依赖于你传进去的历史消息。所以实现多轮对话,本质上就是维护一个消息列表,每次请求把之前的对话记录一起带上。
Spring AI 提供了Message接口,常见实现包括SystemMessage、UserMessage、AssistantMessage。构建多轮对话可以这样:
List<Message> history = new ArrayList<>(); history.add(new SystemMessage("你是购物理赔助手,负责解答售后问题。")); history.add(new UserMessage("我买的耳机左耳没声音了,怎么办?")); history.add(new AssistantMessage("您好,建议先尝试以下步骤:1. 检查蓝牙连接;2. 重启耳机;3. 尝试固件升级。如果问题仍然存在,可以申请售后检测。")); history.add(new UserMessage("我已经尝试过了重置,但左耳还是没声音。")); String answer = chatClient.prompt() .messages(history) .call() .content();这个需求在真实项目中非常高频。但要注意一个很现实的问题:随着对话轮次增加,消息数越来越多,每次请求携带的 token 成本也会线性上涨,还会受到模型上下文窗口大小限制。因此实际项目中需要对历史消息做截断或摘要。
我目前使用的策略是:保留最近 N 轮完整对话(比如 6 轮),更早的对话让模型生成摘要后作为一条系统消息附加。这样既保留了长期话题的关键信息,又控制了 token 消耗。对于普通项目,直接截断最近 10 轮也够用,不用过度设计。
5.3 结构化输出:让大模型返回 JSON 而不是散文
如果你需要模型返回结构化数据,比如从一段文本中提取“人名、公司名、日期”,让模型返回一个 JSON 对象会更容易解析。Spring AI 推荐的方式是借助BeanOutputConverter:
public record PersonInfo(String name, String company, String date) {} BeanOutputConverter<PersonInfo> converter = new BeanOutputConverter<>(PersonInfo.class); String output = chatClient.prompt() .system("从用户输入中提取指定字段,严格输出 JSON 格式。") .user("张三在2024年入职了某科技有限公司。") .call() .content(); PersonInfo info = converter.convert(output);用这种方式,大模型从“聊天框”变成了一个“信息抽取引擎”,可以直接对接业务流程。我做过一个内部 Demo,把用户填写的自然语言工单描述转换成结构化工单对象,效果非常好,解析错误率远低于自己写正则表达式。
5.4 用 Advisor 实现通用增强逻辑
Spring AI 有一个很巧妙的设计叫 Advisor,它类似于 Spring MVC 里的拦截器,可以在真正调用大模型之前和之后插入自定义逻辑。比如自动附加系统 Prompt、自动做敏感词过滤、统计日志等。
注册一个全局 Advisor 只需实现ChatClientAdvisor接口:
public class LoggingAdvisor implements ChatClientAdvisor { private static final Logger log = LoggerFactory.getLogger(LoggingAdvisor.class); @Override public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) { log.info("大模型请求开始,消息条数:{}", request.adviseMessages().size()); long start = System.currentTimeMillis(); AdvisedResponse response = chain.nextAroundCall(request); log.info("大模型请求完成,耗时:{} ms", System.currentTimeMillis() - start); return response; } }然后把 Advisor 注册到 ChatClient 上:
this.chatClient = builder .advisors(new LoggingAdvisor()) .build();如果你的需求是给某些接口单独加 Advisor,而不是全局加,Spring AI 也支持在链式调用的任意位置.advisors(...)动态插入。这种设计让关注点分离做得很干净,日志、限流、审计这些横切逻辑就不用散落在业务代码里了。
6. 常见问题与排查技巧实录
6.1 配置没生效,请求报 401
这是最常见的错误。当你的应用报 401(Unauthorized)时,优先顺序排查:
第一,API Key 是否配置正确。检查环境变量是否真的存在,在 Linux 下可以用echo $ZHIPU_API_KEY看看。很多开发者配置了环境变量但忘了重启 IDE,IDE 不会自动读取新变化的系统环境变量。
第二,看日志里请求的 URL。有些中转网关需要在 yml 里设置base-url,如果没设置,请求会打到官方默认地址,自然就会 401。确认你用的是哪个模型供应商,然后对照 yml 检查 base-url 是否正确。
第三,看模型名是否拼写正确。模型名错误时,部分厂商会返回 invalid_model,但也有一些厂商会返回跟 401 类似的错误,比较迷惑。
排查技巧:在启动类上写一个CommandLineRunner,启动后立刻发起一次最简调用,把异常信息完整打出来:
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean CommandLineRunner debugChat(ChatClient chatClient) { return args -> { String result = chatClient.prompt() .user("你好,请回复'我已就绪'") .call() .content(); System.out.println("调试输出: " + result); }; } }6.2 请求超时:模型响应太慢
大模型推理时间与模型大小、输入长度、并发量都有关系。如果是本地跑 7B 模型,生成速度完全看显卡;如果调云端模型,网络延迟也占了很大比重。
针对超时问题,我推荐的做法是:
第一,把网络超时调大。Spring AI 默认的 HTTP 超时可能短,对于大模型接口来说不够。可以在 yml 里配置:
spring: ai: chat: client: connect-timeout: 30s read-timeout: 120s这个配置我实际用过,效果明显。第二,如果是长文本生成,PC 端没问题,但移动端断连的话,后端最好不要用同步等待那么久,改用流式输出 + SSE 推送的方式,体验会好很多。第三,对下游业务方提供异步任务接口,而不是要求对方一直等到完整结果。
6.3 模型回答质量不稳定,经常跑偏
很多人以为是参数没调好,其实大概率是提示词写得不够明确。同一个问题,如果 user 消息里信息量不足,模型就容易猜。
我列几个比较有效的优化方向:
一是给模型一个“输出模板”。不要只说“帮我总结”,而是说“请按以下格式输出:核心观点、数据支撑、风险提示”。大模型对格式明确的指令遵循度非常高。
二是让模型“思考”而不是直接给答案。在 system 里加一句“请一步步思考再给出最终答案”,有时能明显提升推理类问题的准确率。不过这种效果因人因模型而异,建议实测。
三是合理调整 temperature。做信息抽取、分类、翻译这类确定性任务,把 temperature 调到 0.2 以下;做创意写作、头脑风暴,再拉到 0.8 以上。很多人从头到尾用同一个参数,这是不对的。
6.4 中文乱码与响应截断
中文乱码这个坑其实不在 Spring AI,而在 HTTP 配置。如果你的应用部署在 Tomcat 环境,response 乱码往往是因为缺少编码过滤器。Spring Boot 一般在编码处理上比较省心,但如果你用了自定义的 Filter 或者响应包装,就可能把编码设置覆盖了。排查时可以看看server.servlet.encoding.charset=UTF-8这个配置。
响应截断的情况多见于本地跑小模型,比如上下文窗口只有 8K,而你的输入加上历史输出已经超过了这个限制。截断时模型返回的finishReason会变成length而不是stop。拿到响应后可以检查一下这个字段,如果经常是 length,就需要考虑压缩上下文或者换更大窗口的模型。
6.5 Ctrl+C 也解决不了的启动卡住
还有一种现象:应用启动时一直转圈,也不报错,过一会儿才启动成功。多半是 Spring AI 在启动时尝试发请求做预检,而你的本地模型还在加载中。Ollama 拉取新模型后第一次调用确实要等模型加载,后续会快一些。如果机器配置一般,建议给 Ollama 设置模型预加载,或者把 Spring AI 的初始化检查关掉,具体配置项以当前版本文档为准。
7. 一点个人的使用体会
从“配好 yml 就能聊”开始跑通第一个 Demo,到后面对 ChatClient 做各种扩展改造,Spring AI 给我的整体感受是:它并不试图让你少写代码,而是通过抽象把代码写得更聚焦、更规范。初看 ChatClient 的四步链式调用好像就是语法糖,但真正用下来,会发现这种设计让你几乎不需要记 API,每次写新代码的时候顺着链式结构往里面添加内容就行。
如果你只是想在项目里“体验一下 AI 能力”,按这篇文章的第一章到第四章,大概半天时间就能出一个能用的东西。如果你是想做生产级应用,建议把第五章的流式输出和结构化输出作为标配,把常见的异常场景都覆盖到。
我踩过的最值得一提的坑,是换模型厂商的时候以为“只要改配置就行”,结果发现不同厂商对接口参数的支持并不完全相同。比如有些模型不支持某些特殊参数,启动时会直接报错,有些则是静默忽略。所以,换厂商之后不要直接上生产,先用调试模式跑几个核心场景验证一下,再切换流量。
最后还是那句老话:工具只解决流程问题,真正决定 AI 应用上限的,还是你对业务的理解和对提示词的设计。Spring AI 让调用大模型变得像调数据库连接池一样顺手,但“怎么使用这个能力”这件事,永远得靠人来判断。希望这篇文章能帮你少走一些弯路,早日跑通自己的第一个 Spring AI 应用。