mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
refactor: 合并精确对照+记忆进化为统一记忆整合模块 (consolidator.js)
This commit is contained in:
341
consolidator.js
Normal file
341
consolidator.js
Normal file
@@ -0,0 +1,341 @@
|
||||
// ST-BME: 统一记忆整合引擎
|
||||
// 合并 Mem0 精确对照 + A-MEM 记忆进化为单一阶段
|
||||
// 每个新节点只需 1 次 embed + 1 次 LLM 调用
|
||||
|
||||
import { addEdge, createEdge, getActiveNodes, getNode } from './graph.js';
|
||||
import { callLLMForJSON } from './llm.js';
|
||||
import {
|
||||
buildNodeVectorText,
|
||||
findSimilarNodesByText,
|
||||
validateVectorConfig,
|
||||
} from './vector-index.js';
|
||||
|
||||
function createAbortError(message = '操作已终止') {
|
||||
const error = new Error(message);
|
||||
error.name = 'AbortError';
|
||||
return error;
|
||||
}
|
||||
|
||||
function isAbortError(error) {
|
||||
return error?.name === 'AbortError';
|
||||
}
|
||||
|
||||
function throwIfAborted(signal) {
|
||||
if (signal?.aborted) {
|
||||
throw signal.reason instanceof Error ? signal.reason : createAbortError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一记忆整合系统提示词
|
||||
* 同时完成 Mem0 冲突判定 + A-MEM 进化分析
|
||||
*/
|
||||
const CONSOLIDATION_SYSTEM_PROMPT = `你是一个记忆整合分析器。当新记忆加入知识图谱时,你需要同时完成两项任务:
|
||||
|
||||
**任务一:冲突检测**
|
||||
判断新记忆与最近邻的已有记忆是否冲突或重复:
|
||||
- skip: 新记忆与已有记忆完全重复,应丢弃
|
||||
- merge: 新记忆是对旧记忆的修正或补充,应合并
|
||||
- keep: 新记忆是全新信息,应保留
|
||||
|
||||
**任务二:进化分析**(仅当 action=keep 时需要)
|
||||
分析新记忆是否揭示了关于旧记忆的新信息:
|
||||
- 建立有意义的关联连接
|
||||
- 反向更新旧记忆的描述或分类
|
||||
|
||||
输出严格 JSON:
|
||||
{
|
||||
"action": "keep" | "merge" | "skip",
|
||||
"merge_target_id": "仅 action=merge 时必填:要合并到的旧节点 ID",
|
||||
"merged_fields": { "仅 action=merge 时可选:合并后的字段更新" },
|
||||
"reason": "判定理由(简述)",
|
||||
"evolution": {
|
||||
"should_evolve": true/false,
|
||||
"connections": ["需要建立链接的旧记忆 ID 列表"],
|
||||
"neighbor_updates": [
|
||||
{
|
||||
"nodeId": "需更新的旧节点 ID",
|
||||
"newContext": "基于新信息修正后的描述(不需修改则为 null)",
|
||||
"newTags": ["更新后的分类标签,不需修改则为 null"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
整合规则:
|
||||
- 当 action=skip 时,evolution 可省略或设 should_evolve=false
|
||||
- 当 action=merge 时,evolution 可省略或设 should_evolve=false
|
||||
- 仅当 action=keep 且新信息确实改变了对旧记忆的理解时,才设 should_evolve=true
|
||||
- 例如:揭露卧底身份 → 修正该角色之前事件中的动机描述
|
||||
- 例如:发现地点的隐藏特性 → 更新地点节点的描述
|
||||
- 不要对无关记忆强行建立联系
|
||||
- neighbor_updates 中每条必须有实际意义的修改`;
|
||||
|
||||
/**
|
||||
* 统一记忆整合主函数
|
||||
*
|
||||
* 合并了原先的 mem0ConflictCheck(精确对照)和 evolveMemories(进化),
|
||||
* 实现"1 次 embed + 1 次 LLM"完成冲突检测 + 进化分析。
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} params.graph - 当前图状态
|
||||
* @param {string[]} params.newNodeIds - 本次新创建的节点 ID 列表
|
||||
* @param {object} params.embeddingConfig - Embedding API 配置
|
||||
* @param {object} [params.options]
|
||||
* @param {number} [params.options.neighborCount=5] - 近邻搜索数量
|
||||
* @param {number} [params.options.conflictThreshold=0.85] - 冲突判定阈值(低于此值跳过冲突检测)
|
||||
* @param {string} [params.customPrompt] - 自定义提示词
|
||||
* @param {AbortSignal} [params.signal]
|
||||
* @returns {Promise<{merged: number, skipped: number, kept: number, evolved: number, connections: number, updates: number}>}
|
||||
*/
|
||||
export async function consolidateMemories({
|
||||
graph,
|
||||
newNodeIds,
|
||||
embeddingConfig,
|
||||
options = {},
|
||||
customPrompt,
|
||||
signal,
|
||||
}) {
|
||||
const neighborCount = options.neighborCount ?? 5;
|
||||
const conflictThreshold = options.conflictThreshold ?? 0.85;
|
||||
const stats = {
|
||||
merged: 0,
|
||||
skipped: 0,
|
||||
kept: 0,
|
||||
evolved: 0,
|
||||
connections: 0,
|
||||
updates: 0,
|
||||
};
|
||||
|
||||
if (!newNodeIds || newNodeIds.length === 0) return stats;
|
||||
if (!validateVectorConfig(embeddingConfig).valid) {
|
||||
console.log('[ST-BME] 记忆整合跳过:向量配置不可用');
|
||||
return stats;
|
||||
}
|
||||
|
||||
const activeNodes = getActiveNodes(graph).filter(n => {
|
||||
const text = buildNodeVectorText(n);
|
||||
return typeof text === 'string' && text.length > 0;
|
||||
});
|
||||
|
||||
if (activeNodes.length < 2) return stats;
|
||||
|
||||
for (const newId of newNodeIds) {
|
||||
throwIfAborted(signal);
|
||||
const newNode = getNode(graph, newId);
|
||||
if (!newNode || newNode.archived) continue;
|
||||
|
||||
const queryText = buildNodeVectorText(newNode);
|
||||
if (!queryText) continue;
|
||||
|
||||
// 排除自身的候选池
|
||||
const candidates = activeNodes.filter(n => n.id !== newId);
|
||||
if (candidates.length === 0) {
|
||||
stats.kept++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// ── 1次 Embed:查近邻 ──
|
||||
const neighbors = await findSimilarNodesByText(
|
||||
graph,
|
||||
queryText,
|
||||
embeddingConfig,
|
||||
neighborCount,
|
||||
candidates,
|
||||
signal,
|
||||
);
|
||||
|
||||
if (neighbors.length === 0) {
|
||||
stats.kept++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建近邻描述文本
|
||||
const neighborsContext = neighbors.map(n => {
|
||||
const node = getNode(graph, n.nodeId);
|
||||
if (!node) return null;
|
||||
const fieldsStr = Object.entries(node.fields)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(', ');
|
||||
return `[${node.id}] 类型=${node.type}, ${fieldsStr}, 相似度=${n.score.toFixed(3)}${
|
||||
(node.clusters || []).length > 0 ? `, 分类=${node.clusters.join('/')}` : ''
|
||||
}`;
|
||||
}).filter(Boolean).join('\n');
|
||||
|
||||
const newNodeFieldsStr = Object.entries(newNode.fields)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(', ');
|
||||
|
||||
// 检查是否有高相似度命中(决定是否启用冲突检测部分的提示)
|
||||
const hasHighSimilarity = neighbors[0].score > conflictThreshold;
|
||||
|
||||
const userPrompt = [
|
||||
'## 新加入的记忆',
|
||||
`[${newNode.id}] 类型=${newNode.type}, ${newNodeFieldsStr}`,
|
||||
'',
|
||||
'## 最近邻的已有记忆',
|
||||
neighborsContext,
|
||||
'',
|
||||
`共 ${neighbors.length} 条近邻记忆。`,
|
||||
hasHighSimilarity
|
||||
? `最高相似度 ${neighbors[0].score.toFixed(3)} 超过阈值 ${conflictThreshold},请先判断是否冲突/重复,再分析进化关系。`
|
||||
: '相似度均较低,请重点分析新记忆是否揭示了关于旧记忆的新信息。',
|
||||
].join('\n');
|
||||
|
||||
// ── 1次 LLM:统一判定 ──
|
||||
const decision = await callLLMForJSON({
|
||||
systemPrompt: customPrompt || CONSOLIDATION_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
maxRetries: 1,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!decision) {
|
||||
stats.kept++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 处理 action ──
|
||||
switch (decision.action) {
|
||||
case 'skip': {
|
||||
console.log(`[ST-BME] 记忆整合: skip (重复) — ${newId}`);
|
||||
newNode.archived = true;
|
||||
stats.skipped++;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'merge': {
|
||||
const targetId = decision.merge_target_id;
|
||||
const targetNode = targetId ? getNode(graph, targetId) : null;
|
||||
|
||||
if (targetNode && !targetNode.archived) {
|
||||
console.log(`[ST-BME] 记忆整合: merge ${newId} → ${targetId}`);
|
||||
|
||||
// 合并字段到旧节点
|
||||
if (decision.merged_fields && typeof decision.merged_fields === 'object') {
|
||||
for (const [key, value] of Object.entries(decision.merged_fields)) {
|
||||
if (value != null && value !== '') {
|
||||
targetNode.fields[key] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没提供 merged_fields,将新节点的非空字段补充到旧节点
|
||||
for (const [key, value] of Object.entries(newNode.fields)) {
|
||||
if (value != null && value !== '' && !targetNode.fields[key]) {
|
||||
targetNode.fields[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新旧节点的 seq 为更新的值
|
||||
if (Number.isFinite(newNode.seq) && newNode.seq > (targetNode.seq || 0)) {
|
||||
targetNode.seq = newNode.seq;
|
||||
}
|
||||
|
||||
// 标记旧节点需要 re-embed
|
||||
targetNode.embedding = null;
|
||||
|
||||
// 归档新节点
|
||||
newNode.archived = true;
|
||||
stats.merged++;
|
||||
} else {
|
||||
// merge target 无效,回退为 keep
|
||||
console.warn(`[ST-BME] 记忆整合: merge target ${targetId} 不存在,回退为 keep`);
|
||||
stats.kept++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'keep':
|
||||
default: {
|
||||
stats.kept++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 处理 evolution(仅 keep 时有意义,但也容错处理其它 action) ──
|
||||
const evolution = decision.evolution;
|
||||
if (evolution?.should_evolve && !newNode.archived) {
|
||||
stats.evolved++;
|
||||
console.log(`[ST-BME] 记忆整合/进化触发: ${decision.reason || '(无理由)'}`);
|
||||
|
||||
// 建立关联边
|
||||
if (Array.isArray(evolution.connections)) {
|
||||
for (const targetId of evolution.connections) {
|
||||
if (!getNode(graph, targetId)) continue;
|
||||
const edge = createEdge({
|
||||
fromId: newId,
|
||||
toId: targetId,
|
||||
relation: 'related',
|
||||
strength: 0.7,
|
||||
});
|
||||
if (addEdge(graph, edge)) {
|
||||
stats.connections++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 反向更新旧节点
|
||||
if (Array.isArray(evolution.neighbor_updates)) {
|
||||
for (const update of evolution.neighbor_updates) {
|
||||
if (!update.nodeId) continue;
|
||||
const oldNode = getNode(graph, update.nodeId);
|
||||
if (!oldNode || oldNode.archived) continue;
|
||||
|
||||
let changed = false;
|
||||
|
||||
// 更新 context/state 字段
|
||||
if (update.newContext && typeof update.newContext === 'string') {
|
||||
if (oldNode.fields.state !== undefined) {
|
||||
oldNode.fields.state = update.newContext;
|
||||
changed = true;
|
||||
} else if (oldNode.fields.summary !== undefined) {
|
||||
oldNode.fields.summary = update.newContext;
|
||||
changed = true;
|
||||
} else if (oldNode.fields.core_note !== undefined) {
|
||||
oldNode.fields.core_note = update.newContext;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分类标签
|
||||
if (update.newTags && Array.isArray(update.newTags)) {
|
||||
oldNode.clusters = update.newTags;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
oldNode.embedding = null;
|
||||
if (!oldNode._evolutionHistory) oldNode._evolutionHistory = [];
|
||||
oldNode._evolutionHistory.push({
|
||||
triggeredBy: newId,
|
||||
timestamp: Date.now(),
|
||||
reason: decision.reason || '',
|
||||
});
|
||||
stats.updates++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) throw e;
|
||||
console.error(`[ST-BME] 记忆整合失败 (${newId}):`, e);
|
||||
stats.kept++;
|
||||
}
|
||||
}
|
||||
|
||||
const actionSummary = [];
|
||||
if (stats.merged > 0) actionSummary.push(`合并 ${stats.merged}`);
|
||||
if (stats.skipped > 0) actionSummary.push(`跳过 ${stats.skipped}`);
|
||||
if (stats.kept > 0) actionSummary.push(`保留 ${stats.kept}`);
|
||||
if (stats.evolved > 0) actionSummary.push(`进化 ${stats.evolved}`);
|
||||
if (stats.connections > 0) actionSummary.push(`新链接 ${stats.connections}`);
|
||||
if (stats.updates > 0) actionSummary.push(`回溯更新 ${stats.updates}`);
|
||||
|
||||
if (actionSummary.length > 0) {
|
||||
console.log(`[ST-BME] 记忆整合完成: ${actionSummary.join(', ')}`);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
226
evolution.js
226
evolution.js
@@ -1,226 +0,0 @@
|
||||
// ST-BME: 记忆进化引擎(A-MEM 启发)
|
||||
// 新节点写入后触发,回溯更新相关旧节点的 context/tags/links
|
||||
|
||||
import { getActiveNodes, getNode, createEdge, addEdge } from './graph.js';
|
||||
import { callLLMForJSON } from './llm.js';
|
||||
import {
|
||||
buildNodeVectorText,
|
||||
findSimilarNodesByText,
|
||||
validateVectorConfig,
|
||||
} from './vector-index.js';
|
||||
|
||||
function createAbortError(message = '操作已终止') {
|
||||
const error = new Error(message);
|
||||
error.name = 'AbortError';
|
||||
return error;
|
||||
}
|
||||
|
||||
function isAbortError(error) {
|
||||
return error?.name === 'AbortError';
|
||||
}
|
||||
|
||||
function throwIfAborted(signal) {
|
||||
if (signal?.aborted) {
|
||||
throw signal.reason instanceof Error ? signal.reason : createAbortError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进化系统提示词
|
||||
* 参考 A-MEM process_memory() 的进化决策 Prompt
|
||||
*/
|
||||
const EVOLUTION_SYSTEM_PROMPT = `你是一个记忆进化分析器。当新的记忆加入知识图谱时,你需要分析它与现有记忆的关系。
|
||||
|
||||
你的任务:
|
||||
1. 判断新记忆是否揭示了与旧记忆相关的新信息
|
||||
2. 如果是,决定如何更新旧记忆的描述和分类
|
||||
3. 建立新旧记忆之间的有意义连接
|
||||
|
||||
输出严格 JSON:
|
||||
{
|
||||
"should_evolve": true/false,
|
||||
"reason": "进化理由(简述)",
|
||||
"suggested_connections": ["需要建立链接的旧记忆ID列表"],
|
||||
"neighbor_updates": [
|
||||
{
|
||||
"nodeId": "需更新的旧节点ID",
|
||||
"newContext": "基于新信息修正后的描述(如不需修改则为null)",
|
||||
"newTags": ["更新后的分类标签,如不需修改则为null"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
进化规则:
|
||||
- 仅当新信息确实改变了对旧记忆的理解时才触发进化
|
||||
- 例如:揭露卧底身份 → 修正该角色之前事件中的动机描述
|
||||
- 例如:发现地点的隐藏特性 → 更新地点节点的描述
|
||||
- 不要对无关记忆强行建立联系
|
||||
- neighbor_updates 中每条必须有实际意义的修改`;
|
||||
|
||||
/**
|
||||
* 记忆进化主函数
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} params.graph - 当前图状态
|
||||
* @param {string[]} params.newNodeIds - 本次新创建的节点 ID 列表
|
||||
* @param {object} params.embeddingConfig - Embedding API 配置
|
||||
* @param {object} [params.options]
|
||||
* @returns {Promise<{evolved: number, connections: number, updates: number}>}
|
||||
*/
|
||||
export async function evolveMemories({
|
||||
graph,
|
||||
newNodeIds,
|
||||
embeddingConfig,
|
||||
options = {},
|
||||
customPrompt,
|
||||
signal,
|
||||
}) {
|
||||
const neighborCount = options.neighborCount ?? 5;
|
||||
const stats = { evolved: 0, connections: 0, updates: 0 };
|
||||
|
||||
if (!newNodeIds || newNodeIds.length === 0) return stats;
|
||||
if (!validateVectorConfig(embeddingConfig).valid) {
|
||||
console.log('[ST-BME] 记忆进化跳过:向量配置不可用');
|
||||
return stats;
|
||||
}
|
||||
|
||||
const activeNodes = getActiveNodes(graph);
|
||||
if (activeNodes.length < 2) return stats; // 至少需要 2 个节点才有进化意义
|
||||
|
||||
for (const newId of newNodeIds) {
|
||||
throwIfAborted(signal);
|
||||
const newNode = getNode(graph, newId);
|
||||
if (!newNode) continue;
|
||||
|
||||
const queryText = buildNodeVectorText(newNode);
|
||||
if (!queryText) continue;
|
||||
|
||||
const candidates = activeNodes.filter(n => n.id !== newId);
|
||||
if (candidates.length === 0) continue;
|
||||
|
||||
const neighbors = await findSimilarNodesByText(
|
||||
graph,
|
||||
queryText,
|
||||
embeddingConfig,
|
||||
neighborCount,
|
||||
candidates,
|
||||
signal,
|
||||
);
|
||||
if (neighbors.length === 0) continue;
|
||||
|
||||
// 构建 LLM 上下文
|
||||
const neighborsContext = neighbors.map(n => {
|
||||
const node = getNode(graph, n.nodeId);
|
||||
if (!node) return null;
|
||||
const fieldsStr = Object.entries(node.fields)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(', ');
|
||||
return `[${node.id}] 类型=${node.type}, ${fieldsStr}, 分类=${(node.clusters || []).join('/')}`;
|
||||
}).filter(Boolean).join('\n');
|
||||
|
||||
const newNodeFieldsStr = Object.entries(newNode.fields)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(', ');
|
||||
|
||||
const userPrompt = [
|
||||
'## 新加入的记忆',
|
||||
`[${newNode.id}] 类型=${newNode.type}, ${newNodeFieldsStr}`,
|
||||
'',
|
||||
'## 最近邻的已有记忆',
|
||||
neighborsContext,
|
||||
'',
|
||||
`共 ${neighbors.length} 条近邻记忆。请分析新记忆是否揭示了关于旧记忆的新信息。`,
|
||||
].join('\n');
|
||||
|
||||
try {
|
||||
const decision = await callLLMForJSON({
|
||||
systemPrompt: customPrompt || EVOLUTION_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
maxRetries: 1,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!decision || !decision.should_evolve) continue;
|
||||
|
||||
stats.evolved++;
|
||||
console.log(`[ST-BME] 记忆进化触发: ${decision.reason || '(无理由)'}`);
|
||||
|
||||
// 1. 建立链接(strengthen)
|
||||
if (decision.suggested_connections && Array.isArray(decision.suggested_connections)) {
|
||||
for (const targetId of decision.suggested_connections) {
|
||||
// 验证目标节点存在
|
||||
if (!getNode(graph, targetId)) continue;
|
||||
const edge = createEdge({
|
||||
fromId: newId,
|
||||
toId: targetId,
|
||||
relation: 'related',
|
||||
strength: 0.7,
|
||||
});
|
||||
if (addEdge(graph, edge)) {
|
||||
stats.connections++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 反向更新旧节点(update_neighbor)
|
||||
if (decision.neighbor_updates && Array.isArray(decision.neighbor_updates)) {
|
||||
for (const update of decision.neighbor_updates) {
|
||||
if (!update.nodeId) continue;
|
||||
const oldNode = getNode(graph, update.nodeId);
|
||||
if (!oldNode) continue;
|
||||
|
||||
let changed = false;
|
||||
|
||||
// 更新 context/state 字段
|
||||
if (update.newContext && typeof update.newContext === 'string') {
|
||||
// 根据节点类型选择更新哪个字段
|
||||
if (oldNode.fields.state !== undefined) {
|
||||
oldNode.fields.state = update.newContext;
|
||||
changed = true;
|
||||
} else if (oldNode.fields.summary !== undefined) {
|
||||
oldNode.fields.summary = update.newContext;
|
||||
changed = true;
|
||||
} else if (oldNode.fields.core_note !== undefined) {
|
||||
oldNode.fields.core_note = update.newContext;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分类标签
|
||||
if (update.newTags && Array.isArray(update.newTags)) {
|
||||
oldNode.clusters = update.newTags;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// 标记需要重新生成 embedding
|
||||
oldNode.embedding = null;
|
||||
// 记录进化历史
|
||||
if (!oldNode._evolutionHistory) oldNode._evolutionHistory = [];
|
||||
oldNode._evolutionHistory.push({
|
||||
triggeredBy: newId,
|
||||
timestamp: Date.now(),
|
||||
reason: decision.reason || '',
|
||||
});
|
||||
stats.updates++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
throw e;
|
||||
}
|
||||
console.error(`[ST-BME] 记忆进化失败 (${newId}):`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.evolved > 0) {
|
||||
console.log(
|
||||
`[ST-BME] 记忆进化完成: ${stats.evolved} 次进化, ` +
|
||||
`${stats.connections} 条新链接, ${stats.updates} 个节点回溯更新`,
|
||||
);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
104
extractor.js
104
extractor.js
@@ -22,9 +22,7 @@ import {
|
||||
import { RELATION_TYPES } from "./schema.js";
|
||||
import {
|
||||
buildNodeVectorText,
|
||||
findSimilarNodesByText,
|
||||
isDirectVectorConfig,
|
||||
validateVectorConfig,
|
||||
} from "./vector-index.js";
|
||||
|
||||
function createAbortError(message = "操作已终止") {
|
||||
@@ -69,7 +67,6 @@ export async function extractMemories({
|
||||
schema,
|
||||
embeddingConfig,
|
||||
extractPrompt,
|
||||
v2Options = {},
|
||||
signal = undefined,
|
||||
}) {
|
||||
throwIfAborted(signal);
|
||||
@@ -84,8 +81,7 @@ export async function extractMemories({
|
||||
};
|
||||
}
|
||||
|
||||
const enablePreciseConflict = v2Options.enablePreciseConflict ?? true;
|
||||
const conflictThreshold = v2Options.conflictThreshold ?? 0.85;
|
||||
|
||||
|
||||
const effectiveStartSeq = Number.isFinite(startSeq)
|
||||
? startSeq
|
||||
@@ -154,17 +150,7 @@ export async function extractMemories({
|
||||
};
|
||||
}
|
||||
|
||||
// ========== v2: Mem0 精确对照阶段 ==========
|
||||
if (enablePreciseConflict && validateVectorConfig(embeddingConfig).valid) {
|
||||
await mem0ConflictCheck(
|
||||
graph,
|
||||
result.operations,
|
||||
embeddingConfig,
|
||||
conflictThreshold,
|
||||
effectiveEndSeq,
|
||||
signal,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 执行操作
|
||||
const stats = { newNodes: 0, updatedNodes: 0, newEdges: 0 };
|
||||
@@ -570,92 +556,6 @@ function buildDefaultExtractPrompt(schema) {
|
||||
|
||||
// ==================== v2 增强功能 ====================
|
||||
|
||||
/**
|
||||
* Mem0 启发的精确对照
|
||||
* 对每条 create 操作搜索近邻,高相似度时让 LLM 判断 add/update/skip
|
||||
*/
|
||||
async function mem0ConflictCheck(
|
||||
graph,
|
||||
operations,
|
||||
embeddingConfig,
|
||||
threshold,
|
||||
fallbackSeq,
|
||||
signal,
|
||||
) {
|
||||
const activeNodes = getActiveNodes(graph).filter((node) => {
|
||||
const text = buildNodeVectorText(node);
|
||||
return typeof text === "string" && text.length > 0;
|
||||
});
|
||||
if (activeNodes.length === 0) return;
|
||||
|
||||
for (const op of operations) {
|
||||
if (op.action !== "create") continue;
|
||||
|
||||
const factText =
|
||||
op.fields?.summary || op.fields?.name || op.fields?.title || "";
|
||||
if (!factText) continue;
|
||||
|
||||
try {
|
||||
throwIfAborted(signal);
|
||||
const similar = await findSimilarNodesByText(
|
||||
graph,
|
||||
factText,
|
||||
embeddingConfig,
|
||||
3,
|
||||
activeNodes,
|
||||
signal,
|
||||
);
|
||||
|
||||
if (similar.length > 0 && similar[0].score > threshold) {
|
||||
const topMatch = graph.nodes.find((n) => n.id === similar[0].nodeId);
|
||||
if (!topMatch) continue;
|
||||
|
||||
const topFields = Object.entries(topMatch.fields)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(", ");
|
||||
|
||||
const decision = await callLLMForJSON({
|
||||
systemPrompt: [
|
||||
"判断新信息与已有记忆的关系。输出严格 JSON:",
|
||||
'{"action": "add"|"update"|"skip", "targetId": "旧节点ID", "mergedFields": {}}',
|
||||
"- add: 新信息完全不同,应新建",
|
||||
"- update: 新信息是对旧记忆的修正/补充",
|
||||
"- skip: 与旧记忆完全重复",
|
||||
].join("\n"),
|
||||
userPrompt: [
|
||||
`新信息: [${op.type}] ${factText}`,
|
||||
`最相似旧记忆: [${topMatch.id}] 类型=${topMatch.type}, ${topFields}`,
|
||||
`相似度: ${similar[0].score.toFixed(3)}`,
|
||||
].join("\n"),
|
||||
maxRetries: 1,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (decision?.action === "update" && decision.targetId) {
|
||||
console.log(
|
||||
`[ST-BME] Mem0对照: create->update (${decision.targetId})`,
|
||||
);
|
||||
op.action = "update";
|
||||
op.nodeId = decision.targetId;
|
||||
op.sourceNodeId = op.sourceNodeId || topMatch.id;
|
||||
op.seq = Number.isFinite(op.seq) ? op.seq : fallbackSeq;
|
||||
if (decision.mergedFields) {
|
||||
op.fields = { ...op.fields, ...decision.mergedFields };
|
||||
}
|
||||
} else if (decision?.action === "skip") {
|
||||
console.log("[ST-BME] Mem0对照: create->skip (重复)");
|
||||
op.action = "_skip";
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
throw e;
|
||||
}
|
||||
console.warn("[ST-BME] Mem0对照失败,保持原操作:", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局故事概要生成(MemoRAG 启发)
|
||||
* 基于图中事件/角色/主线自动生成/更新 synopsis 节点
|
||||
|
||||
44
index.js
44
index.js
@@ -15,7 +15,7 @@ import {
|
||||
} from "../../../extensions.js";
|
||||
|
||||
import { compressAll, sleepCycle } from "./compressor.js";
|
||||
import { evolveMemories } from "./evolution.js";
|
||||
import { consolidateMemories } from "./consolidator.js";
|
||||
import {
|
||||
extractMemories,
|
||||
generateReflection,
|
||||
@@ -122,14 +122,10 @@ const defaultSettings = {
|
||||
|
||||
// ====== v2 增强设置 ======
|
||||
|
||||
// ③ A-MEM 记忆进化
|
||||
enableEvolution: true, // 启用记忆进化
|
||||
evoNeighborCount: 5, // 近邻搜索数量
|
||||
evoConsolidateEvery: 50, // 每 N 次进化后整理
|
||||
|
||||
// ② Mem0 精确对照
|
||||
enablePreciseConflict: true, // 启用精确对照
|
||||
conflictThreshold: 0.85, // 相似度阈值
|
||||
// ③ 记忆整合(合并精确对照 + 记忆进化)
|
||||
enableConsolidation: true, // 启用记忆整合
|
||||
consolidationNeighborCount: 5, // 近邻搜索数量
|
||||
consolidationThreshold: 0.85, // 冲突判定相似度阈值
|
||||
|
||||
// ⑨ 全局故事概要
|
||||
enableSynopsis: true, // 启用全局概要
|
||||
@@ -1642,20 +1638,23 @@ async function handleExtractionSuccess(
|
||||
currentGraph.historyState.extractionCount = extractionCount;
|
||||
updateLastExtractedItems(result.newNodeIds || []);
|
||||
|
||||
if (settings.enableEvolution && result.newNodeIds?.length > 0) {
|
||||
if (settings.enableConsolidation && result.newNodeIds?.length > 0) {
|
||||
try {
|
||||
await evolveMemories({
|
||||
await consolidateMemories({
|
||||
graph: currentGraph,
|
||||
newNodeIds: result.newNodeIds,
|
||||
embeddingConfig: getEmbeddingConfig(),
|
||||
options: { neighborCount: settings.evoNeighborCount },
|
||||
customPrompt: settings.evolutionPrompt || undefined,
|
||||
options: {
|
||||
neighborCount: settings.consolidationNeighborCount,
|
||||
conflictThreshold: settings.consolidationThreshold,
|
||||
},
|
||||
customPrompt: settings.consolidationPrompt || undefined,
|
||||
signal,
|
||||
});
|
||||
postProcessArtifacts.push("evolution");
|
||||
postProcessArtifacts.push("consolidation");
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) throw e;
|
||||
console.error("[ST-BME] 记忆进化失败:", e);
|
||||
console.error("[ST-BME] 记忆整合失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2173,10 +2172,6 @@ async function executeExtractionBatch({
|
||||
schema: getSchema(),
|
||||
embeddingConfig: getEmbeddingConfig(),
|
||||
extractPrompt: settings.extractPrompt || undefined,
|
||||
v2Options: {
|
||||
enablePreciseConflict: settings.enablePreciseConflict,
|
||||
conflictThreshold: settings.conflictThreshold,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -3356,18 +3351,21 @@ async function onManualEvolve() {
|
||||
}
|
||||
|
||||
const beforeSnapshot = cloneGraphSnapshot(currentGraph);
|
||||
const result = await evolveMemories({
|
||||
const result = await consolidateMemories({
|
||||
graph: currentGraph,
|
||||
newNodeIds: candidateIds,
|
||||
embeddingConfig: getEmbeddingConfig(),
|
||||
options: { neighborCount: getSettings().evoNeighborCount },
|
||||
options: {
|
||||
neighborCount: getSettings().consolidationNeighborCount,
|
||||
conflictThreshold: getSettings().consolidationThreshold,
|
||||
},
|
||||
});
|
||||
await recordGraphMutation({
|
||||
beforeSnapshot,
|
||||
artifactTags: ["evolution"],
|
||||
artifactTags: ["consolidation"],
|
||||
});
|
||||
toastr.success(
|
||||
`进化完成:${result.evolved} 次进化,${result.connections} 条链接,${result.updates} 个回溯更新`,
|
||||
`整合完成:合并 ${result.merged},跳过 ${result.skipped},保留 ${result.kept},进化 ${result.evolved},新链接 ${result.connections},回溯更新 ${result.updates}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
63
panel.html
63
panel.html
@@ -551,19 +551,12 @@
|
||||
</span>
|
||||
<input id="bme-setting-recall-enabled" type="checkbox" />
|
||||
</label>
|
||||
<label class="bme-toggle-item" for="bme-setting-evolution-enabled">
|
||||
<label class="bme-toggle-item" for="bme-setting-consolidation-enabled">
|
||||
<span class="bme-toggle-copy">
|
||||
<span class="bme-toggle-title">启用记忆进化</span>
|
||||
<span class="bme-toggle-desc">根据新提取结果反向修正既有记忆。</span>
|
||||
<span class="bme-toggle-title">启用记忆整合</span>
|
||||
<span class="bme-toggle-desc">去重/冲突检测 + 反向修正既有记忆 + 建立关联边。</span>
|
||||
</span>
|
||||
<input id="bme-setting-evolution-enabled" type="checkbox" />
|
||||
</label>
|
||||
<label class="bme-toggle-item" for="bme-setting-precise-conflict-enabled">
|
||||
<span class="bme-toggle-copy">
|
||||
<span class="bme-toggle-title">启用精确对照</span>
|
||||
<span class="bme-toggle-desc">识别与既有记忆冲突或需要修订的片段。</span>
|
||||
</span>
|
||||
<input id="bme-setting-precise-conflict-enabled" type="checkbox" />
|
||||
<input id="bme-setting-consolidation-enabled" type="checkbox" />
|
||||
</label>
|
||||
<label class="bme-toggle-item" for="bme-setting-synopsis-enabled">
|
||||
<span class="bme-toggle-copy">
|
||||
@@ -782,42 +775,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card bme-guarded-card" data-guard-settings="enableEvolution">
|
||||
<div class="bme-config-card bme-guarded-card" data-guard-settings="enableConsolidation">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">进化</div>
|
||||
<div class="bme-config-card-subtitle">控制邻域搜索规模和整理频率。</div>
|
||||
<div class="bme-config-card-title">记忆整合</div>
|
||||
<div class="bme-config-card-subtitle">控制近邻搜索规模和冲突判定阈值。</div>
|
||||
</div>
|
||||
<div class="bme-config-guard-note">在“功能开关”中启用后生效。</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-setting-evo-neighbor-count">近邻数量</label>
|
||||
<input id="bme-setting-evo-neighbor-count" class="bme-config-input" type="number" min="1" max="20" />
|
||||
<label for="bme-setting-consolidation-neighbor-count">近邻数量</label>
|
||||
<input id="bme-setting-consolidation-neighbor-count" class="bme-config-input" type="number" min="1" max="20" />
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-setting-evo-consolidate-every">整理频率</label>
|
||||
<label for="bme-setting-consolidation-threshold">冲突判定阈值</label>
|
||||
<input
|
||||
id="bme-setting-evo-consolidate-every"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="1"
|
||||
max="500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card bme-guarded-card" data-guard-settings="enablePreciseConflict">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">冲突对照</div>
|
||||
<div class="bme-config-card-subtitle">调整对照阶段的判定阈值。</div>
|
||||
</div>
|
||||
<div class="bme-config-guard-note">在“功能开关”中启用后生效。</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-setting-conflict-threshold">对照阈值</label>
|
||||
<input
|
||||
id="bme-setting-conflict-threshold"
|
||||
id="bme-setting-consolidation-threshold"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="0.5"
|
||||
@@ -986,26 +959,26 @@
|
||||
<textarea id="bme-setting-recall-prompt" class="bme-config-textarea" placeholder="留空 = 默认召回 prompt"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="bme-prompt-card" data-setting-key="evolutionPrompt" data-default-prompt="evolution">
|
||||
<div class="bme-prompt-card" data-setting-key="consolidationPrompt" data-default-prompt="consolidation">
|
||||
<div class="bme-prompt-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">记忆进化</div>
|
||||
<div class="bme-config-card-subtitle">用于分析新记忆如何修正或连接既有节点。</div>
|
||||
<div class="bme-config-card-title">记忆整合</div>
|
||||
<div class="bme-config-card-subtitle">用于分析新记忆与旧记忆的冲突、去重和进化关系。</div>
|
||||
</div>
|
||||
<div class="bme-prompt-card-actions">
|
||||
<span class="bme-prompt-status"></span>
|
||||
<button
|
||||
class="menu_button bme-prompt-reset"
|
||||
data-setting-key="evolutionPrompt"
|
||||
data-default-prompt="evolution"
|
||||
data-target-id="bme-setting-evolution-prompt"
|
||||
data-setting-key="consolidationPrompt"
|
||||
data-default-prompt="consolidation"
|
||||
data-target-id="bme-setting-consolidation-prompt"
|
||||
type="button"
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="bme-setting-evolution-prompt" class="bme-config-textarea" placeholder="留空 = 默认进化 prompt"></textarea>
|
||||
<textarea id="bme-setting-consolidation-prompt" class="bme-config-textarea" placeholder="留空 = 默认整合 prompt"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="bme-prompt-card" data-setting-key="compressPrompt" data-default-prompt="compress">
|
||||
|
||||
81
panel.js
81
panel.js
@@ -54,28 +54,30 @@ const DEFAULT_PROMPTS = {
|
||||
'{"selected_ids": ["id1", "id2", ...], "reason": "简要说明选择理由"}',
|
||||
].join("\n"),
|
||||
|
||||
evolution: [
|
||||
"你是一个记忆进化分析器。当新的记忆加入知识图谱时,你需要分析它与现有记忆的关系。",
|
||||
consolidation: [
|
||||
"你是一个记忆整合分析器。当新记忆加入知识图谱时,你需要同时完成两项任务:",
|
||||
"",
|
||||
"你的任务:",
|
||||
"1. 判断新记忆是否揭示了与旧记忆相关的新信息",
|
||||
"2. 如果是,决定如何更新旧记忆的描述和分类",
|
||||
"3. 建立新旧记忆之间的有意义连接",
|
||||
"任务一:冲突检测",
|
||||
"- skip: 新记忆与已有记忆完全重复",
|
||||
"- merge: 新记忆是对旧记忆的修正/补充",
|
||||
"- keep: 新记忆是全新信息",
|
||||
"",
|
||||
"任务二:进化分析(仅 action=keep 时)",
|
||||
"- 建立关联连接",
|
||||
"- 反向更新旧记忆",
|
||||
"",
|
||||
"输出严格 JSON:",
|
||||
"{",
|
||||
' "should_evolve": true/false,',
|
||||
' "reason": "进化理由",',
|
||||
' "suggested_connections": ["旧记忆ID"],',
|
||||
' "neighbor_updates": [',
|
||||
' {"nodeId": "旧节点ID", "newContext": "修正描述", "newTags": ["标签"]}',
|
||||
" ]",
|
||||
' "action": "keep"|"merge"|"skip",',
|
||||
' "merge_target_id": "旧节点ID",',
|
||||
' "merged_fields": {},',
|
||||
' "reason": "理由",',
|
||||
' "evolution": {',
|
||||
' "should_evolve": true/false,',
|
||||
' "connections": ["旧记忆ID"],',
|
||||
' "neighbor_updates": [{"nodeId": "旧节点ID", "newContext": "修正描述", "newTags": ["标签"]}]',
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"进化规则:",
|
||||
"- 仅当新信息确实改变了对旧记忆的理解时才触发进化",
|
||||
"- 例如:揭露卧底身份 → 修正该角色之前事件中的动机描述",
|
||||
"- 不要对无关记忆强行建立联系",
|
||||
].join("\n"),
|
||||
|
||||
compress: [
|
||||
@@ -732,11 +734,7 @@ function _refreshConfigTab() {
|
||||
"bme-setting-recall-graph-diffusion-enabled",
|
||||
settings.recallEnableGraphDiffusion ?? true,
|
||||
);
|
||||
_setCheckboxValue("bme-setting-evolution-enabled", settings.enableEvolution ?? true);
|
||||
_setCheckboxValue(
|
||||
"bme-setting-precise-conflict-enabled",
|
||||
settings.enablePreciseConflict ?? true,
|
||||
);
|
||||
_setCheckboxValue("bme-setting-consolidation-enabled", settings.enableConsolidation ?? true);
|
||||
_setCheckboxValue("bme-setting-synopsis-enabled", settings.enableSynopsis ?? true);
|
||||
_setCheckboxValue(
|
||||
"bme-setting-visibility-enabled",
|
||||
@@ -790,16 +788,12 @@ function _refreshConfigTab() {
|
||||
settings.importanceWeight ?? 0.1,
|
||||
);
|
||||
_setInputValue(
|
||||
"bme-setting-evo-neighbor-count",
|
||||
settings.evoNeighborCount ?? 5,
|
||||
"bme-setting-consolidation-neighbor-count",
|
||||
settings.consolidationNeighborCount ?? 5,
|
||||
);
|
||||
_setInputValue(
|
||||
"bme-setting-evo-consolidate-every",
|
||||
settings.evoConsolidateEvery ?? 50,
|
||||
);
|
||||
_setInputValue(
|
||||
"bme-setting-conflict-threshold",
|
||||
settings.conflictThreshold ?? 0.85,
|
||||
"bme-setting-consolidation-threshold",
|
||||
settings.consolidationThreshold ?? 0.85,
|
||||
);
|
||||
_setInputValue("bme-setting-synopsis-every", settings.synopsisEveryN ?? 5);
|
||||
_setInputValue(
|
||||
@@ -855,7 +849,7 @@ function _refreshConfigTab() {
|
||||
|
||||
_setInputValue("bme-setting-extract-prompt", settings.extractPrompt || DEFAULT_PROMPTS.extract);
|
||||
_setInputValue("bme-setting-recall-prompt", settings.recallPrompt || DEFAULT_PROMPTS.recall);
|
||||
_setInputValue("bme-setting-evolution-prompt", settings.evolutionPrompt || DEFAULT_PROMPTS.evolution);
|
||||
_setInputValue("bme-setting-consolidation-prompt", settings.consolidationPrompt || DEFAULT_PROMPTS.consolidation);
|
||||
_setInputValue("bme-setting-compress-prompt", settings.compressPrompt || DEFAULT_PROMPTS.compress);
|
||||
_setInputValue("bme-setting-synopsis-prompt", settings.synopsisPrompt || DEFAULT_PROMPTS.synopsis);
|
||||
_setInputValue("bme-setting-reflection-prompt", settings.reflectionPrompt || DEFAULT_PROMPTS.reflection);
|
||||
@@ -901,12 +895,8 @@ function _bindConfigControls() {
|
||||
_patchSettings({ recallEnableGraphDiffusion: checked });
|
||||
_refreshStageCardStates();
|
||||
});
|
||||
bindCheckbox("bme-setting-evolution-enabled", (checked) => {
|
||||
_patchSettings({ enableEvolution: checked });
|
||||
_refreshGuardedConfigStates();
|
||||
});
|
||||
bindCheckbox("bme-setting-precise-conflict-enabled", (checked) => {
|
||||
_patchSettings({ enablePreciseConflict: checked });
|
||||
bindCheckbox("bme-setting-consolidation-enabled", (checked) => {
|
||||
_patchSettings({ enableConsolidation: checked });
|
||||
_refreshGuardedConfigStates();
|
||||
});
|
||||
bindCheckbox("bme-setting-synopsis-enabled", (checked) => {
|
||||
@@ -969,14 +959,11 @@ function _bindConfigControls() {
|
||||
bindFloat("bme-setting-importance-weight", 0.1, 0, 1, (value) =>
|
||||
_patchSettings({ importanceWeight: value }),
|
||||
);
|
||||
bindNumber("bme-setting-evo-neighbor-count", 5, 1, 20, (value) =>
|
||||
_patchSettings({ evoNeighborCount: value }),
|
||||
bindNumber("bme-setting-consolidation-neighbor-count", 5, 1, 20, (value) =>
|
||||
_patchSettings({ consolidationNeighborCount: value }),
|
||||
);
|
||||
bindNumber("bme-setting-evo-consolidate-every", 50, 1, 500, (value) =>
|
||||
_patchSettings({ evoConsolidateEvery: value }),
|
||||
);
|
||||
bindFloat("bme-setting-conflict-threshold", 0.85, 0.5, 0.99, (value) =>
|
||||
_patchSettings({ conflictThreshold: value }),
|
||||
bindFloat("bme-setting-consolidation-threshold", 0.85, 0.5, 0.99, (value) =>
|
||||
_patchSettings({ consolidationThreshold: value }),
|
||||
);
|
||||
bindNumber("bme-setting-synopsis-every", 5, 1, 100, (value) =>
|
||||
_patchSettings({ synopsisEveryN: value }),
|
||||
@@ -1061,9 +1048,9 @@ function _bindConfigControls() {
|
||||
"recall",
|
||||
);
|
||||
bindPromptText(
|
||||
"bme-setting-evolution-prompt",
|
||||
"evolutionPrompt",
|
||||
"evolution",
|
||||
"bme-setting-consolidation-prompt",
|
||||
"consolidationPrompt",
|
||||
"consolidation",
|
||||
);
|
||||
bindPromptText(
|
||||
"bme-setting-compress-prompt",
|
||||
|
||||
Reference in New Issue
Block a user