SpringAI+DeepSeek大模型应用开发——6基于MongDB持久化对话

持久化对话

默认情况下,聊天记忆存储在内存中ChatMemory chatMemory = new InMemoryChatMemory()

如果需要持久化存储,可以实现一个自定义的聊天记忆存储类,以便将聊天消息存储在你选择的任何持久化存储介质中。

MongoDB

文档型数据库,数据以JSON - like的文档形式存储,具有高度的灵活性和可扩展性。它不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。

当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者消息格式可能会频繁变化时,MongoDB 能很好地适应这种灵活性。例如,一些社交应用中用户可能会发送各种格式的消息,使用 MongoDB 可以方便地存储和管理这些不同类型的数据。

整合SpringBoot

引入MongoDB依赖:

xml 复制代码
<!-- Spring Boot Starter Data MongoDB -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

添加远程连接配置:

yaml 复制代码
#MongoDB连接配置
spring:
    data:
        mongodb:
            uri: mongodb://localhost:27017/chat_memory_db
            username: root
            password: xxx
实体类

映射MongoDB中的文档(相当与MySQL的表)

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chatMessages")
public class ChatMessages {
    //唯一标识,映射到 MongoDB 文档的 _id 字段
    @Id
    private ObjectId id;
    private String conversationId;  //会话ID
    private String messagesJson;  //消息JSON

}
消息序列化器

对聊天消息message进行 序列化 和 反序列化 操作

消息序列化(messagesToJson 方法):

将一组 Message 对象(如 UserMessage、AssistantMessage)转换为 JSON 字符串。

用于将内存中的聊天记录保存到存储介质(如数据库、文件)或通过网络传输。

消息反序列化(messagesFromJson 方法):

将 JSON 字符串还原为 Message 对象列表。

用于从持久化存储或网络接收的数据中恢复聊天消息对象。

支持多态反序列化(MessageDeserializer 类)

根据 JSON 中的 messageType 字段判断消息类型(如 "USER" 或 "ASSISTANT"),并创建对应的子类实例。

解决了 Jackson 默认无法识别接口或抽象类具体实现的问题。

格式美化(可选)

启用了 SerializationFeature.INDENT_OUTPUT,使输出的 JSON 更具可读性(适合调试和日志输出)。

JAVA 复制代码
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;

import java.io.IOException;
import java.util.List;
import java.util.Map;
//聊天消息序列化器
public class MessageSerializer {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    static {
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Message.class, new MessageDeserializer());
        objectMapper.registerModule(module);
    }

    public static String messagesToJson(List<Message> messages) throws JsonProcessingException {
        return objectMapper.writeValueAsString(messages);
    }

    public static List<Message> messagesFromJson(String json) throws JsonProcessingException {
        return objectMapper.readValue(json, new TypeReference<List<Message>>() {});
    }

    private static class MessageDeserializer extends JsonDeserializer<Message> {
        @Override
        public Message deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            Map<String, Object> node = p.readValueAs(Map.class);
            String type = (String) node.get("messageType");

            switch (type) {
                case "USER":
                    return new UserMessage((String) node.get("text"));
                case "ASSISTANT":
                    return new AssistantMessage((String) node.get("text"));
                default:
                    throw new IOException("未知消息类型: " + type);
            }
        }
    }
}
持久化类
java 复制代码
@Component
@RequiredArgsConstructor
public class MongoChatMemory implements ChatMemory {

    @Resource
    private MongoTemplate mongoTemplate;

    @Override
    public void add(String conversationId, List<Message> messages) {
        Query query = new Query(Criteria.where("conversationId").is(conversationId));
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);

        List<Message> updatedMessages;
        if (chatMessages != null) {
            try {
                updatedMessages = new java.util.ArrayList<>(chatMessages.getMessagesJson() != null
                        ? MessageSerializer.messagesFromJson(chatMessages.getMessagesJson()) : Collections.emptyList());
            } catch (JsonProcessingException e) {
                throw new RuntimeException("序列化消息失败", e);
            }
            updatedMessages.addAll(messages);
        } else {
            updatedMessages = new java.util.ArrayList<>(messages);
        }

        try {
            String json = MessageSerializer.messagesToJson(updatedMessages);
            if (chatMessages != null) {
                Update update = new Update().set("messagesJson", json);
                mongoTemplate.updateFirst(query, update, ChatMessages.class);
            } else {
                ChatMessages newChatMessages = new ChatMessages();
                newChatMessages.setConversationId(conversationId);
                newChatMessages.setMessagesJson(json);
                mongoTemplate.insert(newChatMessages);
            }
        } catch (JsonProcessingException e) {
            throw new RuntimeException("序列化消息失败", e);
        }
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
        Query query = new Query(Criteria.where("conversationId").is(conversationId));
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);

        if (chatMessages == null || chatMessages.getMessagesJson() == null) {
            return Collections.emptyList();
        }

        try {
            List<Message> allMessages = MessageSerializer.messagesFromJson(chatMessages.getMessagesJson());
            int size = allMessages.size();
            int fromIndex = Math.max(0, size - lastN);
            return allMessages.subList(fromIndex, size);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化消息失败", e);
        }
    }

    @Override
    public void clear(String conversationId) {
        Query query = new Query(Criteria.where("conversationId").is(conversationId));
        mongoTemplate.remove(query, ChatMessages.class);
    }
}
测试

初始化ChatClient时,注入MongoChatMemory

java 复制代码
//健康报告对话
@Component
@Slf4j
public class HealthReportApp {

    private final ChatClient chatClient1;
    private static final String SYSTEM_PROMPT = "你的名字是"小鹿",你是一家名为"北京协和医院"的智能客服。你是一个训练有素的医疗顾问和医疗伴诊助手。你态度友好、礼貌且言辞简洁。\n" +
            "1、请仅在用户发起第一次会话时,和用户打个招呼,并介绍你是谁。\n" ;

    //初始化ChatClient
    public HealthReportApp(ChatModel dashscopeChatModel,MongoChatMemory mongoChatMemory) throws IOException {
        chatClient1 = ChatClient.builder(dashscopeChatModel)
                .defaultSystem(SYSTEM_PROMPT)  //系统预设
                .defaultAdvisors(
                        new MessageChatMemoryAdvisor(mongoChatMemory), //对话记忆
                        //自定义日志  Advisor,可按需开启
                        new MyLoggerAdvisor(),
                        // 自定义违禁词 Advisor,可按需开启
                        new ProhibitedWordAdvisor()
                        //自定义推理增强,可按需开启
                        //new ReReadingAdvisor()
                )
                .build();
    }

    public record HealthReport(String title, List<String> suggestions) { }
    //生成健康报告对话
    public HealthReport doChatWithReport(String message, String chatId) {
        HealthReport healthReport = chatClient1
                .prompt()
                .system(SYSTEM_PROMPT + "分析用户提供的信息,每次对话后都要生成健康报告,标题为{用户名}的健康报告,内容为建议列表")
                .user(message)
                .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
                .call()
                .entity(HealthReport.class);
        log.info("healthReport: {}", healthReport);
        return  healthReport;
    }

}       

mongodb记录如下

json 复制代码
[ {
  "messageType" : "USER",
  "metadata" : {
    "messageType" : "USER"
  },
  "media" : [ ],
  "text" : "你好,我是程序员kk"
}, {
  "messageType" : "ASSISTANT",
  "metadata" : {
    "messageType" : "ASSISTANT"
  },
  "toolCalls" : [ ],
  "media" : [ ],
  "text" : "{\n  \"suggestions\": [\n    \"您好,程序员kk,我是北京协和医院的智能客服小鹿,很高兴为您服务。\",\n    \"长期从事编程工作可能导致久坐,建议您定时起身活动,保持良好姿势。\",\n    \"注意用眼卫生,每工作40-50分钟休息一下眼睛。\",\n    \"合理安排作息时间,保证充足睡眠以维持身体和心理健康。\"\n  ],\n  \"title\": \"程序员kk的健康报告\"\n}"
}, {
  "messageType" : "USER",
  "metadata" : {
    "messageType" : "USER"
  },
  "media" : [ ],
  "text" : "你是谁"
}, {
  "messageType" : "ASSISTANT",
  "metadata" : {
    "finishReason" : "STOP",
    "id" : "0110f881-f29f-9cef-b142-7a7cf9e3e76c",
    "role" : "ASSISTANT",
    "messageType" : "ASSISTANT",
    "reasoningContent" : ""
  },
  "toolCalls" : [ ],
  "media" : [ ],
  "text" : "{\n  \"suggestions\": [\n    \"您好,我是北京协和医院的智能客服小鹿,很高兴为您服务。\",\n    \"作为您的医疗顾问和伴诊助手,我将为您提供专业建议。\",\n    \"请告诉我您的需求或问题,我会尽力帮助您。\"\n  ],\n  \"title\": \"程序员kk的健康报告\"\n}"
} ]

接口开发

为了在Controller层实现AI对话历史记录的功能,将添加两个接口:根据chatId查询特定对话历史记录和查询所有对话的摘要列表。

java 复制代码
//获取所有conversationId
public List<String> findAllConversationIds(String userId) {
    if (userId == null || userId.isEmpty()) {
        return Collections.emptyList(); // 或抛出异常
    }
    // 构建正则表达式:以 userId + "_" 开头
    Pattern pattern = Pattern.compile("^" + Pattern.quote(userId) + "_");
    // 使用 regex 替代 matches
    Query query = new Query(Criteria.where("conversationId").regex(pattern));
    return mongoTemplate.findDistinct(
        query,
        "conversationId",
        ChatMessages.class,
        String.class
    );
}
java 复制代码
/**
     * 根据 chatId 获取历史聊天记录
     * 支持参数 lastN 控制获取最近 N 条消息(默认获取全部)
     */
    @GetMapping("/history/{chatId}")
    public BaseResponse<List<Message>> getChatHistory(
            @PathVariable String chatId,
            @RequestParam(defaultValue = "-1") int lastN) {
        int effectiveLastN = lastN <= 0 ? Integer.MAX_VALUE : lastN;
        List<Message> history = chatMemory.get(chatId, effectiveLastN);
        return ResultUtils.success(history);
    }

    /**
     * 获取所有 chatId 列表(用于展示对话历史页面)
     */
    @GetMapping("/conversations")
    public BaseResponse<List<String>> getAllConversations() {
        Long currentUserId= BaseContext.getCurrentId();
        String userId = currentUserId.toString();
        List<String> conversationIds =  chatMemory.findAllConversationIds(userId);
        return ResultUtils.success(conversationIds);
    }
    /**
     * 新建对话:生成新的 chatId,并在 MongoDB 中插入一条空记录
     */
    @PostMapping("/conversations/add")
    public BaseResponse<String> createNewConversation() {
        Long currentUserId= BaseContext.getCurrentId();
        String conversationId = currentUserId + "_" + UUID.randomUUID().toString();
        chatMemory.add(conversationId, Collections.emptyList()); // 插入空消息记录
        return ResultUtils.success(conversationId);
    }

    /**
     * 删除指定 chatId 的对话记录
     */
    @DeleteMapping("/conversations/{chatId}")
    public BaseResponse<Boolean> deleteConversation(@PathVariable String chatId) {
        chatMemory.clear(chatId);
        return ResultUtils.success(true);
    }
相关推荐
pianmian13 小时前
类(JavaBean类)和对象
java
我叫小白菜4 小时前
【Java_EE】单例模式、阻塞队列、线程池、定时器
java·开发语言
Albert Edison4 小时前
【最新版】IntelliJ IDEA 2025 创建 SpringBoot 项目
java·spring boot·intellij-idea
超级小忍5 小时前
JVM 中的垃圾回收算法及垃圾回收器详解
java·jvm
weixin_446122465 小时前
JAVA内存区域划分
java·开发语言·redis
勤奋的小王同学~5 小时前
(javaEE初阶)计算机是如何组成的:CPU基本工作流程 CPU介绍 CPU执行指令的流程 寄存器 程序 进程 进程控制块 线程 线程的执行
java·java-ee
TT哇5 小时前
JavaEE==网站开发
java·redis·java-ee
2401_826097625 小时前
JavaEE-Linux环境部署
java·linux·java-ee
缘来是庄6 小时前
设计模式之访问者模式
java·设计模式·访问者模式
Bug退退退1236 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq