Spring AI 初体验:配好 yml 就能聊,ChatClient 四步链式调用
作者:鱼宵 | Spring AI 实战精通营 · 第 1 篇
上周有个同事在群里发了张截图:一个 Spring Boot 工程,代码里从头到尾没出现一个"模型"类,跑起来却能和人一问一答。我第一反应是又有什么黑魔法。后来把工程 clone 下来亲手跑了一遍才发现,原来是 Spring 官方出了个叫Spring AI的框架——把大模型接入做成了"配数据库"一样的事:改 yml、注入一个 ChatClient、写个接口,完事。
这篇文章就带你用最小工程跑通第一个 AI 接口。代码全在仓库的lesson-01/目录里,clone 下来照着命令一步步重跑一遍,十分钟后你也有一个能和人对话的 Spring Boot 项目——尤其是最后那两道挑战题,不亲手跑一次,你真以为 AI 接口很难。
一、核心原理:为什么"配好 yml 就能用"?
一句话:Spring AI 把大模型客户端做成了 Spring 风格的 starter,配置走 yml、使用走注入、组合走 Bean——你一行模型代码都不用写。
1. 自动装配:AI 也住进"精装房"
以前自己接大模型,得手写 HTTP 客户端、自己拼请求体、自己管重试,就像租房后自己买家具、自己拉网线。**自动装配(Auto-Configuration)**是 Spring Boot 的老机制:往 classpath 里丢一个 starter(比如spring-ai-starter-model-openai),启动时框架自动读 yml 里的spring.ai.*配置,把ChatClient和底层模型客户端全部创建好,你只管注入。
类比一下:starter 是"家具套餐",yml 是"装修意见表",注入 ChatClient 就是拎包入住。说白了,这是 Spring Boot 干了几年的老本行,只不过这次伺候的对象从数据库换成了大模型。
2. ChatClient:对话界的 JdbcTemplate
ChatClient 是 Spring AI 的"统一对话入口",所有 AI 交互都从它开始。它和 JdbcTemplate 的关系很像——你不用关心底层连的是谁、怎么连,只调用统一接口。
链式 API 四步(面试必背):
| 步骤 | 代码 | 干什么 |
|---|---|---|
| 1 | prompt() | 开始拼消息(相当于打开对话框) |
| 2(可选) | .system("...") | 塞系统提示词——人设/规则 |
| 3 | .user(msg) | 塞用户问题 |
| 4 | .call() | 调用模型,阻塞等回答回来 |
| 5 | .content() | 取出回答文本 |
类比点外卖:
prompt()打开外卖 App,system()备注"不要辣",user()下单,call()等骑手送到(阻塞等待),content()拆开包装吃。
注意call()是阻塞式:等模型把整段回答生成完才返回,简单但首字延迟高。打字机效果(流式)是第 5 课的主菜,先记住这个对照。
3. SystemPrompt:给模型发一本"入职手册"
SystemPrompt是发给模型的最高优先级"人设/规则说明书"。新员工(模型)上岗先读手册,就知道自己是谁、该怎么说话。本课/interview接口就是给模型发了一本"资深 Java 面试官"手册——同一个问题,有没有这本手册,回答风格天差地别(第四节有真实对比)。
二、动手:十分钟跑通最小工程
环境:Windows + JDK 17 + Maven 3.9+。会
@RestController、看得懂 yml 就行。
第 1 步:30 秒检查环境。
java-version# 期望 True(JDK17 在不在)[bool][Environment]::GetEnvironmentVariable('DEEPSEEK_API_KEY')# 期望 True(Key 配了没,只看存在性别打印)Get-NetTCPConnection-LocalPort 8093-State Listen-ErrorAction SilentlyContinue# 无输出=端口空闲第 2 步:编译 + 启动。
cd 你的课程根目录\spring-ai-journey\lesson-01$env:JAVA_HOME="C:\Program Files\Java\jdk-17"# Maven 必须跑在 JDK 17 上,每个新窗口设一次mvn clean install-DskipTests# 结尾看到 BUILD SUCCESSmvn spring-boot:run# 看到 Tomcat started on port 8093 即启动成功第 3 步:调两个接口(中文参数要先 URL 编码)。
# 通用问答$q=[uri]::EscapeDataString('用一句话介绍你自己')Invoke-RestMethod"http://localhost:8093/chat?msg=$q"# 角色扮演(Java 面试官)$q2=[uri]::EscapeDataString('什么是 final 关键字?')Invoke-RestMethod"http://localhost:8093/interview?msg=$q2"浏览器直接开http://localhost:8093/chat?msg=你好也行,浏览器会自动编码。
三、关键代码:三段文件,逐行拆解
工程是个标准 Spring Boot 项目,真正要看的代码就三处:yml(模型配置)、ChatController(两个接口)、主类(启动)。
第一段:application.yml——模型配置,全课程统一套路。
server:port:8093# 端口按课程分配表:spring-ai 系列 lesson-01 = 8093spring:ai:openai:base-url:${LLM_BASE_URL:https://api.deepseek.com}# 环境变量优先,默认 DeepSeek(兼容 OpenAI 协议)api-key:${DEEPSEEK_API_KEY}# Key 只从环境变量读,文件里永远没有明文!chat:options:model:${LLM_MODEL:deepseek-chat}# 模型名,默认 deepseek-chatmax-tokens:200# 单次回答输出上限:教学演示控成本temperature:0.7# 温度:0=严谨固定,1=天马行空,聊天 0.7 自然这段 yml 有两个值得盯的写法:
${DEEPSEEK_API_KEY}:Spring 占位符语法,启动时从环境变量取值。这就是"Key 不进文件"的标准姿势——就算这份 yml 被传出去了,里面也没有一个能用的 Key。${LLM_BASE_URL:https://api.deepseek.com}:冒号后面是默认值。想切模型就$env:LLM_BASE_URL=...再重启,代码和文件都不用动(第六节讲切模型三件套)。
第二段:ChatController.java——两个接口,总共没几行。
packagecom.springai.lesson01;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;/** * 聊天控制器:本课的 HTTP 入口。 * ChatClient 是 Spring AI 的核心门面(就像 JdbcTemplate 之于数据库), * 由 starter 自动装配——只要 yml 配好模型,注入就能用,一个模型 Bean 都不用写。 */@RestControllerpublicclassChatController{/** * ChatClient 实例:通过 Builder 构建(Spring AI 推荐用法)。 * Builder 由 starter 自动装配,背后是 yml 里 spring.ai.openai.* 配置。 */privatefinalChatClientchatClient;publicChatController(ChatClient.Builderbuilder){this.chatClient=builder.build();}/** * 通用问答:http://localhost:8093/chat?msg=你好 * * prompt() = 开始拼"发给模型的消息";user(msg) = 塞用户问题; * call() = 阻塞式调用(等模型答完才返回,流式是第 5 课的事); * content() = 取出回答文本。 */@GetMapping("/chat")publicStringchat(@RequestParam("msg")Stringmsg){returnchatClient.prompt().user(msg).call().content();}/** * 角色扮演:http://localhost:8093/interview?msg=什么是final * * system(...) 在用户问题之前塞一段"系统提示词"(人设说明书)。 * 对比 /chat 的通用助手口吻,体会 SystemPrompt 的作用。 */@GetMapping("/interview")publicStringinterview(@RequestParam("msg")Stringmsg){returnchatClient.prompt().system("你是资深 Java 技术面试官,语气专业但友好。"+"每次回答先用一句话点评候选人的回答,再追问一个更深入的问题。").user(msg).call().content();}}第三段:Lesson01Application.java——标准启动类,三行搞定。
packagecom.springai.lesson01;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** * Spring AI 实战精通营 · 第 1 课:第一个 AI 接口。 * 启动后访问: * GET http://localhost:8093/chat?msg=你好 —— 通用助手问答 * GET http://localhost:8093/interview?msg=什么是final —— 角色扮演(系统提示词) */@SpringBootApplicationpublicclassLesson01Application{publicstaticvoidmain(String[]args){SpringApplication.run(Lesson01Application.class,args);}}注意一个细节:整个工程没有任何new出来的模型对象。ChatClient.Builder是自动装配的,你只管在构造函数里收——这就是"配好 yml 就能用"的魔法本体。
四、实测输出:同一份代码,加一行 system() 判若两人
以下是 2026-10-05 本机真实运行(DeepSeek 实测,HTTP 200)。先调 /chat:
HTTP 200 我是DeepSeek,一个由深度求索公司创造的AI助手,随时准备用热情细腻的方式帮你解答问题、处理任务!再调 /interview,同一个大模型,同一套代码,只多了.system(...)一行:
HTTP 200 不错,这是个基础但重要的概念。final 关键字在 Java 中表示"最终的、不可改变的",它可以用来修饰变量、方法和类。 既然你提到了 final,那我想追问一下:你能具体说说 final 修饰变量时,对于基本类型和引用类型分别意味着什么吗?看出差别了吗?/chat 是"热情细腻的助手",/interview 变成"先点评一句再追问一道"的面试官——SystemPrompt 的约束力,眼见为实。而且这个输出和 LangChain4j 课第 1 课的玩法二几乎同构,同一业务两套实现,两门课对照着学效率翻倍。
排查提示:如果没看到预期输出——404/端口连不上,看启动日志有没有
Tomcat started on port 8093;401 是 Key 环境变量没生效,检查设置后重启终端;500 多半是编译时maven.compiler.parameters没开(见第八节总结表)。
五、挑战题:改参数,看看会怎样
- ⭐玩温度:yml 里把
temperature改成0.0再调/chat(同一句"用 10 个字介绍你自己"),改成1.5再调一次——对比三次回答的稳定性和发散性。答案在源码的application.yml里,跑出来才知道差距有多大。 - ⭐加一个诗人接口:仿照
/interview加GET /poet?msg=你好,系统提示词换成"你是李白风格的诗人",跑 3 个词看看诗风。答案就在源码ChatController.java——照抄一行.system(...)。 - ⭐⭐切通义:不改任何代码,用环境变量把模型切到通义
qwen-plus($env:LLM_BASE_URL+$env:LLM_MODEL+$env:QWEN_API_KEY),验证"切模型只改配置"。答案在 README 第 4 步,跑通了你就真的吃透了占位符。
六、生产环境进阶:三个加分项
1. Key 不进文件,防泄露红线。api-key 只从环境变量读,实测后 grep 一遍日志和输出,确保没有真实 Key 字样——只允许${DEEPSEEK_API_KEY}占位符出现。
2. max-tokens 控成本。max-tokens: 200是输出上限,长回答会被截断——这是它的教学现场。生产环境按业务调大,同时它也是个天然的"成本刹车"。
3. 切模型三件套。OpenAI =https://api.openai.com/v1+gpt-4o-mini;通义 =https://dashscope.aliyuncs.com/compatible-mode/v1+qwen-plus。三个模型都走 OpenAI 兼容协议,改三处配置就能换供应商,代码零改动——这是 Spring AI 自动装配的隐藏福利。
七、面试回答模板
面试官:Spring AI 的"自动装配"是什么?为什么配好 yml 就能用 AI?
一句话:把大模型客户端做成 Spring 风格的 starter,配好 yml 自动装配成 Bean,注入即用。展开说:starter 进 classpath → 启动时读
spring.ai.*配置 → 框架自动创建 ChatClient 和模型客户端;类比精装房,starter 是家具套餐、yml 是装修意见表。你一行模型代码都不用写。(指向本课第一节 / lesson-01 的 application.yml)
追问:ChatClient 链式 API 每一步在干什么?
prompt()开始拼消息 →.user(msg)塞问题 →.call()阻塞调用 →.content()取文本,system()可插在最前面塞人设。背熟五步,再说一句"call 阻塞 vs stream 流式"是加分项。(指向本课第三节)
追问:Spring AI 和 LangChain4j 最大的工程差异?
Spring AI 靠 yml 自动装配 + 官方生态(VectorStore/Advisor 全家桶),和 Spring 无缝;LangChain4j 靠 Java Bean 手配模型,更轻量、厂商覆盖更广。选型看团队栈:纯 Spring 栈选前者,要多模型灵活切换看后者。(指向本课第一节 + LangChain4j 课第 1 课)
追问:Spring 6 下 @RequestParam 为什么必须开 parameters 编译开关?
Spring 靠反射读方法参数名,JDK 编译默认不保留参数名,不开就 500。pom 里
maven.compiler.parameters=true是 Spring Boot 3 系列通用坑。(指向本课踩坑表)
八、总结表
| 坑 | 现象 | 解法 |
|---|---|---|
| JAVA_HOME 指向 JDK8 | mvn 跑在 Java 8 上 | 构建前$env:JAVA_HOME="C:\Program Files\Java\jdk-17" |
| Spring 6 反射参数名 | @RequestParam 省略名字时 500 | pom 开maven.compiler.parameters=true |
| 中文 URL 参数乱码 | curl 直接带中文 400/乱码 | 用[uri]::EscapeDataString()编码 |
| ChatClient.Builder 注入失败 | 启动报 NoSuchBeanDefinition | 检查 yml 里 api-key 占位符是否配好 |
| 版本不配套 | Spring AI 1.0.9 配 Boot 3.3.x 冲突 | 冻结 Boot 3.5.x,抄本课 pom |
| 端口占用 | Port 8093 was already in use | Get-NetTCPConnection -LocalPort 8093查占用 |
九、关于这个系列
本文是「Java 后端实战精通营」系列第 1 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。
👉Spring AI 实战精通营(10 课):https://gitee.com/j67mk2/spring-ai-journey
- 本文对应源码位置:
lesson-01/(最小 Spring Boot 工程,内含ChatController双接口 +application.yml模型配置)
系列文章一览(按发布顺序):
| 篇 | 主题 |
|---|---|
| 1 | Spring AI 初体验:配好 yml 就能聊,ChatClient 四步链式调用 |
| 2 | Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用 |
| 3 | Spring AI 结构化输出:entity() 把模型回答解析成 JavaBean,别再手撕 JSON |
| 4 | Spring AI 工具调用:@Tool 让大模型自己查订单查库存 |
| 5 | Spring AI 流式输出:Flux + SSE 打字机,回答不再干等三秒 |
| 6 | Spring AI 多模态:给大模型一双眼睛,图片它也能看懂 |
| 7 | Spring AI 向量检索:本地 ONNX 嵌入,文本秒变坐标,知识库零成本起步 |
| 8 | Spring AI RAG 问答助手:回答带引用,AI 不再睁眼说瞎话 |
| 9 | Spring AI Advisor 编排:记忆 + 工具 + RAG 三合一,一个接口全搞定 |
| 10 | Spring AI 企业智能客服:RAG + 工具 + 记忆 + 流式 + 兜底,十课收官 |
下一篇预告:《Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用》——本课的人设提示词是写死在代码里的,下一课把它做成带
{变量}的可复用模板,再塞几个示例(few-shot),你会发现链式 API 越来越"Spring 味"。
跑完有任何报错,把终端输出发评论区,一起排查。
标签建议:SpringAI、大模型、ChatClient
摘要建议(≤256 字):Spring AI 把大模型接入做成了"配数据库"一样的事:改 yml、注入 ChatClient、写个接口就能对话。本文用最小工程跑通第一个 AI 接口,逐行拆解自动装配与链式 API,实测对比 system() 系统提示词的约束力,附 3 道挑战题与面试回答模板,源码在 gitee lesson-01 可 clone 直接跑。