SpringAI2-Spring AI-聊天模型:ChatClient,流式编程,ChatModel
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. Spring AI-聊天模型
- 1. ChatClient
- 1.1 实现简单对话
- 1.2 角色预设
- 1.3 结构化输出
- 1.4 实现流式输出
- 1.5 打印日志
- 1.5.1 Advisors
- 1.5.2 SimpleLoggerAdvisor
- 1.5.2.1 添加 SimpleLoggerAdvisor 到 Advisor链中
- 1.5.2.2 配置⽇志级别
- 1.5.2.3 测试
- 2. 流式编程
- 2.1 SSE协议介绍
- 2.1.1 核⼼特点
- 2.1.2 数据格式
- 2.2.3 服务端实现data
- 2.2.4 客户端实现
- 2.2.5 retry
- 2.2.6 自定义事件
- 2.6.7 结束自动重连
- 2.2 Spring 中SSE实现
- 2.2.1 常⻅的操作符
- 2.2.2 流式响应接⼝
- 3. ChatModel
- 3.1 实现简单对话
- 3.2 ⻆⾊预设
- 3.3 实现流式输出
- 3.4 ChatClient 和ChatModel区别
- 总结
前言
1. Spring AI-聊天模型
Spring AI 的聊天模型, 通过标准化的接⼝设计ChatModel, 使开发⼈员可以将AI模型的聊天功能集成到应⽤程序中.
它利⽤预先训练的语⾔模型, 例如 GPT (Generative Pre-trained Transformer), 以⾃然语⾔⽣成类似⼈类的响应.
API 的⼯作原理通常是向 AI 模型发送提⽰或部分对话, 然后 AI 模型根据其训练数据和对⾃然语⾔模式的理解⽣成响应. 然后, 把响应将返回给应⽤程序, 应⽤程序可以将其呈现给⽤⼾或将其⽤于进⼀步处理.
在Spring AI框架中, ChatModel和ChatClient是构建对话式AI应⽤的两⼤核⼼接⼝. 上⾯我们使⽤了ChatModel完成了与AI模型的交互, 接下来我们对这两个接⼝分别进⾏介绍.
ChatClient 基于ChatModel实现,ChatModel更加底层
1. ChatClient
ChatClient 是 Spring AI 框架中封装复杂交互流程的⾼阶 API 接⼝, 旨在简化开发者与⼤语⾔模型 (如GPT、通义千问等) 的集成过程. ChatClient 提供了与 AI 模型通信的 Fluent API, 它⽀持同步和反应式 (Reactive) 编程模型, 将与 LLM 及其他组件交互的复杂性进⾏封装, 给⽤⼾提供开箱即⽤的服务.
链式调用,可读性更强
ChatClient Fluent API
直接复制官方例子
1.1 实现简单对话
@RestController
@RequestMapping("/chat")
class ChatClientController {private final ChatClient chatClient;public ChatClientController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}@GetMapping("/call")String generation(String userInput) {return this.chatClient.prompt().user(userInput).call().content();}
}
user就是用户输入的提示词
call是调用大模型
content是返回响应
我们用的都是deepseek

1.2 角色预设
现在很多ai都接入了deepseek,比如百度AI,你问他是谁


比如问小白模型—》角色就是问小白
@RestController
@RequestMapping("/chat")
class ChatClientController {private final ChatClient chatClient;public ChatClientController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder
// 设置系统提示词,针对大模型,每次用于请求都会生效这个系统提示词.defaultSystem("你是lyx,是由ck研发的一款智能AI助手,你很擅长医学研究,请以友好的态度来回答问题").build();}@GetMapping("/call")String generation(String userInput) {return this.chatClient.prompt().user(userInput).call().content();}
}
我们现在再问它你是谁

这个设置只针对ChatClient生效,以前设置的OpenAiChatModel不生效,不叫lyx,还叫deepseek
怎么全局生效呢,我们可以注入spring
@Configuration
public class ChatClientConfiguration {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder) {return chatClientBuilder
// 设置系统提示词,针对大模型,每次用于请求都会生效这个系统提示词.defaultSystem("你是lyx,是由ck研发的一款智能AI助手,你很擅长医学研究,请以友好的态度来回答问题").build();}
}
@Autowiredprivate ChatClient chatClient;
然后其他地方注入就可以了

1.3 结构化输出
通过 entity() ⽅法将模型输出转为⾃定义实体, 需确保输出格式符合JSON规范.
借助JDK16提供的新关键词 record 来定义⼀个实体类
dish和ingredients 就是这个Recipe类的属性
record Recipe(String dish, List<String> ingredients ){}@RequestMapping("entity")public String entity(String userInput){Recipe recipe = chatClient.prompt().user(String.format("请帮我生成%s的食谱",userInput)).call().entity(Recipe.class);return recipe.toString();}
这样的话,AI就把我们要生成的数据都分装到Recipe这个类中了,然后返回这个类

所以我们发现SpringAI非常智能,就是我们没有告诉ingredients 这个属性是什么意思,它都会自己分析出来这个属性是什么意思,而且还转换成了这个类型
但是有一个问题就是,因为大模型要计算,所以每次输出我都要等很久
但是我们平时使用的大模型,都是一边输出一边进行计算的,而不是像我们这个一样,计算完了才输出
一边输出一边进行计算的:流式输出
1.4 实现流式输出
⽤⼾和⼤模型进⾏交互时, 由于⼤模型⼀次输出内容较多, 等待全部内容⽣成完毕会导致⽤⼾等待时间过⻓, 这对⽤⼾的体验⾮常不友好. 可以采⽤流式输出的⽅式(例如ChatGPT和DeepSeek逐字显⽰回答)
⼤模型流式输出(Streaming Output)是通过逐步⽣成内容⽽⾮⼀次性返回完整结果的技术. Spring AI使⽤ ChatClient 的 stream() ⽅法⽣成 Flux 流, 适⽤于需要更轻量级客⼾端控制的场景
@GetMapping("/stream")Flux<String> stream(String userInput) {return this.chatClient.prompt().user(userInput).stream()//调用AI,流式返回.content();}
就是call方法换为stream,然后返回结果换为流式的

发现确实是一个字一个字的蹦出来的
但是是乱码呢,因为返回的是中文
@GetMapping(value = "/stream",produces = "text/html;charset=utf-8")Flux<String> stream(String userInput) {return this.chatClient.prompt().user(userInput).stream()//调用AI,流式返回.content();}
produces 就是设置返回的格式,和字符样式utf-8

1.5 打印日志
1.5.1 Advisors
Spring AI 借助Advisors 来实现⽇志打印的功能.
Spring AI中的Advisors是介于⽤⼾请求与AI模型之间的中间件组件, 它的核⼼功能就是对请求进⾏拦截过滤和增强, 帮助我们在API调⽤前后解决各种问题, 例如调⽤前参数如何构建, 调⽤后结果如何处理.
Spring AI 中的 Advisors 是基于 AOP思想实现的, 在具体实现上进⾏了领域适配. 其设计核⼼借鉴了Spring AOP 的拦截机制, 各个Advisor以链式结构运⾏, 序列中的每个Advisor都有机会对传⼊的请求和传出的响应进⾏处理. 这种链式处理机制确保了每个Advisor可以在请求和响应流中添加⾃⼰的逻辑, 从⽽实现更灵活和可定制的功能.
相当于拦截器

LLM是控制器,就是大模型,前后都是拦截器
就是在大模型的请求前后,都是拦截器
请求和响应都有拦截器
应⽤场景:
• 敏感词过滤
• 建⽴聊天历史
• 对话上下⽂管理
1.5.2 SimpleLoggerAdvisor
Spring AI 内置了⼀些Advisor, SimpleLoggerAdvisor 作为其中之⼀, 主要功能是记录⽇志. 使⽤⾮常简单, 开发⼈员只需把它添加到Advisor链中, 即可⾃动记录所有经过该Advisor的聊天请求和响应,并且开发⼈员可以对其进⾏配置, ⽐如⽇志级别和⽇志格式
1.5.2.1 添加 SimpleLoggerAdvisor 到 Advisor链中
可以通过 defaultAdvisors ⽅法 来设置, 通过这种⽅式设置的Advisor会作⽤于ChatClient发起的每⼀次对话
—》所以对chatclient进行配置
chatClientBuilder.defaultAdvisors就可以添加多个Advisor
@Configuration
public class ChatClientConfiguration {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder) {return chatClientBuilder
// 设置系统提示词,针对大模型,每次用于请求都会生效这个系统提示词.defaultSystem("你是lyx,是由ck研发的一款智能AI助手,你很擅长医学研究,请以友好的态度来回答问题").defaultAdvisors(new SimpleLoggerAdvisor()).build();}
}
这样的话,所有使用chatClient的都会生成日志了–》所有请求都会生效
如果只是相对某一个接口生效呢–——》对这个接口进行设置
而且是advisor设置
@GetMapping("/call")String generation(String userInput) {return this.chatClient.prompt().user(userInput).advisors(new SimpleLoggerAdvisor()).call().content();}
其实还可以把new SimpleLoggerAdvisor()交给spring进行管理
我们还是defaultAdvisors,对所有请求生效
1.5.2.2 配置⽇志级别
logging:pattern:console: '%d{HH:mm:ss.SSS} %c %M %L [%thread] %m%n'file: '%d{HH:mm:ss.SSS} %c %M %L [%thread] %m%n'level:org.springframework.ai.chat.client.advisor: debug
主要的配置是这个org.springframework.ai.chat.client.advisor
把这个类配置为debug,就可以看到它的日志了
1.5.2.3 测试


2. 流式编程
2.1 SSE协议介绍
HTTP协议本⾝设计为⽆状态的请求-响应模式, 严格来说, 是⽆法做到服务器主动推送消息到客⼾端, 但通过Server-Sent Events (服务器发送事件, 简称SSE)技术可实现流式传输,允许服务器主动向浏览器推送数据流.
流式输出就是服务器主动向浏览器推送消息—》HTTP做不到
也就是说, 服务器向客⼾端声明, 接下来要发送的是流消息(streaming), 这时客⼾端不会关闭连接, 会⼀直等待服务器发送过来新的数据流
SSE(Server-Sent Events)是⼀种基于 HTTP 的轻量级实时通信协议, 浏览器通过内置的EventSource API接收并处理这些实时事件.

2.1.1 核⼼特点
• 基于 HTTP 协议
复⽤标准 HTTP/HTTPS 协议, ⽆需额外端⼝或协议, 兼容性好且易于部署
• 单向通信机制
SSE 仅⽀持服务器向客⼾端的单向数据推送,客⼾端通过普通 HTTP 请求建⽴连接后,服务器可持续发送数据流,但客⼾端⽆法通过同⼀连接向服务器发送数据.
• ⾃动重连机制
⽀持断线重连, 连接中断时,浏览器会⾃动尝试重新连接(⽀持 retry 字段指定重连间隔)
• ⾃定义消息类型
客⼾端发起请求后, 服务器保持连接开放, 响应头设置 Content-Type: text/event-stream , 标识为事件流格式, 持续推送事件流.
SSE是单向的:服务器向浏览器推送,websocket是双向的

2.1.2 数据格式
服务端向浏览器发送SSE数据, 需要设置必要的HTTP头信息
Content-Type: text/event-stream;charset=utf-8
Connection: keep-alive —》这个是默认设置的
每⼀次发送的消息, 由若⼲个message组成, 每个message之间由 \n\n 分隔, 每个message内部由若⼲⾏组成, 每⼀⾏都是如下格式
[field]: value\n

Field 可以取值为:
• data[必需]: 数据内容
• event[⾮必需]: 表⽰⾃定义的事件类型,默认是message事件
• id[⾮必需]:数据标识符, 相当于每⼀条数据的编号
• retry[⾮必需]: 指定浏览器重新发起连接的时间间隔
除此之外, 还可以有冒号 : 开头的⾏, 表⽰注释.
event: foo\n
data: a foo event\n\ndata: an unnamed event\n\nevent: end\n
data: a bar event\n\n
2.2.3 服务端实现data
@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++) {//发送20条消息,每隔一秒钟发一次当前时间String s = "data:" + new Date() +"\n\n";//两个/n表示这个message结束了writer.write(s);writer.flush();//刷新缓存Thread.sleep(1000);}}
}
2.2.4 客户端实现
<div id="sse"></div>
<script>let eventSource = new EventSource("/sse/data")// onmessage方法是接收到消息调用这个方法,event就是后端的返回结果eventSource.onmessage = function (event){document.getElementById("sse").innerHTML = event.data;}
</script>

这个秒数一直在变化
@RequestMapping("/data")public void data(HttpServletResponse response) throws IOException, InterruptedException {response.setContentType("text/event-stream;charset=utf-8");System.out.println("发起请求");PrintWriter writer = response.getWriter();for (int i = 0; i < 20; i++) {//发送20条消息,每隔一秒钟发一次当前时间String s = "data:" + new Date() +"\n\n";//两个/n表示这个message结束了writer.write(s);writer.flush();//刷新缓存Thread.sleep(1000);}}

我们没有刷新页面,发现它会自动去请求页面
这个就是核心特点中的• ⾃动重连机制
2.2.5 retry
我们可以根据retry字段来指定每次重新连接的间隔时间
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
@RequestMapping("/retry")public void retry(HttpServletResponse response) throws IOException, InterruptedException {response.setContentType("text/event-stream;charset=utf-8");log.info("发起请求,retry");PrintWriter writer = response.getWriter();String s = "retry:"+2000+"\n";//每次重新连接间隔时间为2ss += "data:" + new Date() + "\n\n";//两个/n表示这个message结束了,data必须给writer.write(s);writer.flush();//刷新缓存Thread.sleep(1000);}
let eventSource = new EventSource("/sse/retry")

所以现在每隔两秒重新连接
2.2.6 自定义事件
就是event字段
默认的事件是message事件—》所以前端使用的是onmessage
@RequestMapping("/event")public void event(HttpServletResponse response) throws IOException, InterruptedException {response.setContentType("text/event-stream;charset=utf-8");System.out.println("发起请求event");PrintWriter writer = response.getWriter();for (int i = 0; i < 10; i++) {//发送20条消息,每隔一秒钟发一次当前时间String s = "event: foo\n";s += "data:" + new Date() + "\n\n";writer.write(s);writer.flush();//刷新缓存Thread.sleep(1000L);}}
let eventSource = new EventSource("/sse/event")// foo是指定事件名称,第二个参数表示对这个事件要干什么eventSource.addEventListener("foo",(event)=>{document.getElementById("sse").innerHTML = event.data;})

2.6.7 结束自动重连
@RequestMapping("/end")public void end(HttpServletResponse response) throws IOException, InterruptedException {response.setContentType("text/event-stream;charset=utf-8");System.out.println("发起请求end");PrintWriter writer = response.getWriter();for (int i = 0; i < 10; i++) {//发送20条消息,每隔一秒钟发一次当前时间String s = "event: foo\n";s += "data:" + new Date() + "\n\n";writer.write(s);writer.flush();//刷新缓存Thread.sleep(1000L);}//定义end事件,表示这个流传输结束了writer.write("event: end\ndata: EOF\n\n");//data是必须写的writer.flush();//刷新缓存,发送到前端}
let eventSource = new EventSource("/sse/end")// foo是指定事件名称,第二个参数表示对这个事件要干什么eventSource.addEventListener("foo",(event)=>{document.getElementById("sse").innerHTML = event.data;})eventSource.addEventListener("end",(event)=>{eventSource.close()})


到这里就不动了,说明已经结束了
2.2 Spring 中SSE实现
Spring 4.2 开始就已经⽀持SSE,从Spring 5开始我们可以使⽤WebFlux 更优雅的实现SSE协议. Flux是WebFlux的核⼼API.
快速使⽤
Flux的流程分为三个步骤:
- 创建Flux
创建⼀个Flux数据流, 并有数据源. - 处理数据
使⽤操作符对数据进⾏处理 - 订阅数据
订阅Flux来消费数据, 触发数据的流动
可以把Flux想象成⼀条传送带
• 异步传送:数据像快递包裹⼀样逐个到达, 不⽤等全部到⻬
• 灵活加⼯:⽀持中途修改数据 (如过滤/转换)
• 弹性控制:接收⽅可以调速 (背压机制)
public static void main(String[] args) {Flux<String> flux = Flux.just("Apple","Banana","Cherry","Pear");//创建数据流
// map(String::toUpperCase)表示对传送带上的每个元素变成大写,map(s->s+"-1")表示每个元素加-1,s就是每个元素
// Flux<String> map = flux.map(String::toUpperCase).map(s -> s + "-1");//得到一个新的Flux数据流
// map.subscribe(System.out::println);//订阅数据流---》对数据流的处理就是打印出来每个数据flux.map(String::toUpperCase).map(s -> s + "-1").subscribe(System.out::println);//合并}

但是我们没有感受到是一个一个传送出来的
public static void main(String[] args) {Flux<String> flux = Flux.just("Apple","Banana","Cherry","Pear").delayElements(Duration.ofSeconds(1));//创建数据流flux.map(String::toUpperCase).map(s -> s + "-1").subscribe(System.out::println);//合并}
delayElements表示一秒钟传送到达一个
但是这样还是不行,因为程序执行很快,可能数据还没到达–》程序已经结束了
public static void main(String[] args) throws InterruptedException {Flux<String> flux = Flux.just("Apple","Banana","Cherry","Pear").delayElements(Duration.ofSeconds(1));//创建数据流flux.map(String::toUpperCase).map(s -> s + "-1").subscribe(System.out::println);//合并Thread.sleep(5000);}

这样就是一秒钟一个的来了
使⽤Flux.just(…) 创建⼀个包含指定元素的Flux
也有其他的创建⽅式, ⽐如 fromIterable 和 range
//从集合 (如 List、Set) 创建
Flux.fromIterable(Arrays.asList(1, 2, 3))
//⽣成 1~5的Flux
Flux.range(1, 5)
2.2.1 常⻅的操作符
map():元素⼀对⼀转换
filter():条件过滤,.filter(s -> s.length() > 5)
take() 限制元素数量.take(2) // 只取前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))
2.2.2 流式响应接⼝
//import org.springframework.http.MediaType;@RequestMapping(value = "/stream",produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> stream(){
// Flux.interval(Duration.ofSeconds(1))表示每隔一秒钟发送一个数字---》数字是0,1,2,3,4,5,6
// map表示把这个数字变成当前时间return Flux.interval(Duration.ofSeconds(1)).map(s->new Date().toString());}
let eventSource = new EventSource("/sse/stream")// onmessage方法是接收到消息调用这个方法,event就是后端的返回结果eventSource.onmessage = function (event){document.getElementById("sse").innerHTML = event.data;}
默认还是message事件

3. ChatModel

发现有多个call方法
ChatModel 是Spring AI 构建对话应⽤的核⼼接⼝, 它抽象了应⽤与模型交互的过程, 包括使⽤Prompt 作为输⼊, 使⽤ ChatResponse 作为输出等. ChatModel 的⼯作原理是接收 Prompt 或部分对话作为输⼊, 将输⼊发送给后端⼤模型, 模型根据其训练数据和对⾃然语⾔的理解⽣成对话响应, 应⽤程序可以将响应呈现给⽤⼾或⽤于进⼀步处理


String call(String message) ⽅法就是对 message进⾏了封装, 简化了ChatModel 的初始化使⽤, 避免了更复杂的Prompt输⼊和ChatResponse输出, 本质上调⽤的依然是 ChatResponsecall(Prompt prompt)
ChatClient 本质上也是基于ChatModel 进⾏的封装和增强.
3.1 实现简单对话
@RequestMapping("/chatByPrompt")public String chatByPrompt(String message){Prompt prompt = new Prompt(message);ChatResponse response = chatModel.call(prompt);return response.getResult().getOutput().getText();}

3.2 ⻆⾊预设
@RequestMapping("/role")public String role(String message){SystemMessage systemMessage = new SystemMessage("我是lyx,是一名医学AI助手");UserMessage userMessage = new UserMessage(message);Prompt prompt = new Prompt(systemMessage,userMessage);ChatResponse response = chatModel.call(prompt);return response.getResult().getOutput().getText();}
默认prompt 一个参数的时候,就去调用UserMessage

3.3 实现流式输出
@RequestMapping("/stream")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());//一对一转换}

虽然是流失输出但是还是乱码的情况
@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());//一对一转换}

3.4 ChatClient 和ChatModel区别
ChatClient 对ChatModel进⾏了封装, 相⽐如ChatModel 原⼦类API,ChatClient屏蔽了与AI⼤模型的交互的复杂性, 它⾃动集成提⽰词管理、响应格式化、结构化输出映射等能⼒, 提⾼了开发效率.
ChatModel 是Spring AI 框架中的底层接⼝, 直接与具体的⼤语⾔模型 (如通义千问、OpenAI) 交互,提供基础的 call 和 stream ⽅法, 开发者需⼿动处理提⽰词组装、参数配置和响应解析等细节,在使⽤上相对更加灵活.

