Spring AI ChatClient 完全指南:从基础配置到流式调用

让你的 Spring Boot 应用轻松接入大模型,一文掌握 ChatClient 的核心用法

一、引言

在 AI 与 Java 生态融合的浪潮中,Spring AI 框架为我们提供了一套熟悉的编程模型------就像 Spring 框架中 RestTemplateJdbcTemplate 一样,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=写一首关于代码的诗"
相关推荐
Aaron15885 小时前
RFSOC+VU13P+GPU 在6G互联网中的技术应用
大数据·人工智能·算法·fpga开发·硬件工程·信息与通信·信号处理
Raink老师5 小时前
【AI面试临阵磨枪-31】Agent 反思(Reflection)机制如何实现?作用是什么?
人工智能·ai 面试
安卓程序员_谢伟光5 小时前
如何使用ai开发
人工智能
这张生成的图像能检测吗5 小时前
(论文速读)让机器人像人一样走路:注意力机制如何让腿足机器人征服复杂地形
人工智能·深度学习·计算机视觉·机器人控制
一切皆是因缘际会5 小时前
预制式制衡智能:大模型瓶颈下的 AI 迭代新思路
人工智能·安全·ai·架构
l1t5 小时前
类似 X-13ARIMA-SEATS 功能的 JDemetra+ 安装和使用
java·数据库·r语言
架构源启5 小时前
2026 进阶篇:深入理解Spring Reactor响应式编程的核心引擎(源码级解析+实战避坑)
java·后端·spring
薪火铺子6 小时前
SpringMVC请求处理流程源码解析(第2篇):处理器执行与参数绑定
java·后端·spring
SamDeepThinking6 小时前
一个跑了三年没出过问题的系统,我是怎么设计的
java·后端·架构