feat: improve retrieval recall and maintenance undo

This commit is contained in:
Youzini-afk
2026-04-01 22:37:29 +08:00
parent 1dc87245a7
commit 6f8554e11a
10 changed files with 1550 additions and 63 deletions

View File

@@ -53,19 +53,31 @@ export function timeDecayFactor(createdTime, now = Date.now()) {
export function hybridScore({ export function hybridScore({
graphScore = 0, graphScore = 0,
vectorScore = 0, vectorScore = 0,
lexicalScore = 0,
importance = 5, importance = 5,
createdTime = Date.now(), createdTime = Date.now(),
}, weights = {}) { }, weights = {}) {
const alpha = weights.graphWeight ?? 0.6; const alpha = weights.graphWeight ?? 0.6;
const beta = weights.vectorWeight ?? 0.3; const beta = weights.vectorWeight ?? 0.3;
const gamma = weights.importanceWeight ?? 0.1; const gamma = weights.importanceWeight ?? 0.1;
const delta = weights.lexicalWeight ?? 0;
// 归一化 // 归一化
const normGraph = Math.max(0, Math.min(1, graphScore / 2.0)); // PEDSA 能量范围 [-2, 2] → [0, 1] const normGraph = Math.max(0, Math.min(1, graphScore / 2.0)); // PEDSA 能量范围 [-2, 2] → [0, 1]
const normVec = Math.max(0, Math.min(1, vectorScore)); const normVec = Math.max(0, Math.min(1, vectorScore));
const normLexical = Math.max(0, Math.min(1, lexicalScore));
const normImportance = Math.max(0, Math.min(1, importance / 10.0)); const normImportance = Math.max(0, Math.min(1, importance / 10.0));
const totalWeight = Math.max(
1e-6,
Math.max(0, alpha) + Math.max(0, beta) + Math.max(0, gamma) + Math.max(0, delta),
);
const baseScore = normGraph * alpha + normVec * beta + normImportance * gamma; const baseScore =
(normGraph * alpha +
normVec * beta +
normLexical * delta +
normImportance * gamma) /
totalWeight;
const decay = timeDecayFactor(createdTime); const decay = timeDecayFactor(createdTime);
return baseScore * decay; return baseScore * decay;

376
index.js
View File

@@ -136,16 +136,19 @@ import { resolveConfiguredTimeoutMs } from "./request-timeout.js";
import { retrieve } from "./retriever.js"; import { retrieve } from "./retriever.js";
import { import {
appendBatchJournal, appendBatchJournal,
appendMaintenanceJournal,
buildRecoveryResult, buildRecoveryResult,
buildReverseJournalRecoveryPlan, buildReverseJournalRecoveryPlan,
clearHistoryDirty, clearHistoryDirty,
cloneGraphSnapshot, cloneGraphSnapshot,
createBatchJournalEntry, createBatchJournalEntry,
createMaintenanceJournalEntry,
detectHistoryMutation, detectHistoryMutation,
findJournalRecoveryPoint, findJournalRecoveryPoint,
markHistoryDirty, markHistoryDirty,
normalizeGraphRuntimeState, normalizeGraphRuntimeState,
snapshotProcessedMessageHashes, snapshotProcessedMessageHashes,
undoLatestMaintenance,
} from "./runtime-state.js"; } from "./runtime-state.js";
import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js"; import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js";
import { import {
@@ -157,6 +160,7 @@ import {
onManualEvolveController, onManualEvolveController,
onManualSleepController, onManualSleepController,
onManualSynopsisController, onManualSynopsisController,
onUndoLastMaintenanceController,
onRebuildController, onRebuildController,
onRebuildVectorIndexController, onRebuildVectorIndexController,
onReembedDirectController, onReembedDirectController,
@@ -249,6 +253,10 @@ function getRuntimeDebugState() {
taskPromptBuilds: {}, taskPromptBuilds: {},
taskLlmRequests: {}, taskLlmRequests: {},
injections: {}, injections: {},
maintenance: {
lastAction: null,
lastUndoResult: null,
},
graphPersistence: null, graphPersistence: null,
updatedAt: "", updatedAt: "",
}; };
@@ -281,6 +289,18 @@ function recordGraphPersistenceSnapshot(snapshot = null) {
state.graphPersistence = cloneRuntimeDebugValue(snapshot, null); state.graphPersistence = cloneRuntimeDebugValue(snapshot, null);
} }
function recordMaintenanceDebugSnapshot(patch = {}) {
const state = touchRuntimeDebugState();
const previous = state.maintenance || {
lastAction: null,
lastUndoResult: null,
};
state.maintenance = {
...previous,
...cloneRuntimeDebugValue(patch, {}),
};
}
function readRuntimeDebugSnapshot() { function readRuntimeDebugSnapshot() {
const state = getRuntimeDebugState(); const state = getRuntimeDebugState();
return cloneRuntimeDebugValue( return cloneRuntimeDebugValue(
@@ -289,6 +309,7 @@ function readRuntimeDebugSnapshot() {
taskPromptBuilds: state.taskPromptBuilds, taskPromptBuilds: state.taskPromptBuilds,
taskLlmRequests: state.taskLlmRequests, taskLlmRequests: state.taskLlmRequests,
injections: state.injections, injections: state.injections,
maintenance: state.maintenance,
graphPersistence: state.graphPersistence, graphPersistence: state.graphPersistence,
updatedAt: state.updatedAt, updatedAt: state.updatedAt,
}, },
@@ -297,6 +318,10 @@ function readRuntimeDebugSnapshot() {
taskPromptBuilds: {}, taskPromptBuilds: {},
taskLlmRequests: {}, taskLlmRequests: {},
injections: {}, injections: {},
maintenance: {
lastAction: null,
lastUndoResult: null,
},
graphPersistence: null, graphPersistence: null,
updatedAt: "", updatedAt: "",
}, },
@@ -325,6 +350,11 @@ const defaultSettings = {
recallLlmContextMessages: 4, // 传给 LLM 精排的最近非系统消息数 recallLlmContextMessages: 4, // 传给 LLM 精排的最近非系统消息数
recallEnableMultiIntent: true, recallEnableMultiIntent: true,
recallMultiIntentMaxSegments: 4, recallMultiIntentMaxSegments: 4,
recallEnableContextQueryBlend: true,
recallContextAssistantWeight: 0.2,
recallContextPreviousUserWeight: 0.1,
recallEnableLexicalBoost: true,
recallLexicalWeight: 0.18,
recallTeleportAlpha: 0.15, recallTeleportAlpha: 0.15,
recallEnableTemporalLinks: true, recallEnableTemporalLinks: true,
recallTemporalLinkStrength: 0.2, recallTemporalLinkStrength: 0.2,
@@ -412,6 +442,7 @@ const defaultSettings = {
// ⑩ 反思条目P2 // ⑩ 反思条目P2
enableReflection: true, // 启用反思 enableReflection: true, // 启用反思
reflectEveryN: 10, // 每 N 次提取后反思 reflectEveryN: 10, // 每 N 次提取后反思
maintenanceAutoMinNewNodes: 3,
// UI 面板 // UI 面板
panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet
@@ -4148,6 +4179,117 @@ async function recordGraphMutation({
return vectorSync; return vectorSync;
} }
function noteMaintenanceGate(status, action, reason) {
if (!status || typeof status !== "object") return;
const normalizedAction = String(action || "").trim() || "unknown";
const normalizedReason = String(reason || "").trim();
if (!normalizedReason) return;
const nextDetail = {
action: normalizedAction,
reason: normalizedReason,
};
const previousDetails = Array.isArray(status.maintenanceGateDetails)
? status.maintenanceGateDetails
: [];
status.maintenanceGateApplied = true;
status.maintenanceGateDetails = [...previousDetails, nextDetail];
status.maintenanceGateReason = status.maintenanceGateDetails
.map((item) => `${item.action}: ${item.reason}`)
.join(" | ");
}
function evaluateAutoMaintenanceGate(action, newNodeCount, settings = {}) {
const normalizedAction = String(action || "").trim();
if (!["consolidate", "compress"].includes(normalizedAction)) {
return { blocked: false, reason: "", minNewNodes: 0 };
}
if (settings?.maintenanceAutoMinNewNodes == null) {
return { blocked: false, reason: "", minNewNodes: 0 };
}
const minNewNodes = clampInt(settings.maintenanceAutoMinNewNodes, 3, 1, 50);
const safeNewNodeCount = Math.max(0, Number(newNodeCount) || 0);
if (safeNewNodeCount >= minNewNodes) {
return { blocked: false, reason: "", minNewNodes };
}
return {
blocked: true,
minNewNodes,
reason: `本批只新增 ${safeNewNodeCount} 个节点,低于门槛 ${minNewNodes}`,
};
}
function buildMaintenanceSummary(action, result, mode = "manual") {
const prefix = mode === "auto" ? "自动" : "手动";
switch (String(action || "")) {
case "compress":
return `${prefix}压缩:新建 ${result?.created || 0},归档 ${result?.archived || 0}`;
case "consolidate":
return `${prefix}整合:合并 ${result?.merged || 0},跳过 ${result?.skipped || 0},保留 ${result?.kept || 0},进化 ${result?.evolved || 0},新链接 ${result?.connections || 0},回溯更新 ${result?.updates || 0}`;
case "sleep":
return `${prefix}遗忘:归档 ${result?.forgotten || 0} 个节点`;
default:
return `${prefix}维护已执行`;
}
}
function recordMaintenanceAction({
action,
beforeSnapshot,
mode = "manual",
summary = "",
} = {}) {
if (!currentGraph || !beforeSnapshot) return null;
ensureCurrentGraphRuntimeState();
const entry = createMaintenanceJournalEntry(
beforeSnapshot,
cloneGraphSnapshot(currentGraph),
{
action,
mode,
summary,
},
);
if (!entry) return null;
appendMaintenanceJournal(currentGraph, entry);
recordMaintenanceDebugSnapshot({
lastAction: {
id: entry.id,
action: entry.action,
mode: entry.mode,
summary: entry.summary,
createdAt: entry.createdAt,
maintenanceJournalSize: currentGraph.maintenanceJournal?.length || 0,
},
});
return entry;
}
function undoLastMaintenanceAction() {
if (!currentGraph) {
return { ok: false, reason: "当前没有加载的图谱", entry: null };
}
ensureCurrentGraphRuntimeState();
const result = undoLatestMaintenance(currentGraph);
recordMaintenanceDebugSnapshot({
lastUndoResult: {
ok: Boolean(result?.ok),
reason: String(result?.reason || ""),
action: result?.entry?.action || "",
summary: result?.entry?.summary || "",
createdAt: result?.entry?.createdAt || 0,
maintenanceJournalSize: currentGraph.maintenanceJournal?.length || 0,
updatedAt: new Date().toISOString(),
},
});
return result;
}
function markVectorStateDirty(reason = "向量状态已标记为待重建") { function markVectorStateDirty(reason = "向量状态已标记为待重建") {
if (!currentGraph) return; if (!currentGraph) return;
ensureCurrentGraphRuntimeState(); ensureCurrentGraphRuntimeState();
@@ -6027,6 +6169,78 @@ async function handleExtractionSuccess(
}), }),
) { ) {
const postProcessArtifacts = []; const postProcessArtifacts = [];
const newNodeCount = Array.isArray(result?.newNodeIds)
? result.newNodeIds.length
: 0;
const resolveAutoMaintenanceGate =
typeof evaluateAutoMaintenanceGate === "function"
? evaluateAutoMaintenanceGate
: (action, count, localSettings = {}) => {
const normalizedAction = String(action || "").trim();
if (!["consolidate", "compress"].includes(normalizedAction)) {
return { blocked: false, reason: "", minNewNodes: 0 };
}
if (localSettings?.maintenanceAutoMinNewNodes == null) {
return { blocked: false, reason: "", minNewNodes: 0 };
}
const parsedMinNewNodes = Math.floor(
Number(localSettings.maintenanceAutoMinNewNodes),
);
const minNewNodes =
Number.isFinite(parsedMinNewNodes) && parsedMinNewNodes >= 1
? Math.min(50, parsedMinNewNodes)
: 3;
const safeCount = Math.max(0, Number(count) || 0);
return safeCount >= minNewNodes
? { blocked: false, reason: "", minNewNodes }
: {
blocked: true,
minNewNodes,
reason: `本批只新增 ${safeCount} 个节点,低于门槛 ${minNewNodes}`,
};
};
const applyMaintenanceGateNote =
typeof noteMaintenanceGate === "function"
? noteMaintenanceGate
: (batchStatus, action, reason) => {
if (!batchStatus || !reason) return;
batchStatus.maintenanceGateApplied = true;
const details = Array.isArray(batchStatus.maintenanceGateDetails)
? batchStatus.maintenanceGateDetails
: [];
details.push({
action: String(action || "").trim() || "unknown",
reason: String(reason || ""),
});
batchStatus.maintenanceGateDetails = details;
batchStatus.maintenanceGateReason = details
.map((item) => `${item.action}: ${item.reason}`)
.join(" | ");
};
const summarizeMaintenance =
typeof buildMaintenanceSummary === "function"
? buildMaintenanceSummary
: (action, maintenanceResult, mode = "manual") => {
const prefix = mode === "auto" ? "自动" : "手动";
switch (String(action || "")) {
case "compress":
return `${prefix}压缩:新建 ${maintenanceResult?.created || 0},归档 ${maintenanceResult?.archived || 0}`;
case "consolidate":
return `${prefix}整合:合并 ${maintenanceResult?.merged || 0},跳过 ${maintenanceResult?.skipped || 0},保留 ${maintenanceResult?.kept || 0},进化 ${maintenanceResult?.evolved || 0},新链接 ${maintenanceResult?.connections || 0},回溯更新 ${maintenanceResult?.updates || 0}`;
case "sleep":
return `${prefix}遗忘:归档 ${maintenanceResult?.forgotten || 0} 个节点`;
default:
return `${prefix}维护已执行`;
}
};
const cloneMaintenanceSnapshot =
typeof cloneGraphSnapshot === "function"
? cloneGraphSnapshot
: (value) => JSON.parse(JSON.stringify(value ?? null));
const persistMaintenanceAction =
typeof recordMaintenanceAction === "function"
? recordMaintenanceAction
: () => null;
throwIfAborted(signal, "提取已终止"); throwIfAborted(signal, "提取已终止");
extractionCount++; extractionCount++;
ensureCurrentGraphRuntimeState(); ensureCurrentGraphRuntimeState();
@@ -6035,30 +6249,51 @@ async function handleExtractionSuccess(
setBatchStageOutcome(status, "core", "success"); setBatchStageOutcome(status, "core", "success");
if (settings.enableConsolidation && result.newNodeIds?.length > 0) { if (settings.enableConsolidation && result.newNodeIds?.length > 0) {
try { const gate = resolveAutoMaintenanceGate(
await consolidateMemories({ "consolidate",
graph: currentGraph, newNodeCount,
newNodeIds: result.newNodeIds, settings,
embeddingConfig: getEmbeddingConfig(), );
options: { if (gate.blocked) {
neighborCount: settings.consolidationNeighborCount, applyMaintenanceGateNote(status, "consolidate", gate.reason);
conflictThreshold: settings.consolidationThreshold, pushBatchStageArtifact(status, "structural", "consolidation-skipped");
}, } else {
settings, try {
signal, const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
}); const consolidationResult = await consolidateMemories({
postProcessArtifacts.push("consolidation"); graph: currentGraph,
pushBatchStageArtifact(status, "structural", "consolidation"); newNodeIds: result.newNodeIds,
} catch (e) { embeddingConfig: getEmbeddingConfig(),
if (isAbortError(e)) throw e; options: {
const message = e?.message || String(e) || "记忆整合阶段失败"; neighborCount: settings.consolidationNeighborCount,
setBatchStageOutcome( conflictThreshold: settings.consolidationThreshold,
status, },
"structural", settings,
"partial", signal,
`记忆整合失败: ${message}`, });
); persistMaintenanceAction({
console.error("[ST-BME] 记忆整合失败:", e); action: "consolidate",
beforeSnapshot,
mode: "auto",
summary: summarizeMaintenance(
"consolidate",
consolidationResult,
"auto",
),
});
postProcessArtifacts.push("consolidation");
pushBatchStageArtifact(status, "structural", "consolidation");
} catch (e) {
if (isAbortError(e)) throw e;
const message = e?.message || String(e) || "记忆整合阶段失败";
setBatchStageOutcome(
status,
"structural",
"partial",
`记忆整合失败: ${message}`,
);
console.error("[ST-BME] 记忆整合失败:", e);
}
} }
} }
@@ -6120,9 +6355,18 @@ async function handleExtractionSuccess(
extractionCount % settings.sleepEveryN === 0 extractionCount % settings.sleepEveryN === 0
) { ) {
try { try {
sleepCycle(currentGraph, settings); const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
postProcessArtifacts.push("sleep"); const sleepResult = sleepCycle(currentGraph, settings);
pushBatchStageArtifact(status, "semantic", "sleep"); if ((sleepResult?.forgotten || 0) > 0) {
persistMaintenanceAction({
action: "sleep",
beforeSnapshot,
mode: "auto",
summary: summarizeMaintenance("sleep", sleepResult, "auto"),
});
postProcessArtifacts.push("sleep");
pushBatchStageArtifact(status, "semantic", "sleep");
}
} catch (e) { } catch (e) {
const message = e?.message || String(e) || "主动遗忘阶段失败"; const message = e?.message || String(e) || "主动遗忘阶段失败";
setBatchStageOutcome( setBatchStageOutcome(
@@ -6137,18 +6381,39 @@ async function handleExtractionSuccess(
try { try {
throwIfAborted(signal, "提取已终止"); throwIfAborted(signal, "提取已终止");
const compressionResult = await compressAll( const gate = resolveAutoMaintenanceGate(
currentGraph, "compress",
getSchema(), newNodeCount,
getEmbeddingConfig(),
false,
undefined,
signal,
settings, settings,
); );
if (compressionResult.created > 0 || compressionResult.archived > 0) { if (gate.blocked) {
postProcessArtifacts.push("compression"); applyMaintenanceGateNote(status, "compress", gate.reason);
pushBatchStageArtifact(status, "structural", "compression"); pushBatchStageArtifact(status, "structural", "compression-skipped");
} else {
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
const compressionResult = await compressAll(
currentGraph,
getSchema(),
getEmbeddingConfig(),
false,
undefined,
signal,
settings,
);
if (compressionResult.created > 0 || compressionResult.archived > 0) {
persistMaintenanceAction({
action: "compress",
beforeSnapshot,
mode: "auto",
summary: summarizeMaintenance(
"compress",
compressionResult,
"auto",
),
});
postProcessArtifacts.push("compression");
pushBatchStageArtifact(status, "structural", "compression");
}
} }
} catch (error) { } catch (error) {
if (isAbortError(error)) throw error; if (isAbortError(error)) throw error;
@@ -6198,6 +6463,18 @@ async function handleExtractionSuccess(
setBatchStageOutcome(status, "finalize", "success"); setBatchStageOutcome(status, "finalize", "success");
} }
status.maintenanceJournalSize =
currentGraph?.maintenanceJournal?.length || 0;
if (
status.maintenanceGateApplied &&
!status.maintenanceGateReason &&
Array.isArray(status.maintenanceGateDetails)
) {
status.maintenanceGateReason = status.maintenanceGateDetails
.map((item) => `${item.action}: ${item.reason}`)
.join(" | ");
}
return { return {
postProcessArtifacts, postProcessArtifacts,
vectorHashesInserted: vectorSync?.insertedHashes || [], vectorHashesInserted: vectorSync?.insertedHashes || [],
@@ -7107,6 +7384,12 @@ function buildRecallRetrieveOptions(settings, context) {
probRecallChance: settings.probRecallChance ?? 0.15, probRecallChance: settings.probRecallChance ?? 0.15,
enableMultiIntent: settings.recallEnableMultiIntent ?? true, enableMultiIntent: settings.recallEnableMultiIntent ?? true,
multiIntentMaxSegments: settings.recallMultiIntentMaxSegments ?? 4, multiIntentMaxSegments: settings.recallMultiIntentMaxSegments ?? 4,
enableContextQueryBlend: settings.recallEnableContextQueryBlend ?? true,
contextAssistantWeight: settings.recallContextAssistantWeight ?? 0.2,
contextPreviousUserWeight:
settings.recallContextPreviousUserWeight ?? 0.1,
enableLexicalBoost: settings.recallEnableLexicalBoost ?? true,
lexicalWeight: settings.recallLexicalWeight ?? 0.18,
teleportAlpha: settings.recallTeleportAlpha ?? 0.15, teleportAlpha: settings.recallTeleportAlpha ?? 0.15,
enableTemporalLinks: settings.recallEnableTemporalLinks ?? true, enableTemporalLinks: settings.recallEnableTemporalLinks ?? true,
temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2, temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2,
@@ -7426,6 +7709,7 @@ async function onRebuild() {
async function onManualCompress() { async function onManualCompress() {
return await onManualCompressController({ return await onManualCompressController({
buildMaintenanceSummary,
cloneGraphSnapshot, cloneGraphSnapshot,
compressAll, compressAll,
ensureGraphMutationReady, ensureGraphMutationReady,
@@ -7433,6 +7717,7 @@ async function onManualCompress() {
getEmbeddingConfig, getEmbeddingConfig,
getSchema, getSchema,
getSettings, getSettings,
recordMaintenanceAction,
recordGraphMutation, recordGraphMutation,
toastr, toastr,
}); });
@@ -7577,10 +7862,12 @@ async function onReroll({ fromFloor } = {}) {
async function onManualSleep() { async function onManualSleep() {
return await onManualSleepController({ return await onManualSleepController({
buildMaintenanceSummary,
cloneGraphSnapshot, cloneGraphSnapshot,
ensureGraphMutationReady, ensureGraphMutationReady,
getCurrentGraph: () => currentGraph, getCurrentGraph: () => currentGraph,
getSettings, getSettings,
recordMaintenanceAction,
recordGraphMutation, recordGraphMutation,
sleepCycle, sleepCycle,
toastr, toastr,
@@ -7603,6 +7890,7 @@ async function onManualSynopsis() {
async function onManualEvolve() { async function onManualEvolve() {
return await onManualEvolveController({ return await onManualEvolveController({
buildMaintenanceSummary,
cloneGraphSnapshot, cloneGraphSnapshot,
consolidateMemories, consolidateMemories,
ensureGraphMutationReady, ensureGraphMutationReady,
@@ -7610,11 +7898,24 @@ async function onManualEvolve() {
getEmbeddingConfig, getEmbeddingConfig,
getLastExtractedItems: () => lastExtractedItems, getLastExtractedItems: () => lastExtractedItems,
getSettings, getSettings,
recordMaintenanceAction,
recordGraphMutation, recordGraphMutation,
toastr, toastr,
}); });
} }
async function onUndoLastMaintenance() {
return await onUndoLastMaintenanceController({
ensureGraphMutationReady,
getCurrentGraph: () => currentGraph,
markVectorStateDirty,
refreshPanelLiveState,
saveGraphToChat,
toastr,
undoLastMaintenance: undoLastMaintenanceAction,
});
}
async function onRebuildVectorIndex(range = null) { async function onRebuildVectorIndex(range = null) {
return await onRebuildVectorIndexController( return await onRebuildVectorIndexController(
{ {
@@ -7713,6 +8014,7 @@ async function onReembedDirect() {
import: onImportGraph, import: onImportGraph,
rebuild: onRebuild, rebuild: onRebuild,
evolve: onManualEvolve, evolve: onManualEvolve,
undoMaintenance: onUndoLastMaintenance,
testEmbedding: onTestEmbedding, testEmbedding: onTestEmbedding,
testMemoryLLM: onTestMemoryLLM, testMemoryLLM: onTestMemoryLLM,
fetchMemoryLLMModels: onFetchMemoryLLMModels, fetchMemoryLLMModels: onFetchMemoryLLMModels,

View File

@@ -269,6 +269,10 @@
<i class="fa-solid fa-moon"></i> <i class="fa-solid fa-moon"></i>
<span>执行遗忘</span> <span>执行遗忘</span>
</button> </button>
<button class="bme-action-btn" id="bme-act-undo-maintenance" type="button">
<i class="fa-solid fa-rotate-left"></i>
<span>撤销最近维护</span>
</button>
<button class="bme-action-btn" id="bme-act-reroll" type="button"> <button class="bme-action-btn" id="bme-act-reroll" type="button">
<i class="fa-solid fa-rotate"></i> <i class="fa-solid fa-rotate"></i>
<span>重新提取</span> <span>重新提取</span>
@@ -1187,13 +1191,78 @@
<div> <div>
<div class="bme-config-subgroup-title">更多高级项</div> <div class="bme-config-subgroup-title">更多高级项</div>
<div class="bme-config-subgroup-desc"> <div class="bme-config-subgroup-desc">
从 DPP 多样性去重开始,收纳共现补强和弱信号残差召回。 收纳上下文混合查询、文字补分,以及 DPP、共现补强和弱信号残差召回。
</div> </div>
</div> </div>
<span class="bme-collapsible-indicator" aria-hidden="true"> <span class="bme-collapsible-indicator" aria-hidden="true">
<i class="fa-solid fa-chevron-down"></i> <i class="fa-solid fa-chevron-down"></i>
</span> </span>
</summary> </summary>
<div class="bme-config-subgroup">
<div class="bme-config-subgroup-title">查询纠偏</div>
<div class="bme-config-subgroup-desc">
让召回在“那后来呢”“他为什么这么做”这类追问里,也能借最近上下文稳一点。
</div>
<label
class="bme-inline-checkbox"
for="bme-setting-recall-context-query-blend-enabled"
>
<input
id="bme-setting-recall-context-query-blend-enabled"
type="checkbox"
/>
<span>启用上下文混合查询</span>
</label>
<div class="bme-config-row">
<label for="bme-setting-recall-context-assistant-weight"
>最近 assistant 权重</label
>
<input
id="bme-setting-recall-context-assistant-weight"
class="bme-config-input"
type="number"
min="0"
max="1"
step="0.01"
/>
</div>
<div class="bme-config-row">
<label for="bme-setting-recall-context-previous-user-weight"
>上一条 user 权重</label
>
<input
id="bme-setting-recall-context-previous-user-weight"
class="bme-config-input"
type="number"
min="0"
max="1"
step="0.01"
/>
</div>
<label
class="bme-inline-checkbox"
for="bme-setting-recall-lexical-boost-enabled"
>
<input
id="bme-setting-recall-lexical-boost-enabled"
type="checkbox"
/>
<span>启用文字命中补分</span>
</label>
<div class="bme-config-row">
<label for="bme-setting-recall-lexical-weight"
>文字补分权重</label
>
<input
id="bme-setting-recall-lexical-weight"
class="bme-config-input"
type="number"
min="0"
max="1"
step="0.01"
/>
</div>
</div>
<div class="bme-config-subgroup"> <div class="bme-config-subgroup">
<div class="bme-config-subgroup-title">DPP 与共现补强</div> <div class="bme-config-subgroup-title">DPP 与共现补强</div>
<div class="bme-config-subgroup-desc"> <div class="bme-config-subgroup-desc">
@@ -1615,6 +1684,29 @@
</div> </div>
</div> </div>
<div class="bme-config-card">
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">自动维护门槛</div>
<div class="bme-config-card-subtitle">
新增节点太少时,自动整合和自动压缩直接跳过,避免小批次也跑重维护。
</div>
</div>
</div>
<div class="bme-config-row">
<label for="bme-setting-maintenance-auto-min-new-nodes"
>最少新增节点数</label
>
<input
id="bme-setting-maintenance-auto-min-new-nodes"
class="bme-config-input"
type="number"
min="1"
max="50"
/>
</div>
</div>
<div <div
class="bme-config-card bme-guarded-card" class="bme-config-card bme-guarded-card"
data-guard-settings="enableProbRecall" data-guard-settings="enableProbRecall"

View File

@@ -78,6 +78,7 @@ const GRAPH_WRITE_ACTION_IDS = [
"bme-act-sleep", "bme-act-sleep",
"bme-act-synopsis", "bme-act-synopsis",
"bme-act-evolve", "bme-act-evolve",
"bme-act-undo-maintenance",
"bme-act-import", "bme-act-import",
"bme-act-rebuild", "bme-act-rebuild",
"bme-act-vector-rebuild", "bme-act-vector-rebuild",
@@ -1255,6 +1256,7 @@ function _bindActions() {
"bme-act-import": "import", "bme-act-import": "import",
"bme-act-rebuild": "rebuild", "bme-act-rebuild": "rebuild",
"bme-act-evolve": "evolve", "bme-act-evolve": "evolve",
"bme-act-undo-maintenance": "undoMaintenance",
"bme-act-vector-rebuild": "rebuildVectorIndex", "bme-act-vector-rebuild": "rebuildVectorIndex",
"bme-act-vector-reembed": "reembedDirect", "bme-act-vector-reembed": "reembedDirect",
}; };
@@ -1268,6 +1270,7 @@ function _bindActions() {
import: "导入图谱", import: "导入图谱",
rebuild: "重建图谱", rebuild: "重建图谱",
evolve: "强制进化", evolve: "强制进化",
undoMaintenance: "撤销最近维护",
rebuildVectorIndex: "重建向量", rebuildVectorIndex: "重建向量",
reembedDirect: "直连重嵌", reembedDirect: "直连重嵌",
}; };
@@ -1434,6 +1437,14 @@ function _refreshConfigTab() {
"bme-setting-recall-multi-intent-enabled", "bme-setting-recall-multi-intent-enabled",
settings.recallEnableMultiIntent ?? true, settings.recallEnableMultiIntent ?? true,
); );
_setCheckboxValue(
"bme-setting-recall-context-query-blend-enabled",
settings.recallEnableContextQueryBlend ?? true,
);
_setCheckboxValue(
"bme-setting-recall-lexical-boost-enabled",
settings.recallEnableLexicalBoost ?? true,
);
_setCheckboxValue( _setCheckboxValue(
"bme-setting-recall-temporal-links-enabled", "bme-setting-recall-temporal-links-enabled",
settings.recallEnableTemporalLinks ?? true, settings.recallEnableTemporalLinks ?? true,
@@ -1506,6 +1517,18 @@ function _refreshConfigTab() {
"bme-setting-recall-multi-intent-max-segments", "bme-setting-recall-multi-intent-max-segments",
settings.recallMultiIntentMaxSegments ?? 4, settings.recallMultiIntentMaxSegments ?? 4,
); );
_setInputValue(
"bme-setting-recall-context-assistant-weight",
settings.recallContextAssistantWeight ?? 0.2,
);
_setInputValue(
"bme-setting-recall-context-previous-user-weight",
settings.recallContextPreviousUserWeight ?? 0.1,
);
_setInputValue(
"bme-setting-recall-lexical-weight",
settings.recallLexicalWeight ?? 0.18,
);
_setInputValue( _setInputValue(
"bme-setting-recall-teleport-alpha", "bme-setting-recall-teleport-alpha",
settings.recallTeleportAlpha ?? 0.15, settings.recallTeleportAlpha ?? 0.15,
@@ -1578,6 +1601,10 @@ function _refreshConfigTab() {
"bme-setting-forget-threshold", "bme-setting-forget-threshold",
settings.forgetThreshold ?? 0.5, settings.forgetThreshold ?? 0.5,
); );
_setInputValue(
"bme-setting-maintenance-auto-min-new-nodes",
settings.maintenanceAutoMinNewNodes ?? 3,
);
_setInputValue("bme-setting-sleep-every", settings.sleepEveryN ?? 10); _setInputValue("bme-setting-sleep-every", settings.sleepEveryN ?? 10);
_setInputValue( _setInputValue(
"bme-setting-prob-recall-chance", "bme-setting-prob-recall-chance",
@@ -1689,6 +1716,12 @@ function _bindConfigControls() {
bindCheckbox("bme-setting-recall-multi-intent-enabled", (checked) => { bindCheckbox("bme-setting-recall-multi-intent-enabled", (checked) => {
_patchSettings({ recallEnableMultiIntent: checked }); _patchSettings({ recallEnableMultiIntent: checked });
}); });
bindCheckbox("bme-setting-recall-context-query-blend-enabled", (checked) => {
_patchSettings({ recallEnableContextQueryBlend: checked });
});
bindCheckbox("bme-setting-recall-lexical-boost-enabled", (checked) => {
_patchSettings({ recallEnableLexicalBoost: checked });
});
bindCheckbox("bme-setting-recall-temporal-links-enabled", (checked) => { bindCheckbox("bme-setting-recall-temporal-links-enabled", (checked) => {
_patchSettings({ recallEnableTemporalLinks: checked }); _patchSettings({ recallEnableTemporalLinks: checked });
}); });
@@ -1760,6 +1793,23 @@ function _bindConfigControls() {
8, 8,
(value) => _patchSettings({ recallMultiIntentMaxSegments: value }), (value) => _patchSettings({ recallMultiIntentMaxSegments: value }),
); );
bindFloat(
"bme-setting-recall-context-assistant-weight",
0.2,
0,
1,
(value) => _patchSettings({ recallContextAssistantWeight: value }),
);
bindFloat(
"bme-setting-recall-context-previous-user-weight",
0.1,
0,
1,
(value) => _patchSettings({ recallContextPreviousUserWeight: value }),
);
bindFloat("bme-setting-recall-lexical-weight", 0.18, 0, 1, (value) =>
_patchSettings({ recallLexicalWeight: value }),
);
bindFloat("bme-setting-recall-teleport-alpha", 0.15, 0, 1, (value) => bindFloat("bme-setting-recall-teleport-alpha", 0.15, 0, 1, (value) =>
_patchSettings({ recallTeleportAlpha: value }), _patchSettings({ recallTeleportAlpha: value }),
); );
@@ -1843,6 +1893,13 @@ function _bindConfigControls() {
bindFloat("bme-setting-forget-threshold", 0.5, 0.1, 1, (value) => bindFloat("bme-setting-forget-threshold", 0.5, 0.1, 1, (value) =>
_patchSettings({ forgetThreshold: value }), _patchSettings({ forgetThreshold: value }),
); );
bindNumber(
"bme-setting-maintenance-auto-min-new-nodes",
3,
1,
50,
(value) => _patchSettings({ maintenanceAutoMinNewNodes: value }),
);
bindNumber("bme-setting-sleep-every", 10, 1, 200, (value) => bindNumber("bme-setting-sleep-every", 10, 1, 200, (value) =>
_patchSettings({ sleepEveryN: value }), _patchSettings({ sleepEveryN: value }),
); );
@@ -2903,6 +2960,7 @@ function _renderTaskDebugTab(state) {
const promptBuild = runtimeDebug?.taskPromptBuilds?.[state.taskType] || null; const promptBuild = runtimeDebug?.taskPromptBuilds?.[state.taskType] || null;
const llmRequest = runtimeDebug?.taskLlmRequests?.[state.taskType] || null; const llmRequest = runtimeDebug?.taskLlmRequests?.[state.taskType] || null;
const recallInjection = runtimeDebug?.injections?.recall || null; const recallInjection = runtimeDebug?.injections?.recall || null;
const maintenanceDebug = runtimeDebug?.maintenance || null;
const graphPersistence = runtimeDebug?.graphPersistence || null; const graphPersistence = runtimeDebug?.graphPersistence || null;
return ` return `
@@ -2923,6 +2981,9 @@ function _renderTaskDebugTab(state) {
<div class="bme-config-card"> <div class="bme-config-card">
${_renderTaskDebugGraphPersistenceCard(graphPersistence)} ${_renderTaskDebugGraphPersistenceCard(graphPersistence)}
</div> </div>
<div class="bme-config-card">
${_renderTaskDebugMaintenanceCard(maintenanceDebug)}
</div>
<div class="bme-config-card"> <div class="bme-config-card">
${_renderTaskDebugPromptCard(state.taskType, promptBuild)} ${_renderTaskDebugPromptCard(state.taskType, promptBuild)}
</div> </div>
@@ -2937,6 +2998,32 @@ function _renderTaskDebugTab(state) {
`; `;
} }
function _renderTaskDebugMaintenanceCard(maintenanceDebug) {
const lastAction = maintenanceDebug?.lastAction || null;
const lastUndoResult = maintenanceDebug?.lastUndoResult || null;
if (!lastAction && !lastUndoResult) {
return `
<div class="bme-config-card-title">维护账本状态</div>
<div class="bme-config-help">当前还没有最近维护或撤销快照。</div>
`;
}
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">维护账本状态</div>
<div class="bme-config-card-subtitle">
最近一次维护记录和最近一次撤销结果。
</div>
</div>
<span class="bme-task-pill">${_escHtml(lastAction?.action || lastUndoResult?.action || "maintenance")}</span>
</div>
${_renderDebugDetails("最近维护", lastAction)}
${_renderDebugDetails("最近撤销", lastUndoResult)}
`;
}
function _renderTaskDebugGraphPersistenceCard(graphPersistence) { function _renderTaskDebugGraphPersistenceCard(graphPersistence) {
if (!graphPersistence) { if (!graphPersistence) {
return ` return `

View File

@@ -126,10 +126,15 @@ function createRetrievalMeta(enableLLMRecall) {
diffusionHits: 0, diffusionHits: 0,
scoredCandidates: 0, scoredCandidates: 0,
segmentsUsed: [], segmentsUsed: [],
queryBlendActive: false,
queryBlendParts: [],
queryBlendWeights: {},
vectorMergedHits: 0, vectorMergedHits: 0,
seedCount: 0, seedCount: 0,
temporalSyntheticEdgeCount: 0, temporalSyntheticEdgeCount: 0,
teleportAlpha: 0, teleportAlpha: 0,
lexicalBoostedNodes: 0,
lexicalTopHits: [],
cooccurrenceBoostedNodes: 0, cooccurrenceBoostedNodes: 0,
candidatePoolBeforeDpp: 0, candidatePoolBeforeDpp: 0,
candidatePoolAfterDpp: 0, candidatePoolAfterDpp: 0,
@@ -159,6 +164,421 @@ function clampRange(value, fallback, min = 0, max = 1) {
return Math.max(min, Math.min(max, parsed)); return Math.max(min, Math.min(max, parsed));
} }
function normalizeQueryText(value, maxLength = 400) {
const normalized = String(value ?? "")
.replace(/\r\n/g, "\n")
.replace(/\s+/g, " ")
.trim();
if (!normalized) return "";
return normalized.slice(0, Math.max(1, maxLength));
}
function createTextPreview(text, maxLength = 120) {
const normalized = normalizeQueryText(text, maxLength + 4);
if (!normalized) return "";
return normalized.length > maxLength
? `${normalized.slice(0, maxLength)}...`
: normalized;
}
function roundBlendWeight(value) {
return Math.round((Number(value) || 0) * 1000) / 1000;
}
function uniqueStrings(values = [], maxLength = 400) {
const result = [];
const seen = new Set();
for (const value of values) {
const text = normalizeQueryText(value, maxLength);
const key = text.toLowerCase();
if (!text || seen.has(key)) continue;
seen.add(key);
result.push(text);
}
return result;
}
function parseRecallContextLine(line = "") {
const raw = String(line ?? "").trim();
if (!raw) return null;
const bracketMatch = raw.match(/^\[(user|assistant)\]\s*:\s*([\s\S]*)$/i);
if (bracketMatch) {
const role = String(bracketMatch[1] || "").toLowerCase();
const text = normalizeQueryText(bracketMatch[2] || "");
return text ? { role, text } : null;
}
const plainMatch = raw.match(
/^(user|assistant|用户|助手|ai)\s*[:]\s*([\s\S]*)$/i,
);
if (!plainMatch) return null;
const roleToken = String(plainMatch[1] || "").toLowerCase();
const role =
roleToken === "assistant" || roleToken === "助手" || roleToken === "ai"
? "assistant"
: "user";
const text = normalizeQueryText(plainMatch[2] || "");
return text ? { role, text } : null;
}
function buildContextQueryBlend(
userMessage,
recentMessages = [],
{
enabled = true,
assistantWeight = 0.2,
previousUserWeight = 0.1,
maxTextLength = 400,
} = {},
) {
const currentText = normalizeQueryText(userMessage, maxTextLength);
const normalizedAssistantWeight = clampRange(assistantWeight, 0.2, 0, 1);
const normalizedPreviousUserWeight = clampRange(
previousUserWeight,
0.1,
0,
1,
);
const currentWeight = Math.max(
0,
1 - normalizedAssistantWeight - normalizedPreviousUserWeight,
);
let assistantText = "";
let previousUserText = "";
const parsedMessages = Array.isArray(recentMessages)
? recentMessages.map((line) => parseRecallContextLine(line)).filter(Boolean)
: [];
for (let index = parsedMessages.length - 1; index >= 0; index--) {
const item = parsedMessages[index];
if (!assistantText && item.role === "assistant") {
assistantText = normalizeQueryText(item.text, maxTextLength);
}
if (
!previousUserText &&
item.role === "user" &&
normalizeQueryText(item.text, maxTextLength).toLowerCase() !==
currentText.toLowerCase()
) {
previousUserText = normalizeQueryText(item.text, maxTextLength);
}
if (assistantText && previousUserText) break;
}
const rawParts = [
{
kind: "currentUser",
label: "当前用户消息",
text: currentText,
weight: enabled ? currentWeight : 1,
},
];
if (enabled && assistantText) {
rawParts.push({
kind: "assistantContext",
label: "最近 assistant 回复",
text: assistantText,
weight: normalizedAssistantWeight,
});
}
if (enabled && previousUserText) {
rawParts.push({
kind: "previousUser",
label: "上一条 user 消息",
text: previousUserText,
weight: normalizedPreviousUserWeight,
});
}
const dedupedParts = [];
const seen = new Set();
for (const part of rawParts) {
const text = normalizeQueryText(part.text, maxTextLength);
const key = text.toLowerCase();
if (!text || seen.has(key)) continue;
seen.add(key);
dedupedParts.push({
...part,
text,
});
}
if (dedupedParts.length === 0) {
return {
active: false,
parts: [],
currentText: "",
assistantText: "",
previousUserText: "",
combinedText: "",
};
}
const totalWeight = dedupedParts.reduce(
(sum, part) => sum + Math.max(0, Number(part.weight) || 0),
0,
);
const normalizedParts = dedupedParts.map((part) => ({
...part,
weight:
totalWeight > 0
? roundBlendWeight((Math.max(0, Number(part.weight) || 0) || 0) / totalWeight)
: roundBlendWeight(1 / dedupedParts.length),
}));
const combinedText =
normalizedParts.length <= 1
? normalizedParts[0]?.text || ""
: normalizedParts
.map((part) => `${part.label}:\n${part.text}`)
.join("\n\n");
return {
active: enabled && normalizedParts.length > 1,
parts: normalizedParts,
currentText: currentText || normalizedParts[0]?.text || "",
assistantText,
previousUserText,
combinedText,
};
}
function buildVectorQueryPlan(
blendPlan,
{ enableMultiIntent = true, maxSegments = 4 } = {},
) {
const plan = [];
let currentSegments = [];
for (const part of blendPlan?.parts || []) {
let queries = [part.text];
if (part.kind === "currentUser" && enableMultiIntent) {
currentSegments = splitIntentSegments(part.text, { maxSegments });
queries = uniqueStrings([
part.text,
...currentSegments.filter((item) => item !== part.text),
]);
} else {
queries = uniqueStrings([part.text]);
}
plan.push({
kind: part.kind,
label: part.label,
weight: part.weight,
queries,
});
}
return {
plan,
currentSegments,
};
}
function buildLexicalQuerySources(
userMessage,
{ enableMultiIntent = true, maxSegments = 4 } = {},
) {
const currentText = normalizeQueryText(userMessage, 400);
const segments = enableMultiIntent
? splitIntentSegments(currentText, { maxSegments })
: [];
return {
sources: uniqueStrings([currentText, ...segments]),
segments,
};
}
function normalizeLexicalText(value = "") {
return normalizeQueryText(value, 600).toLowerCase();
}
function buildLexicalUnits(text = "") {
const normalized = normalizeLexicalText(text);
if (!normalized) return [];
const rawTokens = normalized.match(/[a-z0-9]+|[\u4e00-\u9fff]+/g) || [];
const units = [];
for (const token of rawTokens) {
if (token.length >= 2) {
units.push(token);
}
if (/[\u4e00-\u9fff]/.test(token) && token.length > 2) {
for (let index = 0; index < token.length - 1; index++) {
units.push(token.slice(index, index + 2));
}
}
}
return [...new Set(units)];
}
function computeTokenOverlapScore(sourceUnits = [], targetUnits = []) {
if (!sourceUnits.length || !targetUnits.length) return 0;
const targetSet = new Set(targetUnits);
let overlap = 0;
for (const unit of sourceUnits) {
if (targetSet.has(unit)) {
overlap += 1;
}
}
return overlap / Math.max(1, sourceUnits.length);
}
function scoreFieldMatch(
fieldText,
querySources = [],
{ exact = 1, includes = 0.9, overlap = 0.6 } = {},
) {
const normalizedField = normalizeLexicalText(fieldText);
if (!normalizedField) return 0;
const fieldUnits = buildLexicalUnits(normalizedField);
let best = 0;
for (const sourceText of querySources) {
const normalizedSource = normalizeLexicalText(sourceText);
if (!normalizedSource) continue;
if (normalizedSource === normalizedField) {
best = Math.max(best, exact);
continue;
}
if (
Math.min(normalizedSource.length, normalizedField.length) >= 2 &&
(normalizedSource.includes(normalizedField) ||
normalizedField.includes(normalizedSource))
) {
best = Math.max(best, includes);
}
const overlapScore = computeTokenOverlapScore(
buildLexicalUnits(normalizedSource),
fieldUnits,
);
best = Math.max(best, overlapScore * overlap);
}
return Math.min(1, best);
}
function collectNodeLexicalTexts(node, fieldNames = []) {
const values = [];
for (const fieldName of fieldNames) {
const value = node?.fields?.[fieldName];
if (typeof value === "string" && value.trim()) {
values.push(value.trim());
} else if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === "string" && item.trim()) {
values.push(item.trim());
}
}
}
}
return values;
}
function computeLexicalScore(node, querySources = []) {
if (!node || !Array.isArray(querySources) || querySources.length === 0) {
return 0;
}
const primaryTexts = collectNodeLexicalTexts(node, ["name", "title"]);
const secondaryTexts = collectNodeLexicalTexts(node, [
"summary",
"insight",
"state",
"traits",
"participants",
"status",
]);
const combinedText = [...primaryTexts, ...secondaryTexts].join(" ");
const primaryScore = primaryTexts.reduce(
(best, value) =>
Math.max(
best,
scoreFieldMatch(value, querySources, {
exact: 1,
includes: 0.92,
overlap: 0.72,
}),
),
0,
);
const secondaryScore = secondaryTexts.reduce(
(best, value) =>
Math.max(
best,
scoreFieldMatch(value, querySources, {
exact: 0.82,
includes: 0.68,
overlap: 0.52,
}),
),
0,
);
const tokenScore = scoreFieldMatch(combinedText, querySources, {
exact: 0.65,
includes: 0.55,
overlap: 0.45,
});
if (primaryScore <= 0 && secondaryScore <= 0 && tokenScore <= 0) {
return 0;
}
return Math.min(
1,
Math.max(
primaryScore,
secondaryScore * 0.82,
tokenScore * 0.7,
primaryScore * 0.75 + secondaryScore * 0.35 + tokenScore * 0.2,
),
);
}
function buildLexicalTopHits(scoredNodes = [], maxCount = 5) {
return scoredNodes
.filter((item) => (Number(item?.lexicalScore) || 0) > 0)
.sort((a, b) => {
const lexicalDelta =
(Number(b?.lexicalScore) || 0) - (Number(a?.lexicalScore) || 0);
if (lexicalDelta !== 0) return lexicalDelta;
return (Number(b?.finalScore) || 0) - (Number(a?.finalScore) || 0);
})
.slice(0, Math.max(1, maxCount))
.map((item) => ({
nodeId: item.nodeId,
type: item.node?.type || "",
label:
item.node?.fields?.name ||
item.node?.fields?.title ||
item.node?.fields?.summary ||
item.nodeId,
lexicalScore: Math.round((Number(item.lexicalScore) || 0) * 1000) / 1000,
finalScore: Math.round((Number(item.finalScore) || 0) * 1000) / 1000,
}));
}
function scaleVectorResults(results = [], weight = 1) {
return (Array.isArray(results) ? results : []).map((item) => ({
...item,
score: (Number(item?.score) || 0) * Math.max(0, Number(weight) || 0),
}));
}
/** /**
* 三层混合检索管线 * 三层混合检索管线
* *
@@ -248,6 +668,21 @@ export async function retrieve({
10, 10,
); );
const residualTopK = clampPositiveInt(options.residualTopK, 5); const residualTopK = clampPositiveInt(options.residualTopK, 5);
const enableContextQueryBlend = options.enableContextQueryBlend ?? true;
const contextAssistantWeight = clampRange(
options.contextAssistantWeight,
0.2,
0,
1,
);
const contextPreviousUserWeight = clampRange(
options.contextPreviousUserWeight,
0.1,
0,
1,
);
const enableLexicalBoost = options.enableLexicalBoost ?? true;
const lexicalWeight = clampRange(options.lexicalWeight, 0.18, 0, 10);
let activeNodes = getActiveNodes(graph).filter( let activeNodes = getActiveNodes(graph).filter(
(node) => (node) =>
@@ -270,6 +705,29 @@ export async function retrieve({
); );
const vectorValidation = validateVectorConfig(embeddingConfig); const vectorValidation = validateVectorConfig(embeddingConfig);
const retrievalMeta = createRetrievalMeta(enableLLMRecall); const retrievalMeta = createRetrievalMeta(enableLLMRecall);
const contextQueryBlend = buildContextQueryBlend(userMessage, recentMessages, {
enabled: enableContextQueryBlend,
assistantWeight: contextAssistantWeight,
previousUserWeight: contextPreviousUserWeight,
});
retrievalMeta.queryBlendActive = contextQueryBlend.active;
retrievalMeta.queryBlendParts = (contextQueryBlend.parts || []).map((part) => ({
kind: part.kind,
label: part.label,
weight: part.weight,
text: createTextPreview(part.text),
length: part.text.length,
}));
retrievalMeta.queryBlendWeights = Object.fromEntries(
(contextQueryBlend.parts || []).map((part) => [part.kind, part.weight]),
);
const lexicalQuery = buildLexicalQuerySources(
contextQueryBlend.currentText || userMessage,
{
enableMultiIntent,
maxSegments: multiIntentMaxSegments,
},
);
console.log( console.log(
`[ST-BME] 检索开始: ${nodeCount} 个活跃节点${enableVisibility ? " (认知边界已启用)" : ""}`, `[ST-BME] 检索开始: ${nodeCount} 个活跃节点${enableVisibility ? " (认知边界已启用)" : ""}`,
); );
@@ -299,25 +757,25 @@ export async function retrieve({
const vectorStartedAt = nowMs(); const vectorStartedAt = nowMs();
if (enableVectorPrefilter && vectorValidation.valid) { if (enableVectorPrefilter && vectorValidation.valid) {
console.log("[ST-BME] 第1层: 向量预筛"); console.log("[ST-BME] 第1层: 向量预筛");
const segments = enableMultiIntent const queryPlan = buildVectorQueryPlan(contextQueryBlend, {
? splitIntentSegments(userMessage, { enableMultiIntent,
maxSegments: multiIntentMaxSegments, maxSegments: multiIntentMaxSegments,
}) });
: [];
const queries = [userMessage, ...segments.filter((item) => item !== userMessage)];
const groups = []; const groups = [];
retrievalMeta.segmentsUsed = segments; retrievalMeta.segmentsUsed = queryPlan.currentSegments;
for (const queryText of queries) { for (const part of queryPlan.plan) {
const results = await vectorPreFilter( for (const queryText of part.queries) {
graph, const results = await vectorPreFilter(
queryText, graph,
activeNodes, queryText,
embeddingConfig, activeNodes,
normalizedTopK, embeddingConfig,
signal, normalizedTopK,
); signal,
groups.push(results); );
groups.push(scaleVectorResults(results, part.weight || 1));
}
} }
const merged = mergeVectorResults( const merged = mergeVectorResults(
@@ -332,7 +790,12 @@ export async function retrieve({
} }
retrievalMeta.timings.vector = roundMs(nowMs() - vectorStartedAt); retrievalMeta.timings.vector = roundMs(nowMs() - vectorStartedAt);
exactEntityAnchors.push(...extractEntityAnchors(userMessage, activeNodes)); exactEntityAnchors.push(
...extractEntityAnchors(
contextQueryBlend.currentText || userMessage,
activeNodes,
),
);
supplementalAnchorNodeIds = collectSupplementalAnchorNodeIds( supplementalAnchorNodeIds = collectSupplementalAnchorNodeIds(
graph, graph,
vectorResults, vectorResults,
@@ -354,7 +817,7 @@ export async function retrieve({
residualBasisMaxNodes, residualBasisMaxNodes,
); );
residualResult = await runResidualRecall({ residualResult = await runResidualRecall({
queryText: userMessage, queryText: contextQueryBlend.combinedText || userMessage,
graph, graph,
embeddingConfig, embeddingConfig,
basisNodes, basisNodes,
@@ -514,22 +977,39 @@ export async function retrieve({
for (const [nodeId, scores] of scoreMap) { for (const [nodeId, scores] of scoreMap) {
const node = getNode(graph, nodeId); const node = getNode(graph, nodeId);
if (!node || node.archived) continue; if (!node || node.archived) continue;
const lexicalScore = enableLexicalBoost
? computeLexicalScore(node, lexicalQuery.sources)
: 0;
const finalScore = hybridScore( const finalScore = hybridScore(
{ {
graphScore: scores.graphScore, graphScore: scores.graphScore,
vectorScore: scores.vectorScore, vectorScore: scores.vectorScore,
lexicalScore,
importance: node.importance, importance: node.importance,
createdTime: node.createdTime, createdTime: node.createdTime,
}, },
weights, {
...weights,
lexicalWeight: enableLexicalBoost ? lexicalWeight : 0,
},
); );
scoredNodes.push({ nodeId, node, finalScore, ...scores }); scoredNodes.push({
nodeId,
node,
finalScore,
lexicalScore,
...scores,
});
} }
scoredNodes.sort((a, b) => b.finalScore - a.finalScore); scoredNodes.sort((a, b) => b.finalScore - a.finalScore);
retrievalMeta.scoredCandidates = scoredNodes.length; retrievalMeta.scoredCandidates = scoredNodes.length;
retrievalMeta.lexicalBoostedNodes = scoredNodes.filter(
(item) => (Number(item.lexicalScore) || 0) > 0,
).length;
retrievalMeta.lexicalTopHits = buildLexicalTopHits(scoredNodes);
retrievalMeta.timings.scoring = roundMs(nowMs() - scoringStartedAt); retrievalMeta.timings.scoring = roundMs(nowMs() - scoringStartedAt);
let selectedNodeIds; let selectedNodeIds;

View File

@@ -1,6 +1,7 @@
// ST-BME: 运行时状态与历史恢复辅助 // ST-BME: 运行时状态与历史恢复辅助
const BATCH_JOURNAL_LIMIT = 96; const BATCH_JOURNAL_LIMIT = 96;
const MAINTENANCE_JOURNAL_LIMIT = 20;
export const BATCH_JOURNAL_VERSION = 2; export const BATCH_JOURNAL_VERSION = 2;
export function buildVectorCollectionId(chatId) { export function buildVectorCollectionId(chatId) {
@@ -49,6 +50,10 @@ export function createDefaultBatchJournal() {
return []; return [];
} }
export function createDefaultMaintenanceJournal() {
return [];
}
export function normalizeGraphRuntimeState(graph, chatId = "") { export function normalizeGraphRuntimeState(graph, chatId = "") {
if (!graph || typeof graph !== "object") { if (!graph || typeof graph !== "object") {
return graph; return graph;
@@ -165,6 +170,11 @@ export function normalizeGraphRuntimeState(graph, chatId = "") {
graph.batchJournal = Array.isArray(graph.batchJournal) graph.batchJournal = Array.isArray(graph.batchJournal)
? graph.batchJournal.slice(-BATCH_JOURNAL_LIMIT) ? graph.batchJournal.slice(-BATCH_JOURNAL_LIMIT)
: createDefaultBatchJournal(); : createDefaultBatchJournal();
graph.maintenanceJournal = Array.isArray(graph.maintenanceJournal)
? graph.maintenanceJournal
.filter((entry) => entry && typeof entry === "object")
.slice(-MAINTENANCE_JOURNAL_LIMIT)
: createDefaultMaintenanceJournal();
graph.lastProcessedSeq = historyState.lastProcessedAssistantFloor; graph.lastProcessedSeq = historyState.lastProcessedAssistantFloor;
return graph; return graph;
} }
@@ -549,6 +559,212 @@ export function appendBatchJournal(graph, entry) {
} }
} }
export function createMaintenanceJournalEntry(
snapshotBefore,
snapshotAfter,
meta = {},
) {
const beforeNodes = buildNodeMap(snapshotBefore?.nodes || []);
const afterNodes = buildNodeMap(snapshotAfter?.nodes || []);
const beforeEdges = buildEdgeMap(snapshotBefore?.edges || []);
const afterEdges = buildEdgeMap(snapshotAfter?.edges || []);
const restoreNodes = [];
const restoreEdges = [];
const deleteNodeIds = [];
const deleteEdgeIds = [];
const postNodes = [];
const postEdges = [];
for (const [nodeId, beforeNode] of beforeNodes.entries()) {
const afterNode = afterNodes.get(nodeId);
if (!afterNode) {
restoreNodes.push(cloneGraphSnapshot(beforeNode));
continue;
}
if (!hasMeaningfulNodeChange(beforeNode, afterNode)) continue;
restoreNodes.push(cloneGraphSnapshot(beforeNode));
postNodes.push(cloneGraphSnapshot(afterNode));
}
for (const [nodeId, afterNode] of afterNodes.entries()) {
if (beforeNodes.has(nodeId)) continue;
deleteNodeIds.push(nodeId);
postNodes.push(cloneGraphSnapshot(afterNode));
}
for (const [edgeId, beforeEdge] of beforeEdges.entries()) {
const afterEdge = afterEdges.get(edgeId);
if (!afterEdge) {
restoreEdges.push(cloneGraphSnapshot(beforeEdge));
continue;
}
if (!hasMeaningfulEdgeChange(beforeEdge, afterEdge)) continue;
restoreEdges.push(cloneGraphSnapshot(beforeEdge));
postEdges.push(cloneGraphSnapshot(afterEdge));
}
for (const [edgeId, afterEdge] of afterEdges.entries()) {
if (beforeEdges.has(edgeId)) continue;
deleteEdgeIds.push(edgeId);
postEdges.push(cloneGraphSnapshot(afterEdge));
}
if (
restoreNodes.length === 0 &&
restoreEdges.length === 0 &&
deleteNodeIds.length === 0 &&
deleteEdgeIds.length === 0
) {
return null;
}
return {
id: `maintenance-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
createdAt: Date.now(),
action: String(meta.action || "unknown"),
mode:
meta.mode === "auto" || meta.mode === "manual" ? meta.mode : "manual",
summary: String(meta.summary || ""),
inversePatch: {
restoreNodes,
restoreEdges,
deleteNodeIds,
deleteEdgeIds,
},
postCheck: {
nodes: postNodes,
edges: postEdges,
},
};
}
export function appendMaintenanceJournal(graph, entry) {
if (!entry || typeof entry !== "object") return;
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
graph.maintenanceJournal.push(entry);
if (graph.maintenanceJournal.length > MAINTENANCE_JOURNAL_LIMIT) {
graph.maintenanceJournal = graph.maintenanceJournal.slice(
-MAINTENANCE_JOURNAL_LIMIT,
);
}
}
export function getLatestMaintenanceJournalEntry(graph) {
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
const journal = Array.isArray(graph?.maintenanceJournal)
? graph.maintenanceJournal
: [];
return journal.length > 0 ? journal[journal.length - 1] : null;
}
function validateMaintenanceUndoState(graph, entry) {
const currentNodes = buildNodeMap(graph?.nodes || []);
const currentEdges = buildEdgeMap(graph?.edges || []);
const expectedNodes = entry?.postCheck?.nodes || [];
const expectedEdges = entry?.postCheck?.edges || [];
for (const snapshot of expectedNodes) {
const current = currentNodes.get(snapshot?.id);
if (!current) {
return {
ok: false,
reason: `节点 ${snapshot?.id || "unknown"} 已被后续操作改写`,
};
}
if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
return {
ok: false,
reason: `节点 ${snapshot?.id || "unknown"} 当前状态已变化,无法安全撤销`,
};
}
}
for (const snapshot of expectedEdges) {
const current = currentEdges.get(snapshot?.id);
if (!current) {
return {
ok: false,
reason: `${snapshot?.id || "unknown"} 已被后续操作改写`,
};
}
if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
return {
ok: false,
reason: `${snapshot?.id || "unknown"} 当前状态已变化,无法安全撤销`,
};
}
}
return { ok: true, reason: "" };
}
export function applyMaintenanceInversePatch(graph, inversePatch = {}) {
if (!graph || !inversePatch || typeof inversePatch !== "object") {
return graph;
}
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
const deleteNodeIds = new Set(inversePatch.deleteNodeIds || []);
const deleteEdgeIds = new Set(inversePatch.deleteEdgeIds || []);
const restoreNodes = Array.isArray(inversePatch.restoreNodes)
? inversePatch.restoreNodes
: [];
const restoreEdges = Array.isArray(inversePatch.restoreEdges)
? inversePatch.restoreEdges
: [];
graph.edges = (graph.edges || []).filter(
(edge) =>
!deleteEdgeIds.has(edge.id) &&
!deleteNodeIds.has(edge.fromId) &&
!deleteNodeIds.has(edge.toId),
);
graph.nodes = (graph.nodes || []).filter((node) => !deleteNodeIds.has(node.id));
for (const nodeSnapshot of restoreNodes) {
upsertById(graph.nodes, cloneGraphSnapshot(nodeSnapshot));
}
for (const edgeSnapshot of restoreEdges) {
upsertById(graph.edges, cloneGraphSnapshot(edgeSnapshot));
}
sanitizeGraphReferences(graph);
return graph;
}
export function undoLatestMaintenance(graph) {
normalizeGraphRuntimeState(graph, graph?.historyState?.chatId || "");
const entry = getLatestMaintenanceJournalEntry(graph);
if (!entry) {
return {
ok: false,
reason: "当前没有可撤销的维护记录",
entry: null,
};
}
const validation = validateMaintenanceUndoState(graph, entry);
if (!validation.ok) {
return {
ok: false,
reason: validation.reason,
entry,
};
}
applyMaintenanceInversePatch(graph, entry.inversePatch || {});
graph.maintenanceJournal = graph.maintenanceJournal.slice(0, -1);
return {
ok: true,
reason: "",
entry,
remaining: graph.maintenanceJournal.length,
};
}
function upsertById(list, item) { function upsertById(list, item) {
const index = list.findIndex((entry) => entry.id === item.id); const index = list.findIndex((entry) => entry.id === item.id);
if (index >= 0) { if (index >= 0) {

View File

@@ -46,6 +46,11 @@ assert.equal(defaultSettings.recallLlmCandidatePool, 30);
assert.equal(defaultSettings.recallLlmContextMessages, 4); assert.equal(defaultSettings.recallLlmContextMessages, 4);
assert.equal(defaultSettings.recallEnableMultiIntent, true); assert.equal(defaultSettings.recallEnableMultiIntent, true);
assert.equal(defaultSettings.recallMultiIntentMaxSegments, 4); assert.equal(defaultSettings.recallMultiIntentMaxSegments, 4);
assert.equal(defaultSettings.recallEnableContextQueryBlend, true);
assert.equal(defaultSettings.recallContextAssistantWeight, 0.2);
assert.equal(defaultSettings.recallContextPreviousUserWeight, 0.1);
assert.equal(defaultSettings.recallEnableLexicalBoost, true);
assert.equal(defaultSettings.recallLexicalWeight, 0.18);
assert.equal(defaultSettings.recallTeleportAlpha, 0.15); assert.equal(defaultSettings.recallTeleportAlpha, 0.15);
assert.equal(defaultSettings.recallEnableTemporalLinks, true); assert.equal(defaultSettings.recallEnableTemporalLinks, true);
assert.equal(defaultSettings.recallTemporalLinkStrength, 0.2); assert.equal(defaultSettings.recallTemporalLinkStrength, 0.2);
@@ -64,6 +69,7 @@ assert.equal(defaultSettings.recallResidualTopK, 5);
assert.equal(defaultSettings.injectDepth, 9999); assert.equal(defaultSettings.injectDepth, 9999);
assert.equal(defaultSettings.enabled, true); assert.equal(defaultSettings.enabled, true);
assert.equal(defaultSettings.enableReflection, true); assert.equal(defaultSettings.enableReflection, true);
assert.equal(defaultSettings.maintenanceAutoMinNewNodes, 3);
assert.equal(defaultSettings.embeddingTransportMode, "direct"); assert.equal(defaultSettings.embeddingTransportMode, "direct");
assert.equal(defaultSettings.taskProfilesVersion, 3); assert.equal(defaultSettings.taskProfilesVersion, 3);
assert.ok(defaultSettings.taskProfiles); assert.ok(defaultSettings.taskProfiles);

View File

@@ -0,0 +1,173 @@
import assert from "node:assert/strict";
import {
appendMaintenanceJournal,
createMaintenanceJournalEntry,
normalizeGraphRuntimeState,
undoLatestMaintenance,
} from "../runtime-state.js";
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function buildNode(id, extra = {}) {
return {
id,
type: "character",
archived: false,
seq: 1,
seqRange: [1, 1],
importance: 5,
fields: {},
childIds: [],
parentId: null,
prevId: null,
nextId: null,
...extra,
};
}
function buildEdge(id, fromId, toId, extra = {}) {
return {
id,
fromId,
toId,
relation: "related",
strength: 1,
...extra,
};
}
{
const before = {
nodes: [buildNode("sleep-1")],
edges: [],
};
const after = clone(before);
after.nodes[0].archived = true;
const graph = normalizeGraphRuntimeState(clone(after), "chat-sleep");
const entry = createMaintenanceJournalEntry(before, after, {
action: "sleep",
mode: "manual",
summary: "手动遗忘:归档 1 个节点",
});
appendMaintenanceJournal(graph, entry);
const result = undoLatestMaintenance(graph);
assert.equal(result.ok, true);
assert.equal(graph.nodes[0].archived, false);
assert.equal(graph.maintenanceJournal.length, 0);
}
{
const before = {
nodes: [
buildNode("child-1"),
buildNode("child-2"),
buildNode("location-1", { type: "location", fields: { title: "大厅" } }),
],
edges: [buildEdge("edge-old", "child-1", "location-1")],
};
const after = clone(before);
after.nodes[0].archived = true;
after.nodes[0].parentId = "parent-1";
after.nodes[1].archived = true;
after.nodes[1].parentId = "parent-1";
after.nodes.push(
buildNode("parent-1", {
level: 1,
fields: { summary: "压缩父节点" },
childIds: ["child-1", "child-2"],
}),
);
after.edges.push(buildEdge("edge-new", "parent-1", "location-1"));
const graph = normalizeGraphRuntimeState(clone(after), "chat-compress");
const entry = createMaintenanceJournalEntry(before, after, {
action: "compress",
mode: "manual",
summary: "手动压缩:新建 1归档 2",
});
appendMaintenanceJournal(graph, entry);
const result = undoLatestMaintenance(graph);
assert.equal(result.ok, true);
assert.equal(graph.nodes.some((node) => node.id === "parent-1"), false);
assert.equal(
graph.edges.some((edge) => edge.id === "edge-new"),
false,
);
assert.equal(
graph.nodes.find((node) => node.id === "child-1")?.archived,
false,
);
assert.equal(
graph.nodes.find((node) => node.id === "child-2")?.archived,
false,
);
}
{
const before = {
nodes: [
buildNode("new-1", { fields: { summary: "新线索" } }),
buildNode("old-1", { fields: { summary: "旧描述" } }),
],
edges: [],
};
const after = clone(before);
after.nodes[0].archived = true;
after.nodes[1].fields.summary = "被新信息修正后的旧描述";
after.edges.push(buildEdge("edge-merge", "new-1", "old-1"));
const graph = normalizeGraphRuntimeState(clone(after), "chat-consolidate");
const entry = createMaintenanceJournalEntry(before, after, {
action: "consolidate",
mode: "manual",
summary: "手动整合:合并 1更新 1",
});
appendMaintenanceJournal(graph, entry);
const result = undoLatestMaintenance(graph);
assert.equal(result.ok, true);
assert.equal(
graph.nodes.find((node) => node.id === "new-1")?.archived,
false,
);
assert.equal(
graph.nodes.find((node) => node.id === "old-1")?.fields?.summary,
"旧描述",
);
assert.equal(
graph.edges.some((edge) => edge.id === "edge-merge"),
false,
);
}
{
const before = {
nodes: [buildNode("sleep-2")],
edges: [],
};
const after = clone(before);
after.nodes[0].archived = true;
const graph = normalizeGraphRuntimeState(clone(after), "chat-diverged");
const entry = createMaintenanceJournalEntry(before, after, {
action: "sleep",
mode: "manual",
summary: "手动遗忘:归档 1 个节点",
});
appendMaintenanceJournal(graph, entry);
graph.nodes[0].importance = 9;
const result = undoLatestMaintenance(graph);
assert.equal(result.ok, false);
assert.match(result.reason, /当前状态已变化|已被后续操作改写/);
assert.equal(graph.maintenanceJournal.length, 1);
}
console.log("maintenance-journal tests passed");

View File

@@ -145,8 +145,12 @@ const retrieve = await loadRetrieve({
async runResidualRecall() { async runResidualRecall() {
return { triggered: false, hits: [], skipReason: "residual-disabled-test" }; return { triggered: false, hits: [], skipReason: "residual-disabled-test" };
}, },
hybridScore: ({ graphScore = 0, vectorScore = 0, importance = 0 }) => hybridScore: ({
graphScore + vectorScore + importance, graphScore = 0,
vectorScore = 0,
lexicalScore = 0,
importance = 0,
}) => graphScore + vectorScore + lexicalScore + importance,
reinforceAccessBatch() {}, reinforceAccessBatch() {},
validateVectorConfig() { validateVectorConfig() {
return { valid: true }; return { valid: true };
@@ -214,6 +218,32 @@ assert.equal(state.diffusionCalls.length, 0);
assert.equal(state.llmCalls.length, 0); assert.equal(state.llmCalls.length, 0);
assert.deepEqual(Array.from(noStageResult.selectedNodeIds), ["rule-2", "rule-1"]); assert.deepEqual(Array.from(noStageResult.selectedNodeIds), ["rule-2", "rule-1"]);
state.vectorCalls.length = 0;
await retrieve({
graph,
userMessage: "他后来怎么做?",
recentMessages: [
"[assistant]: 他提到了规则二的限制",
"[user]: 我们先看规则一",
"[user]: 他后来怎么做?",
],
embeddingConfig: {},
schema,
options: {
topK: 4,
maxRecallNodes: 2,
enableVectorPrefilter: true,
enableGraphDiffusion: false,
enableLLMRecall: false,
enableMultiIntent: false,
enableContextQueryBlend: true,
},
});
assert.deepEqual(
state.vectorCalls.map((item) => item.message),
["他后来怎么做?", "他提到了规则二的限制", "我们先看规则一"],
);
state.vectorCalls.length = 0; state.vectorCalls.length = 0;
state.diffusionCalls.length = 0; state.diffusionCalls.length = 0;
state.llmCalls.length = 0; state.llmCalls.length = 0;
@@ -235,7 +265,10 @@ const llmPoolResult = await retrieve({
llmCandidatePool: 2, llmCandidatePool: 2,
}, },
}); });
assert.deepEqual(state.vectorCalls, [{ topK: 4, message: "请根据规则给出结论" }]); assert.deepEqual(state.vectorCalls, [
{ topK: 4, message: "请根据规则给出结论" },
{ topK: 4, message: "现在该怎么做?" },
]);
assert.equal(state.diffusionCalls.length, 0); assert.equal(state.diffusionCalls.length, 0);
assert.equal(state.llmCandidateCount, 2); assert.equal(state.llmCandidateCount, 2);
assert.deepEqual(Array.from(llmPoolResult.selectedNodeIds), ["rule-2", "rule-1"]); assert.deepEqual(Array.from(llmPoolResult.selectedNodeIds), ["rule-2", "rule-1"]);
@@ -366,4 +399,49 @@ const cappedResult = await retrieve({
}); });
assert.equal(cappedResult.selectedNodeIds.length, 1); assert.equal(cappedResult.selectedNodeIds.length, 1);
const lexicalGraph = {
nodes: [
{
id: "char-1",
type: "character",
importance: 1,
createdTime: 1,
archived: false,
fields: { name: "Alice", summary: "常驻角色" },
seqRange: [1, 1],
},
{
id: "char-2",
type: "character",
importance: 1,
createdTime: 1,
archived: false,
fields: { name: "Bob", summary: "常驻角色" },
seqRange: [1, 1],
},
],
edges: [],
};
const lexicalSchema = [{ id: "character", label: "角色", alwaysInject: false }];
const lexicalResult = await retrieve({
graph: lexicalGraph,
userMessage: "Alice 现在怎么样了",
recentMessages: [],
embeddingConfig: {},
schema: lexicalSchema,
options: {
topK: 2,
maxRecallNodes: 1,
enableVectorPrefilter: false,
enableGraphDiffusion: false,
enableLLMRecall: false,
enableDiversitySampling: false,
enableLexicalBoost: true,
},
});
assert.deepEqual(Array.from(lexicalResult.selectedNodeIds), ["char-1"]);
assert.equal(lexicalResult.meta.retrieval.queryBlendActive, false);
assert.equal(lexicalResult.meta.retrieval.lexicalBoostedNodes, 1);
assert.equal(lexicalResult.meta.retrieval.lexicalTopHits[0]?.nodeId, "char-1");
console.log("retrieval-config tests passed"); console.log("retrieval-config tests passed");

View File

@@ -101,6 +101,12 @@ export async function onManualCompressController(runtime) {
undefined, undefined,
runtime.getSettings(), runtime.getSettings(),
); );
runtime.recordMaintenanceAction?.({
action: "compress",
beforeSnapshot,
mode: "manual",
summary: runtime.buildMaintenanceSummary?.("compress", result, "manual"),
});
await runtime.recordGraphMutation({ await runtime.recordGraphMutation({
beforeSnapshot, beforeSnapshot,
artifactTags: ["compression"], artifactTags: ["compression"],
@@ -386,6 +392,12 @@ export async function onManualSleepController(runtime) {
const beforeSnapshot = runtime.cloneGraphSnapshot(graph); const beforeSnapshot = runtime.cloneGraphSnapshot(graph);
const result = runtime.sleepCycle(graph, runtime.getSettings()); const result = runtime.sleepCycle(graph, runtime.getSettings());
runtime.recordMaintenanceAction?.({
action: "sleep",
beforeSnapshot,
mode: "manual",
summary: runtime.buildMaintenanceSummary?.("sleep", result, "manual"),
});
await runtime.recordGraphMutation({ await runtime.recordGraphMutation({
beforeSnapshot, beforeSnapshot,
artifactTags: ["sleep"], artifactTags: ["sleep"],
@@ -439,6 +451,12 @@ export async function onManualEvolveController(runtime) {
conflictThreshold: settings.consolidationThreshold, conflictThreshold: settings.consolidationThreshold,
}, },
}); });
runtime.recordMaintenanceAction?.({
action: "consolidate",
beforeSnapshot,
mode: "manual",
summary: runtime.buildMaintenanceSummary?.("consolidate", result, "manual"),
});
await runtime.recordGraphMutation({ await runtime.recordGraphMutation({
beforeSnapshot, beforeSnapshot,
artifactTags: ["consolidation"], artifactTags: ["consolidation"],
@@ -447,3 +465,26 @@ export async function onManualEvolveController(runtime) {
`整合完成:合并 ${result.merged},跳过 ${result.skipped},保留 ${result.kept},进化 ${result.evolved},新链接 ${result.connections},回溯更新 ${result.updates}`, `整合完成:合并 ${result.merged},跳过 ${result.skipped},保留 ${result.kept},进化 ${result.evolved},新链接 ${result.connections},回溯更新 ${result.updates}`,
); );
} }
export async function onUndoLastMaintenanceController(runtime) {
const graph = runtime.getCurrentGraph();
if (!graph) return;
if (!runtime.ensureGraphMutationReady("撤销最近维护")) return;
const result = runtime.undoLastMaintenance?.();
if (!result?.ok) {
runtime.toastr.warning(result?.reason || "撤销最近维护失败");
return { handledToast: true };
}
runtime.markVectorStateDirty?.("撤销维护后需要重建向量索引");
runtime.saveGraphToChat?.({ reason: "maintenance-undo-complete" });
runtime.refreshPanelLiveState?.();
runtime.toastr.success(
`已撤销最近维护:${result.entry?.summary || result.entry?.action || "未知操作"}`,
);
return {
handledToast: true,
result,
};
}