mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(memory): add Dreaming recall tracking and scored emergence
This commit is contained in:
parent
dd5ec7ff3c
commit
015abcef96
@ -11,6 +11,7 @@ import vip.mate.agent.model.AgentEntity;
|
|||||||
import vip.mate.agent.repository.AgentMapper;
|
import vip.mate.agent.repository.AgentMapper;
|
||||||
import vip.mate.exception.MateClawException;
|
import vip.mate.exception.MateClawException;
|
||||||
import vip.mate.llm.event.ModelConfigChangedEvent;
|
import vip.mate.llm.event.ModelConfigChangedEvent;
|
||||||
|
import vip.mate.memory.service.MemoryRecallTracker;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -31,6 +32,7 @@ public class AgentService {
|
|||||||
|
|
||||||
private final AgentMapper agentMapper;
|
private final AgentMapper agentMapper;
|
||||||
private final AgentGraphBuilder agentGraphBuilder;
|
private final AgentGraphBuilder agentGraphBuilder;
|
||||||
|
private final MemoryRecallTracker memoryRecallTracker;
|
||||||
|
|
||||||
/** 运行时 Agent 实例缓存(agentId -> BaseAgent) */
|
/** 运行时 Agent 实例缓存(agentId -> BaseAgent) */
|
||||||
private final Map<Long, BaseAgent> agentInstances = new ConcurrentHashMap<>();
|
private final Map<Long, BaseAgent> agentInstances = new ConcurrentHashMap<>();
|
||||||
@ -73,11 +75,13 @@ public class AgentService {
|
|||||||
// ==================== 运行时入口 ====================
|
// ==================== 运行时入口 ====================
|
||||||
|
|
||||||
public String chat(Long agentId, String message, String conversationId) {
|
public String chat(Long agentId, String message, String conversationId) {
|
||||||
|
memoryRecallTracker.trackRecalls(agentId, message);
|
||||||
BaseAgent agent = getOrBuildAgent(agentId);
|
BaseAgent agent = getOrBuildAgent(agentId);
|
||||||
return agent.chat(message, conversationId);
|
return agent.chat(message, conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flux<String> chatStream(Long agentId, String message, String conversationId) {
|
public Flux<String> chatStream(Long agentId, String message, String conversationId) {
|
||||||
|
memoryRecallTracker.trackRecalls(agentId, message);
|
||||||
BaseAgent agent = getOrBuildAgent(agentId);
|
BaseAgent agent = getOrBuildAgent(agentId);
|
||||||
return agent.chatStream(message, conversationId);
|
return agent.chatStream(message, conversationId);
|
||||||
}
|
}
|
||||||
@ -88,6 +92,7 @@ public class AgentService {
|
|||||||
|
|
||||||
public Flux<StreamDelta> chatStructuredStream(Long agentId, String message, String conversationId,
|
public Flux<StreamDelta> chatStructuredStream(Long agentId, String message, String conversationId,
|
||||||
String requesterId) {
|
String requesterId) {
|
||||||
|
memoryRecallTracker.trackRecalls(agentId, message);
|
||||||
BaseAgent agent = getOrBuildAgent(agentId);
|
BaseAgent agent = getOrBuildAgent(agentId);
|
||||||
|
|
||||||
if (agent instanceof StructuredStreamCapable capable) {
|
if (agent instanceof StructuredStreamCapable capable) {
|
||||||
@ -101,6 +106,7 @@ public class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String execute(Long agentId, String goal, String conversationId) {
|
public String execute(Long agentId, String goal, String conversationId) {
|
||||||
|
memoryRecallTracker.trackRecalls(agentId, goal);
|
||||||
BaseAgent agent = getOrBuildAgent(agentId);
|
BaseAgent agent = getOrBuildAgent(agentId);
|
||||||
return agent.execute(goal, conversationId);
|
return agent.execute(goal, conversationId);
|
||||||
}
|
}
|
||||||
@ -116,6 +122,7 @@ public class AgentService {
|
|||||||
*/
|
*/
|
||||||
public String chatWithReplay(Long agentId, String userMessage, String conversationId,
|
public String chatWithReplay(Long agentId, String userMessage, String conversationId,
|
||||||
String toolCallPayload) {
|
String toolCallPayload) {
|
||||||
|
memoryRecallTracker.trackRecalls(agentId, userMessage);
|
||||||
BaseAgent agent = getOrBuildAgent(agentId);
|
BaseAgent agent = getOrBuildAgent(agentId);
|
||||||
return agent.chatWithReplay(userMessage, conversationId, toolCallPayload);
|
return agent.chatWithReplay(userMessage, conversationId, toolCallPayload);
|
||||||
}
|
}
|
||||||
@ -130,6 +137,7 @@ public class AgentService {
|
|||||||
|
|
||||||
public Flux<StreamDelta> chatWithReplayStream(Long agentId, String userMessage, String conversationId,
|
public Flux<StreamDelta> chatWithReplayStream(Long agentId, String userMessage, String conversationId,
|
||||||
String toolCallPayload, String requesterId) {
|
String toolCallPayload, String requesterId) {
|
||||||
|
memoryRecallTracker.trackRecalls(agentId, userMessage);
|
||||||
BaseAgent agent = getOrBuildAgent(agentId);
|
BaseAgent agent = getOrBuildAgent(agentId);
|
||||||
return agent.chatWithReplayStream(userMessage, conversationId, toolCallPayload,
|
return agent.chatWithReplayStream(userMessage, conversationId, toolCallPayload,
|
||||||
requesterId != null ? requesterId : "");
|
requesterId != null ? requesterId : "");
|
||||||
|
|||||||
@ -38,4 +38,15 @@ public class MemoryProperties {
|
|||||||
|
|
||||||
/** 构建对话 transcript 时的最大消息数(防止过长) */
|
/** 构建对话 transcript 时的最大消息数(防止过长) */
|
||||||
private int maxTranscriptMessages = 30;
|
private int maxTranscriptMessages = 30;
|
||||||
|
|
||||||
|
// ==================== Dreaming 配置 ====================
|
||||||
|
|
||||||
|
/** 启用定时 Dreaming(自动执行记忆整合) */
|
||||||
|
private boolean dreamingEnabled = true;
|
||||||
|
|
||||||
|
/** Dreaming 调度 cron 表达式(Spring 6 字段格式,默认每天凌晨 3 点) */
|
||||||
|
private String dreamingCron = "0 0 3 * * ?";
|
||||||
|
|
||||||
|
/** 记忆召回评分阈值,低于此分的候选不进入 LLM 整合 */
|
||||||
|
private double emergenceScoreThreshold = 0.4;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
package vip.mate.memory.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory 模块 Schema 迁移
|
||||||
|
* <p>
|
||||||
|
* 确保 mate_memory_recall 表存在(兼容已有部署)。
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@Order(201)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MemorySchemaMigration implements ApplicationRunner {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
try {
|
||||||
|
jdbcTemplate.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS mate_memory_recall (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
filename VARCHAR(256) NOT NULL,
|
||||||
|
snippet_hash VARCHAR(64),
|
||||||
|
snippet_preview VARCHAR(512),
|
||||||
|
recall_count INT NOT NULL DEFAULT 0,
|
||||||
|
daily_count INT NOT NULL DEFAULT 0,
|
||||||
|
query_hashes TEXT,
|
||||||
|
score DOUBLE NOT NULL DEFAULT 0.0,
|
||||||
|
last_recalled_at DATETIME,
|
||||||
|
promoted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
create_time DATETIME NOT NULL,
|
||||||
|
update_time DATETIME NOT NULL,
|
||||||
|
deleted INT NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_memory_recall_agent ON mate_memory_recall(agent_id)");
|
||||||
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_memory_recall_agent_file ON mate_memory_recall(agent_id, filename)");
|
||||||
|
jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_memory_recall_score ON mate_memory_recall(agent_id, score)");
|
||||||
|
log.info("[MemorySchemaMigration] mate_memory_recall schema migration completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[MemorySchemaMigration] Migration failed (table may already exist): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package vip.mate.memory.model;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆召回追踪实体
|
||||||
|
* <p>
|
||||||
|
* 记录 workspace 文件在对话上下文注入时的召回信息,
|
||||||
|
* 用于加权评分驱动的记忆整合(Dreaming)。
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("mate_memory_recall")
|
||||||
|
public class MemoryRecallEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 关联 Agent ID */
|
||||||
|
private Long agentId;
|
||||||
|
|
||||||
|
/** 文件名,如 "memory/2026-04-01.md"、"MEMORY.md" */
|
||||||
|
private String filename;
|
||||||
|
|
||||||
|
/** 被召回片段的 SHA-256 哈希(可为 null,文件级追踪时不填) */
|
||||||
|
private String snippetHash;
|
||||||
|
|
||||||
|
/** 片段预览(前 200 字符,方便调试) */
|
||||||
|
private String snippetPreview;
|
||||||
|
|
||||||
|
/** 累计召回次数 */
|
||||||
|
private Integer recallCount;
|
||||||
|
|
||||||
|
/** 当日召回次数(每轮 dreaming 重置) */
|
||||||
|
private Integer dailyCount;
|
||||||
|
|
||||||
|
/** 不同 user query hash 的 JSON 数组 */
|
||||||
|
private String queryHashes;
|
||||||
|
|
||||||
|
/** 加权评分 */
|
||||||
|
private Double score;
|
||||||
|
|
||||||
|
/** 最近一次召回时间 */
|
||||||
|
private LocalDateTime lastRecalledAt;
|
||||||
|
|
||||||
|
/** 是否已提升到 MEMORY.md */
|
||||||
|
private Boolean promoted;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package vip.mate.memory.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import vip.mate.memory.model.MemoryRecallEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆召回追踪 Mapper
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MemoryRecallMapper extends BaseMapper<MemoryRecallEntity> {
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package vip.mate.memory.scheduler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import vip.mate.agent.AgentService;
|
||||||
|
import vip.mate.agent.model.AgentEntity;
|
||||||
|
import vip.mate.memory.MemoryProperties;
|
||||||
|
import vip.mate.memory.service.MemoryEmergenceService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dreaming 定时调度器
|
||||||
|
* <p>
|
||||||
|
* 按配置的 cron 表达式定期执行记忆整合,
|
||||||
|
* 遍历所有启用的 Agent,对每个 Agent 执行评分驱动的 emergence。
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DreamingScheduler {
|
||||||
|
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final MemoryEmergenceService emergenceService;
|
||||||
|
private final MemoryProperties properties;
|
||||||
|
|
||||||
|
@Scheduled(cron = "${mate.memory.dreaming-cron:0 0 3 * * ?}")
|
||||||
|
public void runDreaming() {
|
||||||
|
if (!properties.isDreamingEnabled()) {
|
||||||
|
log.debug("[Dreaming] Scheduled dreaming is disabled, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[Dreaming] Starting scheduled dreaming cycle");
|
||||||
|
List<AgentEntity> agents = agentService.listAgents();
|
||||||
|
|
||||||
|
int success = 0;
|
||||||
|
int failed = 0;
|
||||||
|
|
||||||
|
for (AgentEntity agent : agents) {
|
||||||
|
if (!Boolean.TRUE.equals(agent.getEnabled())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
emergenceService.consolidate(agent.getId());
|
||||||
|
success++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
failed++;
|
||||||
|
log.warn("[Dreaming] Failed for agent={} ({}): {}",
|
||||||
|
agent.getId(), agent.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[Dreaming] Cycle completed: {} succeeded, {} failed", success, failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,11 +15,13 @@ import vip.mate.agent.prompt.PromptLoader;
|
|||||||
import vip.mate.llm.service.ModelConfigService;
|
import vip.mate.llm.service.ModelConfigService;
|
||||||
import vip.mate.llm.model.ModelConfigEntity;
|
import vip.mate.llm.model.ModelConfigEntity;
|
||||||
import vip.mate.memory.MemoryProperties;
|
import vip.mate.memory.MemoryProperties;
|
||||||
|
import vip.mate.memory.model.MemoryRecallEntity;
|
||||||
import vip.mate.workspace.document.WorkspaceFileService;
|
import vip.mate.workspace.document.WorkspaceFileService;
|
||||||
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记忆整合服务
|
* 记忆整合服务
|
||||||
@ -39,6 +41,7 @@ public class MemoryEmergenceService {
|
|||||||
private final AgentGraphBuilder agentGraphBuilder;
|
private final AgentGraphBuilder agentGraphBuilder;
|
||||||
private final MemoryProperties properties;
|
private final MemoryProperties properties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final MemoryRecallService recallService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行记忆整合:将 daily notes 中的重复模式提炼到 MEMORY.md
|
* 执行记忆整合:将 daily notes 中的重复模式提炼到 MEMORY.md
|
||||||
@ -84,13 +87,34 @@ public class MemoryEmergenceService {
|
|||||||
// 3. 读取现有 MEMORY.md
|
// 3. 读取现有 MEMORY.md
|
||||||
String memoryContent = readFileContentSafe(agentId, "MEMORY.md");
|
String memoryContent = readFileContentSafe(agentId, "MEMORY.md");
|
||||||
|
|
||||||
// 4. 构建 prompt 并调用 LLM
|
// 4. 计算召回评分(必须在 resetDailyCounts 之前,否则 velocity 信号被清零)
|
||||||
|
List<MemoryRecallEntity> scoredCandidates = recallService.computeScores(agentId);
|
||||||
|
boolean hasScoredCandidates = !scoredCandidates.isEmpty();
|
||||||
|
|
||||||
|
// 5. 评分快照完成后再重置 dailyCount,为下一轮积累
|
||||||
|
recallService.resetDailyCounts(agentId);
|
||||||
|
|
||||||
String systemPrompt = PromptLoader.loadPrompt("memory/emergence-system");
|
String systemPrompt = PromptLoader.loadPrompt("memory/emergence-system");
|
||||||
String userTemplate = PromptLoader.loadPrompt("memory/emergence-user");
|
String userPrompt;
|
||||||
String userPrompt = userTemplate
|
|
||||||
.replace("{memory}", memoryContent)
|
if (hasScoredCandidates) {
|
||||||
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
// 使用评分增强的 prompt
|
||||||
.replace("{daily_notes}", dailyNotes);
|
String candidatesText = formatScoredCandidates(scoredCandidates);
|
||||||
|
String userTemplate = PromptLoader.loadPrompt("memory/emergence-scored-user");
|
||||||
|
userPrompt = userTemplate
|
||||||
|
.replace("{memory}", memoryContent)
|
||||||
|
.replace("{scored_candidates}", candidatesText)
|
||||||
|
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||||
|
.replace("{daily_notes}", dailyNotes);
|
||||||
|
log.info("[Memory] Emergence with {} scored candidates for agent={}", scoredCandidates.size(), agentId);
|
||||||
|
} else {
|
||||||
|
// 冷启动:回退到原有纯 LLM 逻辑
|
||||||
|
String userTemplate = PromptLoader.loadPrompt("memory/emergence-user");
|
||||||
|
userPrompt = userTemplate
|
||||||
|
.replace("{memory}", memoryContent)
|
||||||
|
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||||
|
.replace("{daily_notes}", dailyNotes);
|
||||||
|
}
|
||||||
|
|
||||||
String llmResponse;
|
String llmResponse;
|
||||||
try {
|
try {
|
||||||
@ -122,6 +146,19 @@ public class MemoryEmergenceService {
|
|||||||
workspaceFileService.saveFile(agentId, "MEMORY.md", newContent);
|
workspaceFileService.saveFile(agentId, "MEMORY.md", newContent);
|
||||||
String reason = root.path("reason").asText("");
|
String reason = root.path("reason").asText("");
|
||||||
log.info("[Memory] Emergence completed for agent={}: {}", agentId, reason);
|
log.info("[Memory] Emergence completed for agent={}: {}", agentId, reason);
|
||||||
|
|
||||||
|
// 逐候选检查:只有内容被 LLM 实际采纳(出现在新 MEMORY.md 中)的才标记为已提升
|
||||||
|
if (hasScoredCandidates) {
|
||||||
|
List<Long> promotedIds = scoredCandidates.stream()
|
||||||
|
.filter(c -> candidateAdoptedInMemory(c, newContent))
|
||||||
|
.map(MemoryRecallEntity::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!promotedIds.isEmpty()) {
|
||||||
|
recallService.markPromoted(promotedIds);
|
||||||
|
}
|
||||||
|
log.info("[Memory] Promoted {}/{} recall candidates for agent={}",
|
||||||
|
promotedIds.size(), scoredCandidates.size(), agentId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -129,6 +166,48 @@ public class MemoryEmergenceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatScoredCandidates(List<MemoryRecallEntity> candidates) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (MemoryRecallEntity entry : candidates) {
|
||||||
|
sb.append(String.format("### %s (score=%.2f, recalls=%d)\n",
|
||||||
|
entry.getFilename(), entry.getScore(), entry.getRecallCount()));
|
||||||
|
if (entry.getSnippetPreview() != null) {
|
||||||
|
sb.append(entry.getSnippetPreview());
|
||||||
|
sb.append("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断候选片段是否被 LLM 实际采纳到新 MEMORY.md 中。
|
||||||
|
* 通过检查片段预览中的关键短语(取前 3 个非空行的前 20 字符)是否出现在新内容中。
|
||||||
|
*/
|
||||||
|
private boolean candidateAdoptedInMemory(MemoryRecallEntity candidate, String newMemoryContent) {
|
||||||
|
String preview = candidate.getSnippetPreview();
|
||||||
|
if (preview == null || preview.isBlank() || newMemoryContent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 从 snippet 提取关键短语进行匹配
|
||||||
|
String[] lines = preview.split("\n");
|
||||||
|
int matched = 0;
|
||||||
|
int checked = 0;
|
||||||
|
for (String line : lines) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (trimmed.isEmpty() || trimmed.startsWith("#")) continue;
|
||||||
|
if (checked >= 3) break;
|
||||||
|
checked++;
|
||||||
|
// 取行的核心内容(去掉 markdown 标记),检查是否出现在新 MEMORY.md 中
|
||||||
|
String key = trimmed.replaceAll("^[-*>]+\\s*", "");
|
||||||
|
if (key.length() > 20) key = key.substring(0, 20);
|
||||||
|
if (key.length() >= 5 && newMemoryContent.contains(key)) {
|
||||||
|
matched++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 至少有 1 个关键短语命中才算采纳
|
||||||
|
return matched > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private ChatModel buildChatModel() {
|
private ChatModel buildChatModel() {
|
||||||
ModelConfigEntity defaultModel = modelConfigService.getDefaultModel();
|
ModelConfigEntity defaultModel = modelConfigService.getDefaultModel();
|
||||||
return agentGraphBuilder.buildRuntimeChatModel(defaultModel);
|
return agentGraphBuilder.buildRuntimeChatModel(defaultModel);
|
||||||
|
|||||||
@ -0,0 +1,257 @@
|
|||||||
|
package vip.mate.memory.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
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.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆召回追踪与评分服务
|
||||||
|
* <p>
|
||||||
|
* 记录 workspace 文件的召回频率、查询多样性等信号,
|
||||||
|
* 计算加权评分用于 Dreaming 记忆整合。
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MemoryRecallService {
|
||||||
|
|
||||||
|
private final MemoryRecallMapper recallMapper;
|
||||||
|
private final MemoryProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private static final int MAX_QUERY_HASHES = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录一次文件召回
|
||||||
|
*/
|
||||||
|
public void recordRecall(Long agentId, String filename, String snippetText, String userQueryHash) {
|
||||||
|
if (agentId == null || filename == null || filename.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String snippetHash = sha256(snippetText);
|
||||||
|
String preview = snippetText != null && snippetText.length() > 200
|
||||||
|
? snippetText.substring(0, 200)
|
||||||
|
: snippetText;
|
||||||
|
|
||||||
|
MemoryRecallEntity existing = recallMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<MemoryRecallEntity>()
|
||||||
|
.eq(MemoryRecallEntity::getAgentId, agentId)
|
||||||
|
.eq(MemoryRecallEntity::getFilename, filename)
|
||||||
|
.eq(MemoryRecallEntity::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
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) {
|
||||||
|
hashes.add(userQueryHash);
|
||||||
|
}
|
||||||
|
existing.setQueryHashes(toJson(hashes));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (userQueryHash != null) {
|
||||||
|
entity.setQueryHashes(toJson(List.of(userQueryHash)));
|
||||||
|
}
|
||||||
|
|
||||||
|
recallMapper.insert(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有记录的 dailyCount(在每轮 dreaming 开始时调用)
|
||||||
|
*/
|
||||||
|
public void resetDailyCounts(Long agentId) {
|
||||||
|
recallMapper.update(null,
|
||||||
|
new LambdaUpdateWrapper<MemoryRecallEntity>()
|
||||||
|
.eq(MemoryRecallEntity::getAgentId, agentId)
|
||||||
|
.eq(MemoryRecallEntity::getDeleted, 0)
|
||||||
|
.set(MemoryRecallEntity::getDailyCount, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未提升的候选列表
|
||||||
|
*/
|
||||||
|
public List<MemoryRecallEntity> listCandidates(Long agentId) {
|
||||||
|
return recallMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<MemoryRecallEntity>()
|
||||||
|
.eq(MemoryRecallEntity::getAgentId, agentId)
|
||||||
|
.eq(MemoryRecallEntity::getPromoted, false)
|
||||||
|
.eq(MemoryRecallEntity::getDeleted, 0)
|
||||||
|
.orderByDesc(MemoryRecallEntity::getScore));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算加权评分,返回超过阈值的高分候选
|
||||||
|
*/
|
||||||
|
public List<MemoryRecallEntity> computeScores(Long agentId) {
|
||||||
|
List<MemoryRecallEntity> candidates = listCandidates(agentId);
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归一化参数
|
||||||
|
int maxRecallCount = candidates.stream()
|
||||||
|
.mapToInt(MemoryRecallEntity::getRecallCount)
|
||||||
|
.max().orElse(1);
|
||||||
|
int maxQueryDiversity = candidates.stream()
|
||||||
|
.mapToInt(e -> parseQueryHashes(e.getQueryHashes()).size())
|
||||||
|
.max().orElse(1);
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 查询多样性 (0.20)
|
||||||
|
int queryCount = parseQueryHashes(entry.getQueryHashes()).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(score);
|
||||||
|
recallMapper.updateById(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates.stream()
|
||||||
|
.filter(e -> e.getScore() >= threshold)
|
||||||
|
.sorted(Comparator.comparingDouble(MemoryRecallEntity::getScore).reversed())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记候选为已提升
|
||||||
|
*/
|
||||||
|
public void markPromoted(List<Long> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) return;
|
||||||
|
recallMapper.update(null,
|
||||||
|
new LambdaUpdateWrapper<MemoryRecallEntity>()
|
||||||
|
.in(MemoryRecallEntity::getId, ids)
|
||||||
|
.set(MemoryRecallEntity::getPromoted, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 内部工具方法 ====================
|
||||||
|
|
||||||
|
private double computeFreshness(String filename, LocalDateTime now) {
|
||||||
|
// 从 "memory/2026-04-01.md" 或 "memory/2026-04-01.md#section" 提取日期
|
||||||
|
if (filename == null || !filename.startsWith("memory/")) {
|
||||||
|
return 0.5; // 非 daily note 给中间值
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String datePart = filename.replace("memory/", "");
|
||||||
|
// 剥离 #anchor(片段级追踪产生的 section key)
|
||||||
|
int hashIdx = datePart.indexOf('#');
|
||||||
|
if (hashIdx > 0) {
|
||||||
|
datePart = datePart.substring(0, hashIdx);
|
||||||
|
}
|
||||||
|
datePart = datePart.replace(".md", "");
|
||||||
|
String[] parts = datePart.split("-");
|
||||||
|
if (parts.length == 3) {
|
||||||
|
LocalDateTime fileDate = LocalDateTime.of(
|
||||||
|
Integer.parseInt(parts[0]),
|
||||||
|
Integer.parseInt(parts[1]),
|
||||||
|
Integer.parseInt(parts[2]),
|
||||||
|
0, 0);
|
||||||
|
long daysAgo = ChronoUnit.DAYS.between(fileDate, now);
|
||||||
|
return Math.max(0, 1.0 - (double) daysAgo / 30.0); // 30 天线性衰减
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseQueryHashes(String json) {
|
||||||
|
if (json == null || json.isBlank()) return new ArrayList<>();
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, new TypeReference<>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toJson(List<String> list) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(list);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
package vip.mate.memory.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
||||||
|
import vip.mate.workspace.document.repository.WorkspaceFileMapper;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆召回追踪器
|
||||||
|
* <p>
|
||||||
|
* 在每次对话时异步记录哪些 workspace 文件/片段被实际注入到上下文中。
|
||||||
|
* 追踪粒度为片段级(daily note 按 ## 标题拆分),而非文件级。
|
||||||
|
*
|
||||||
|
* @author MateClaw Team
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MemoryRecallTracker {
|
||||||
|
|
||||||
|
private final MemoryRecallService recallService;
|
||||||
|
private final WorkspaceFileMapper workspaceFileMapper;
|
||||||
|
|
||||||
|
/** 用于拆分 daily note 中的二级标题片段 */
|
||||||
|
private static final Pattern SECTION_PATTERN = Pattern.compile("(?m)^## .+");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步追踪一次对话中被实际注入到 system prompt 的文件片段。
|
||||||
|
* <p>
|
||||||
|
* 仅追踪满足 buildSystemPrompt 注入条件的文件(enabled=true, content 非空)。
|
||||||
|
* 对 daily note 类文件(memory/*.md),按 ## 标题拆分为独立片段追踪。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param userQuery 用户消息(用于计算 query hash)
|
||||||
|
*/
|
||||||
|
@Async
|
||||||
|
public void trackRecalls(Long agentId, String userQuery) {
|
||||||
|
try {
|
||||||
|
// 精确复现 buildSystemPrompt 的注入条件
|
||||||
|
List<WorkspaceFileEntity> injectedFiles = workspaceFileMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<WorkspaceFileEntity>()
|
||||||
|
.eq(WorkspaceFileEntity::getAgentId, agentId)
|
||||||
|
.eq(WorkspaceFileEntity::getEnabled, true)
|
||||||
|
.isNotNull(WorkspaceFileEntity::getContent)
|
||||||
|
.ne(WorkspaceFileEntity::getContent, "")
|
||||||
|
.orderByAsc(WorkspaceFileEntity::getSortOrder));
|
||||||
|
|
||||||
|
if (injectedFiles.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String queryHash = sha256Short(userQuery);
|
||||||
|
int trackedCount = 0;
|
||||||
|
|
||||||
|
for (WorkspaceFileEntity file : injectedFiles) {
|
||||||
|
String content = file.getContent();
|
||||||
|
if (content == null || content.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = file.getFilename();
|
||||||
|
|
||||||
|
if (filename.startsWith("memory/") && filename.endsWith(".md")) {
|
||||||
|
// daily note: 按 ## 标题拆分为独立片段
|
||||||
|
trackedCount += trackDailyNoteSnippets(agentId, filename, content, queryHash);
|
||||||
|
} else {
|
||||||
|
// 非 daily note (PROFILE.md, MEMORY.md 等): 文件级追踪
|
||||||
|
recallService.recordRecall(agentId, filename, content, queryHash);
|
||||||
|
trackedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("[MemoryRecall] Tracked {} snippets for agent={}", trackedCount, agentId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[MemoryRecall] Failed to track recalls for agent={}: {}", agentId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 daily note 按 ## 标题拆分为独立片段,分别追踪
|
||||||
|
*/
|
||||||
|
private int trackDailyNoteSnippets(Long agentId, String filename, String content, String queryHash) {
|
||||||
|
Matcher matcher = SECTION_PATTERN.matcher(content);
|
||||||
|
List<Integer> sectionStarts = new java.util.ArrayList<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
sectionStarts.add(matcher.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionStarts.isEmpty()) {
|
||||||
|
// 没有 ## 标题,整个文件作为一个片段
|
||||||
|
recallService.recordRecall(agentId, filename, content.trim(), queryHash);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
// 如果 ## 前有内容,作为第一个片段
|
||||||
|
if (sectionStarts.get(0) > 0) {
|
||||||
|
String preamble = content.substring(0, sectionStarts.get(0)).trim();
|
||||||
|
if (!preamble.isEmpty()) {
|
||||||
|
recallService.recordRecall(agentId, filename + "#preamble", preamble, queryHash);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < sectionStarts.size(); i++) {
|
||||||
|
int start = sectionStarts.get(i);
|
||||||
|
int end = (i + 1 < sectionStarts.size()) ? sectionStarts.get(i + 1) : content.length();
|
||||||
|
String snippet = content.substring(start, end).trim();
|
||||||
|
if (!snippet.isEmpty()) {
|
||||||
|
// 从 ## 标题行提取 section 标识
|
||||||
|
String firstLine = snippet.contains("\n") ? snippet.substring(0, snippet.indexOf('\n')).trim() : snippet;
|
||||||
|
String sectionKey = filename + "#" + sanitizeSectionKey(firstLine);
|
||||||
|
recallService.recordRecall(agentId, sectionKey, snippet, queryHash);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizeSectionKey(String heading) {
|
||||||
|
// "## Some Title" -> "some-title"
|
||||||
|
return heading.replaceFirst("^#+\\s*", "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll("[^a-z0-9\\u4e00-\\u9fff]+", "-")
|
||||||
|
.replaceAll("^-|-$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sha256Short(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 (int i = 0; i < 8 && i < hash.length; i++) {
|
||||||
|
hex.append(String.format("%02x", hash[i]));
|
||||||
|
}
|
||||||
|
return hex.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -409,3 +409,25 @@ ALTER TABLE mate_message ADD COLUMN IF NOT EXISTS metadata JSON;
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_guard_audit_conv ON mate_tool_guard_audit_log(conversation_id);
|
CREATE INDEX IF NOT EXISTS idx_guard_audit_conv ON mate_tool_guard_audit_log(conversation_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_guard_audit_time ON mate_tool_guard_audit_log(create_time);
|
CREATE INDEX IF NOT EXISTS idx_guard_audit_time ON mate_tool_guard_audit_log(create_time);
|
||||||
|
|
||||||
|
-- 记忆召回追踪表(Dreaming 评分驱动记忆整合)
|
||||||
|
CREATE TABLE IF NOT EXISTS mate_memory_recall (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL,
|
||||||
|
filename VARCHAR(256) NOT NULL,
|
||||||
|
snippet_hash VARCHAR(64),
|
||||||
|
snippet_preview VARCHAR(512),
|
||||||
|
recall_count INT NOT NULL DEFAULT 0,
|
||||||
|
daily_count INT NOT NULL DEFAULT 0,
|
||||||
|
query_hashes TEXT,
|
||||||
|
score DOUBLE NOT NULL DEFAULT 0.0,
|
||||||
|
last_recalled_at DATETIME,
|
||||||
|
promoted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
create_time DATETIME NOT NULL,
|
||||||
|
update_time DATETIME NOT NULL,
|
||||||
|
deleted INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memory_recall_agent ON mate_memory_recall(agent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memory_recall_agent_file ON mate_memory_recall(agent_id, filename);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memory_recall_score ON mate_memory_recall(agent_id, score);
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
## 现有 MEMORY.md 内容
|
||||||
|
```
|
||||||
|
{memory}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高分召回候选(按评分排序)
|
||||||
|
|
||||||
|
以下文件在对话中被频繁引用,评分基于召回频率、时效性、查询多样性等维度自动计算。
|
||||||
|
**优先整合评分高的内容**,它们是用户实际在对话中反复使用的记忆。
|
||||||
|
|
||||||
|
{scored_candidates}
|
||||||
|
|
||||||
|
## 最近 {day_range} 天的 daily notes(补充参考)
|
||||||
|
|
||||||
|
{daily_notes}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
请分析以上内容,优先将高分召回候选中的稳定信息整合到 MEMORY.md 中。
|
||||||
|
低分或未被引用的内容仅作补充参考。严格输出 JSON 格式。
|
||||||
Loading…
Reference in New Issue
Block a user