perf(memory): fix N+1 queries, remove redundant SHA-256, cap DREAMS.md growth

This commit is contained in:
matevip 2026-04-07 01:42:57 +08:00
parent a8613fb05c
commit e6be23a040
2 changed files with 56 additions and 56 deletions

View File

@ -212,11 +212,23 @@ public class MemoryEmergenceService {
diary.append("\n"); diary.append("\n");
} }
// 读取现有 DREAMS.md追加到开头最新在最上面 // 读取现有 DREAMS.md追加新日记
String existing = readFileContentSafe(agentId, "DREAMS.md"); String existing = readFileContentSafe(agentId, "DREAMS.md");
String newContent = existing.isBlank() String newContent = existing.isBlank()
? "# Dreaming 整合日记\n\n" + diary ? "# Dreaming 整合日记\n\n" + diary
: existing + "\n" + diary; : existing + "\n" + diary;
// 防止无限膨胀超过 20KB 时截断只保留最近的内容
if (newContent.length() > 20_000) {
int cutPoint = newContent.length() - 16_000;
// 找到下一个 "## " 标记作为安全截断点
int safePoint = newContent.indexOf("\n## ", cutPoint);
if (safePoint > 0) {
newContent = "# Dreaming 整合日记\n\n> 早期记录已归档\n\n"
+ newContent.substring(safePoint + 1);
}
}
workspaceFileService.saveFile(agentId, "DREAMS.md", newContent); workspaceFileService.saveFile(agentId, "DREAMS.md", newContent);
log.info("[Memory] Dream diary appended for agent={}", agentId); log.info("[Memory] Dream diary appended for agent={}", agentId);
} catch (Exception e) { } catch (Exception e) {

View File

@ -11,8 +11,6 @@ import vip.mate.memory.MemoryProperties;
import vip.mate.memory.model.MemoryRecallEntity; import vip.mate.memory.model.MemoryRecallEntity;
import vip.mate.memory.repository.MemoryRecallMapper; import vip.mate.memory.repository.MemoryRecallMapper;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
@ -45,7 +43,7 @@ public class MemoryRecallService {
return; return;
} }
String snippetHash = sha256(snippetText); // snippet preview 只取前 200 字符避免对大文件做完整 SHA-256
String preview = snippetText != null && snippetText.length() > 200 String preview = snippetText != null && snippetText.length() > 200
? snippetText.substring(0, 200) ? snippetText.substring(0, 200)
: snippetText; : snippetText;
@ -63,10 +61,8 @@ public class MemoryRecallService {
existing.setRecallCount(existing.getRecallCount() + 1); existing.setRecallCount(existing.getRecallCount() + 1);
existing.setDailyCount(existing.getDailyCount() + 1); existing.setDailyCount(existing.getDailyCount() + 1);
existing.setLastRecalledAt(now); existing.setLastRecalledAt(now);
existing.setSnippetHash(snippetHash);
existing.setSnippetPreview(preview); existing.setSnippetPreview(preview);
// 追加 query hash去重最多 MAX_QUERY_HASHES
if (userQueryHash != null) { if (userQueryHash != null) {
List<String> hashes = parseQueryHashes(existing.getQueryHashes()); List<String> hashes = parseQueryHashes(existing.getQueryHashes());
if (!hashes.contains(userQueryHash) && hashes.size() < MAX_QUERY_HASHES) { if (!hashes.contains(userQueryHash) && hashes.size() < MAX_QUERY_HASHES) {
@ -77,10 +73,11 @@ public class MemoryRecallService {
recallMapper.updateById(existing); recallMapper.updateById(existing);
} else { } else {
// 防并发trackRecalls trackActiveRetrieval 可能同时插入同一 filename
try {
MemoryRecallEntity entity = new MemoryRecallEntity(); MemoryRecallEntity entity = new MemoryRecallEntity();
entity.setAgentId(agentId); entity.setAgentId(agentId);
entity.setFilename(filename); entity.setFilename(filename);
entity.setSnippetHash(snippetHash);
entity.setSnippetPreview(preview); entity.setSnippetPreview(preview);
entity.setRecallCount(1); entity.setRecallCount(1);
entity.setDailyCount(1); entity.setDailyCount(1);
@ -96,6 +93,11 @@ public class MemoryRecallService {
} }
recallMapper.insert(entity); recallMapper.insert(entity);
} catch (org.springframework.dao.DuplicateKeyException e) {
// 并发插入冲突退化为更新
log.debug("[MemoryRecall] Concurrent insert for {}, retrying as update", filename);
recordRecall(agentId, filename, snippetText, userQueryHash);
}
} }
} }
@ -133,17 +135,20 @@ public class MemoryRecallService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
// 预解析 queryHashes避免重复 JSON 反序列化每条记录只解析一次
Map<Long, List<String>> queryHashCache = new HashMap<>();
for (MemoryRecallEntity e : candidates) {
queryHashCache.put(e.getId(), parseQueryHashes(e.getQueryHashes()));
}
// 前置硬门控不满足的直接跳过评分 // 前置硬门控不满足的直接跳过评分
int minRecallCount = properties.getEmergenceMinRecallCount(); int minRecallCount = properties.getEmergenceMinRecallCount();
int minUniqueQueries = properties.getEmergenceMinUniqueQueries(); int minUniqueQueries = properties.getEmergenceMinUniqueQueries();
int maxAgeDays = properties.getEmergenceMaxAgeDays(); int maxAgeDays = properties.getEmergenceMaxAgeDays();
candidates = candidates.stream().filter(e -> { candidates = candidates.stream().filter(e -> {
// 门控 1最少召回次数
if (e.getRecallCount() < minRecallCount) return false; if (e.getRecallCount() < minRecallCount) return false;
// 门控 2最少不同查询数 if (queryHashCache.getOrDefault(e.getId(), Collections.emptyList()).size() < minUniqueQueries) return false;
if (parseQueryHashes(e.getQueryHashes()).size() < minUniqueQueries) return false;
// 门控 3最大年龄
if (maxAgeDays > 0 && e.getCreateTime() != null) { if (maxAgeDays > 0 && e.getCreateTime() != null) {
long ageDays = ChronoUnit.DAYS.between(e.getCreateTime(), now); long ageDays = ChronoUnit.DAYS.between(e.getCreateTime(), now);
if (ageDays > maxAgeDays) return false; if (ageDays > maxAgeDays) return false;
@ -160,43 +165,40 @@ public class MemoryRecallService {
.mapToInt(MemoryRecallEntity::getRecallCount) .mapToInt(MemoryRecallEntity::getRecallCount)
.max().orElse(1); .max().orElse(1);
int maxQueryDiversity = candidates.stream() int maxQueryDiversity = candidates.stream()
.mapToInt(e -> parseQueryHashes(e.getQueryHashes()).size()) .mapToInt(e -> queryHashCache.getOrDefault(e.getId(), Collections.emptyList()).size())
.max().orElse(1); .max().orElse(1);
double halfLifeDays = 7.0; double halfLifeDays = 7.0;
double threshold = properties.getEmergenceScoreThreshold(); double threshold = properties.getEmergenceScoreThreshold();
for (MemoryRecallEntity entry : candidates) { for (MemoryRecallEntity entry : candidates) {
// 1. 频率 (0.30)
double frequency = (double) entry.getRecallCount() / Math.max(maxRecallCount, 1); double frequency = (double) entry.getRecallCount() / Math.max(maxRecallCount, 1);
// 2. 时效性 (0.25) 指数衰减
double recency = 0.0; double recency = 0.0;
if (entry.getLastRecalledAt() != null) { if (entry.getLastRecalledAt() != null) {
long daysSinceRecall = ChronoUnit.DAYS.between(entry.getLastRecalledAt(), now); long daysSinceRecall = ChronoUnit.DAYS.between(entry.getLastRecalledAt(), now);
recency = Math.exp(-0.693 * daysSinceRecall / halfLifeDays); // ln(2) 0.693 recency = Math.exp(-0.693 * daysSinceRecall / halfLifeDays);
} }
// 3. 查询多样性 (0.20) int queryCount = queryHashCache.getOrDefault(entry.getId(), Collections.emptyList()).size();
int queryCount = parseQueryHashes(entry.getQueryHashes()).size();
double diversity = (double) queryCount / Math.max(maxQueryDiversity, 1); double diversity = (double) queryCount / Math.max(maxQueryDiversity, 1);
// 4. 内容新鲜度 (0.15) 根据文件名日期
double freshness = computeFreshness(entry.getFilename(), now); double freshness = computeFreshness(entry.getFilename(), now);
// 5. 召回速度 (0.10) dailyCount / recallCount
double velocity = entry.getRecallCount() > 0 double velocity = entry.getRecallCount() > 0
? (double) entry.getDailyCount() / entry.getRecallCount() ? (double) entry.getDailyCount() / entry.getRecallCount()
: 0.0; : 0.0;
double score = 0.30 * frequency entry.setScore(0.30 * frequency + 0.25 * recency + 0.20 * diversity
+ 0.25 * recency + 0.15 * freshness + 0.10 * velocity);
+ 0.20 * diversity }
+ 0.15 * freshness
+ 0.10 * velocity;
entry.setScore(score); // 批量更新分数一次 SQL 替代 N
recallMapper.updateById(entry); for (MemoryRecallEntity entry : candidates) {
recallMapper.update(null,
new LambdaUpdateWrapper<MemoryRecallEntity>()
.eq(MemoryRecallEntity::getId, entry.getId())
.set(MemoryRecallEntity::getScore, entry.getScore()));
} }
return candidates.stream() return candidates.stream()
@ -316,18 +318,4 @@ public class MemoryRecallService {
} }
} }
private String sha256(String text) {
if (text == null || text.isBlank()) return null;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (Exception e) {
return null;
}
}
} }