新增用户长记忆链路并注入聊天上下文#1
Conversation
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/1913171a-3d6b-403b-a06e-34e4582a8616 Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/1913171a-3d6b-403b-a06e-34e4582a8616 Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/1913171a-3d6b-403b-a06e-34e4582a8616 Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/f30dae57-1e8a-4ae2-9648-c0c291488a2a Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/f30dae57-1e8a-4ae2-9648-c0c291488a2a Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/f30dae57-1e8a-4ae2-9648-c0c291488a2a Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
该 PR 在后端聊天链路中新增“用户长记忆”能力:从新日记中抽取长期画像属性并落库,在聊天时将这些背景知识注入 system prompt;同时补齐“按关键词/日期检索历史日记摘要”的 Function Calling 支持,帮助模型在需要更早历史时主动查询。
Changes:
- 新增
user_profile_memory表与对应 Entity/Mapper,并实现MemoryExtractionService:从日记内容抽取并同步长期画像属性。 - 新增
DiarySearchRequest/DiarySearchResult与DiaryService.searchOwnDiarySummaries,并在AIConfiguration注册diarySearchFunction,ChatService开启函数能力。 - 更新聊天与日记相关测试,覆盖函数注册、长记忆 prompt 生成、历史日记检索行为。
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| CLAUDE.md | 更新项目结构/数据库与聊天链路说明,补充长记忆注入与最新迁移版本信息 |
| backend/moodcopilot/src/test/java/com/moodcopilot/diary/DiaryRecommendationServiceTest.java | 增加日记摘要检索的单测,并适配 DiaryService 新构造参数 |
| backend/moodcopilot/src/test/java/com/moodcopilot/diary/DiaryControllerMockMvcTest.java | 适配 runAiAnalysis 方法签名变更并增强验证 |
| backend/moodcopilot/src/test/java/com/moodcopilot/ai/MemoryExtractionServiceTest.java | 新增长记忆抽取与 prompt 构造的单测 |
| backend/moodcopilot/src/test/java/com/moodcopilot/ai/ChatServiceTest.java | 新增聊天/回复路径对 diarySearchFunction 注册的单测 |
| backend/moodcopilot/src/test/java/com/moodcopilot/ai/ChatControllerMockMvcTest.java | 适配 ChatController 注入长记忆背景并覆盖接口行为 |
| backend/moodcopilot/src/main/resources/db/migration/V1_14__add_user_profile_memory.sql | 新增 user_profile_memory 表结构与索引/外键 |
| backend/moodcopilot/src/main/java/com/moodcopilot/mapper/UserProfileMemoryMapper.java | 新增 UserProfileMemory 的 MyBatis-Plus Mapper |
| backend/moodcopilot/src/main/java/com/moodcopilot/entity/UserProfileMemoryEntity.java | 新增 user_profile_memory 实体定义 |
| backend/moodcopilot/src/main/java/com/moodcopilot/diary/DiaryService.java | 新增历史日记摘要检索;AI 分析后触发长记忆抽取同步;构造函数增加依赖 |
| backend/moodcopilot/src/main/java/com/moodcopilot/diary/DiarySearchResult.java | 新增检索结果 record(含日期与 snippet) |
| backend/moodcopilot/src/main/java/com/moodcopilot/diary/DiarySearchRequest.java | 新增检索请求 record(keyword/startDate/endDate) |
| backend/moodcopilot/src/main/java/com/moodcopilot/diary/DiaryController.java | 适配 runAiAnalysis 新签名并传入 userId |
| backend/moodcopilot/src/main/java/com/moodcopilot/config/AIConfiguration.java | 注册 diarySearchFunction FunctionCallback;更新聊天 system prompt 指引模型调用函数 |
| backend/moodcopilot/src/main/java/com/moodcopilot/ai/MemoryExtractionService.java | 新增长记忆抽取、清洗与同步落库;生成聊天注入用背景 prompt |
| backend/moodcopilot/src/main/java/com/moodcopilot/ai/DiarySearchFunctionSupport.java | 新增函数名常量封装 |
| backend/moodcopilot/src/main/java/com/moodcopilot/ai/ChatService.java | chat/reply 支持注入长记忆背景并开启 diarySearchFunction |
| backend/moodcopilot/src/main/java/com/moodcopilot/ai/ChatController.java | 在 chat/reply 前读取长记忆背景并传入 ChatService |
| backend/moodcopilot/mvnw | 引入 Maven Wrapper 脚本以便统一构建/测试入口 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Async("aiExecutor") | ||
| public void extractAndSyncMemory(Long userId, String diaryContent) { | ||
| try { | ||
| List<UserProfileMemoryEntity> existing = listUserMemories(userId); | ||
| String prompt = buildExtractionUserPrompt(diaryContent, existing); | ||
| String json = analysisChatClient.prompt() | ||
| .system(MEMORY_EXTRACTION_PROMPT) | ||
| .user(prompt) | ||
| .call() | ||
| .content(); | ||
| MemoryExtractionResponse response = objectMapper.readValue(json, MemoryExtractionResponse.class); | ||
| syncMemories(userId, existing, sanitizeAttributes(response.attributes())); | ||
| } catch (Exception e) { | ||
| log.warn("长记忆提取失败,userId={}: {}", userId, e.getMessage()); | ||
| } | ||
| } |
| private List<MemoryAttribute> sanitizeAttributes(List<MemoryAttribute> attributes) { | ||
| if (attributes == null || attributes.isEmpty()) { | ||
| return List.of(); | ||
| } | ||
| Map<String, MemoryAttribute> deduped = new LinkedHashMap<>(); | ||
| for (MemoryAttribute attribute : attributes) { | ||
| if (attribute == null || attribute.attributeKey() == null || attribute.attributeValue() == null) { | ||
| continue; | ||
| } | ||
| String key = attribute.attributeKey().trim(); | ||
| String value = attribute.attributeValue().trim(); | ||
| if (key.isEmpty() || value.isEmpty()) { | ||
| continue; | ||
| } | ||
| deduped.put(key, new MemoryAttribute(key, value)); | ||
| } | ||
| return List.copyOf(deduped.values()); | ||
| } |
| public String buildUserMemoryPrompt() { | ||
| List<UserProfileMemoryEntity> memories = listUserMemories(currentUser().getId()); | ||
| if (memories.isEmpty()) { | ||
| return ""; | ||
| } | ||
| StringBuilder sb = new StringBuilder("以下是用户已经形成的长期背景知识:\n"); | ||
| for (UserProfileMemoryEntity memory : memories) { | ||
| sb.append("- ").append(memory.getAttributeKey()).append(":") | ||
| .append(memory.getAttributeValue()).append("\n"); | ||
| } | ||
| return sb.toString(); | ||
| } |
| private UserEntity currentUser() { | ||
| Authentication auth = SecurityContextHolder.getContext().getAuthentication(); | ||
| if (auth != null && auth.getPrincipal() instanceof UserEntity user) { | ||
| return user; | ||
| } | ||
| throw new IllegalStateException("用户未登录"); | ||
| } |
|
@copilot 审查pr,并查看与本地的区别,若无问题则合并到master |
我已按 0f2af04 复查:本地工作树干净,且与远端 PR 分支无差异; |
|
@copilot 修掉问题 |
|
@copilot 都可以修 |
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/7c4bd1be-dfcf-419d-a1cb-aaa674aa0e2a Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Choco-emmm/MoodCopilot/sessions/7c4bd1be-dfcf-419d-a1cb-aaa674aa0e2a Co-authored-by: Choco-emmm <266224693+Choco-emmm@users.noreply.github.com>
已修完并推到 |
|
@copilot 修完之后可以合并到主分支然后拉到本地来了 |
当前分支已经可合并,最新修复提交是 |
- 记忆解绑:删除日记不再触发全量画像重建,改为用户手动管理 - 新增 GET/DELETE /api/memory 接口 + SettingsPage 记忆管理面板 - 清理 MemoryExtractionService 重建相关方法(~400行),保留增量更新 - 修复报告缓存过期时自动调用 AI 生成总结的问题 - 修复 AI 聊天回复中泄露内部编号(#1/#5)的 prompt 问题 - 修复手机端聊天消息偶发丢失(saveToBackend race condition) - 修复报告生成按钮无 loading 反馈的体验问题 - 修复日记编辑模式下原文与编辑框重复回显 - 修复后端时区为 Asia/Shanghai(Dockerfile JVM + JDBC + Jackson)
- 记忆解绑:删除日记不再触发全量画像重建,改为用户手动管理 - 新增 GET/DELETE /api/memory 接口 + SettingsPage 记忆管理面板 - 清理 MemoryExtractionService 重建相关方法(~400行),保留增量更新 - 修复报告缓存过期时自动调用 AI 生成总结的问题 - 修复 AI 聊天回复中泄露内部编号(#1/#5)的 prompt 问题 - 修复手机端聊天消息偶发丢失(saveToBackend race condition) - 修复报告生成按钮无 loading 反馈的体验问题 - 修复日记编辑模式下原文与编辑框重复回显 - 修复后端时区为 Asia/Shanghai(Dockerfile JVM + JDBC + Jackson)
- 记忆解绑:删除日记不再触发全量画像重建,改为用户手动管理 - 新增 GET/DELETE /api/memory 接口 + SettingsPage 记忆管理面板 - 清理 MemoryExtractionService 重建相关方法(~400行),保留增量更新 - 修复报告缓存过期时自动调用 AI 生成总结的问题 - 修复 AI 聊天回复中泄露内部编号(#1/#5)的 prompt 问题 - 修复手机端聊天消息偶发丢失(saveToBackend race condition) - 修复报告生成按钮无 loading 反馈的体验问题 - 修复日记编辑模式下原文与编辑框重复回显 - 修复后端时区为 Asia/Shanghai(Dockerfile JVM + JDBC + Jackson)
本 PR 在聊天链路中新增用户长记忆能力:从新日记中抽取长期画像属性并落库,在聊天时将这些背景知识注入上下文;同时补齐按关键词 / 日期检索历史日记摘要的 Function Calling 支持,帮助模型在需要更早历史时主动查询。
在原始实现基础上,已根据审查反馈补充长记忆链路的安全与一致性修复,包括:
attributeKey/attributeValue增加长度限制、字符过滤、单行化与兜底转义,保证落库和 prompt 注入可控BAD_REQUEST,避免ChatController直接调用时返回 500测试与校验已完成:
./mvnw test通过