


个人主页:手握风云
目录
[1.1. 实现简单对话](#1.1. 实现简单对话)
[1.2. 角色预设](#1.2. 角色预设)
[1.3. 结构化输出](#1.3. 结构化输出)
[1.4. 实现流式输出](#1.4. 实现流式输出)
[1.5. 打印日志](#1.5. 打印日志)
[2.1. SSE 协议介绍](#2.1. SSE 协议介绍)
[2.2. Spring 中 SSE 实现](#2.2. Spring 中 SSE 实现)
[3.1. 概述](#3.1. 概述)
[3.2. 实现简单对话](#3.2. 实现简单对话)
[3.3. 角色预设](#3.3. 角色预设)
[3.4. 实现流式输出](#3.4. 实现流式输出)
[3.5. ChatModel 与 ChatClient 的核心区别](#3.5. ChatModel 与 ChatClient 的核心区别)
一、ChatClient
ChatClient 是 Spring AI 框架中封装了复杂交互流程的高阶 API 接口,旨在简化开发者与大语言模型的集成过程。它提供了一套极具表现力的流式(Fluent)API,支持同步和响应式编程模型,能让你开箱即用,避免处理底层组装 Prompt 的繁琐逻辑。
1.1. 实现简单对话
最基础的用法就是一问一答。我们只需要传入用户的输入内容,调用 call() 方法发送请求,并通过 content() 提取返回的字符串即可:
java
package com.yang.ai.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/client")
public class ChatClientController {
private final ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@GetMapping("/call")
public String generation(String userInput) {
return this.chatClient.prompt()
// 用户输入的信息
.user(userInput)
// 请求大模型
.call()
// 返回文本
.content();
}
}
访问接口:http://127.0.0.1:8080/client/call?userInput=你是谁?
1.2. 角色预设
如果你想给你的 AI 助手设定一个"人设"(比如一个专属的编码助手),可以通过 ChatClient.Builder 中的 defaultSystem() 方法来配置默认的系统消息。这样配置后,这段文本会作为基础角色设定,注入到每次对话的上下文中:
java
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.
defaultSystem("你叫编码助手,擅长 Java 语法和集合框架。").
build();
}
我们依然访问接口:http://127.0.0.1:8080/client/call?userInput=你是谁?

1.3. 结构化输出
在业务开发中,我们通常需要大模型返回 JSON 或特定的数据结构,而不是纯文本。借助 entity() 方法和 JDK 16 引入的 record,可以轻松实现结构化转换:
java
package com.yang.ai.configuration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfiguration {
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
return chatClientBuilder
.build();
}
}
java
// 1. 定义实体类
record Recipe(String dish, List<String> ingredients) {
}
// 2. 调用模型并映射
@RequestMapping("/entity")
public String entity(String userInput) {
Recipe recipe = chatClient.prompt()
.user(String.format("请帮我生成%s的食谱", userInput))
.call()
.entity(Recipe.class); // 自动将模型输出转为自定义实体
return recipe.toString();
}

1.4. 实现流式输出
用户和大模型进⾏交互时,由于大模型⼀次输出内容较多,等待全部内容⽣成完毕会导致用户等待时间过长,这对用户的体验⾮常不友好。可以采用流式输出的方式。ChatClient 作为高阶 API,通过链式调用封装了流式请求的所有细节,核心是用 stream() 替代同步调用的 call(),直接返回 Flux<String>,无需手动解析响应。
java
// 防止出现乱码,采用 UTF-8
@GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.stream()
.content();
}
1.5. 打印日志
Advisors(拦截/增强器): 它是介于用户请求与 AI 模型之间的中间件组件。其核心功能借鉴了 Spring AOP 的思想,以链式结构运行,能够对请求进行拦截、过滤和增强(例如在调用 API 前后处理参数和结果、敏感词过滤、管理对话上下文等)。
SimpleLoggerAdvisor: 它是 Spring AI 内置的 Advisor 之一,专门用于记录日志。只需将其添加到 Advisor 链中,它就能自动记录所有经过该 Advisor 的聊天请求(Request)和响应(Response)。

开发人员可以通过两种方式配置日志增强器:1. 全局配置,默认作用于所有对话,在使用 ChatClient.Builder 构建客户端时,通过 defaultAdvisors() 方法全局挂载。这样设置后,该 Advisor 会作用于 ChatClient 发起的每一次对话。2. 请求级别配置(作用于单次对话): 可以在发起具体的请求时通过 .advisors() 单独配置。注意: 如果在全局和单次对话中都设置了同一类型的 Advisor,单次对话(请求级别)的优先级高于全局默认配置。
由于 SimpleLoggerAdvisor 的日志默认是以 Debug 级别输出的,因此必须在 Spring Boot 的配置文件中调整对应包的日志打印级别:
java
logging:
level:
org.springframework.ai.chat.client.advisor: debug
java
package com.yang.ai.configuration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfiguration {
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
return chatClientBuilder
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}

二、流式编程
2.1. SSE 协议介绍
SSE(Server-Sent Events,服务器发送事件)是一种基于 HTTP 的轻量级实时通信协议,主要用于解决传统 HTTP 无状态且无法由服务器主动推送消息的问题,从而实现流式传输(Streaming)。
核心运行机制与特点:
- 基于 HTTP 协议:它直接复用标准的 HTTP/HTTPS 协议进行通信,不需要引入额外的端口或复杂的协议,因此兼容性极好且易于部署。
- 单向数据推送 :SSE 采用的是服务器向客户端的单向通信机制。一旦客户端通过普通 HTTP 请求建立了连接,服务器就可以在这个通道上持续不断地推送数据流,但客户端无法通过该连接向服务器反向发送数据。
- 持久连接不断开:服务器会在响应时声明发送的是流消息,此时客户端收到声明后不会主动关闭连接,而是保持连接开放,一直等待接收新的数据流。
- 自动重连机制:当网络或连接意外中断时,浏览器具备自动断线重连的能力,服务器还可以通过特殊的字段指定重连的时间间隔。
要成功实现 SSE,服务端必须遵循特定的数据格式标准:服务端在响应时,必须设置 Content-Type: text/event-stream;charset=utf-8,以此告知客户端这是一个事件流。消息组装与分隔:每次发送内容由若干 message 组成,不同 message 间用 \n\n 严格分隔。每个 message 内包含若干行,每行标准格式为 [field]: value\n 。支持的字段:data [必需]:实际推送的数据内容。event [非必需]:表示自定义的事件类型(默认情况下是 message 事件)。id [非必需]:数据的唯一标识符,类似于数据的编号。retry [非必需]:用于告诉浏览器在断开连接后,重新发起连接需要等待的时间间隔。
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: end\n
data: a bar event\n\n
客户端 API 的处理:在前端浏览器中,处理 SSE 数据流非常简便,主要依赖于浏览器内置的 EventSource API。 开发者只需实例化一个 EventSource 对象并指向服务端的流接口,然后通过挂载 onmessage 事件(或监听自定义的 event 事件),即可实时获取 event.data 中的数据并渲染到页面上。
服务端代码实现:
java
package com.yang.ai.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
@RestController
@RequestMapping("/sse")
public class SseController {
@RequestMapping("/data")
public void data(HttpServletResponse response) throws IOException, InterruptedException {
response.setContentType("text/event-stream;charset=utf-8");
PrintWriter writer = response.getWriter();
for (int i = 0; i < 20; i++) {
String s = "data: " + new Date() + "\n\n";
writer.write(s);
writer.flush();
Thread.sleep(1000L);
}
}
}
客户端 API:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE</title>
</head>
<body>
<div id="sse"></div>
<script>
let eventSource = new EventSource('/sse/data');
eventSource.onmessage = function(event) {
document.getElementById('sse').innerHTML += event.data + '<br/>';
}
</script>
</body>
</html>

java
@RequestMapping("/retry")
public void retry(HttpServletResponse response) throws IOException {
log.info("发起请求: retry");
response.setContentType("text/event-stream;charset=utf-8");
PrintWriter writer = response.getWriter();
String s = "retry: 2000\n";
s += "data: " + new Date() + "\n\n";
writer.write(s);
writer.flush();
}
html
<div id="sse"></div>
<script>
let eventSource = new EventSource('/sse/retry');
eventSource.onmessage = function(event) {
document.getElementById('sse').innerHTML += event.data + '<br/>';
}
eventSource.onmessage
</script>

java
@RequestMapping("/event")
public void event(HttpServletResponse response) throws IOException, InterruptedException {
log.info("发起请求: event");
response.setContentType("text/event-stream;charset=utf-8");
PrintWriter writer = response.getWriter();
for (int i = 0; i < 10; i++) {
String s = "event: foo\n";
s += "data: " + new Date() + "\n\n";
writer.write(s);
writer.flush();
Thread.sleep(1000L);
}
}
html
<script>
let eventSource = new EventSource('/sse/event');
eventSource.addEventListener("foo", (event) => {
document.getElementById('sse').innerHTML += event.data + '<br/>';
})
</script>

2.2. Spring 中 SSE 实现
Spring 从 4.2 版本开始就已经支持 SSE。从 Spring 5 开始,推荐使用 WebFlux 框架来更优雅地实现 SSE 协议,其核心 API 是 Flux。
在 WebFlux 中,可以将 Flux 想象成一条数据传送带,它具备以下三大核心特点:
- 异步传送:数据就像快递包裹一样逐个到达,接收方不需要等待所有数据全部到齐即可开始处理。
- 灵活加工:支持在中途通过操作符对数据进行修改,如过滤、转换等。
- 弹性控制:接收方可以通过"背压机制"来调控数据的接收速度。

使用 Flux 进行流式编程通常遵循以下固定流程:
- 创建 Flux:建立一个 Flux 数据流并指定其数据源。
- 处理数据:调用相关的操作符(如 map 等)对流转中的数据进行加工。
- 订阅数据:通过 subscribe() 订阅 Flux 来消费数据。注意: 只有触发订阅动作后,数据才会真正开始流动。


以下是 Flux 流中常见操作符:
| 操作符 | 作用 | 示例代码片段 |
|---|---|---|
map() |
元素一对一转换 | .map(String::toUpperCase) |
filter() |
条件过滤 | .filter(s -> s.length() > 5) |
take() |
限制元素数量 | .take(2) |
merge() |
合并多个 Flux (不保证顺序) | Flux.merge(Flux.just("A"), Flux.just("B")) |
concat() |
顺序拼接多个 Flux (保证顺序) | Flux.concat(Flux.just("A"), Flux.just("B")) |
delayElements() |
延迟元素发射 | .delayElements(Duration.ofSeconds(1)) |
java
@RequestMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.interval(Duration.ofSeconds(1)).map(s -> new Date().toString());
}
html
<script>
let eventSource = new EventSource('/sse/stream');
eventSource.onmessage = function(event) {
document.getElementById('sse').innerHTML += event.data + '<br/>';
}
</script>

三、ChatModel
3.1. 概述
java
package com.yang.ai.controller;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/ds")
public class DeepSeekChatModel {
@Autowired
private OpenAiChatModel chatModel;
@GetMapping("/chat")
public String chat(String message) {
return chatModel.call(message);
}
}
ChatModel 抽象了应用程序与 AI 模型交互的过程。它的工作原理是接收 Prompt(提示词)作为输入,将请求发送给后端大模型,并返回 ChatResponse 对象作为输出。虽然它提供了一个简单的 String call(String message) 方法,但这个方法本质上是对传入的字符串进行了封装,底层依然是调用核心的 ChatResponse call(Prompt prompt) 方法。这样设计是为了简化初学者的使用,避免处理复杂的输入输出解析。
以下是 ChatModel 的定义:
java
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {
default String call(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
Generation generation = this.call(prompt).getResult();
return generation != null ? generation.getOutput().getText() : "";
}
default String call(Message... messages) {
Prompt prompt = new Prompt(Arrays.asList(messages));
Generation generation = this.call(prompt).getResult();
return generation != null ? generation.getOutput().getText() : "";
}
......
}

3.2. 实现简单对话
如果不使用简化的 call(String) 方法,开发者需要手动构建 Prompt 对象来进行交互。通过 new Prompt(message) 将用户文本包装为提示词对象,调用 call() 后,从返回的 ChatResponse 对象中层层解析获取纯文本结果。
java
@GetMapping("/chatByPrompt")
public String chatByPrompt(String message) {
Prompt prompt = new Prompt(message);
ChatResponse response = chatModel.call(prompt);
return response
.getResult()
.getOutput()
.getText();
}
测试接口:http://127.0.0.1:8080/ds/chatByPrompt?message=如果让奥格威给特斯拉汽车写广告语,他会怎么写?
3.3. 角色预设
ChatModel 支持通过 Prompt 来预设角色,这是引导大模型输出特定风格或专业内容的核心手段。首先需要分别创建 SystemMessage 和 UserMessage 。将这两种消息组合成一个 List,并封装进 Prompt 中传递给 ChatModel 执行。模型会结合整个对话历史(包括系统指令)生成符合上下文的连贯回复。
java
@GetMapping("/role")
public String role(String message) {
SystemMessage systemMessage = new SystemMessage("你叫编码助手,擅长 Java 语法和集合框架。");
UserMessage userMessage = new UserMessage(message);
Prompt prompt = new Prompt(systemMessage, userMessage);
ChatResponse response = chatModel.call(prompt);
return response
.getResult()
.getOutput()
.getText();
}
测试接口:http://127.0.0.1:8080/ds/role?message=你是谁?

3.4. 实现流式输出
为了解决大段文本生成导致的长时间等待问题,提升用户体验,ChatModel 提供了流式输出能力。
java
@RequestMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String message) {
Prompt prompt = new Prompt(message);
Flux<ChatResponse> response = chatModel.stream(prompt);
return response.map(x -> x.getResult().getOutput().getText());
}
测试接口:http://127.0.0.1:8080/ds/stream?message=你是谁?

3.5. ChatModel 与 ChatClient 的核心区别
这两个接口是 Spring AI 与大模型交互的两大基石,但设计理念和适用场景不同:
- ChatModel(底层与灵活):它是底层的通信接口,直接与大模型(如通义千问、OpenAI 等)交互。它只提供基础的 call 和 stream 方法,开发者需要手动处理提示词组装、参数配置和响应解析等细节。优势在于高度灵活,适合深度定制。
- ChatClient(高阶与高效):它是基于 ChatModel 进行的封装和增强。通过 Fluent API(流式 API),它屏蔽了底层复杂的交互细节,自动集成了提示词管理、响应格式化、结构化输出映射(如自动转为实体类)等能力,能大幅提高日常业务开发效率。