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");
}
// 读取现有 DREAMS.md追加到开头最新在最上面
// 读取现有 DREAMS.md追加新日记
String existing = readFileContentSafe(agentId, "DREAMS.md");
String newContent = existing.isBlank()
? "# Dreaming 整合日记\n\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);
log.info("[Memory] Dream diary appended for agent={}", agentId);
} catch (Exception e) {

View File

@ -11,8 +11,6 @@ import vip.mate.memory.MemoryProperties;
import vip.mate.memory.model.MemoryRecallEntity;
import vip.mate.memory.repository.MemoryRecallMapper;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
@ -45,7 +43,7 @@ public class MemoryRecallService {
return;
}
String snippetHash = sha256(snippetText);
// snippet preview 只取前 200 字符避免对大文件做完整 SHA-256
String preview = snippetText != null && snippetText.length() > 200
? snippetText.substring(0, 200)
: snippetText;
@ -63,10 +61,8 @@ public class MemoryRecallService {
existing.setRecallCount(existing.getRecallCount() + 1);
existing.setDailyCount(existing.getDailyCount() + 1);
existing.setLastRecalledAt(now);
existing.setSnippetHash(snippetHash);
existing.setSnippetPreview(preview);
// 追加 query hash去重最多 MAX_QUERY_HASHES
if (userQueryHash != null) {
List<String> hashes = parseQueryHashes(existing.getQueryHashes());
if (!hashes.contains(userQueryHash) && hashes.size() < MAX_QUERY_HASHES) {
@ -77,25 +73,31 @@ public class MemoryRecallService {
recallMapper.updateById(existing);
} else {
MemoryRecallEntity entity = new MemoryRecallEntity();
entity.setAgentId(agentId);
entity.setFilename(filename);
entity.setSnippetHash(snippetHash);
entity.setSnippetPreview(preview);
entity.setRecallCount(1);
entity.setDailyCount(1);
entity.setLastRecalledAt(now);
entity.setPromoted(false);
entity.setScore(0.0);
entity.setCreateTime(now);
entity.setUpdateTime(now);
entity.setDeleted(0);
// 防并发trackRecalls trackActiveRetrieval 可能同时插入同一 filename
try {
MemoryRecallEntity entity = new MemoryRecallEntity();
entity.setAgentId(agentId);
entity.setFilename(filename);
entity.setSnippetPreview(preview);
entity.setRecallCount(1);
entity.setDailyCount(1);
entity.setLastRecalledAt(now);
entity.setPromoted(false);
entity.setScore(0.0);
entity.setCreateTime(now);
entity.setUpdateTime(now);
entity.setDeleted(0);
if (userQueryHash != null) {
entity.setQueryHashes(toJson(List.of(userQueryHash)));
if (userQueryHash != null) {
entity.setQueryHashes(toJson(List.of(userQueryHash)));
}
recallMapper.insert(entity);
} catch (org.springframework.dao.DuplicateKeyException e) {
// 并发插入冲突退化为更新
log.debug("[MemoryRecall] Concurrent insert for {}, retrying as update", filename);
recordRecall(agentId, filename, snippetText, userQueryHash);
}
recallMapper.insert(entity);
}
}
@ -133,17 +135,20 @@ public class MemoryRecallService {
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 minUniqueQueries = properties.getEmergenceMinUniqueQueries();
int maxAgeDays = properties.getEmergenceMaxAgeDays();
candidates = candidates.stream().filter(e -> {
// 门控 1最少召回次数
if (e.getRecallCount() < minRecallCount) return false;
// 门控 2最少不同查询数
if (parseQueryHashes(e.getQueryHashes()).size() < minUniqueQueries) return false;
// 门控 3最大年龄
if (queryHashCache.getOrDefault(e.getId(), Collections.emptyList()).size() < minUniqueQueries) return false;
if (maxAgeDays > 0 && e.getCreateTime() != null) {
long ageDays = ChronoUnit.DAYS.between(e.getCreateTime(), now);
if (ageDays > maxAgeDays) return false;
@ -160,43 +165,40 @@ public class MemoryRecallService {
.mapToInt(MemoryRecallEntity::getRecallCount)
.max().orElse(1);
int maxQueryDiversity = candidates.stream()
.mapToInt(e -> parseQueryHashes(e.getQueryHashes()).size())
.mapToInt(e -> queryHashCache.getOrDefault(e.getId(), Collections.emptyList()).size())
.max().orElse(1);
double halfLifeDays = 7.0;
double threshold = properties.getEmergenceScoreThreshold();
for (MemoryRecallEntity entry : candidates) {
// 1. 频率 (0.30)
double frequency = (double) entry.getRecallCount() / Math.max(maxRecallCount, 1);
// 2. 时效性 (0.25) 指数衰减
double recency = 0.0;
if (entry.getLastRecalledAt() != null) {
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 = parseQueryHashes(entry.getQueryHashes()).size();
int queryCount = queryHashCache.getOrDefault(entry.getId(), Collections.emptyList()).size();
double diversity = (double) queryCount / Math.max(maxQueryDiversity, 1);
// 4. 内容新鲜度 (0.15) 根据文件名日期
double freshness = computeFreshness(entry.getFilename(), now);
// 5. 召回速度 (0.10) dailyCount / recallCount
double velocity = entry.getRecallCount() > 0
? (double) entry.getDailyCount() / entry.getRecallCount()
: 0.0;
double score = 0.30 * frequency
+ 0.25 * recency
+ 0.20 * diversity
+ 0.15 * freshness
+ 0.10 * velocity;
entry.setScore(0.30 * frequency + 0.25 * recency + 0.20 * diversity
+ 0.15 * freshness + 0.10 * velocity);
}
entry.setScore(score);
recallMapper.updateById(entry);
// 批量更新分数一次 SQL 替代 N
for (MemoryRecallEntity entry : candidates) {
recallMapper.update(null,
new LambdaUpdateWrapper<MemoryRecallEntity>()
.eq(MemoryRecallEntity::getId, entry.getId())
.set(MemoryRecallEntity::getScore, entry.getScore()));
}
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;
}
}
}