1.对话机器人
1.1对话机器人-初步实现
1.1.1引入依赖
xml
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<!-- 核心:引入 Spring AI BOM,统一管理所有相关依赖版本 -->
<dependencyManagement>
<dependencies>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>aliyun-releases</id>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1.1.2配置模型信息
yml
# ollama方式连接
spring:
application:
name: ai-demo
ai:
ollama:
base-url: http://localhost:11434 # ollama服务地址, 这就是默认值
chat:
model: deepseek-r1:7b # 模型名称
options:
temperature: 0.8 # 模型温度,影响模型生成结果的随机性,越小越稳定
# api方式连接
spring:
application:
name: ai-demo # 应用名称归属 spring.application 节点
ai:
openai:
base-url: https://api.deepseek.com
api-key: 8888
chat:
options:
model: deepseek-chat
temperature: 0.8 # 模型温度,缩进对齐 options 子节点
1.1.3编写配置类CommonConfiguration
java
public org.springframework.ai.chat.client.ChatClient chatClient(OpenAiChatModel openAiChatModel,ChatMemory chatMemory){
return org.springframework.ai.chat.client.ChatClient.builder(openAiChatModel)
.defaultSystem("你是一个智能助手,名字叫煋玥")
.build();
}
1.1.4同步调用
同步调用,需要所有响应结果全部返回后才能返回给前端。
启动项目,在浏览器中访问:http://localhost:8080/ai/chat?prompt=你好
java
@RestController
@RequestMapping(value = "/ChatController")
public class ChatController {
@Autowired
ChatClient client;
@GetMapping("/chat")
String generation(String question) {
return client.prompt()
.user(question)
.call()
.content();
}
}
1.1.5流式调用
SpringAI中使用了WebFlux技术实现流式调用
java
@GetMapping(value = "/chatStream", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestParam(value = "question") String question, @RequestParam("chatId") String chatId){
return client.prompt()
.user(question)
.stream()
.content();
}
1.2对话机器人-日志功能
1.2.1添加日志
修改CommonConfiguration,给ChatClient添加日志Advisor
java
@Bean //ChatClient
public org.springframework.ai.chat.client.ChatClient chatClient(OpenAiChatModel openAiChatModel,ChatMemory chatMemory){
return org.springframework.ai.chat.client.ChatClient.builder(openAiChatModel)
.defaultSystem("你是一个智能助手,名字叫煋玥")
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
1.2.2修改日志级别
在application.yaml中添加日志配置,更新日志级别:
yml
logging:
level:
org.springframework.ai: debug # AI对话的日志级别
com.itheima.ai: debug # 本项目的日志级别
1.3会话记忆功能
1.3.1实现原理
让AI有会话记忆的方式就是把每一次历史对话内容拼接到Prompt中,一起发送过去。
我们并不需要自己来拼接,SpringAI自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求AI时会自动拼接,非常方便。
1.3.2注册ChatMemory对象(与视频有变动)
ChatMemory接口声明如下
java
public interface ChatMemory {
// TODO: consider a non-blocking interface for streaming usages
default void add(String conversationId, Message message) {
this.add(conversationId, List.of(message));
}
// 添加会话信息到指定conversationId的会话历史中
void add(String conversationId, List<Message> messages);
// 根据conversationId查询历史会话
List<Message> get(String conversationId, int lastN);
// 清除指定conversationId的会话历史
void clear(String conversationId);
}
可以看到,所有的会话记忆都是与conversationId有关联的,也就是会话Id,将来不同会话id的记忆自然是分开管理的。
与视频讲解中不同的是,SpirngAI中,ChatMemory的实现,现在统一为:MessageWindowChatMemory
在CommonConfiguration中注册ChatMemory对象:
java
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository()) // 设置存储库
.maxMessages(10) // 记忆窗口大小(保留最近的10条消息)
.build();
}
# 也可以直接
@Bean
public ChatMemory chatMemory() {
// 使用 MessageWindowChatMemory 作为默认内存策略(窗口消息保留)
return MessageWindowChatMemory.builder().build();
}
可以去查看MessageWindowChatMemory的源码
chatMemoryRepository:可以设置存储库,例如Redis,这里的InMemory是保存到内存中
maxMessages:设置窗口大小,指拼接prompt的时候将最近的多少条数据一起发送
MessageWindowChatMemory默认使用的存储库就是InMemory,默认窗口大小是20
想要使用其他的存储库,在1.5.3里有通过数据库的方式进行存储
1.3.3添加会话记忆Advisor
bash
@Bean //ChatClient
public org.springframework.ai.chat.client.ChatClient chatClient(OpenAiChatModel openAiChatModel,ChatMemory chatMemory){
return org.springframework.ai.chat.client.ChatClient.builder(openAiChatModel)
.defaultSystem("你是一个智能助手,名字叫煋玥")
.defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
1.3.4设置会话id
java
@GetMapping(value = "/chatStream", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestParam(value = "question") String question, @RequestParam("chatId") String chatId){
return client.prompt()
.user(question)
.advisors(advisorSpec -> advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId))
.stream()
.content();
}
1.3.5 会话记忆持久化
继承重写implements ChatMemory
java
package com.ruoyi.xingyueai.component;
import com.ruoyi.xingyueai.entity.ChatMemoryEntity;
import com.ruoyi.xingyueai.service.ChatMemoryService;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 基于 MySQL + MyBatis-Plus 的 ChatMemory 实现(持久化会话记忆)
* @author xingyue
*/
@Component
public class MySqlChatMemory implements ChatMemory {
private final ChatMemoryService chatMemoryService;
// 构造注入 Service
public MySqlChatMemory(ChatMemoryService chatMemoryService) {
this.chatMemoryService = chatMemoryService;
}
/**
* 单条消息保存(ChatMemory 接口必须实现的抽象方法)
* @param conversationId 会话ID
* @param message 单条消息
*/
@Override
public void add(String conversationId, Message message) {
// 1. 转换消息角色
String role = switch (message.getMessageType()) {
case USER -> "user";
case ASSISTANT -> "assistant";
case SYSTEM -> "system";
default -> "unknown";
};
// 2. 调用简化后的工具方法,提取文本内容(适配当前 Content 接口)
String content = extractTextContentFromMessage(message);
// 3. 非空判断,调用 Service 保存到数据库
if (!content.isBlank()) {
chatMemoryService.saveChatRecord(conversationId, role, content);
}
}
/**
* 批量保存会话消息
* @param conversationId 会话ID
* @param messages 消息列表
*/
@Override
public void add(String conversationId, List<Message> messages) {
// 调用单参数 add 方法,批量处理每条消息(无递归,安全可靠)
messages.forEach(message -> this.add(conversationId, message));
}
/**
* 单参数获取所有消息(ChatMemory 接口必须实现的抽象方法)
* @param conversationId 会话ID
* @return 该会话的所有消息列表
*/
public List<Message> get(String conversationId) {
// 1. 从数据库查询该会话的所有历史记录(按创建时间正序)
List<ChatMemoryEntity> chatRecords = chatMemoryService.getChatRecordsByChatId(conversationId);
if (chatRecords.isEmpty()) {
return new ArrayList<>();
}
// 2. 转换为 Spring AI 的 Message 列表,返回给上层调用
return chatRecords.stream()
.map(record -> {
return switch (record.getRole()) {
case "user" -> new UserMessage(record.getContent());
case "assistant" -> new org.springframework.ai.chat.messages.AssistantMessage(record.getContent());
case "system" -> new SystemMessage(record.getContent());
default -> new UserMessage(record.getContent());
};
})
.collect(Collectors.toList());
}
/**
* 获取指定会话的最后 N 条消息
* @param conversationId 会话ID
* @param lastN 最后 N 条消息
* @return 截取后的消息列表
*/
@Override
public List<Message> get(String conversationId, int lastN) {
// 调用单参数 get 方法,获取所有消息(无递归,打破无限循环)
List<Message> allMessages = this.get(conversationId);
if (allMessages.isEmpty() || lastN <= 0) {
return new ArrayList<>();
}
// 截取最后 N 条消息,处理边界情况避免数组越界
int startIndex = Math.max(0, allMessages.size() - lastN);
return allMessages.subList(startIndex, allMessages.size());
}
/**
* 清空指定会话的记忆
* @param conversationId 会话ID
*/
@Override
public void clear(String conversationId) {
chatMemoryService.clearChatMemory(conversationId);
}
/**
* 工具方法:从 Message 中提取纯文本内容(适配当前 Content 接口,直接调用 getText())
* @param message 对话消息
* @return 纯文本内容
*/
private String extractTextContentFromMessage(Message message) {
// 核心修正:直接调用 Content 接口的 getText() 方法,无需 ContentItem
String text = message.getText();
// 非空保护,避免返回 null 导致后续数据库操作异常
return text == null ? "" : text.trim();
}
}
修改ChatClient配置
java
@Bean
public MySqlChatMemory chatMemory(ChatMemoryService chatMemoryService) {
return new MySqlChatMemory(chatMemoryService);
}
/**
* 构建 ChatClient OpenAiChatModel
*/
@Bean
public ChatClient chatClient(OpenAiChatModel openAiChatModel, MySqlChatMemory mySqlChatMemory) {
return ChatClient.builder(openAiChatModel)
.defaultSystem("你是一个智能助手,名字叫煋玥")
.defaultAdvisors(
new SimpleLoggerAdvisor(), // 日志顾问
MessageChatMemoryAdvisor.builder(mySqlChatMemory).build() // 绑定自定义持久化记忆
)
.build();
}
实体类
java
@Data
@TableName("chat_memory")
public class ChatMemoryEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 会话唯一标识
*/
private String chatId;
/**
* 角色(user/assistant/system)
*/
private String role;
/**
* 对话内容
*/
private String content;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除(0=未删除,1=已删除)
*/
@TableLogic
private Integer deleted;
}
mapper
java
@Mapper
public interface ChatMemoryMapper extends BaseMapper<ChatMemoryEntity> {
/**
* 根据会话ID查询对话记录(按创建时间正序)
* @param chatId 会话唯一标识
* @return 对话记录列表
*/
List<ChatMemoryEntity> selectByChatId(@Param("chatId") String chatId);
}
service
java
public interface ChatMemoryService extends IService<ChatMemoryEntity> {
/**
* 根据会话ID查询对话记录
* @param chatId 会话唯一标识
* @return 对话记录列表
*/
List<ChatMemoryEntity> getChatRecordsByChatId(String chatId);
/**
* 保存会话记录
* @param chatId 会话唯一标识
* @param role 角色
* @param content 对话内容
*/
void saveChatRecord(String chatId, String role, String content);
/**
* 清空指定会话的记忆
* @param chatId 会话唯一标识
*/
void clearChatMemory(String chatId);
}
serviceimpl
java
@Service
public class ChatMemoryServiceImpl extends ServiceImpl<ChatMemoryMapper, ChatMemoryEntity> implements ChatMemoryService {
@Override
public List<ChatMemoryEntity> getChatRecordsByChatId(String chatId) {
return baseMapper.selectByChatId(chatId);
}
@Override
public void saveChatRecord(String chatId, String role, String content) {
ChatMemoryEntity entity = new ChatMemoryEntity();
entity.setChatId(chatId);
entity.setRole(role);
entity.setContent(content);
this.save(entity);
}
@Override
public void clearChatMemory(String chatId) {
// 此处可实现物理删除或逻辑删除,推荐逻辑删除
List<ChatMemoryEntity> records = this.getChatRecordsByChatId(chatId);
if (!records.isEmpty()) {
records.forEach(record -> record.setDeleted(1));
this.updateBatchById(records);
}
}
}
controller
jaca
@GetMapping("/getAllHistoryAll")
public Result<List<Message>> getAllHistory(@RequestParam @NotBlank(message = "会话ID不能为空") String chatId) {
try {
// 调用 MySqlChatMemory 的 get 方法,传入 lastN 为极大值(获取全部消息)
// 也可直接复用 mySqlChatMemory 中的数据库查询逻辑(chatMemoryService.getChatRecordsByChatId)
List<Message> allHistory = mySqlChatMemory.get(chatId, Integer.MAX_VALUE);
return Result.success(allHistory);
} catch (Exception e) {
return Result.error("查询全部历史消息失败:" + e.getMessage());
}
}
@GetMapping("/getAllHistorylast")
public Result<List<Message>> getLastNHistory(@Validated HistoryQueryDTO historyQueryDTO) {
try {
String conversationId = historyQueryDTO.getChatId();
Integer lastN = historyQueryDTO.getLastN();
// 若 lastN 为 null,默认返回全部;否则返回指定条数
List<Message> lastNHistory = mySqlChatMemory.get(conversationId, lastN == null ? Integer.MAX_VALUE : lastN);
return Result.success(lastNHistory);
} catch (Exception e) {
return Result.error("查询最后N条历史消息失败:" + e.getMessage());
}
}
mysql
sql
CREATE TABLE chat_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL, -- 如 USER, ASSISTANT
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
1.4会话历史功能
service
java
public interface ChatHistoryService {
/**
* 保存聊天记录
* @param type 业务类型,如:chat,service,pdf
* @param chatId 聊天会话ID
*/
void save(String type, String chatId);
/**
* TODO 删除聊天记录
* @param type
* @param chatId
*/
void delete(String type, String chatId);
/**
* 获取聊天记录
* @param type 业务类型,如:chat,service,pdf
* @return 会话ID列表
*/
List<String> getChatIds(String type);
}
实现类内存存储
java
@Repository
public class InMemoryChatHistoryServiceImpl implements ChatHistoryService {
private final Map<String, List<String>> chatHistory = new HashMap<>();
/**
* 实现保存聊天记录功能
* @param type
* @param chatId
*/
@Override
public void save(String type, String chatId) {
/*if (!chatHistory.containsKey(type))
{
chatHistory.put(type, new ArrayList<>());
}
List<String> chatIds = chatHistory.get(type);
以上代码可以简化为下面一行代码
*/
List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
if (chatIds.contains(chatId))
{
return;
}
chatIds.add(chatId);
}
/**
* TODO 实现删除功能
* @param type
* @param chatId
*/
@Override
public void delete(String type, String chatId) {
}
/**
* 实现获取聊天记录功能
* @param type
* @return
*/
@Override
public List<String> getChatIds(String type) {
/*if (!chatHistory.containsKey(type))
{
return new ArrayList<>();
}
return chatHistory.get(type);
简化为以下一行代码
*/
return chatHistory.getOrDefault(type, new ArrayList<>());
}
}
实现类mysql存储
java
@Repository
public class InSqlChatHistoryServiceImpl implements ChatHistoryService {
@Autowired
private ChatHistoryMapper chatHistoryMapper;
/**
* 保存chatId到数据库
* @param type 业务类型,如:chat,service,pdf
* @param chatId 聊天会话ID
*/
@Override
public void save(String type, String chatId) {
// 先查询是否已存在
if (exists(type, chatId)) return;
ChatHistory chatHistory = new ChatHistory();
chatHistory.setType(type);
chatHistory.setChatId(chatId);
chatHistoryMapper.insert(chatHistory);
}
// 判断 chatId 是否已存在
private boolean exists(String type, String chatId) {
List<String> chatIds = chatHistoryMapper.selectChatIdsByType(type);
return chatIds.contains(chatId);
}
/**
* TODO 删除
* @param type
* @param chatId
*/
@Override
public void delete(String type, String chatId) {
}
/**
* 根据类型获取聊天记录
* @param type
* @return
*/
@Override
public List<String> getChatIds(String type) {
return chatHistoryMapper.selectChatIdsByType(type);
}
}
@Mapper
public interface ChatHistoryMapper {
/**
* 插入一条聊天记录
* @param chatHistory
*/
@Insert("INSERT INTO chat_history (type, chat_id) VALUES (#{type}, #{chatId})")
void insert(ChatHistory chatHistory);
/**
* 删除一条聊天记录
* @param type
* @param chatId
*/
@Delete("DELETE FROM chat_history WHERE type = #{type} AND chat_id = #{chatId}")
void delete(@Param("type") String type, @Param("chatId") String chatId);
/**
* 根据type获取聊天记录的chatIds
* @param type
* @return
*/
@Select("SELECT chat_id FROM chat_history WHERE type = #{type}")
List<String> selectChatIdsByType(String type);
}
实体类 sql
bash
@Data
public class ChatHistory {
private String id;
private String type;
private String chatId;
}
CREATE TABLE chat_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
type VARCHAR(255) NOT NULL,
chat_id VARCHAR(255) NOT NULL
);
controller
java
@GetMapping(value = "/chatStream", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestParam(value = "question") String question, @RequestParam("chatId") String chatId){
// 保存会话ID
chatHistoryRepository.save(ChatType.CHAT.getValue(), chatId);
return client.prompt()
.user(question)
.advisors(advisorSpec -> advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId))
.stream()
.content();
}