Spring AI 应用示例-聊天机器人
本文介绍了基于SpringAI实现的聊天机器人项目,主要包括核心功能实现和接口设计。项目支持文本对话、多轮上下文记忆和历史记录管理三大功能。通过ChatClient与AI模型交互,结合ChatMemory接口实现对话历史存储和管理,使用InMemoryChatMemory处理会话记忆。共5个核心接口:进行流式对话、获取会话列表、查看历史记录、删除会话记录等。
目录
在 Spring AI-CSDN博客 文章中,我们学习了 Spring AI 的基础知识,在本篇文章中,我们就来实现一个简单的 聊天机器人 示例,来更好的学习和使用 Spring AI
项目代码:project: 存放项目代码
核心功能
我们首先来看需要实现的核心功能:
对话:
(1)支持用户与机器人进行文本对话
(2) 实时响应用户输入,输出自然语言回复
多轮对话:
(1)能够理解和处理多轮对话,保持上下文连续性
(2)支持基于上下文的智能应答
历史记录:
(1)自动保存用户和机器人的对话历史
(2)支持用户查看历史对话记录
界面设计

前端代码获取: 项目完整代码/聊天机器人/chat-bot/src/main/resources/static/chat-board.css · Echo/project - 码云 - 开源中国
项目创建
创建 Spring Boot 项目,并在 pom 文件中添加依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</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>1.0.0-M6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
添加配置文件:
server:
port: 8080
spring:
application:
name: chat-bot
ai:
openai:
api-key: 申请的API-Key
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
logging:
level:
org.springframework.ai.chat.client.advisor: debug
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
接口定义
项目中要实现的接口主要有:
进行对话
[请求]
GET/POST /chat/stream
[参数]
prompt(String):用户输入的消息内容
chatId(String):会话标识id,由前端生成,不重复,新建会话时,创建新的 chatId
[响应]
Flux<String>:流式返回的机器人回复内容
获取会话列表
[请求]
GET/POST /chat/getChatIds
[参数]
无
[响应]
List<ChatInfo>:会话id列表
获取会话记录
[请求]
GET/POST /chat/getChatHistory
[参数]
chatId(String):会话标识id
[响应]
List<MessageVO>:会话历史记录
删除会话记录
[请求]
GET/POST /chat/deleteChat
[参数]
chatId(String):删除的会话id
[响应]
true/false:删除是否成功
接下来,就是编写后端代码,实现对应功能了,我们先来实现对话功能
后端实现
进行对话
在这里,我们借助 ChatClient 与 AI模型 进行交互,因此,我们先配置 Client:
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你叫小小鱼, 擅长Java 和 Python")
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}
然后进行流式返回:
@Slf4j
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private ChatClient chatClient;
/**
* 进行对话
* @param prompt
* @param chatId
* @return
*/
@GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String prompt, String chatId) {
log.info("进行对话,chatId:{}, prompt:{}", chatId, prompt);
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
}
前端输入内容进行测试:

但是,当前实现的会话功能是没有记忆力的,也就是说,多轮对话之间是没有关系的,如:

就如上图,输入 "再+1",是希望在原来答案 2 的基础上 +1,返回 3,但由于此时会话没有记忆,因此返回的结果不符合我们的预期,这种情况比较影响用户的体验
因此,接下来,我们就来看如何实现模型的对话记忆
对话记忆
大模型本身是无状态,即不具备记忆能力的,它们不会保留有关以前的交互信息
那么,想要让大模型记住之前的聊天内容,需要把之前的聊天内容与新的提示词一起发送给模型
在 Spring AI 中,提供了 ChatMemory 接口,负责存储和管理对话历史,它的主要作用包括:
1. 保证上下文连贯性:通过存储用户与 AI 的历史消息(如用户提问、AI 回复、系统提示),让模型在后续对话中理解上下文
2. 支持多用户隔离:通过 forUser(userId) 方法,为每个用户维护独立的对话记忆,避免不同用户的对话历史混串
3. 灵活的存储策略:支持多种存储实现(如内存、Redis、数据库),适应不同环境需求(开发/测试/生产)
4. 可扩展的记忆管理:允许自定义记忆的保留时长、轮次限制(记忆窗口)、过滤规则(如忽略敏感消息),满足复杂业务需求
接下来,我们就来看 ChatMemory 的使用
ChatMemory
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));
}
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId);
}
核心功能方法:
| 方法签名 | 功能说明 | 参数说明 | 返回值 |
|---|---|---|---|
add(String, Message) |
添加单条消息到指定会话 |
|
void |
add(String, List<Message>) |
批量添加消息到指定会话 |
|
void |
get(String, int) |
获取会话的最近消息 |
|
List<Message> |
clear(String) |
清空指定会话的记忆 | conversationId: 会话ID |
void |
可以看到,所有的方法都依赖 conversationId 参数,确保不同会话的记忆完全隔离,且接口未限定存储实现(内存/Redis/DB),支持灵活扩展
Spring AI 提供了默认实现 InMemoryChatMemory,用于在内存中存储对话历史:
public class InMemoryChatMemory implements ChatMemory {
Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();
@Override
public void add(String conversationId, List<Message> messages) {
this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
this.conversationHistory.get(conversationId).addAll(messages);
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<Message> all = this.conversationHistory.get(conversationId);
return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
}
@Override
public void clear(String conversationId) {
this.conversationHistory.remove(conversationId);
}
}
使用 ConcurrentHashMap来区分不同的对话(key:会话Id,value:消息列表) ,并保证线程安全的并发访问
接下来,我们就通过 InMemoryChatMemory 来实现历史对话的存储
定义 ChatMemory 并将其注入到 ChatClient 中:
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你叫小小鱼, 擅长Java 和 Python")
.defaultAdvisors(new SimpleLoggerAdvisor(), new MessageChatMemoryAdvisor(chatMemory()))
.build();
}
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
}
通过 MessageChatMemoryAdvisor 将 ChatMemory 添加到 ChatClient 中,从而实现在对话过程中自动将用户输入和AI响应添加到记忆,并在请求AI前自动注入相关历史上下文,以及为不同会话(conversationId)维护独立记忆,防止不同用户的对话历史相互干扰
工作原理:

当用户发送请求时,Advisor 激活,从 ChatMemory 中获取历史列表,此时可对消息进行处理,如过滤无关消息、进行优先级排序等,然后再将历史消息和当前消息发送给 AI 模型
当我们向模型发送请求时,传入chatId:
@GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String prompt, String chatId) {
log.info("进行对话,chatId:{}, prompt:{}", chatId, prompt);
return chatClient.prompt()
.user(prompt)
// 获取 chatId 的对话记忆
.advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
.stream()
.content();
}
此时,我们再次进行测试:

此时 AI 模型就能够结合之前的历史记录进行分析,并给出预期结果
此时我们观察后端日志:

可以看到请求中包含 SystemMessage(你叫小小鱼, 擅长Java 和 Python)、之前发送的UserMessage(1+1=)以及回复的 AssistantMessage(1+1=2)
历史记录
ChatMemory 提供了 get 接口,可以根据会话id获取会话记录,我们可以通过一个 List 来存储会话id列表,再根据会话id来获取会话记录
定义 ChatInfo 来存储会话记录:
@Data
public class ChatInfo {
private String chatId;
private String title;
public ChatInfo(){}
public ChatInfo(String chatId, String title) {
this.chatId = chatId;
this.title = title;
}
}
定义 ChatHistoryRepository 接口来管理 ChatInfo:
public interface ChatHistoryRepository {
/**
* 保存会话信息
* @param chatId
* @param chatInfo
*/
void save(String chatId, ChatInfo chatInfo);
/**
* 获取所有会话
* @return
*/
List<ChatInfo> getChatList();
/**
* 删除会话id
* @param chatId
*/
void deletedChatId(String chatId);
}
MemoryChatHistoryRepository 实现 ChatInfo 管理:
@Repository
public class MemoryChatHistoryRepository implements ChatHistoryRepository {
private final Map<String, ChatInfo> chatHistory = new LinkedHashMap<>();
@Override
public void save(String chatId, ChatInfo chatInfo) {
// 覆盖相同的 chatId 信息, 保存最后一次消息信息
chatHistory.put(chatId, chatInfo);
}
@Override
public List<ChatInfo> getChatList() {
return chatHistory.values().stream().toList();
}
@Override
public void deletedChatId(String chatId) {
chatHistory.remove(chatId);
}
}
修改 stream 方法,实现会话存储:
@Slf4j
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private MemoryChatHistoryRepository chatHistoryRepository;
@Autowired
private ChatClient chatClient;
/**
* 进行对话
* @param prompt
* @param chatId
* @return
*/
@GetMapping(value = "/stream", produces = "text/html;charset=utf-8")
public Flux<String> stream(String prompt, String chatId) {
log.info("进行对话,chatId:{}, prompt:{}", chatId, prompt);
// 保存会话信息
chatHistoryRepository.save(chatId, new ChatInfo(chatId, prompt));
return chatClient.prompt()
.user(prompt)
// 获取 chatId 的对话记忆
.advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
.stream()
.content();
}
}
实现获取会话列表接口:
@Autowired
private MemoryChatHistoryRepository chatHistoryRepository;
@GetMapping("/getChatIds")
public List<ChatInfo> getChatIds() {
return chatHistoryRepository.getChatList();
}
定义消息 MessageVO 类型:
@Data
public class MessageVO {
private String role;
private String content;
public MessageVO(Message message) {
switch (message.getMessageType()) {
case USER -> this.role = "user";
case ASSISTANT -> this.role = "assistant";
case SYSTEM -> this.role = "system";
case TOOL -> this.role = "tool";
}
this.content = message.getText();
}
}
实现获取会话记录接口:
@Autowired
private ChatMemory chatMemory;
/**
* 获取所有会话信息
* @param chatId
* @return
*/
@GetMapping("/getChatHistory")
public List<MessageVO> getChatHistory(String chatId) {
log.info("获取 chatId:{} 会话信息", chatId);
List<Message> messages = chatMemory.get(chatId, 20);
if (messages.isEmpty()) {
return List.of();
}
return messages.stream()
.map(MessageVO::new)
.collect(Collectors.toList());
}
测试:

能够正确显示会话信息和会话消息
删除记录
实现删除接口:
@GetMapping("/deleteChat")
public Boolean deleteChat(String chatId) {
log.info("删除 chatId: {} 会话记录", chatId);
try {
// 删除会话记录
chatMemory.clear(chatId);
// 删除会话id
chatHistoryRepository.deletedChatId(chatId);
return true;
} catch (Exception e) {
log.error("删除 chatId: {} 会话信息异常", chatId);
e.printStackTrace();
return false;
}
}
点击删除:

删除记录成功:

至此,聊天机器人 的核心功能就已实现完毕
更多推荐

所有评论(0)