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 SUCCESS
mvn 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 = 8093
spring:
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-chat
max-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——两个接口,总共没几行。
package com.springai.lesson01;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 聊天控制器:本课的 HTTP 入口。
* ChatClient 是 Spring AI 的核心门面(就像 JdbcTemplate 之于数据库),
* 由 starter 自动装配——只要 yml 配好模型,注入就能用,一个模型 Bean 都不用写。
*/
@RestController
public class ChatController {
/**
* ChatClient 实例:通过 Builder 构建(Spring AI 推荐用法)。
* Builder 由 starter 自动装配,背后是 yml 里 spring.ai.openai.* 配置。
*/
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 通用问答:http://localhost:8093/chat?msg=你好
*
* prompt() = 开始拼"发给模型的消息";user(msg) = 塞用户问题;
* call() = 阻塞式调用(等模型答完才返回,流式是第 5 课的事);
* content() = 取出回答文本。
*/
@GetMapping("/chat")
public String chat(@RequestParam("msg") String msg) {
return chatClient.prompt()
.user(msg)
.call()
.content();
}
/**
* 角色扮演:http://localhost:8093/interview?msg=什么是final
*
* system(...) 在用户问题之前塞一段"系统提示词"(人设说明书)。
* 对比 /chat 的通用助手口吻,体会 SystemPrompt 的作用。
*/
@GetMapping("/interview")
public String interview(@RequestParam("msg") String msg) {
return chatClient.prompt()
.system("你是资深 Java 技术面试官,语气专业但友好。"
+ "每次回答先用一句话点评候选人的回答,再追问一个更深入的问题。")
.user(msg)
.call()
.content();
}
}
第三段:Lesson01Application.java——标准启动类,三行搞定。
package com.springai.lesson01;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring AI 实战精通营 · 第 1 课:第一个 AI 接口。
* 启动后访问:
* GET http://localhost:8093/chat?msg=你好 —— 通用助手问答
* GET http://localhost:8093/interview?msg=什么是final —— 角色扮演(系统提示词)
*/
@SpringBootApplication
public class Lesson01Application {
public static void main(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 课):gitee.com/j67mk2/spri…
- 本文对应源码位置:
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 直接跑。