mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
perf(memory): fix N+1 queries, remove redundant SHA-256, cap DREAMS.md growth
This commit is contained in:
parent
a8613fb05c
commit
e6be23a040
@ -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) {
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user