持久化對話
默認情況下,聊天記憶存儲在內存中ChatMemory chatMemory = new InMemoryChatMemory()
。
如果需要持久化存儲,可以實現一個自定義的聊天記憶存儲類,以便將聊天消息存儲在你選擇的任何持久化存儲介質中。
MongoDB
文檔型數據庫,數據以JSON - like
的文檔形式存儲,具有高度的靈活性和可擴展性。它不需要預先定義嚴格的表結構,適合存儲半結構化或非結構化的數據。
當聊天記憶中包含多樣化的信息,如文本消息、圖片、語音等多媒體數據,或者消息格式可能會頻繁變化時,MongoDB 能很好地適應這種靈活性。例如,一些社交應用中用戶可能會發送各種格式的消息,使用 MongoDB 可以方便地存儲和管理這些不同類型的數據。
整合SpringBoot
引入MongoDB依賴:
<!-- Spring Boot Starter Data MongoDB -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
添加遠程連接配置:
#MongoDB連接配置
spring:data:mongodb:uri: mongodb://localhost:27017/chat_memory_dbusername: rootpassword: xxx
實體類
映射MongoDB中的文檔(相當與MySQL的表)
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 字段@Idprivate ObjectId id;private String conversationId; //會話IDprivate String messagesJson; //消息JSON}
消息序列化器
對聊天消息message
進行 序列化 和 反序列化 操作
消息序列化(messagesToJson 方法):
將一組 Message 對象(如 UserMessage、AssistantMessage)轉換為 JSON 字符串。
用于將內存中的聊天記錄保存到存儲介質(如數據庫、文件)或通過網絡傳輸。
消息反序列化(messagesFromJson 方法):
將 JSON 字符串還原為 Message 對象列表。
用于從持久化存儲或網絡接收的數據中恢復聊天消息對象。
支持多態反序列化(MessageDeserializer 類)
根據 JSON 中的 messageType 字段判斷消息類型(如 “USER” 或 “ASSISTANT”),并創建對應的子類實例。
解決了 Jackson 默認無法識別接口或抽象類具體實現的問題。
格式美化(可選)
啟用了 SerializationFeature.INDENT_OUTPUT,使輸出的 JSON 更具可讀性(適合調試和日志輸出)。
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> {@Overridepublic 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);}}}
}
持久化類
@Component
@RequiredArgsConstructor
public class MongoChatMemory implements ChatMemory {@Resourceprivate MongoTemplate mongoTemplate;@Overridepublic 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);}}@Overridepublic 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);}}@Overridepublic void clear(String conversationId) {Query query = new Query(Criteria.where("conversationId").is(conversationId));mongoTemplate.remove(query, ChatMessages.class);}
}
測試
初始化ChatClient
時,注入MongoChatMemory
//健康報告對話
@Component
@Slf4j
public class HealthReportApp {private final ChatClient chatClient1;private static final String SYSTEM_PROMPT = "你的名字是“小鹿”,你是一家名為“北京協和醫院”的智能客服。你是一個訓練有素的醫療顧問和醫療伴診助手。你態度友好、禮貌且言辭簡潔。\n" +"1、請僅在用戶發起第一次會話時,和用戶打個招呼,并介紹你是誰。\n" ;//初始化ChatClientpublic 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記錄如下
[ {"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查詢特定對話歷史記錄和查詢所有對話的摘要列表。
//獲取所有conversationId
public List<String> findAllConversationIds(String userId) {if (userId == null || userId.isEmpty()) {return Collections.emptyList(); // 或拋出異常}// 構建正則表達式:以 userId + "_" 開頭Pattern pattern = Pattern.compile("^" + Pattern.quote(userId) + "_");// 使用 regex 替代 matchesQuery query = new Query(Criteria.where("conversationId").regex(pattern));return mongoTemplate.findDistinct(query,"conversationId",ChatMessages.class,String.class);
}
/*** 根據 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);}