让你的 Spring Boot 应用轻松接入大模型,一文掌握 ChatClient 的核心用法
一、引言
在 AI 与 Java 生态融合的浪潮中,Spring AI 框架为我们提供了一套熟悉的编程模型------就像 Spring 框架中 RestTemplate 或 JdbcTemplate 一样,ChatClient 就是与大模型对话的「标配客户端」。无论你使用的是 OpenAI、DeepSeek 还是其他大模型,ChatClient 都能以统一的方式完成 Prompt 构建、多轮对话、参数调优等任务。
本文将从实际代码出发,带你深入理解 ChatClient 的创建方式、Prompt 构建思路、多轮对话的手动实现、同步/流式调用以及模型参数配置,最后给出一个可直接复制运行的完整 Controller。
二、ChatClient 的创建
2.1 通过 Builder 创建(推荐)

最标准的用法是注入 ChatClient.Builder,然后调用 build() 生成实例。这种方式既简洁,又保留了后续自定义的能力。
@RestController
public class DemoController {
private final ChatClient chatClient;
public DemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
}
2.2 带默认 System Prompt 的 ChatClient

很多时候,我们希望某个 ChatClient 始终携带固定的角色设定(例如永远作为一个面试官、翻译官或代码审查员)。可以在 Builder 阶段通过 defaultSystem 预设:
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient interviewChatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你是一个技术面试官,用提问的方式检验候选人对知识的掌握程度。只出题,不给答案。")
.build();
}
}
2.3 不同业务场景注册不同的 Bean(@Qualifier)
一个应用中通常会有多个不同职责的 ChatClient:客服机器人、面试官、代码助手......Spring 允许你注册多个 Bean,并用 @Qualifier 区分。
@Bean
@Qualifier("interviewer")
public ChatClient interviewer(ChatClient.Builder builder) {
return builder.defaultSystem("你是面试官,只问技术问题,不解释答案。").build();
}
@Bean
@Qualifier("translator")
public ChatClient translator(ChatClient.Builder builder) {
return builder.defaultSystem("你是一个翻译官,将用户输入翻译成英文。").build();
}
使用时:
@Autowired
@Qualifier("interviewer")
private ChatClient interviewer;
三、Prompt 构建:user、system、messages 三种姿势

我们用一个统一的"出面试题"场景,对比三种写法的实际效果。
假设用户输入"JVM"这个词,下面三个接口会给出截然不同的回答:
@RestController
@RequestMapping("/prompt-demo")
public class PromptDemoController {
private final ChatClient chatClient;
public PromptDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
// ① 只有 user 消息 ------ 没有任何约束,模型自由发挥
@GetMapping("/simple")
public String simple(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
// ② user + system 消息 ------ system 固定角色,输出风格稳定
@GetMapping("/with-system")
public String withSystem(@RequestParam String message) {
return chatClient.prompt()
.system("你是一个面试官,用提问的方式检验候选人对知识的掌握程度。只出题,不给答案。")
.user(message)
.call()
.content();
}
// ③ 动态模板变量 ------ 同一套模板,通过参数控制输出方向
@GetMapping("/template")
public String template(
@RequestParam String topic,
@RequestParam(defaultValue = "中级") String difficulty) {
return chatClient.prompt()
.user(u -> u.text("请出一道关于 {topic} 的 {difficulty} 难度 Java 面试题,只出题,不给答案。")
.param("topic", topic)
.param("difficulty", difficulty))
.call()
.content();
}
}
效果对比(以输入"JVM"为例):
| 接口 | 典型回答 |
|---|---|
/simple?message=JVM |
"JVM是Java虚拟机,它负责将字节码解释或编译为机器码。主要组成部分包括类加载器、运行时数据区、执行引擎......" (可能是一段概念解释) |
/with-system?message=JVM |
"请解释一下JVM的内存模型。年轻代和老年代分别存储什么对象?" (必然是以提问形式出现,符合面试官角色) |
/template?topic=JVM&difficulty=高级 |
"请分析G1垃圾回收器中Mixed GC的触发条件和Region划分策略。" (严格按主题和难度出题) |
结论 :system 消息是控制模型角色和输出风格的开关;模板变量可以让一个接口覆盖海量组合,非常适用于产品化场景。
四、多轮对话与消息列表手动构造

4.1 为什么要手动构造消息列表?
大模型本身是无状态的------每次调用都是一次全新的请求,它完全不记得上一轮说了什么。如果你问"它和 Spring 框架有什么区别",模型根本不知道"它"指的是谁。
让模型"记住"对话的方法只有一个:把历史对话一起发过去。
假设第一轮对话是:
用户:什么是 Spring Boot
助手:Spring Boot 是基于 Spring 的快速开发框架......
第二轮用户接着问:"它和 Spring 框架有什么区别?" 模型要理解"它"指的是 Spring Boot,就必须在第二轮的请求中包含第一轮的完整内容:
[system: 你是Java助手]
[user: 什么是 Spring Boot]
[assistant: Spring Boot 是......]
[user: 它和 Spring 框架有什么区别]
4.2 消息列表中的三种角色
-
SystemMessage:给模型的"幕后指令",用户不可见,用于设定角色和规则。 -
UserMessage:用户发送的消息。 -
AssistantMessage:模型上一轮的回复(构造历史时使用)。
4.3 代码示例:手动构造历史消息
@RestController
@RequestMapping("/messages-demo")
public class MessagesDemoController {
private final ChatClient chatClient;
public MessagesDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@PostMapping
public String chat(@RequestBody ChatHistoryRequest request) {
List<Message> messages = List.of(
new SystemMessage("你是一个 Java 技术助手"),
new UserMessage("什么是 Spring Boot"), // 第一轮用户问题
new AssistantMessage(request.lastAssistantReply()), // 第一轮模型回答
new UserMessage(request.currentQuestion()) // 第二轮用户问题
);
return chatClient.prompt()
.messages(messages)
.call()
.content();
}
record ChatHistoryRequest(String lastAssistantReply, String currentQuestion) {}
}
4.4 测试对比:有历史 vs 无历史
# ✅ 有历史 ------ 模型知道"它"指的是 Spring Boot
curl -X POST http://localhost:8080/messages-demo \
-H "Content-Type: application/json" \
-d '{
"lastAssistantReply": "Spring Boot 是基于 Spring 的快速开发框架,通过自动配置简化了项目搭建",
"currentQuestion": "它和 Spring 框架有什么区别"
}'
# 回答:"Spring Boot 是 Spring 框架的扩展,它通过自动配置和starter依赖简化了开发,而Spring框架提供核心的IoC和AOP等基础能力......"
# ❌ 没有历史 ------ 模型一头雾水
curl "http://localhost:8080/prompt-demo/simple?message=它和Spring框架有什么区别"
# 回答:"您提到的'它'指的是什么?请提供更多上下文。" 或者给一个完全无关的回答
💡 实用提示 :本节只是为了演示底层原理。在实际项目中,你完全不必手动管理这些消息列表------后面会介绍的
ChatMemory会自动帮你维护历史消息,存储、截断、注入全部自动处理。
五、调用方式、流式输出:call vs stream

ChatClient 提供两种调用模式:同步 call 和 流式 stream。
@RestController
@RequestMapping("/call-demo")
public class CallDemoController {
private final ChatClient chatClient;
public CallDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
// ① 同步调用,只拿文本内容
@GetMapping("/sync")
public String sync(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
// ② 同步调用,拿完整响应(含 Token 用量)
@GetMapping("/token-usage")
public TokenUsageResponse tokenUsage(@RequestParam String message) {
ChatResponse response = chatClient.prompt()
.user(message)
.call()
.chatResponse();
Usage usage = response.getMetadata().getUsage();
return new TokenUsageResponse(
response.getResult().getOutput().getText(),
usage.getPromptTokens(),
usage.getCompletionTokens(),
usage.getTotalTokens()
);
}
// ③ 流式调用 ------ 返回 Flux<String>,适合 SSE 打字机效果
@GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> stream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
record TokenUsageResponse(String content, Integer inputTokens,
Integer outputTokens, Integer totalTokens) {}
}
⚠️ 注意:流式输出一定要加上 produces = "text/event-stream;charset=UTF-8"
六、模型参数配置

6.1 在配置文件中设置默认参数
spring:
ai:
openai:
chat:
options:
model: deepseek-chat
temperature: 0.7 # 创意度,0~2,越高越随机
max-tokens: 2048
top-p: 1.0
6.2 在代码中动态覆盖参数
不同业务场景可能需要不同生成风格(创意写作 vs 代码生成),可以在每次调用时覆盖参数。
@RestController
@RequestMapping("/options-demo")
public class OptionsDemoController {
private final ChatClient chatClient;
public OptionsDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
// 创意模式:高 temperature,适合写作、头脑风暴
@GetMapping("/creative")
public String creative(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.options(OpenAiChatOptions.builder()
.temperature(1.5)
.maxTokens(500)
.build())
.call()
.content();
}
// 精确模式:低 temperature,适合代码生成、数据提取
@GetMapping("/precise")
public String precise(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.options(OpenAiChatOptions.builder()
.temperature(0.1)
.maxTokens(1000)
.build())
.call()
.content();
}
// 厂商无关写法:使用通用 ChatOptions(不依赖 OpenAI 具体实现)
@GetMapping("/generic")
public String generic(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.options(ChatOptions.builder()
.temperature(0.8)
.maxTokens(1000)
.build())
.call()
.content();
}
}
七、实战:直接可运行的完整 Controller

下面是一个整合了所有知识点的 ChatController,复制到你的 Spring AI 项目中即可直接使用。
package com.jichi.springai.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.List;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个专业的 Java 技术助手,回答简洁准确,代码示例使用 Java 21 语法。")
.build();
}
/**
* 基础对话
* GET /api/chat?message=什么是Spring Boot
*/
@GetMapping
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
/**
* 临时覆盖 System Prompt
* POST /api/chat/with-role
* {"systemPrompt":"你是诗人","userMessage":"写首诗"}
*/
@PostMapping("/with-role")
public String chatWithRole(@RequestBody ChatRequest request) {
return chatClient.prompt()
.system(request.systemPrompt())
.user(request.userMessage())
.call()
.content();
}
/**
* 模板变量替换
* GET /api/chat/template?topic=JVM&difficulty=高级
*/
@GetMapping("/template")
public String chatWithTemplate(
@RequestParam String topic,
@RequestParam(defaultValue = "中级") String difficulty) {
return chatClient.prompt()
.user(u -> u.text("请出一道关于 {topic} 的 {difficulty} 难度面试题")
.param("topic", topic)
.param("difficulty", difficulty))
.call()
.content();
}
/**
* 返回完整响应(含 Token 用量)
* GET /api/chat/detail?message=你好
*/
@GetMapping("/detail")
public ChatDetailResponse chatDetail(@RequestParam String message) {
ChatResponse response = chatClient.prompt()
.user(message)
.call()
.chatResponse();
return new ChatDetailResponse(
response.getResult().getOutput().getText(),
response.getMetadata().getUsage().getTotalTokens()
);
}
/**
* 创意模式(高 temperature)
* GET /api/chat/creative?message=给我起个公司名
*/
@GetMapping("/creative")
public String creativeChat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.options(OpenAiChatOptions.builder()
.temperature(1.2)
.maxTokens(500)
.build())
.call()
.content();
}
/**
* 流式输出(打字机效果)
* GET /api/chat/stream?message=写首诗
*/
@GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
/**
* 手动构造多轮消息(演示底层用法,实际项目用 ChatMemory)
* POST /api/chat/history
*/
@PostMapping("/history")
public String chatWithHistory(@RequestBody HistoryRequest request) {
List<Message> messages = List.of(
new SystemMessage("你是一个 Java 技术助手"),
new UserMessage(request.previousQuestion()),
new AssistantMessage(request.previousAnswer()),
new UserMessage(request.currentQuestion())
);
return chatClient.prompt()
.messages(messages)
.call()
.content();
}
// DTO
record ChatRequest(String systemPrompt, String userMessage) {}
record ChatDetailResponse(String content, Long totalTokens) {}
record HistoryRequest(String previousQuestion, String previousAnswer, String currentQuestion) {}
}
启动应用后,可使用以下命令快速测试:
# 基础对话
curl "http://localhost:8080/api/chat?message=什么是JVM"
# 创意模式
curl "http://localhost:8080/api/chat/creative?message=给我起个有意思的项目名"
# 模板变量
curl "http://localhost:8080/api/chat/template?topic=Redis&difficulty=高级"
# 带 Token 用量
curl "http://localhost:8080/api/chat/detail?message=你好"
# 流式输出
curl -N "http://localhost:8080/api/chat/stream?message=写一首关于代码的诗"