Java AI - Spring AI基础入门:常见组件、常规对话、流式对话、基于Redis的会话记忆【含源码】
Spring AI
参考网址
https://spring.p2hp.com/index.html
https://spring.io/projects/spring-ai
介绍
Spring AI 是一款 AI 工程领域的应用程序框架,核心是将 Spring 生态的可移植性、模块化等设计原则延伸至 AI 领域,推广以 POJO 作为 AI 应用的构建块。它提供了开发 AI 大模型应用所需的基础抽象模型及多种实现方式,支持开发者以少量代码改动完成组件替换,同时具备友好的 API,最终目的是简化 AI 大模型应用的开发流程。
Spring AI 的主要功能
- 对主流 AI 大模型供应商提供了支持,比如:OpenAI、DeepSeek、Microsoft、Ollama、Amazon、Google HuggingFace 等。
- 支持 AI 大模型类型包括:聊天、文本到图像、文本到声音等。
- 支持主流的 Embedding Models(嵌入模型)和向量数据库,比如:Azure Vector Search、Chroma、Milvus、Neo4j、Redis、PineCone、PostgreSQL/PGVector 等。
- 把 AI 大模型输出映射到简单的 Java 对象(POJOs)上。
- 支持了函数调用 (Function Calling) 功能。
- 为数据工程提供 ETL(数据抽取、转换和加载)框架。
- 支持 Spring Boot 自动配置和快速启动,便于运行 AI 模型和管理向量库。
Chat API
注底层厂商的请求格式、认证方式、响应解析等细节。
简单说:Spring AI Chat API 是开发者与 LLM 之间的 “翻译官” 和 “统一接口”,让你写一次代码,就能适配多个大模型。
核心定位与解决的问题
在 Spring AI 出现前,对接不同大模型需要处理:
- 不同厂商的请求参数(如 OpenAI 的 messages vs Anthropic 的 prompt);
- 认证方式差异(API Key、OAuth 等);
- 响应格式解析(不同结构的 JSON 结果);
- 流式 / 非流式调用的实现差异;
- 对话上下文管理(多轮对话的历史消息维护)。
Spring AI Chat API 统一了这些差异,提供:
- 标准化的 “请求 - 响应” 模型;
- 内置的对话上下文管理;
- 一致的流式 / 非流式调用接口;
- 可插拔的大模型适配(通过 ChatClient 实现);
- 与 Spring 生态(如 Spring Boot、依赖注入)无缝集成。
核心概念
- ChatClient:作为各主流大模型的标准化客户端抽象,核心职责是封装与底层模型的通信逻辑,包括请求的构建与发送、响应的接收与解析,是开发者与大模型交互的统一入口。
- Prompt:对话请求的顶层封装载体,内部聚合了两类核心要素:一是构成对话内容的Message集合,二是影响模型生成行为的配置选项(Option)。
- Message:对话交互的基础数据单元,不仅承载着待传递给大模型的具体内容,还包含角色标识(如用户、系统、助手)、附加属性等元信息,同时通过明确的类型划分(如用户消息、系统提示、工具反馈消息)适配不同交互场景。
- Option:模型生成过程的参数配置项,用于精细化控制输出效果。例如temperature参数(取值范围通常为 0-1),其值越低,模型输出越聚焦事实、严谨一致;值越高,则输出越具随机性与创造性。
- ChatResponse:大模型响应结果的统一封装对象,内部聚合了生成结果、响应元数据(如调用耗时、Token 统计)等核心信息,其中核心数据为Generation。
- Generation:封装了大模型生成的具体业务内容,是ChatResponse中最核心的有效数据载体,直接对应开发者所需的模型输出结果。
三个层次
前面我们知道SpringAI支持大模型的多种能力,聊天只是其中一种,因此就有一个代表最顶层的抽象层,与大模型有关的各种能力,都在此有个定义,然后是代表各种能力的抽象层,如聊天、图片、嵌入式处理等,最后是每一种能力在各类具体大模型上的实现,如下图所示
到现在为止咱们还没有看一行代码一个API,但是从理论上对Chat API的定位、关系已经基本了解了
官方示意图
最下面橙色这层,中间是client,这里有两种,ModelClient代表了常规的请求响应,StreamingModelClient代表了流式响应(数据并非一次性传输,而是建立链接后源源不断的输出)
client的左侧是request,里面包含了option,至于prompt,那是Chat的概念,所以不会出现在橙色这一层
client右侧是response,同样只有抽象的ResponseMeta和ResultMetaData,generation是Chat的概念,不会在橙色这一层出现
再往上看,绿色的就是功能抽象层了,ChatClient继承了ModelClient,Prompt继承了ModelRequest,代表Chat领域的请求,同理CharResponse继承了ModelResponse
有了理论基础,一张官方图就让我们看清了Chat API的大概,现在还缺点东西,就是具体的实现层,毕竟有很多种大模型能,最终编码时还是要用到实现层的类,有没有什么方式将实现层完美的展现出来?
实现层和功能抽象层的关系
这张图是 Spring AI 中 ChatClient 相关组件的类层级与接口继承关系图,核心呈现了 “通用 API 抽象 → 专用接口 → 厂商具体实现” 的设计架构,清晰展示了 Spring AI 如何通过标准化接口统一不同大模型的对话调用能力,以下是详细拆解:
一、核心架构层级(从上到下:抽象→实现)
图中组件按 “通用抽象 → 专用扩展 → 厂商适配” 分为 3 个核心层级,每层职责明确:
1. 最顶层:通用模型 API 抽象(基础接口)
定义了所有 AI 模型交互的通用规范,不局限于 “对话” 场景,是 Spring AI 统一接口设计的根基:
- ModelClient:
- 核心定位:通用模型 API 顶层接口,支持所有模型类型(文本、图像、音频等)的同步调用。
- 核心方法:call(TReq) : TResp(接收泛型请求 TReq,返回泛型响应 TResp),实现 “输入 - 输出” 的标准化映射。
- StreamingModelClient:
- 核心定位:通用流式模型 API 接口,继承自通用模型 API 的设计思路,专注于 “流式响应” 场景。
- 核心方法:streamingCall(TReq) : Flux(返回响应式编程的 Flux 流,支持逐段接收模型输出,如 ChatGPT 打字效果)。
2. 中间层:对话专用 API 接口(功能扩展)
基于顶层通用 API,针对 “对话交互” 场景做专项抽象,屏蔽非对话模型的无关能力,聚焦聊天场景需求:
- ChatClient:
- 核心定位:通用对话完成 API 接口,继承自 ModelClient 的设计思想(图中 “IS A” 表示继承关系),专门用于非流式对话调用。
- 核心方法:call(Prompt) : ChatResponse(输入为 Spring AI 标准化的 Prompt 对象,输出为对话专用的 ChatResponse,适配多轮对话、角色消息等场景)。
- StreamingChatClient:
- 核心定位:流式对话 API 接口,继承自 StreamingModelClient 的设计思路,专门用于流式对话调用。
- 核心方法:call(Prompt) : Flux(输入为 Prompt,输出为 ChatResponse 的 Flux 流,支持实时接收对话响应)。
3. 最底层:厂商具体实现类(落地适配)
针对不同大模型厂商的对话模型,提供 ChatClient 或 StreamingChatClient 的具体实现,是开发者实际使用的 “厂商专属客户端”,无需关注底层调用细节:
图中包含 11 个主流厂商 / 平台的实现类,覆盖云厂商、开源模型、本地部署模型等场景:
- OpenAI 生态:OpenAiChatClient(OpenAI 原生模型,如 GPT-3.5/4)、AzureOpenAiChatClient(Azure 托管的 OpenAI 模型)。
- Google 生态:VertexAiPaLm2ChatClient(Google Vertex AI 上的 PaLM 2 模型)、VertexAiGeminiChatClient(Google Vertex AI 上的 Gemini 模型)。
- AWS Bedrock 生态:BedrockAnthropicChatClient(Bedrock 上的 Anthropic Claude 模型)、BedrockTitanChatClient(Bedrock 上的 Titan 模型)、BedrockLlama2ChatClient(Bedrock 上的 Llama 2 模型)、BedrockCohereChatClient(Bedrock 上的 Cohere 模型)。
- 开源 / 本地模型:MistralAiChatClient(Mistral 开源模型)、HuggingfaceChatClient(Hugging Face 开源模型,如 BERT、GPT-2)、OllamaChatClient(Ollama 本地部署模型,如 Llama 2、Mistral 本地版)。
二、核心设计思想与价值
这张图直观体现了 Spring AI 的核心设计理念:
- 接口抽象解耦:通过 ModelClient/ChatClient 等顶层接口定义标准,厂商实现类专注于 “适配自身 API”,开发者依赖接口编程,无需修改代码即可切换模型。
- 流式与非流式分离:将 “同步单次响应”(ChatClient)与 “流式增量响应”(StreamingChatClient)拆分为两个接口,适配不同业务场景(如简单问答用非流式,实时聊天用流式)。
- 覆盖全场景模型:实现类涵盖云厂商(OpenAI、Azure、Google、AWS)、开源社区(Hugging Face、Mistral)、本地部署(Ollama),满足不同用户的模型选择需求。
核心类和接口

ChatClient
- 大模型聊天功能的客户端接口,在进程中,其实现就是各大模型对应的客户端类
- 主要是call方法,这就是最常规的聊天功能,调用call发送请求,返回值就是大模型的响应
StreamingChatClient
- 这也是客户端类,用于调用大模型的功能,与ChatClient不同的是,ChatClient是请求响应,返回对象ChatResponse就是大模型返回的全部内容,而StreamingChatClient返回的是Flux,这是流式返回,可以讲大模型的响应进行流式输出,如果您使用过各种大模型聊天工具,会发现响应的内容并非一次性展现,而是一段一段的内容,持续不断的展现出来,这就是流式响应的效果
- 注意注解FunctionalInterface,表明这是个函数式接口
Prompt
- 前面看过了ChatClient和StreamingChatClient,会发现入参都是Prompt,可见这就是和大模型一次聊天的入参
- 下面是Prompt的源码,去掉了构造函数、toString这些之后就会发现,最重要的是Message和ChatOption,所以Prompt只是个打包,真正要提交到大模型的其实是Message和ChatOption
- 如果您对OpenAI有所了解,就知道prompt(提示词)并非只有用户输入的聊天内容那么简单,而是system、user 、assistant等多种类型 ,所以这里的Prompt并非只是一个外壳那么简单,它与不同类型的message、不同的辅助类等一起提供了完善的提示词功能,这个会有单独的文章来说明和实战,本篇只要记得它的最终形态就是打好的包用于提交给大模型
- 如果只是最基本的聊天,下面这个构造方法来创建对象就行了
public Prompt(String contents) {
this(new UserMessage(contents));
}
Message
- Message很好理解:在聊天过程中,聊天内容对应的对象,请求和响应用的都是Message,不过由于消息类型的多样性,Message被设计成了接口,根据不同类型都有对应的实现
- Message自身非常简单,能保证使用方取到消息内容、类型即可
- 另外要注意的是消息类型,一共四种
public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
FUNCTION("function");
}
ChatOptions
- ChatOptions代表可以传递给大模型的控制参数,具体有哪些参数和大模型自身开放的特性有关,举个例子,下面是OpenAI开放的参数
- presencePenalty : 影响模型在生成文本时重复词语或概念的倾向
- frequencyPenalty:影响模型在生成文本时对已出现过词语的偏好程度
- 按照上面的解释,既然各种大模型都有自己的参数,那么设计ChatOptions能干啥?应该能放一些通用的控制参数吧,打开代码一看果然如此,共有三个通用参数。
/**
* The ChatOptions represent the common options, portable across different chat models.
*/
public interface ChatOptions extends ModelOptions {
// 大模型生成的内容应该更严谨还是更有创造性
Float getTemperature();
// 返回概率超过P的所有内容
Float getTopP();
// 返回概率最高的前K个内容
Integer getTopK();
}
- ChatOptions只是接口,对应的实现是ChatOptionsImpl,源码没啥好看的,就是temperature、topP、topK的get和set而已,为了实例化ChatOptionsImpl,还有配套工具ChatOptionsBuilder,用法如下
ChatOptions portablePromptOptions = ChatOptionsBuilder.builder()
.withTemperature(0.9f)
.withTopK(100)
.withTopP(0.6f)
.build();
ChatResponse
- 看完请求该看响应了,既然Generation才是真正的响应内容,那么ChatResponse也就是个壳,里面包了Generation,打开源码一看,只有Generation和ChatResponseMetadata,这个ChatResponseMetadata可以理解为元信息,主要返回了大模型的API的使用情况说明,以及限速的详细信息
public class ChatResponse implements ModelResponse<Generation> {
private final ChatResponseMetadata chatResponseMetadata;
private final List<Generation> generations;
@Override
public ChatResponseMetadata getMetadata() {...}
@Override
public List<Generation> getResults() {...}
// other methods omitted
}
Generation
- Generation中有响应的具体信息,由ChatGenerationMetadata和AssistantMessage组成
public class Generation implements ModelResult<AssistantMessage> {
private AssistantMessage assistantMessage;
private ChatGenerationMetadata chatGenerationMetadata;
@Override
public AssistantMessage getOutput() {...}
@Override
public ChatGenerationMetadata getMetadata() {...}
// other methods omitted
}
- ChatGenerationMetadata代表返回内容的元信息,包含了结束原因、生成内容的过滤规则
- AssistantMessage更容易理解了:类型是ASSISTANT的消息,这个assistant就是助理角色,assistant消息就是大模型返回的聊天响应,源码如下
public class AssistantMessage extends AbstractMessage {
public AssistantMessage(String content) {
super(MessageType.ASSISTANT, content);
}
public AssistantMessage(String content, Map<String, Object> properties) {
super(MessageType.ASSISTANT, content, properties);
}
@Override
public String toString() {
return "AssistantMessage{" + "content='" + getContent() + '\'' + ", properties=" + properties + ", messageType="
+ messageType + '}';
}
}
示例代码
同步对话
后端代码
依赖
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.3.4</spring-boot.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.55</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
application.yml主配置文件
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode # 基础 URL
api-key: sk-a4ea34ca69f********91167245b1 # 百炼大模型API 密钥
chat:
options:
model: qwen-plus # 模型名称
temperature: 0.1 # 温度,控制生成文本的随机性,默认值为 0.1, 范围为 [0.0, 1.0], 较高的温度会使生成的文本更加随机,而较低的温度会使生成的文本更加确定
max-tokens: 2000 # 最大令牌数,控制生成文本的长度,默认值为 2000, 范围为 [100, 4000]
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mall_116?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root1234
server:
port: 8080
logging:
level:
org.springframework.ai: DEBUG
com.woniuxy.spring.ai.base: DEBUG
温度 (Temperature)
是控制生成文本随机性与创造性的核心参数,数值越高输出越灵活多样,越低则越确定、保守。
1.核心作用
- 本质是调节模型采样概率:低温时,模型优先选择概率最高的词;高温时,会放大低概率词的选中概率。
- 直接影响输出风格:是 “按规矩作答” 还是 “自由发挥” 的关键开关。
2.数值范围与效果
- 低温(0–0.3):输出高度确定、精准,重复率可能较高。
适合场景:事实问答、代码生成、专业文档撰写等需要准确的场景。 - 中温(0.4–0.7):平衡准确性与创造性,输出自然流畅。
适合场景:日常对话、文案创作、邮件撰写等多数通用场景。 - 高温(0.8–1.0):输出随机性强,充满创意但可能偏离主题、逻辑跳跃。
适合场景:诗歌创作、脑洞故事、营销创意发散等需要突破常规的场景。
3.关键注意点
- 数值并非越高越好,过高可能导致输出无意义、逻辑混乱。
- 不同模型的 Temperature 基准略有差异,但核心逻辑一致。
聊天配置类
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
同步聊天服务接口
public interface ChatService {
public String chat(String message);
}
服务实现类
import com.woniuxy.spring.ai.base.memory.RedisChatMemory;
import com.woniuxy.spring.ai.base.service.ChatRecordService;
import com.woniuxy.spring.ai.base.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {
private final ChatClient chatClient;
private final RedisChatMemory redisChatMemory;
private final ChatRecordService chatRecordService;
/**
* 同步对话
*/
@Override
public String chat(String message) {
log.info("发送消息到阿里百炼: {}", message);
String response = chatClient.prompt(message)
.call()
.content();
log.info("收到阿里百炼响应: {}", response);
return response;
}
}
控制层接口
import com.woniuxy.woniuspringai.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.Map;
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
/**
* 同步对话
*/
@PostMapping("/chat")
public Map<String, String> chat(@RequestBody Map<String, String> request) {
String message = request.get("message");
String response = chatService.chat(message);
return Map.of("response", response);
}
}
跨域配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class RedisConfigurationclass CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 1. 允许所有域名
config.addAllowedOriginPattern("*");
// 2. 允许所有请求头
config.addAllowedHeader(CorsConfiguration.ALL);
// 3. 允许所有请求方法
config.addAllowedMethod(CorsConfiguration.ALL);
// 4. 允许携带Cookie
config.setAllowCredentials(true);
// 5. 预检请求有效期
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
前端代码
安装axios、router、element-plus
npm install axios vue@3 vue-router@4
npm install element-plus --save
app.vue
<template>
<router-view />
</template>
<script setup>
</script>
封装axios
src/api/aiChat.js
import axios from 'axios';
const baseURL = 'http://localhost:8080'
// 创建axios实例
const apiClient = axios.create({
baseURL: baseURL + '/api/ai', // 后端接口基础路径
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
// 普通对话接口
export const chat = async (message) => {
const res = await apiClient.post('/chat', { message });
return res.data;
};
聊天页面
src/views/AiChat.vue
<template>
<el-container style="max-width: 800px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">
<el-header>
<h1 style="margin: 0; color: #303133;">AI 对话助手</h1>
</el-header>
<el-main>
<!-- 普通对话 -->
<el-card shadow="hover" style="margin-bottom: 20px;">
<template #header>
<span style="font-size: 16px; font-weight: 600;">普通对话</span>
</template>
<el-form :inline="true" :model="normalForm" style="margin-bottom: 15px;">
<el-form-item label="" prop="message">
<el-input
v-model="normalForm.message"
placeholder="请输入对话内容..."
style="width: 100%;"
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="sendNormalChat"
:loading="normalChatLoading"
>
发送
</el-button>
</el-form-item>
</el-form>
<el-card v-if="normalChatResponse" shadow="never" style="background: #f8f9fa; border: 1px solid #e9ecef;">
<div style="white-space: pre-wrap; color: #303133;">
<span style="font-weight: 600;">AI 回复:</span>
{{ normalChatResponse }}
</div>
</el-card>
</el-card>
</el-main>
</el-container>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus'; // 引入消息提示组件
import { chat } from '@/api/aiChat';
// 普通对话 - 改用 reactive 表单对象(Element Plus 推荐)
const normalForm = reactive({
message: ''
});
const normalChatLoading = ref(false);
const normalChatResponse = ref('');
// 普通对话发送方法
const sendNormalChat = async () => {
if (!normalForm.message.trim()) {
ElMessage.warning('请输入对话内容!');
return;
}
normalChatLoading.value = true;
normalChatResponse.value = '';
try {
const res = await chat(normalForm.message.trim());
normalChatResponse.value = res.response;
} catch (err) {
console.error('普通对话失败:', err);
normalChatResponse.value = '对话失败,请重试';
ElMessage.error('对话失败:' + err.message || '未知错误');
} finally {
normalChatLoading.value = false;
}
};
</script>
<style scoped>
</style>
main.js
import { createApp } from 'vue';
import App from './App.vue';
import AIChat from './views/AIChat.vue';
import { createRouter, createWebHistory } from 'vue-router';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 路由配置
const routes = [
{ path: '/', component: AIChat }
];
const router = createRouter({
history: createWebHistory(),
routes
});
const app = createApp(App);
app.use(router);
app.use(ElementPlus)
app.mount('#app');
流式对话
后端实现
SSE
SSE(Server-Sent Events,服务器推送事件)是一种 基于 HTTP 的简单流式通信协议,核心是让服务器能主动、持续地向客户端推送数据,而无需客户端反复发起请求(即 “一次连接,多次推送”)。
可以把它理解为:客户端和服务器建立一条 “长连接”,服务器像 “发消息” 一样,把数据一块一块推给客户端,客户端收到就实时处理
核心特点
- 单向通信:只有服务器往客户端推数据,客户端不能通过这条连接给服务器发数据(如果客户端要发消息,需单独用 HTTP 请求);
- 基于 HTTP/HTTPS:不用额外开端口,和普通接口用一样的协议,防火墙、代理都能兼容;
- 文本协议:推送的数据是文本格式(通常是 JSON 字符串),简单易解析;
- 自动重连:客户端(如浏览器的 EventSource API)默认支持断连后自动重连,不用手动处理;
- 长连接:连接建立后会保持,直到服务器主动关闭或客户端断开(区别于普通 HTTP 短连接)。
和普通 HTTP 接口的核心区别
常见使用场景
- AI 流式回复(如 ChatGPT、你的 chat 接口场景):AI 生成内容时逐字 / 逐句推给前端,避免用户等待;
- 实时通知(如系统公告、订单状态变更):服务器有新消息时,立即推给在线用户;
- 实时数据更新(如股票行情、监控数据):周期性推送最新数据,客户端实时展示;
- 长文本返回(如 AI 写文章、生成报告):分块推送避免 “加载空白屏”。
Flux
Flux是 Reactor 响应式编程框架(Spring WebFlux 核心依赖)中的核心类型,代表0 到 N 个字符串类型元素的异步序列,是实现「流式响应」(如 AI 流式回复、实时日志推送)的关键。
简单理解:
- 传统同步返回(如 String):一次性返回所有数据;
- Flux:异步、分批次返回数据,每产生一个元素就推送给前端,直到序列结束 / 出错。

代码实现
/**
* 流式对话
*/
public Flux<String> chatStream(String message) {
log.info("发送流式消息到阿里百炼: {}", message);
return chatClient.prompt(message)
.stream()
.content()
.concatWith(Mono.just("[DONE]")) // 确保最后发送[DONE]标记
.onErrorResume(e -> {
log.error("流式对话异常", e);
return Flux.just("对话出错:" + e.getMessage(), "[DONE]");
});
}
控制层接口
/**
* 流式对话接口:必须设置 Content-Type: text/event-stream
*/
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam("message") String message) {
return chatService.chatStream(message);
}
前端
EventSource
EventSource 是 HTML5 新增的服务器推送事件(Server-Sent Events,SSE) 的前端实现,用于建立浏览器与服务器的单向长连接,让服务器可以主动、持续地向客户端推送数据(仅支持文本格式)。
核心特点:
- 单向通信:仅服务器 → 客户端(如需双向通信用 WebSocket);
- 基于 HTTP:无需额外协议,兼容现有 HTTP 生态(如 CORS、认证);
- 自动重连:连接断开后默认自动重试(可配置);
- 轻量级:API 简单,无需复杂封装,适合流式文本传输(如 AI 流式回复、实时日志)。
核心属性 / 方法
代码实现
aiChat.js
// 流式对话
export const chatStream = (message, onMessage, onComplete) => {
const source = new EventSource(`${baseURL}/api/ai/chat/stream?message=${encodeURIComponent(message)}`);
source.onmessage = (event) => {
let rawData = event.data.trim();
// 检测到[DONE]:关闭连接 + 触发完成回调
if (rawData.includes('[DONE]')) {
source.close();
// 通知页面:流式结束,关闭加载状态
typeof onComplete === 'function' && onComplete();
return;
}
let content = rawData.replace(/^data:\s*/, '').replace(/^\[DONE\]*/, '');
if (content) {
onMessage(content);
}
};
// 新增:连接错误时也触发完成回调(兜底)
source.onerror = () => {
source.close();
typeof onComplete === 'function' && onComplete();
};
return () => {
if (source.readyState !== EventSource.CLOSED) {
source.close();
}
};
};
AiChat.vue
- 新增标签
<el-card shadow="hover">
<template #header>
<span style="font-size: 16px; font-weight: 600;">流式对话</span>
</template>
<el-form :inline="true" :model="streamForm" style="margin-bottom: 15px;">
<el-form-item label="" prop="message">
<el-input
v-model="streamForm.message"
:rows="4"
placeholder="请输入对话内容..."
style="width: 100%;"
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="sendStreamChat"
:loading="streamChatLoading"
:disabled="streamChatLoading"
>
发送
</el-button>
<el-button
type="danger"
@click="stopStreamChat"
v-show="streamChatLoading"
>
停止
</el-button>
</el-form-item>
</el-form>
<el-card shadow="never" style="background: #f8f9fa; border: 1px solid #e9ecef; min-height: 100px;">
<div style="white-space: pre-wrap; color: #303133;">
<span style="font-weight: 600;">AI 回复:</span>
{{ streamChatResponse }}
</div>
</el-card>
</el-card>
- 新增代码
import { chat, chatStream } from '@/api/aiChat';
// 流式对话发送方法
const sendStreamChat = () => {
if (!streamForm.message.trim()) {
ElMessage.warning('请输入对话内容!');
return;
}
streamChatLoading.value = true;
streamChatResponse.value = '';
// 关闭之前的流(如果有)
if (closeStream) closeStream();
// 调用流式接口
closeStream = chatStream(
streamForm.message.trim(),
(chunk) => {
streamChatResponse.value += chunk;
},
// 完成回调:关闭加载状态 + 提示
() => {
streamChatLoading.value = false;
closeStream = null;
ElMessage.success('流式对话完成!');
}
);
};
// 停止流式对话(增加消息提示)
const stopStreamChat = () => {
if (closeStream) {
closeStream();
closeStream = null;
}
streamChatLoading.value = false;
ElMessage.info('已停止流式对话');
};
ChatClient
ChatClient 是 Spring AI 框架中为大模型对话交互设计的核心客户端工具,也是开发者与 AI 模型(如 OpenAI、阿里通义千问、百度文心一言等)进行对话的 “极简入口”。它基于 Spring AI 抽象的 ChatModel 构建,封装了底层复杂的 API 调用、参数拼接、响应解析逻辑,让开发者以 “声明式、链式调用” 的方式快速实现同步 / 流式对话、多轮上下文管理、Prompt 模板调用等核心能力,无需关注底层通信细节。
核心定位与价值
-
简化交互成本:无需手动构建 HTTP 请求、解析 JSON 响应,一行链式代码即可完成 “发送 Prompt → 接收 AI 响应” 的全流程;
-
统一交互范式:无论对接 OpenAI、阿里百炼还是其他兼容 OpenAI 协议的模型,ChatClient 提供完全一致的调用方式,切换模型无需修改业务代码;
-
灵活扩展能力:支持绑定对话参数(温度、最大令牌数)、上下文内存(ChatMemory)、Prompt 模板,适配同步对话、流式输出、多轮对话等全场景。
Prompt
Prompt(提示词)是开发者 / 用户向 AI 模型传递意图、约束、要求的结构化指令,也是 Spring AI 与大模型交互的核心载体 —— 它不仅是简单的文本提问,更是一套 “告诉 AI 该做什么、怎么做、输出什么格式” 的完整逻辑表达,直接决定 AI 响应的精准度和贴合度。
在 Spring AI 体系中,Prompt 既可以是纯文本形式(如 “帮我写一份 3 天成都旅行攻略”)
也可以是通过PromptTemplate构建的结构化模板(绑定动态参数、系统提示、格式约束),其核心价值在于将模糊的业务需求转化为 AI 可理解的清晰指令:比如为 “生成家常菜菜谱” 的需求,Prompt 会明确食材、口味、烹饪难度、输出格式(如分点的食材清单 + 步骤),甚至加入否定条件(“不要复杂步骤”)和风格要求(“口语化、新手友好”),让 AI 跳出通用化输出,精准匹配实际场景。
从构成来看,一个优质的 Prompt 通常包含角色定位(如 “资深家常菜大厨”)、核心需求(如 “用土豆和牛肉做微辣家常菜”)、约束条件(如 “3 人份、步骤≤5 步”)、输出格式(如 Markdown 列表)四大要素,而非单一的 “提问式文本”。在 Spring AI 的ChatClient调用中,Prompt 是连接业务逻辑与 AI 能力的桥梁 —— 通过prompt()方法传入后,AI 模型会基于 Prompt 的指令完成思考、推理并生成响应,而PromptTemplate则进一步让 Prompt 实现 “模板化、参数化”,适配旅行攻略、朋友圈文案、菜谱生成等多样化的生活化场景,大幅降低重复编写指令的成本。
Message
介绍
Message(消息)是 Spring AI 构建对话交互的原子级数据载体,也是构成 Prompt 和 Response 的核心基础 —— 它并非简单的文本字符串,而是封装了 “内容 + 角色 + 元数据” 的结构化对象,是大模型理解 “谁在说话、说什么、为什么说” 的最小单元,贯穿从 Prompt 构建到 Response 解析的全流程。
在 Spring AI 体系中,所有对话交互(无论是用户提问、系统提示,还是 AI 回复)都会被封装为 Message 实例,其核心价值在于:通过明确的 “角色标识” 区分对话参与方(用户 / 系统 / AI 助手),通过 “元数据” 携带额外上下文(如消息 ID、时间戳、模型参数),让大模型能够理解对话的语义边界和上下文关联,而非单纯处理无差别的文本。
- 角色(Role):定义消息的 “发送方身份”
角色是 Message 最关键的属性,决定了大模型对消息内容的解读逻辑,Spring AI 中核心角色包括:
- 内容(Content):消息的核心文本 / 数据
即消息的实际内容,可分为两种形式:
- 纯文本:最常见的形式,如用户的提问、系统的提示词、AI 的文本响应;
- 多模态内容:高级场景下支持文本 + 图片 / 文件(如 “分析这张图片中的菜品做法”),Spring AI 通过 MultiModalContent 封装。
- 元数据(Metadata):附加的上下文信息
可选的键值对数据,用于携带业务或技术上下文,典型用途:
- 技术元数据:消息 ID、发送时间戳、关联的会话 ID、模型参数(如本次消息对应的 temperature=0.8);
- 业务元数据:用户 ID、消息优先级、是否需要保留上下文、响应格式要求(如 “返回 Markdown”)。
核心类型与典型用法
Spring AI 围绕 Message 抽象提供了多个具象实现,适配不同对话场景:
- UserMessage:用户消息(最常用)
封装用户发起的指令 / 提问,是构建 Prompt 的核心组件:
public Flux<String> userMessageStream(String message) {
// 创建用户消息
UserMessage userMessage = new UserMessage(message);
// 发送用户消息并返回流式响应
return chatClient.prompt()
.messages(userMessage)
.stream()
.content()
.concatWith(Mono.just("[DONE]")) // 确保最后发送[DONE]标记
.onErrorResume(e -> {
log.error("流式对话异常", e);
return Flux.just("对话出错:" + e.getMessage(), "[DONE]");
});
}
提取公共代码
// 提取的公共方法 - 处理流式对话的核心逻辑
private Flux<String> handleStreamingChat(ChatClient.StreamResponseSpec streamResponseSpec) {
return streamResponseSpec.content()
.concatWith(Mono.just("[DONE]")) // 确保最后发送[DONE]标记
.onErrorResume(e -> {
log.error("流式对话异常", e);
return Flux.just("对话出错:" + e.getMessage(), "[DONE]");
});
}
修改userMessageStream
public Flux<String> userMessageStream(String message) {
// 创建用户消息
UserMessage userMessage = new UserMessage(message);
ChatClient.StreamResponseSpec stream = chatClient
.prompt()
.messages(userMessage)
.stream();
// 发送用户消息并返回流式响应
return handleStreamingChat(stream);
}
- SystemMessage:系统消息(约束 AI 行为)
用于设定 AI 的角色、规则,优先级高于用户消息,是打造精准 Prompt 的关键:
public Flux<String> systemMessageStream(String message) {
// 创建系统消息
SystemMessage systemMessage =
new SystemMessage("你是朋友圈文案创作专家,风格要求沙雕有趣,字数控制在50字以内,带1-2个emoji,不要矫情");
// 创建用户消息
UserMessage userMessage = new UserMessage(message);
// 创建用户消息, 并添加到消息列表
ChatClient.StreamResponseSpec stream = chatClient
.prompt()
.messages(systemMessage, userMessage)
.stream();
// 发送系统消息并返回流式响应
return handleStreamingChat(stream);
}
3.不使用Message
public Flux<String> generateTravelGuide(TravelGuide travelGuide) {
// 生活化的模板,语气亲切,符合日常旅行规划需求
String template = """
你是一位擅长小众路线的旅行规划师,主打「性价比高、人少景美」的非热门路线。
请按以下要求生成{days}天{destination}旅行攻略({travelType}出行,预算{budget}元):
### 核心要求(优先级:性价比>体验>景点数量)
1. 输出格式:按天数分模块,每个模块包含「景点+美食+住宿」,用Markdown列表展示;
2. 景点约束:
- 不推荐XX/XX等网红打卡点;
- 每个景点标注「开放时间+门票+交通方式」;
3. 成本约束:
- 美食人均≤50元,附具体店铺名称;
- 住宿≤200元/晚,按区域推荐(比如古城/郊区);
4. 额外要求:
- 补充1个当地小众玩法(比如菜市场逛吃);
- 语言口语化,像朋友推荐,不要官方话术;
- 严格控制总花费不超预算。
5. 字数200以内。
""";
PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of(
"destination", travelGuide.getDestination(),
"days", travelGuide.getDays(),
"travelType", travelGuide.getTravelType(),
"budget", travelGuide.getBudget()
));
// 发送提示并返回流式响应
return handleStreamingChat(chatClient.prompt(prompt).stream());
}
TravelGuide
@Data
public class TravelGuide {
private String destination; // 目的地
private Integer days; // 天数
private String travelType; // 旅行类型
private String budget; // 预算
}
控制层接口
/**
* 生成旅游指南接口
*/
@PostMapping("/travel/guide")
public Flux<String> generateTravelGuide(@RequestBody TravelGuide travelGuide) {
return chatService.generateTravelGuide(travelGuide);
}
利用Psotman进行测试,测试参数
{
"destination":"成都",
"days": 1,
"travelType": "汽车",
"budget": 200
}
4.最佳实践
实际开发中,可结合「模板化 + 系统消息」提升灵活性,比如给 PromptTemplate 补充系统消息(兼顾规则约束和参数化):
// 融合写法:SystemMessage(角色约束) + PromptTemplate(参数化用户指令)
public Flux<String> generateTravelGuide(TravelGuide travelGuide) {
// 1. 系统消息:定义AI角色(全局约束)
SystemMessage systemMsg = new SystemMessage("你是旅行规划师,主打小众性价比路线,语言口语化像朋友推荐");
// 2. 模板化用户消息:参数化指令
String userTemplate = """
请帮我生成一份{days}天的{destination}旅行攻略,适配{travelType}出行,预算{budget}元左右。
要求:
1. 按天数拆分行程,每天推荐2-3个核心景点;
2. 推荐当地必吃的特色美食(附具体店铺名称+人均消费);
3. 推荐性价比高的住宿(按区域划分,标注价格区间)。
4. 字数200以内。
""";
PromptTemplate userPromptTemplate = new PromptTemplate(userTemplate);
Message userMsg = new UserMessage(userPromptTemplate.create(Map.of(
"destination", travelGuide.getDestination(),
"days", travelGuide.getDays(),
"travelType", travelGuide.getTravelType(),
"budget", travelGuide.getBudget()
)).getContents());
// 3. 组合为Prompt
Prompt prompt = new Prompt(List.of(systemMsg, userMsg));
return handleStreamingChat(chatClient.prompt(prompt).stream());
}
```java
## 提示词技巧
Prompt(提示词)是 Spring AI 与大模型交互的核心,好的 Prompt 能让 AI 输出更精准、贴合需求的结果。
1、**技巧 1:明确 “角色 + 目标”,让 AI 精准定位**
核心逻辑:先给 AI 设定清晰的角色(比如 “旅行规划师”“家常菜大厨”),再明确输出目标,避免 AI 输出偏离方向。
正确示例(旅行攻略)
```java
String template = """
你是一位擅长做小众旅行攻略的资深旅行规划师,主打性价比高、人少景美的路线。
请帮我生成一份{days}天的{destination}旅行攻略,适配{travelType}出行,预算{budget}元左右。
""";
反例(模糊无角色)
String template = """
帮我写个{destination}的旅行攻略,玩{days}天,花{budget}元。
""";
效果差异
- 正例:AI 会以 “小众旅行规划师” 的视角,优先推荐非热门景点,符合 “性价比 + 人少” 的核心需求;
- 反例:AI 可能输出通用的热门景点攻略,忽略 “小众、性价比” 的核心诉求。
2、技巧 2:拆解 “约束条件”,越具体越精准
核心逻辑:把需求拆成 可量化、可执行的约束条件(比如字数、格式、内容维度),避免模糊的 “要详细一点”“要好看一点”。
正确示例(朋友圈文案)
String template = """
请帮我写一条适合朋友圈的文案,要求:
1. 场景:{scene},风格:{style};
2. 字数控制在{wordLimit}字以内(严格遵守);
3. 必须带1-2个贴合场景的emoji,不堆砌;
4. 语言自然不矫情,提供3个不同版本。
""";
反例(模糊约束)
String template = """
帮我写个{scene}的朋友圈文案,要{style}风格,字数别太长,带点emoji。
""";
效果差异
- 正例:AI 会严格控制字数,精准匹配 emoji,且输出 3 个版本供选择;
- 反例:AI 可能输出超长文案,emoji 堆砌(比如🍉🍹☀️🥳),且只有 1 个版本。
3**、技巧 3:指定 “输出格式”,适配业务场景**
核心逻辑:明确要求 AI 按固定格式输出(比如 Markdown、列表、JSON、分点),避免后续解析麻烦。
正确示例(家常菜谱)
String template = """
请以{ingredients}为主要食材,创作一道家常菜,要求按以下格式输出:
### 【菜名】
1. 食材清单(含用量):
- 主料:XXX(XX克)
- 辅料:XXX(XX克)
2. 烹饪步骤(分点,每步不超过2句话):
① XXX
② XXX
3. 烹饪小技巧(1-2条):
- XXX
4. 营养亮点:XXX
""";
反例(无格式要求)
String template = """
用{ingredients}做一道菜,写清楚食材和步骤。
""";
效果差异
- 正例:AI 输出结构化的菜谱,可直接复制到 APP / 文档,甚至后续能解析成 Java 对象;
- 反例:AI 输出大段纯文本,食材和步骤混在一起,阅读和复用成本高。
4、技巧 4:加入 “否定条件”,规避无效输出
核心逻辑:明确告诉 AI “不要做什么”,比如 “不要官方话术”“不要复杂步骤”,进一步缩小输出范围。
正确示例(旅行攻略)
String template = """
请生成{days}天{destination}旅行攻略,要求:
1. 推荐的景点不要包含XX(热门网红打卡点);
2. 住宿推荐不要推荐五星级酒店(超预算);
3. 语言不要用官方旅游宣传话术,口语化像朋友推荐。
""";
反例(无否定条件)
String template = """
生成{days}天{destination}旅行攻略,推荐便宜的住宿和小众景点。
""";
效果差异
- 正例:AI 会精准避开网红点、五星级酒店,输出符合预算和 “小众” 需求的内容;
- 反例:AI 可能仍会夹杂热门景点,或推荐高端民宿,偏离 “便宜” 的核心。
5、技巧 5:使用 “示例引导”,对齐输出风格
核心逻辑:如果对输出风格有明确要求(比如 “沙雕风”“治愈风”),直接给 1 个示例,AI 会更贴合预期。
正确示例(朋友圈文案)
String template = """
请帮我写{scene}的朋友圈文案,风格:{style},要求:
1. 字数{wordLimit}字以内,带1-2个emoji;
2. 参考示例风格:
【沙雕风撸猫示例】:我家逆子今天终于肯让摸了😼,感动到想给它开罐罐!
3. 提供3个版本,不要和示例重复。
""";
反例(无示例)
String template = """
写{scene}的沙雕风朋友圈文案,{wordLimit}字以内,带emoji。
""";
效果差异
- 正例:AI 输出的文案会贴合 “沙雕 + 口语化” 的风格,比如 “撸猫五分钟,挨揍两小时😾,这届猫主子太难伺候了”;
- 反例:AI 可能输出生硬的文案,比如 “今日撸猫,心情愉悦😺,沙雕的一天”,风格不符。
6、技巧 6:分层 “优先级”,突出核心需求
核心逻辑:当需求有多个维度时,明确 “优先级”(比如 “性价比优先 > 景点数量”),避免 AI 顾此失彼。
正确示例(旅行攻略)
String template = """
生成{days}天{destination}旅行攻略,预算{budget}元,要求:
1. 优先级:性价比>景点数量>打卡效率;
2. 每日行程控制在2-3个景点,总花费不超预算;
3. 优先推荐免费/低价景点,美食人均不超50元。
""";
反例(无优先级)
String template = """
生成{days}天{destination}旅行攻略,要便宜、景点多、玩得快。
""";
效果差异
- 正例:AI 会优先选择免费景点,控制美食成本,哪怕景点数量少,也保证不超预算;
- 反例:AI 可能推荐大量景点,导致人均消费超标,偏离 “便宜” 的核心。
7、技巧 7:适配 “模型特性”,优化 Prompt 长度
核心逻辑:
不同模型(比如 qwen-turbo/qwen-plus)对 Prompt 长度的支持不同,遵循 “短而精” 原则:
- 轻量模型(qwen-turbo):Prompt 控制在 500 字以内,核心信息前置;
- 高性能模型(qwen-plus):可适当增加细节,但避免冗余。
优化示例(轻量模型适配)
// 核心信息前置,删减冗余描述
String template = """
角色:小众旅行规划师(性价比优先)
需求:{days}天{destination}@{travelType},预算{budget}元
要求:
1. 每日2-3个小众景点(免费/低价);
2. 美食人均≤50元,住宿≤200元/晚;
3. 口语化,分天数输出。
""";
实战:结合技巧的完整 Prompt 模板(旅行攻略)
public Flux<String> generateTravelGuide(TravelGuide travelGuide) {
// 融合所有核心技巧:角色+约束+格式+否定+优先级
String template = """
你是一位擅长小众路线的旅行规划师,主打「性价比高、人少景美」的非热门路线。
请按以下要求生成{days}天{destination}旅行攻略({travelType}出行,预算{budget}元):
### 核心要求(优先级:性价比>体验>景点数量)
1. 输出格式:按天数分模块,每个模块包含「景点+美食+住宿」,用Markdown列表展示;
2. 景点约束:
- 不推荐XX/XX等网红打卡点;
- 每个景点标注「开放时间+门票+交通方式」;
3. 成本约束:
- 美食人均≤50元,附具体店铺名称;
- 住宿≤200元/晚,按区域推荐(比如古城/郊区);
4. 额外要求:
- 补充1个当地小众玩法(比如菜市场逛吃);
- 语言口语化,像朋友推荐,不要官方话术;
- 严格控制总花费不超预算。
5. 字数200以内。
""";
PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of(
"destination", travelGuide.getDestination(),
"days", travelGuide.getDays(),
"travelType", travelGuide.getTravelType(),
"budget", travelGuide.getBudget()
));
return handleStreamingChat(chatClient.prompt(prompt).stream());
}
总结:Prompt 编写黄金公式
角色定位 + 清晰目标 + 量化约束 + 输出格式 + 否定条件 + 优先级
- 角色定位:让 AI 知道 “我是谁”;
- 清晰目标:让 AI 知道 “要做什么”;
- 量化约束:让 AI 知道 “要做到什么程度”;
- 输出格式:让 AI 知道 “按什么格式输出”;
- 否定条件:让 AI 知道 “不要做什么”;
- 优先级:让 AI 知道 “先满足什么”。
结合这个公式,无论是生活化场景(菜谱、文案),还是技术场景(代码生成、接口文档),都能写出精准的 Prompt,让 Spring AI 的交互效果翻倍。
Response
Response(响应)是 AI 模型接收到 Prompt 指令后返回的结果集合,也是 Spring AI 封装大模型输出的核心数据结构 , 它并非简单的文本字符串,而是包含核心响应内容、元数据、对话上下文、状态信息的完整对象,是开发者获取 AI 输出、解析交互结果的唯一入口。
在 Spring AI 体系中,最核心的响应类型是 ChatResponse(对应对话场景),所有通过 ChatClient 发起的调用(同步 / 流式)最终都会落地为 ChatResponse 或其衍生形式,其核心价值在于:将大模型返回的原始 JSON 响应(如 OpenAI / 阿里百炼的 API 响应)封装为标准化、易解析的 Java 对象,屏蔽不同模型的响应格式差异,让开发者无需关注底层协议细节,直接通过统一 API 提取所需信息。
Response 的核心构成
以 ChatResponse 为例,一个完整的 ChatResponse 包含三大核心部分,覆盖从 “内容” 到 “元信息” 的全维度数据:
1**. 核心响应内容(最常用)**
即 AI 生成的文本结果,也是开发者最常提取的部分,可通过 content() 快速获取:
// 简化调用:直接提取核心文本
String text = chatClient.prompt("帮我写一句下午茶文案").call().content();
// 完整获取 ChatResponse 后提取内容
ChatResponse response = chatClient.prompt("帮我写一句下午茶文案").call().chatResponse();
String text = response.getResult().getOutput().getContent();
2. 元数据(辅助信息)
包含 AI 生成响应的关键技术参数,用于监控、调试或优化交互效果,核心包括:
- token 统计:生成响应消耗的令牌数(prompt_tokens/response_tokens/total_tokens),可用于成本核算;
- 模型信息:生成响应的具体模型(如 qwen-plus/qwen-turbo);
- 完成状态:响应是否完整生成(如 finish_reason,值为 stop 表示正常完成,length 表示因令牌数限制截断);
- 响应 ID:唯一标识单次交互的 ID,用于问题排查。
示例提取元数据:
ChatResponse response = chatClient.prompt("生成3天成都旅行攻略").call().chatResponse();
// 获取 token 统计
TokenUsage tokenUsage = response.getMetadata().get(TokenUsage.METADATA_KEY, TokenUsage.class);
log.info("消耗提示词令牌:{},生成响应令牌:{}", tokenUsage.getPromptTokens(), tokenUsage.getCompletionTokens());
// 获取完成状态
String finishReason = response.getResult().getMetadata().get("finish_reason", String.class);
3. 对话上下文标识
包含 conversationId(会话 ID)、role(角色标识,如 assistant 表示 AI 响应)等信息,用于多轮对话中关联上下文,配合 ChatMemory 实现连续交互。
Response 的两种核心形态
同步 vs 流式
Spring AI 针对不同交互场景,将 Response 封装为两种易用形态,适配不同业务需求:
- 同步响应(完整结果)
适用于 “一次性获取完整结果” 的场景(如生成菜谱、旅行攻略),通过 call() 触发,返回完整的 ChatResponse:
// 同步获取完整响应(包含所有内容+元数据)
ChatResponse fullResponse = chatClient.prompt("用土豆牛肉做微辣家常菜").call().chatResponse();
// 提取核心文本
String recipe = fullResponse.getResult().getOutput().getContent();
// 提取元数据
String model = fullResponse.getMetadata().get("model", String.class);
- 流式响应(逐段结果)
适用于 “实时展示生成过程” 的场景(如聊天机器人打字机效果、长文本生成),通过 stream() 触发,返回 Flux(响应式流),每一个流元素都是一段碎片化的响应:
// 流式获取响应(逐字/逐句返回)
Flux<String> streamContent = chatClient.prompt("讲一个50字的暖心小故事")
.stream()
.content(); // 直接提取流式文本
// 消费流式响应
streamContent.subscribe(
chunk -> log.info("收到流式片段:{}", chunk), // 每收到一段内容触发
error -> log.error("流式响应异常:{}", error), // 异常处理
() -> log.info("流式响应完成") // 完成回调
);
流式响应的核心特点是 “边生成、边返回”,每个片段都是一个迷你 ChatResponse,最终拼接为完整文本,适合前端实时渲染场景。
与 Prompt 的关联逻辑
Prompt 是 “输入指令”,Response 是 “输出结果”,二者形成完整的交互闭环:
- 开发者通过 Prompt 传递意图(如 “生成 3 人份土豆牛肉菜谱”);
- AI 模型基于 Prompt 生成原始响应(包含文本 + 元数据);
- Spring AI 将原始响应封装为 ChatResponse;
- 开发者从 ChatResponse 中提取核心内容、元数据,完成业务逻辑处理。

AiException
AiException 是 Spring AI 框架为大模型交互场景量身定制的核心异常体系,是所有 AI 相关错误的统一封装载体 —— 它并非单一异常类,而是以 AiException 为父类,涵盖模型调用失败、参数非法、响应解析错误、限流超时等细分场景的异常家族,核心作用是将底层复杂的错误(如 HTTP 连接失败、模型 API 报错、JSON 解析异常)转化为开发者可理解、可处理的标准化异常,屏蔽不同 AI 模型的错误格式差异。
AiException 的核心定位与价值
在 Spring AI 调用流程中(ChatClient → ChatModel → 大模型 API),任何环节的错误都会被封装为 AiException 及其子类,而非抛出原生的 RestClientException、JsonParseException 等底层异常,其核心价值体现在:
- 统一异常范式:无论对接 OpenAI、阿里百炼还是百度文心一言,所有 AI 交互错误都以 AiException 体系抛出,开发者无需针对不同模型适配不同异常类型;
- 错误语义化:异常信息中包含 “错误类型 + 业务上下文”(如 “模型调用失败:API 密钥无效 | 会话 ID:user001”),而非单纯的 “连接超时”,便于快速定位问题;
- 可定制化处理:支持捕获细分异常(如 AiResponseException、AiConnectionException),实现差异化的错误处理逻辑(如密钥错误提示用户更换、限流错误触发重试)。
AiException 的核心分类与典型场景
Spring AI 的 AiException 体系包含多个细分子类,覆盖 AI 交互全链路错误,以下是开发中最常见的类型及触发场景:
- 基础父类:AiException
所有 AI 相关异常的根类,可作为 “兜底捕获” 的异常类型,适用于无需区分具体错误场景的通用处理(如统一返回 “AI 服务暂不可用”)。
- 细分核心子类(高频场景)

AiException 的捕获与处理 【待定】
在 Spring AI 开发中,通常通过「全局异常处理器 + 局部 try-catch」结合的方式处理 AiException,以下是贴合实际业务的示例:
- 局部捕获(针对性处理)
适用于需要对特定 AI 调用做差异化处理的场景(如旅行攻略生成失败时提示用户简化需求):
@Service
public class PromptTemplateService {
private final ChatClient chatClient;
// 生成旅行攻略:捕获细分异常并差异化处理
public String generateTravelGuide(String destination, Integer days, String travelType, String budget) {
String template = "你是资深旅行规划师...";
PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of("destination", destination, "days", days, "travelType", travelType, "budget", budget));
try {
return chatClient.prompt(prompt).call().content();
} catch (AiAuthenticationException e) {
// 鉴权错误:提示管理员检查 API 密钥
log.error("AI 鉴权失败:{}", e.getMessage(), e);
return "服务异常:AI 密钥无效,请联系管理员处理";
} catch (AiResourceExhaustedException e) {
// 限流/配额不足:提示用户稍后重试
log.error("AI 资源耗尽:{}", e.getMessage(), e);
return "当前调用量过大,请5分钟后重试";
} catch (AiInvalidArgumentException e) {
// 参数错误:提示用户修正输入(如天数为负数、预算非数字)
log.error("AI 参数错误:{}", e.getMessage(), e);
return "输入参数无效:" + e.getMessage() + ",请检查后重新提交";
} catch (AiException e) {
// 兜底捕获所有 AI 异常
log.error("AI 交互失败:{}", e.getMessage(), e);
return "旅行攻略生成失败,请稍后重试";
}
}
}
2. 全局异常处理器(统一返回格式)
适用于整个项目的 AI 异常统一封装,避免重复编写 try-catch 逻辑(结合 Spring Boot 全局异常处理):
@RestControllerAdvice
public class AiGlobalExceptionHandler {
// 捕获所有 AiException 及其子类
@ExceptionHandler(AiException.class)
public ResponseEntity<Result<String>> handleAiException(AiException e) {
// 构建标准化错误响应
Result<String> errorResult = Result.<String>builder()
.code(500)
.msg("AI 服务异常:" + e.getMessage())
.data(null)
.build();
// 针对细分异常调整响应码(如鉴权错误返回 401,参数错误返回 400)
if (e instanceof AiAuthenticationException) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResult);
} else if (e instanceof AiInvalidArgumentException) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResult);
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}
}
// 兜底捕获所有未处理异常
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<String>> handleException(Exception e) {
Result<String> errorResult = Result.<String>builder()
.code(500)
.msg("系统异常:" + e.getMessage())
.data(null)
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}
}
// 配套的统一响应体
@Data
@Builder
public class Result<T> {
private Integer code;
private String msg;
private T data;
}
会话记忆
ChatMemory(对话记忆)是 Spring AI 为多轮对话场景设计的核心组件,本质是 “对话消息的持久化 / 缓存管理器”—— 它自动存储、维护、检索历史 Message 集合,让 AI 能够 “记住” 之前的对话内容,实现连续、上下文关联的交互,而非每次调用都 “从零开始”。
在无 ChatMemory 的情况下,AI 每次调用都是独立的(“失忆式交互”):比如先问 “推荐大理小众景点”,再问 “这些景点附近有便宜住宿吗?”,AI 无法关联前序问题;而接入 ChatMemory 后,它会自动将历史消息(用户提问 + AI 回复)传入新的 Prompt,让 AI 理解上下文,实现 “有记忆的对话”。
基于Redis的会话记忆
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis配置类
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
//设置工厂
template.setConnectionFactory(factory);
//创建对象映射
ObjectMapper mapper = new ObjectMapper();
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
//(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(mapper, Object.class);
//指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
mapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
//指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer
//等会抛出异常
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//
//字符串序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
//hash的key也采用String的方式
template.setHashKeySerializer(stringRedisSerializer);
//value采用jackson的方式
template.setValueSerializer(jackson2JsonRedisSerializer);
//hash的value也采用Jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
ChatMemory实现类
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class RedisChatMemory {
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
// 统一角色常量为大写(匹配MessageType)
public static final String ROLE_USER = MessageType.USER.name(); // "USER"
public static final String ROLE_ASSISTANT = MessageType.ASSISTANT.name(); // "ASSISTANT"
public static final int DEFAULT_LAST_N = 20;
/**
* 添加消息
*/
public void add(String conversationId, String message, String role) {
// 1. 角色校验(使用统一的常量)
if (!ROLE_USER.equals(role) && !ROLE_ASSISTANT.equals(role)) {
throw new IllegalArgumentException("角色必须是'" + ROLE_USER + "'或'" + ROLE_ASSISTANT + "'");
}
// 2. 获取【完整】历史数据(新增方法,不截断)
List<Message> fullHistory = getFullHistory(conversationId);
// 3. 追加新消息
fullHistory.add(ROLE_USER.equals(role) ? new UserMessage(message) : new AssistantMessage(message));
// 4. 转换为DTO(
List<RedisChatMsgItem> RedisChatMsgItemS = fullHistory.stream()
.map(msg -> new RedisChatMsgItem(msg.getMessageType().name(), msg.getText()))
.collect(Collectors.toList());
// 5. 写入Redis(覆盖为完整的新历史)
try {
redisTemplate.opsForValue().set(
conversationId,
objectMapper.writeValueAsString(RedisChatMsgItemS),
Duration.ofDays(1)
);
} catch (Exception e) {
throw new RuntimeException("存储聊天历史失败", e); // 不建议只打印,需抛出异常
}
}
/**
* 读取完整的历史消息
*/
private List<Message> getFullHistory(String conversationId) {
String messages = (String) redisTemplate.opsForValue().get(conversationId);
if (Objects.isNull(messages)) {
return new ArrayList<>();
}
try {
List<RedisChatMsgItem> RedisChatMsgItemS = objectMapper.readValue(messages, new TypeReference<>() {});
return RedisChatMsgItemS.stream()
.map(dto -> {
if (ROLE_USER.equals(dto.getRole())) {
return new UserMessage(dto.getContent());
} else if (ROLE_ASSISTANT.equals(dto.getRole())) {
return new AssistantMessage(dto.getContent());
} else {
throw new IllegalArgumentException("未知角色:" + dto.getRole());
}
})
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("读取聊天历史失败", e);
}
}
/**
* 返回截断后的历史(仅用于展示,不影响追加逻辑)
*/
public List<Message> get(String conversationId, int lastN) {
List<Message> fullHistory = getFullHistory(conversationId);
// 仅在查询时截断,不修改完整历史
if (fullHistory.size() > lastN) {
return fullHistory.subList(fullHistory.size() - lastN, fullHistory.size())
.stream()
.collect(Collectors.toList()); // 转换为普通List,避免SubList视图问题
}
return new ArrayList<>(fullHistory); // 返回新List,防止外部修改
}
public void clear(String conversationId) {
redisTemplate.delete(conversationId);
}
public List<Message> get(String conversationId) {
return get(conversationId, DEFAULT_LAST_N);
}
// 内部类:Redis存储的消息项(角色+内容)
@Data
public static class RedisChatMsgItem {
private String role;
private String content;
public RedisChatMsgItem() {}
public RedisChatMsgItem(String role, String content) {
this.role = role;
this.content = content;
}
}
}
聊天参数对象:封装会话id
@Data
public class SessionMessage {
private String conversationId;
private String message;
}
聊天服务实现
// 注入redis聊天对象
private final RedisChatMemory redisChatMemory;
public Flux<String> chatStream(SessionMessage sessionMessage) {
log.info("发送流式消息到阿里百炼: {}", sessionMessage.getMessage());
if (sessionMessage.getConversationId() == null) {
sessionMessage.setConversationId("default");
}
// 步骤1:读取Redis中的历史消息
List<Message> historyMessages = redisChatMemory.get(sessionMessage.getConversationId(), RedisChatMemory.DEFAULT_LAST_N);
// 步骤2:构建完整的消息列表(历史 + 本次新用户消息)
List<Message> allMessages = new ArrayList<>(historyMessages);
allMessages.add(new UserMessage(sessionMessage.getMessage())); // 核心:把本次新消息加入列表
// 步骤3:将本次新消息存入Redis(持久化)
redisChatMemory.add(sessionMessage.getConversationId(), sessionMessage.getMessage(), MessageType.USER.name());
// 步骤4:调用AI时传入完整的消息列表(历史+新消息)
ChatClient.StreamResponseSpec streamResponseSpec = chatClient.prompt()
.user(sessionMessage.getMessage()) // 满足ChatClient校验
.messages(allMessages) // 核心:传入包含本次新消息的完整列表
.stream();
return handleStreamingChat(sessionMessage.getConversationId(), streamResponseSpec);
}
处理流式对话
// 提取的公共方法 - 处理流式对话的核心逻辑
private Flux<String> handleStreamingChat(String conversationId, ChatClient.StreamResponseSpec streamResponseSpec) {
// 临时收集AI回复片段
List<String> aiResponseSegments = new ArrayList<>();
Flux<String> contentFlux = streamResponseSpec.content()
.filter(content -> content != null && !content.trim().isEmpty())
.doOnNext(segment -> aiResponseSegments.add(segment)) // 收集片段
.onErrorResume(e -> {
log.error("流式对话异常", e);
return Flux.just("对话出错:" + e.getMessage());
});
// 最后拼接完整回复存入Redis(仅存一次)
return contentFlux.concatWith(Mono.fromRunnable(() -> {
if (!aiResponseSegments.isEmpty()) {
String fullResponse = String.join("", aiResponseSegments);
redisChatMemory.add(conversationId, fullResponse, RedisChatMemory.ROLE_ASSISTANT);
}
}))
.concatWith(Mono.just("[DONE]"));
}
消息持久化
导入依赖
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
数据库、mybatis-plus配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mall_116?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root1234
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true # 开启下划线转驼峰命名
建表sql
create table chat_record(
id bigint primary key,
conversation_id varchar(64),
role varchar(32),
content text,
create_time datetime
);
实体类
@Data
@TableName("chat_record")
public class ChatRecord {
@TableId(type = IdType.ASSIGN_ID)
private Long id; // 主键
private String conversationId; // 会话ID
private String role; // 角色:USER/ASSISTANT
private String content; // 消息内容
private LocalDateTime createTime; // 创建时间
}
线程池异步
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync // 必须开启异步注解支持
public class AsyncThreadPoolConfig {
/**
* 对话记录落库专用线程池(指定bean名称,供@Async引用)
*/
@Bean(name = "chatTaskExecutor") // 命名为chatTaskExecutor(WebMvc默认优先识别这个名称)
public ThreadPoolTaskExecutor chatTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数(根据服务器配置调整,比如4核8G设为8)
// 获取到当前服务器CPU核心数
int cpus = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(cpus);
// 最大线程数(核心线程忙不过来时,最多扩容到这个数)
executor.setMaxPoolSize(cpus * 2);
// 队列容量(核心线程满了,任务先入队列,避免直接创建新线程)
executor.setQueueCapacity(1000);
// 线程空闲时间(超过60秒空闲的非核心线程会被销毁)
executor.setKeepAliveSeconds(60);
// 线程命名前缀(便于日志排查问题)
executor.setThreadNamePrefix("chat-async-");
// 拒绝策略(队列满+最大线程数满时,让提交任务的线程执行,避免任务丢失)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化线程池(必须调用,否则线程池不生效)
executor.initialize();
return executor;
}
}
配置到mvc
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC 异步请求配置(解决默认SimpleAsyncTaskExecutor告警)
*/
@Configuration
@RequiredArgsConstructor
public class WebMvcAsyncConfig implements WebMvcConfigurer {
// 注入自定义的线程池(指定bean名称)
private final ThreadPoolTaskExecutor chatTaskExecutor;
/**
* 配置MVC异步请求的线程池
*/
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 1. 指定MVC异步请求使用的线程池
configurer.setTaskExecutor(chatTaskExecutor);
// 2. 可选:设置异步请求超时时间(默认30秒,根据业务调整)
configurer.setDefaultTimeout(60 * 1000); // 60秒
}
}
mapper
@Mapper
public interface ChatRecordMapper extends BaseMapper<ChatRecord> {
}
service
public interface ChatRecordService extends IService<ChatRecord> {
public void asyncSaveChatRecord(String conversationId, String role, String content);
}
实现类
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatRecordServiceImpl extends ServiceImpl<ChatRecordMapper, ChatRecord> implements ChatRecordService {
private final ChatRecordMapper chatRecordMapper;
/**
* 异步保存对话记录到数据库(核心:@Async指定线程池)
* @param conversationId 会话ID
* @param role 角色(USER/ASSISTANT)
* @param content 消息内容
*/
@Override
@Async("chatTaskExecutor") // 绑定自定义线程池
public void asyncSaveChatRecord(String conversationId, String role, String content) {
try {
ChatRecord record = new ChatRecord();
record.setConversationId(conversationId);
record.setRole(role);
record.setContent(content);
record.setCreateTime(LocalDateTime.now());
chatRokenException e) {
log.error("异步落库失败 线程id:{} | 会话ID:{} | 角色:{}", Thread.currentThread().getId(), conversationId, role, e);
}
}
}
修改聊天服务,注入ChatRecordService,新增落库代码调用
private final ChatRecordService chatRecordService;
@Override
public Flux<String> chatStream(SessionMessage sessionMessage) {
log.info("发送流式消息到阿里百炼: {}", sessionMessage.getMessage());
if (sessionMessage.getConversationId() == null) {
sessionMessage.setConversationId("default");
}
// 步骤1:读取Redis中的历史消息
List<Message> historyMessages = redisChatMemory.get(sessionMessage.getConversationId(), RedisChatMemory.DEFAULT_LAST_N);
// 步骤2:构建完整的消息列表(历史 + 本次新用户消息)
List<Message> allMessages = new ArrayList<>(historyMessages);
allMessages.add(new UserMessage(sessionMessage.getMessage())); // 核心:把本次新消息加入列表
// 步骤3:将本次新消息存入Redis(持久化)
redisChatMemory.add(sessionMessage.getConversationId(), sessionMessage.getMessage(), MessageType.USER.name());
// 持久化到数据库
chatRecordService.asyncSaveChatRecord(sessionMessage.getConversationId(), RedisChatMemory.ROLE_USER, sessionMessage.getMessage());
// 步骤4:调用AI时传入完整的消息列表(历史+新消息)
ChatClient.StreamResponseSpec streamResponseSpec = chatClient.prompt()
.user(sessionMessage.getMessage()) // 满足ChatClient校验
.messages(allMessages) // 核心:传入包含本次新消息的完整列表
.stream();
return handleStreamingChat(sessionMessage.getConversationId(), streamResponseSpec);
}
// 提取的公共方法 - 处理流式对话的核心逻辑
private Flux<String> handleStreamingChat(String conversationId, ChatClient.StreamResponseSpec streamResponseSpec) {
// 临时收集AI回复片段
List<String> aiResponseSegments = new ArrayList<>();
Flux<String> contentFlux = streamResponseSpec.content()
.filter(content -> content != null && !content.trim().isEmpty())
.doOnNext(segment -> aiResponseSegments.add(segment)) // 收集片段
.onErrorResume(e -> {
log.error("流式对话异常", e);
return Flux.just("对话出错:" + e.getMessage());
});
// 最后拼接完整回复存入Redis(仅存一次)
return contentFlux.concatWith(Mono.fromRunnable(() -> {
if (!aiResponseSegments.isEmpty()) {
String fullResponse = String.join("", aiResponseSegments);
redisChatMemory.add(conversationId, fullResponse, RedisChatMemory.ROLE_ASSISTANT);
// 持久化到数据库
chatRecordService.asyncSaveChatRecord(conversationId, RedisChatMemory.ROLE_ASSISTANT, fullResponse);
}
}))
.concatWith(Mono.just("[DONE]"));
}
测试
问题1:介绍一下成都,50字以内
问题二:有哪些美食,50字以内
问题三:推荐5家火锅,50字以内
更多推荐




所有评论(0)