> 技术文档 > Spring AI进阶:AI聊天机器人之ChatMemory持久化(二)_spring ai chatmemory

Spring AI进阶:AI聊天机器人之ChatMemory持久化(二)_spring ai chatmemory

        继上篇我们介绍了如何使用Spring AI+DeepSeek本地模型搭建AI聊天机器人,本期主要介绍如何实现聊天上下文以及聊天记录如何持久化。

        上篇地址:Spring AI进阶:使用DeepSeek本地模型搭建AI聊天机器人(一)-CSDN博客

        在智能对话系统的开发中,如何让AI记住用户的对话历史是一个关键挑战。想象一个电商客服场景:用户询问“我昨天看的那款手机有货吗?”——如果系统无法关联之前的对话,用户体验将大打折扣。本文将基于Spring AI框架,手把手实现一个支持多轮上下文记忆且具备Redis持久化能力的智能对话系统,助你构建真正具备记忆的AI助手。

一、ChatClient

        Spring AI的ChatClient是一个核心组件,旨在简化与大语言模型(如ChatGPT、Azure OpenAI等)的交互,支持同步和异步调用,并提供了高度抽象的API以适配多种模型提供商。

1. 核心功能与设计理念

  • 统一接口ChatClient通过流畅的API设计,允许开发者以一致的方式调用不同模型(如OpenAI、Azure OpenAI、Amazon Bedrock等),无需关注底层实现差异。

  • 多模态支持:处理文本、图像、音频等多种输入类型,支持生成式对话、函数调用、结构化输出等场景。

  • 同步与异步调用

    • 同步:通过call()方法直接返回完整响应,适用于简单请求。

    • 异步流式响应:使用stream()结合Flux实现流式传输,提升用户体验(如打字机效果)。

2. 核心API与使用方式

构建提示(Prompt)

  • 用户消息(UserMessage)与系统消息(SystemMessage)

    • 用户消息代表用户输入,系统消息用于引导模型行为(例如角色设定)。

    • 支持参数化占位符,动态替换运行时变量(如{voice})。

chatClient.prompt() .system(sp -> sp.param(\"voice\", \"Pirate\")) .user(\"讲个笑话\") .call();

响应处理

  • 多种返回格式

    • 字符串内容content()直接返回模型生成的文本。

    • 结构化对象:通过entity()将响应自动映射到Java类(如ActorFilms)。

    • 流式响应:使用Flux逐步接收结果。

Flux flux = streamingChatClient.stream(\"讲个笑话\").content();

3. 配置与扩展

自动配置

  • 默认配置:通过Spring Boot的application.yml设置API密钥、模型名称(如gpt-3.5-turbo)及参数(如temperature)。

    spring: ai: openai: api-key: sk-xxx chat: options: model: gpt-4 temperature: 0.7

编程式配置

  • 自定义默认值:通过ChatClient.Builder预设系统消息、函数或参数,简化运行时调用。

/** * @author xtwang * @des AI聊天配置 * @date 2025/2/11 上午9:39 */@Configurationpublic class ChatConfig { @Bean public ChatClient chatClient(OllamaChatModel ollamaChatModel, RedisChatMemory redisChatMemory) { return ChatClient.builder(ollamaChatModel) .defaultSystem(\"你是一个智能助手。能帮助用户解决各种问题,你很有礼貌,回答问题条理清晰。你的首选语言是中文。\") .defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory)) .build(); }}

检索增强生成(RAG)

  • 结合向量数据库(如Azure Vector Search),通过QuestionAnswerAdvisor动态附加上下文到提示中,提升模型回答的准确性。

二、Spring AI Message 

Message 接口的各种实现对应于 AI 模型可以处理的不同类别的消息。模型根据对话角色区分消息类别。

Spring AI Message API

如下所述,这些角色实际上由 MessageType 映射。

1、角色

        每个消息都被分配了一个特定的角色。这些角色对消息进行分类,为 AI 模型阐明提示词每个部分的上下文和目的。这种结构化的方法增强了与 AI 通信的细致性和有效性,因为提示词的每个部分都在交互中扮演着独特且明确的角色。

主要角色包括:

  • 系统角色:指导 AI 的行为和响应风格,设置 AI 如何解释和回复输入的参数或规则。这类似于在开始对话之前向 AI 提供说明。

  • 用户角色:表示用户的输入——他们对 AI 提出的问题、命令或陈述。此角色至关重要,因为它构成了 AI 响应的基础。

  • 助手角色:AI 对用户输入的响应。它不仅仅是一个答案或反应,它对于保持对话的流畅性至关重要。通过跟踪 AI 之前的回复(其“助手角色”消息),系统确保交互连贯且与上下文相关。助手消息还可以包含函数工具调用请求信息。这就像 AI 中的一项特殊功能,在需要时用于执行特定功能,例如计算、获取数据或其他超出简单对话的任务。

  • 工具/函数角色:工具/函数角色侧重于响应工具调用助手消息返回其他信息。

角色在 Spring AI 中表示为枚举,如下所示:

public enum MessageType {USER(\"user\"),ASSISTANT(\"assistant\"),SYSTEM(\"system\"),TOOL(\"tool\"); ...}

2、消息

     那么消息也根据角色类型对应有以下几种消息类型。

  • UserMessage:用户消息,指用户输入的消息,比如提问的问题。
  • SystemMessage:系统限制性消息,这种消息比较特殊,权重很大,AI会优先依据SystemMessage里的内容进行回复。
  • AssistantMessage:大模型回复的消息。
  • FunctionMessage:函数调用消息,开发中一般使用不到,一般无需关心。

所以我们会将 UserMessage、SystemMessage、AssistantMessage 、FunctionMessage放在一个队列中,然后将整个队列发送给聊天模型,然后聊天模型就会根据整个聊天信息对回复内容进行判断。

三、ChatMemory Advisor

Spring AI 框架提供了一些内置的Advisor来增强您的 AI 交互。以下是可用Advisor的概述:

  • MessageChatMemoryAdvisor

    检索记忆并将其作为消息集合添加到提示中。这种方法保持了对话历史记录的结构。请注意,并非所有 AI 模型都支持这种方法。

  • PromptChatMemoryAdvisor

    检索内存中的记忆并将其合并到提示的系统文本中。

  • VectorStoreChatMemoryAdvisor

    从向量数据库检索记忆并将其添加到提示的系统文本中。此Advisor可用于有效地搜索和检索大型数据集中的相关信息。

四、聊天记忆实现

        通过对上述知识点的学习,我们对如何实现聊天上下文记忆有了一个初步的认知,那么接下来通过实践来实现我们的需求。创建工程及配置依赖包相关步骤请查看:Spring AI进阶:使用DeepSeek本地模型搭建AI聊天机器人(一)-CSDN博客

1、创建一个controller,引入OllamaChatModel、ChatClient、ChatMemory依赖

package com.wanganui.controller;import io.swagger.v3.oas.annotations.OpenAPIDefinition;import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.info.Info;import io.swagger.v3.oas.annotations.tags.Tag;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;import org.springframework.ai.chat.memory.InMemoryChatMemory;import org.springframework.ai.chat.messages.Message;import org.springframework.ai.chat.model.ChatResponse;import org.springframework.ai.ollama.OllamaChatModel;import org.springframework.http.MediaType;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;import java.util.List;import java.util.Map;import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;/** * @author xtwang * @des 聊天记录实现 * @date 2025/2/6 上午10:47 */@RestController@RequestMapping(\"/chat\")@OpenAPIDefinition(info = @Info(title = \"Chat API\", version = \"1.0\", description = \"API for chat operations\"))@Tag(name = \"AI聊天\", description = \"聊天记录实现\")public class ChatController { private final OllamaChatModel ollamaChatModel; private final ChatClient chatClient; private final InMemoryChatMemory chatMemory = new InMemoryChatMemory(); public ChatController(OllamaChatModel ollamaChatModel) { this.ollamaChatModel = ollamaChatModel; this.chatClient = ChatClient.builder(ollamaChatModel) .defaultSystem(\"你是一个生活助手,乐于帮助人解决问题,无论问什么都要礼貌回答,遇到代码问题一律回复不知道。\") .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)).build(); }}

2、编写聊天接口,设置两个参数,message(消息参数)、sessionId(会话参数),这里我们不使用流式输出,无法直接看到结果,后续会单独介绍如何在前端实现流式输出。

@Operation(summary = \"普通聊天\")@GetMapping(\"/ai/generate\")public ResponseEntity generate(@RequestParam(value = \"message\", defaultValue = \"讲个笑话\") String message, @RequestParam String sessionId) { return ResponseEntity.ok(chatClient.prompt().user(message) .advisors(advisorSpec -> advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) .call().content());}

Advisor参数说明:

  • CHAT_MEMORY_CONVERSATION_ID_KEY:会话key,用于区分会话。
  • CHAT_MEMORY_RETRIEVE_SIZE_KEY:发送给聊天模型的上下文长度。

 3、编写获取聊天记录接口,用sessionId作为参数,设置返回最近10条聊天记录。

@Operation(summary = \"获取聊天记录\")@GetMapping(\"/ai/messages\")public List getMessages(@RequestParam String sessionId) { return chatMemory.get(sessionId, 10);}

    4、然后启动项目,测试大模型是否能记住我们发过的消息

    可以看到,大模型已成功记录我们的聊天记录,并且根据聊天内容作出了回答。

    5、测试调用一下聊天记录接口

    可以看到我们的聊天记录已成功被记录。但是目前的聊天记录是存储在内存中,程序一旦关闭聊天记录就失效了,那么如何能让聊天记录能够永久存储呢,解决方法就是重写ChatMemory,结合Redis实现聊天记录的持久化。

    五、重写ChatMemory

            通过ChatMemory的源码可以看出它是一个接口,提供了将消息添加到对话、从对话中检索消息以及清除对话历史记录的方法,那么我们可以根据它的结构来实现一个自定义的ChatMemory。

     1、新建一个类命名为RedisChatMemory 实现ChatMemory接口,并添加@Component注解将它注册成一个服务。引入RedisTemplate依赖并实现ChatMemory定义的方法。

    package com.wanganui.chat;import lombok.RequiredArgsConstructor;import org.springframework.ai.chat.memory.ChatMemory;import org.springframework.ai.chat.messages.Message;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import java.util.List;/** * @author xtwang * @des RedisChatMemory */@Component@RequiredArgsConstructorpublic class RedisChatMemory implements ChatMemory { private static final String REDIS_KEY_PREFIX = \"chatmemory:\"; private final RedisTemplate redisTemplate; @Override public void add(String conversationId, List messages) { String key = REDIS_KEY_PREFIX + conversationId; // 存储到 Redis redisTemplate.opsForList().rightPushAll(key, messages); } @Override public List get(String conversationId, int lastN) { String key = REDIS_KEY_PREFIX + conversationId; // 从 Redis 获取最新的 lastN 条消息 List serializedMessages = redisTemplate.opsForList().range(key, -lastN, -1); if (serializedMessages != null) { return serializedMessages; } return List.of(); } @Override public void clear(String conversationId) { redisTemplate.delete(REDIS_KEY_PREFIX + conversationId); }}

    2、可以看到这里的RedisTemplate定义的模版类型约束是Message,只用于Message的数据交互,那么就意味着在配置RedisTemplate时就要指定属性类型,其次Message是接口,在反序列化时需要明确指定其具体实现类,所以我们要根据Message来自定义一个序列化器。

    package com.wanganui.config;import com.fasterxml.jackson.core.JsonParser;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.DeserializationContext;import com.fasterxml.jackson.databind.JsonDeserializer;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.node.ObjectNode;import org.springframework.ai.chat.messages.AssistantMessage;import org.springframework.ai.chat.messages.Message;import org.springframework.ai.chat.messages.UserMessage;import org.springframework.data.redis.serializer.RedisSerializer;import java.io.IOException;/** * @author xtwang * @des 聊天消息序列化器 * @date 2025/2/11 下午2:22 */public class MessageRedisSerializer implements RedisSerializer { private final ObjectMapper objectMapper; private final JsonDeserializer messageDeserializer; public MessageRedisSerializer(ObjectMapper objectMapper) { this.objectMapper = objectMapper; this.messageDeserializer = new JsonDeserializer() { @Override public Message deserialize(JsonParser jp, DeserializationContext ctx)  throws IOException { ObjectNode root = jp.readValueAsTree(); String type = root.get(\"messageType\").asText(); return switch (type) {  case \"USER\" -> new UserMessage(root.get(\"text\").asText());  case \"ASSISTANT\" -> new AssistantMessage(root.get(\"text\").asText());  default -> throw new UnsupportedOperationException(\"未知的消息类型\"); }; } }; } @Override public byte[] serialize(Message message) { try { return objectMapper.writeValueAsBytes(message); } catch (JsonProcessingException e) { throw new RuntimeException(\"无法序列化\", e); } } @Override public Message deserialize(byte[] bytes) { if (bytes == null || bytes.length == 0) { return null; } try { return messageDeserializer.deserialize(objectMapper.getFactory().createParser(bytes), objectMapper.getDeserializationContext()); } catch (Exception e) { throw new RuntimeException(\"无法反序列化\", e); } }}

    3、再将序列化器配置到RedisTemplate

    package com.wanganui.config;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;import org.springframework.ai.chat.messages.Message;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.StringRedisSerializer;import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;/** * @author xtwang */@Configurationpublic class RedisConfig { @Bean public RedisTemplate messageRedisTemplate(RedisConnectionFactory factory, Jackson2ObjectMapperBuilder builder) { RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(factory); // 使用String序列化器作为key的序列化方式 template.setKeySerializer(new StringRedisSerializer()); // 使用自定义的Message序列化器作为value的序列化方式 template.setValueSerializer(new MessageRedisSerializer(builder.build())); // 设置hash类型的key和value序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new MessageRedisSerializer(builder.build())); template.afterPropertiesSet(); return template; } @Bean public ObjectMapper objectMapper() { return new ObjectMapper().registerModule(new JavaTimeModule()); }}

    4、在完成上述步骤后,需要将我们的RedisChatMemory通过Spring AI的Advisor添加到聊天大模型中

    package com.wanganui.chat;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;import org.springframework.ai.ollama.OllamaChatModel;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * @author xtwang * @des AI聊天配置 * @date 2025/2/11 上午9:39 */@Configurationpublic class ChatConfig { @Bean public ChatClient chatClient(OllamaChatModel ollamaChatModel, RedisChatMemory redisChatMemory) { return ChatClient.builder(ollamaChatModel) .defaultSystem(\"你是一个智能助手。能帮助用户解决各种问题,你很有礼貌,回答问题条理清晰。你的首选语言是中文。\") .defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory)) .build(); }}

    5、到目前我们已经完成了对ChatMemory的持久化存储,新建一个Controller,引入ChatClient、RedisChatMemory,并添加聊天及聊天记录接口

    package com.wanganui.controller;import com.github.xiaoymin.knife4j.core.util.StrUtil;import com.wanganui.chat.ChatSession;import com.wanganui.chat.ChatSessionService;import com.wanganui.chat.RedisChatMemory;import io.swagger.v3.oas.annotations.OpenAPIDefinition;import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.info.Info;import io.swagger.v3.oas.annotations.tags.Tag;import lombok.RequiredArgsConstructor;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.messages.Message;import org.springframework.ai.chat.model.ChatResponse;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.util.Assert;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;import java.util.List;import java.util.UUID;import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;/** * @author xtwang * @des * @date 2025/2/6 上午10:47 */@RestController@RequestMapping(\"/api/chat\")@OpenAPIDefinition(info = @Info(title = \"Chat API\", version = \"1.0\", description = \"API for chat operations\"))@Tag(name = \"AI聊天\", description = \"集成机器人角色设定,会话存储,聊天记录持久化\")@RequiredArgsConstructorpublic class AIChatController { private final ChatClient chatClient; private final RedisChatMemory redisChatMemory; private final ChatSessionService chatSessionService; @Operation(summary = \"流式回答聊天\") @GetMapping(value = \"/ai/generateStream\", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux generateStream(@RequestParam(value = \"message\", defaultValue = \"讲个笑话\") String message, @RequestParam String sessionId, @RequestParam String userId) { Assert.notNull(message, \"message不能为空\"); Assert.notNull(userId, \"userId不能为空\"); // 默认生成一个会话 if (StrUtil.isBlank(sessionId)) { sessionId = UUID.randomUUID().toString(); ChatSession chatSession = new ChatSession().setSessionId(sessionId).setSessionName(message.length() >= 15 ? message.substring(0, 15) : message); chatSessionService.saveSession(chatSession, userId); } String finalSessionId = sessionId; return chatClient.prompt().user(message).advisors(advisorSpec -> advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, finalSessionId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)).stream().chatResponse(); } @Operation(summary = \"获取聊天记录\") @GetMapping(\"/ai/messages\") public ResponseEntity<List> getMessages(@RequestParam String sessionId) { Assert.notNull(sessionId, \"sessionId不能为空\"); return ResponseEntity.ok(redisChatMemory.get(sessionId, 10)); } @Operation(summary = \"获取会话列表\") @GetMapping(\"/ai/sessions\") public ResponseEntity<List> getSessions(@RequestParam String userId) { Assert.notNull(userId, \"userId不能为空\"); return ResponseEntity.ok(chatSessionService.getSessions(userId)); } @Operation(summary = \"普通聊天\") @GetMapping(value = \"/ai/generate\") public ResponseEntity generate(@RequestParam(value = \"message\", defaultValue = \"讲个笑话\") String message, @RequestParam String sessionId, @RequestParam String userId) { Assert.notNull(message, \"message不能为空\"); Assert.notNull(userId, \"userId不能为空\"); // 默认生成一个会话 if (StrUtil.isBlank(sessionId)) { sessionId = UUID.randomUUID().toString(); ChatSession chatSession = new ChatSession().setSessionId(sessionId).setSessionName(message.length() >= 15 ? message.substring(0, 15) : message); chatSessionService.saveSession(chatSession, userId); } String finalSessionId = sessionId; return ResponseEntity.ok(chatClient.prompt().user(message).advisors(advisorSpec -> advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, finalSessionId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)).call().content()); }}

    6、运行程序,查看结果

    session的功能以及实现逻辑我就不再过多阐述,直接贴上代码:

    package com.wanganui.chat;import io.swagger.v3.oas.annotations.media.Schema;import lombok.Data;import lombok.experimental.Accessors;/** * @author xtwang * @des 聊天会话 * @date 2025/2/11 下午3:03 */@Data@Accessors(chain = true)public class ChatSession { @Schema(description = \"会话id\") private String sessionId; @Schema(description = \"会话名称\") private String sessionName;}
    package com.wanganui.chat;import com.alibaba.fastjson.JSON;import lombok.RequiredArgsConstructor;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.List;/** * @author xtwang * @des 会话服务 * @date 2025/2/11 下午3:10 */@Service@RequiredArgsConstructorpublic class ChatSessionService { public static final String CHAT_SESSION_PREFIX = \"chat_session:\"; private final StringRedisTemplate stringRedisTemplate; /** * 保存会话 * * @param chatSession 会话 */ public void saveSession(ChatSession chatSession, String userId) { String key = CHAT_SESSION_PREFIX + userId; stringRedisTemplate.opsForList().leftPush(key, JSON.toJSONString(chatSession)); } /** * 获取会话列表 * * @return 会话列表 */ public List getSessions(String userId) { String key = CHAT_SESSION_PREFIX + userId; List strings = stringRedisTemplate.opsForList().range(key, 0, -1); if (strings != null) { return strings.stream().map(s -> JSON.parseObject(s, ChatSession.class)).toList(); } return List.of(); }}

    六:总结

    技术方案全景

    1. 核心架构
       Spring AI + DeepSeek模型 + Redis持久化
       └─ ChatClient API层 → 记忆管理 → Redis存储 → 会话服务

    2. 关键技术栈
       - 多态序列化:Jackson自定义TypeResolver + 类型白名单
       - 上下文管理:MessageChatMemoryAdvisor + 滑动窗口策略
       - 持久化方案:Redis Hash结构 + TTL自动过期

    核心实现要点
    // 关键技术点代码映射1. 多态序列化配置ObjectMapper().activateDefaultTyping(...) // 启用类型推导2. 记忆存储结构redisTemplate.opsForList().rightPushAll(key, messages) // 对话历史存储3. 上下文检索策略redisTemplate.opsForList().range(key, -lastN, -1) // 滑动窗口读取
    方案优势对比
    特性 内存实现 Redis持久化方案 上下文容量 单机内存限制 支持TB级存储 会话恢复能力 重启失效 跨进程/设备持久化 性能表现 微秒级延迟 <50ms读取延迟 扩展性 单节点部署 支持分布式架构

    该方案通过深度整合Spring AI生态与Redis持久化能力,实现了具备企业级可用性的对话记忆系统,在保证低延迟响应的同时,为后续实现多模态对话、长期记忆演进奠定了技术基础。

    参考资料:

            基础架构文档:Spring AI进阶:使用DeepSeek本地模型搭建AI聊天机器人(一)-CSDN博客

            Spring AI 文档:简介 :: Spring AI 参考 - Spring 框架

            源码参考 :ai-chat: Spring AI 相关技术介绍

    下篇我们将学习了解一下如何在前端实现聊天内容的流式输出,类似于ChatGPT的那种打字机效果,尽情期待!