Spring AI 初体验:配好 yml 就能聊,ChatClient 四步链式调用

0 阅读6分钟

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 四步(面试必背):

步骤代码干什么
1prompt()开始拼消息(相当于打开对话框)
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 没开(见第八节总结表)。

五、挑战题:改参数,看看会怎样

  1. ⭐ 玩温度:yml 里把 temperature 改成 0.0 再调 /chat(同一句"用 10 个字介绍你自己"),改成 1.5 再调一次——对比三次回答的稳定性和发散性。答案在源码的 application.yml 里,跑出来才知道差距有多大。
  2. ⭐ 加一个诗人接口:仿照 /interview 加 GET /poet?msg=你好,系统提示词换成"你是李白风格的诗人",跑 3 个词看看诗风。答案就在源码 ChatController.java——照抄一行 .system(...)。
  3. ⭐⭐ 切通义:不改任何代码,用环境变量把模型切到通义 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 指向 JDK8mvn 跑在 Java 8 上构建前 $env:JAVA_HOME="C:\Program Files\Java\jdk-17"
Spring 6 反射参数名@RequestParam 省略名字时 500pom 开 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 useGet-NetTCPConnection -LocalPort 8093 查占用

九、关于这个系列

本文是「Java 后端实战精通营」系列第 1 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。

👉 Spring AI 实战精通营(10 课):gitee.com/j67mk2/spri…

  • 本文对应源码位置:lesson-01/(最小 Spring Boot 工程,内含 ChatController 双接口 + application.yml 模型配置)

系列文章一览(按发布顺序):

篇主题
1Spring AI 初体验:配好 yml 就能聊,ChatClient 四步链式调用
2Spring AI 提示词模板:{变量} 参数化 + few-shot,一条提示词反复用
3Spring AI 结构化输出:entity() 把模型回答解析成 JavaBean,别再手撕 JSON
4Spring AI 工具调用:@Tool 让大模型自己查订单查库存
5Spring AI 流式输出:Flux + SSE 打字机,回答不再干等三秒
6Spring AI 多模态:给大模型一双眼睛,图片它也能看懂
7Spring AI 向量检索:本地 ONNX 嵌入,文本秒变坐标,知识库零成本起步
8Spring AI RAG 问答助手:回答带引用,AI 不再睁眼说瞎话
9Spring AI Advisor 编排:记忆 + 工具 + RAG 三合一,一个接口全搞定
10Spring 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 直接跑。