Просмотр исходного кода

feat: 集成 JDBC Chat Memory 实现 AI 对话上下文记忆

- 添加 spring-ai-starter-model-chat-memory-repository-jdbc 依赖
- 配置 MessageWindowChatMemory(窗口 20 条)+ JdbcChatMemoryRepository
- ChatReq 新增 conversationId,前端生成 UUID 关联会话
- ChatService 简化为直接接收 ChatReq 对象

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tanlie 2 недель назад
Родитель
Сommit
2a6fa8704a

+ 4 - 0
wishing-platform/platform-service/platform-service-mobile/pom.xml

@@ -62,6 +62,10 @@
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-starter-model-deepseek</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <finalName>wishing-mobile</finalName>

+ 26 - 10
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/ChatClientConfig.java

@@ -1,6 +1,11 @@
 package cn.qinys.platform.config;
 
+import jakarta.annotation.Resource;
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.memory.MessageWindowChatMemory;
+import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
 import org.springframework.ai.chat.model.ChatModel;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -8,9 +13,21 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 public class ChatClientConfig {
 
+    @Resource
+    JdbcChatMemoryRepository chatMemoryRepository;
+
     @Bean
-    ChatClient chatClient(ChatModel chatModel) {
-        var builder = ChatClient.builder(chatModel)
+    MessageChatMemoryAdvisor chatMemoryAdvisor() {
+        ChatMemory chatMemory = MessageWindowChatMemory.builder()
+                .chatMemoryRepository(chatMemoryRepository)
+                .maxMessages(20)
+                .build();
+        return MessageChatMemoryAdvisor.builder(chatMemory).build();
+    }
+
+    @Bean
+    ChatClient chatClient(ChatModel chatModel, MessageChatMemoryAdvisor memoryAdvisor) {
+        return ChatClient.builder(chatModel)
                 .defaultSystem("""
                         你是许愿树的智能客服小助手,名叫"小愿"。
                         你的职责是:
@@ -19,24 +36,23 @@ public class ChatClientConfig {
                         3. 为用户推荐附近适合许愿的地点
                         4. 用温暖、友好的语气与用户交流
                         5. 适当使用 emoji 让对话更生动
-
+                        
                         【重要】一键许愿功能:
                         当用户表达"帮我许愿""替我许愿""想许愿""许个愿""我要许愿"等意图时,你必须执行以下步骤:
                         1. 如果用户还没说愿望内容,引导用户说出具体愿望
                         2. 用户说出愿望后,整理成一句20-50字精炼的愿望文字
                         3. 从以下类别选2-3个标签:健康、平安、爱情、事业、学业、财富、梦想、好运、祈福、家庭
                         4. 你的回复末尾必须严格包含以下格式(必须一字不差,这是系统指令):
-
+                        
                         [WISH]{"content":"整理后的愿望文字","tags":["标签1","标签2"]}[/WISH]
-
+                        
                         示例:用户说"帮我许愿,希望家人健康",你应该回复:
                         我帮你整理好了愿望,确认一下哦~ [WISH]{"content":"希望家人身体健康,平安喜乐","tags":["健康","平安","家庭"]}[/WISH]
-
+                        
                         回复控制在 100-200 字以内,简洁明了。
                         如果用户问的问题与许愿无关,礼貌地引导回许愿相关话题。
-                        """);
-
-
-        return builder.build();
+                        """)
+                .defaultAdvisors(memoryAdvisor)
+                .build();
     }
 }

+ 1 - 1
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/ChatController.java

@@ -19,7 +19,7 @@ public class ChatController {
     /** 发送消息 */
     @PostMapping
     public Result<ChatResp> chat(@RequestBody @Valid ChatReq req) {
-        String reply = chatService.chat(req.getMessage(), req.getTreeId());
+        String reply = chatService.chat(req);
         ChatResp resp = new ChatResp();
         resp.setReply(reply);
         return new Result<>(resp);

+ 4 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatReq.java

@@ -13,4 +13,8 @@ public class ChatReq implements Serializable {
 
     /** 许愿树 ID(可选,用于关联上下文) */
     private Integer treeId;
+
+    /** 会话 ID,用于关联上下文记忆 */
+    @NotBlank(message = "conversationId 不能为空")
+    private String conversationId;
 }

+ 3 - 2
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ChatService.java

@@ -1,5 +1,6 @@
 package cn.qinys.platform.mobile.service;
 
+import cn.qinys.platform.mobile.req.ChatReq;
 import cn.qinys.platform.mobile.resp.ChatHistoryResp;
 
 /**
@@ -8,9 +9,9 @@ import cn.qinys.platform.mobile.resp.ChatHistoryResp;
 public interface ChatService {
 
     /**
-     * 发送消息并获取 AI 回复(自动保存聊天记录)
+     * 发送消息并获取 AI 回复(自动保存聊天记录,基于 conversationId 记忆上下文
      */
-    String chat(String message, Integer treeId);
+    String chat(ChatReq req);
 
     /**
      * 获取当前用户在某棵树下的聊天历史

+ 8 - 7
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ChatServiceImpl.java

@@ -3,6 +3,7 @@ package cn.qinys.platform.mobile.service.impl;
 import cn.qinys.platform.base.security.utils.CurrentUtils;
 import cn.qinys.platform.entity.wishing.ChatMessage;
 import cn.qinys.platform.mobile.mapper.ChatMessageMapper;
+import cn.qinys.platform.mobile.req.ChatReq;
 import cn.qinys.platform.mobile.resp.ChatHistoryResp;
 import cn.qinys.platform.mobile.service.ChatService;
 import cn.qinys.platform.mobile.tool.DateTimeTools;
@@ -11,6 +12,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -27,17 +29,16 @@ public class ChatServiceImpl implements ChatService {
 
 
     @Override
-    public String chat(String message, Integer treeId) {
+    public String chat(ChatReq req) {
         String userId = getUserId();
-
         // 保存用户消息
-        saveMessage(userId, treeId, "user", message);
-
+        saveMessage(userId, req.getTreeId(), "user", req.getMessage());
         String reply;
         try {
             reply = chatClient.prompt()
-                    .user(message)
-                    .tools(new DateTimeTools(), new WeatherTools(treeId))
+                    .user(req.getMessage())
+                    .tools(new DateTimeTools(), new WeatherTools(req.getTreeId()))
+                    .advisors(a -> a.param("chat_memory_conversation_id", req.getConversationId()))
                     .call()
                     .content();
         } catch (Exception e) {
@@ -46,7 +47,7 @@ public class ChatServiceImpl implements ChatService {
         }
 
         // 保存 AI 回复
-        saveMessage(userId, treeId, "ai", reply);
+        saveMessage(userId, req.getTreeId(), "ai", reply);
 
         return reply;
     }

+ 1 - 1
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/tool/WeatherTools.java

@@ -15,6 +15,7 @@ import org.springframework.web.client.RestTemplate;
 public class WeatherTools {
 
     String key = "88a0c82c3f60a748c6c638ef9db16bca";
+    String url = "https://restapi.amap.com/v3/weather/weatherInfo?city={0}&key={1}";
 
     private Integer treeId;
 
@@ -37,7 +38,6 @@ public class WeatherTools {
             return null;
         }
         RestTemplate restTemplate = SpringUtil.getBean(RestTemplate.class);
-        String url = "https://restapi.amap.com/v3/weather/weatherInfo?city={0}&key={1}";
         return restTemplate.getForObject(url, String.class, extension.getAdcode(), key);
     }
 

+ 6 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/resources/application.yml

@@ -7,6 +7,12 @@ spring:
     multipart:
       max-file-size: 50MB
       max-request-size: 50MB
+  ai:
+    chat:
+      memory:
+        repository:
+          jdbc:
+            initialize-schema: never
 
   application:
     name: wishing-mobile

+ 2 - 2
wishing-tree-h5/src/api/chat.ts

@@ -6,10 +6,10 @@ export interface ChatMsg {
   createdAt: string
 }
 
-export async function sendMessage(message: string, treeId?: number): Promise<string> {
+export async function sendMessage(message: string, conversationId: string, treeId?: number): Promise<string> {
   const res = await request<{ reply: string }>('/mobile/chat', {
     method: 'POST',
-    body: JSON.stringify({ message, treeId }),
+    body: JSON.stringify({ message, treeId, conversationId }),
   })
   return res.data.reply
 }

+ 2 - 1
wishing-tree-h5/src/components/ChatWidget.vue

@@ -109,6 +109,7 @@ const sending = ref(false)
 const messages = ref<ChatMsg[]>([])
 const msgBody = ref<HTMLElement | null>(null)
 const historyLoaded = ref(false)
+const conversationId = ref(crypto.randomUUID())
 
 /** 从 AI 回复中提取 [WISH]...[/WISH] 标记,兼容各种格式变体 */
 function parseWish(content: string): { text: string; wish: WishSuggestion | null } {
@@ -170,7 +171,7 @@ async function send() {
 
   sending.value = true
   try {
-    const reply = await sendMessage(text, props.treeId)
+    const reply = await sendMessage(text, conversationId.value, props.treeId)
     const { text: cleanText, wish } = parseWish(reply)
 
     // 如果 AI 生成了一键许愿建议,但用户不在许愿范围内,直接拒绝