mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Harden recall flow and JSON task prompts
This commit is contained in:
@@ -302,9 +302,14 @@ async function summarizeBatch(
|
|||||||
compressPromptBuild,
|
compressPromptBuild,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
);
|
);
|
||||||
|
const llmSystemPrompt =
|
||||||
|
Array.isArray(promptPayload.promptMessages) &&
|
||||||
|
promptPayload.promptMessages.length > 0
|
||||||
|
? String(promptPayload.systemPrompt || "")
|
||||||
|
: String(promptPayload.systemPrompt || systemPrompt || "");
|
||||||
|
|
||||||
return await callLLMForJSON({
|
return await callLLMForJSON({
|
||||||
systemPrompt: promptPayload.systemPrompt || systemPrompt,
|
systemPrompt: llmSystemPrompt,
|
||||||
userPrompt: promptPayload.userPrompt,
|
userPrompt: promptPayload.userPrompt,
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@@ -341,10 +341,14 @@ export async function consolidateMemories({
|
|||||||
consolidationPromptBuild,
|
consolidationPromptBuild,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
);
|
);
|
||||||
|
const llmSystemPrompt =
|
||||||
|
Array.isArray(promptPayload.promptMessages) &&
|
||||||
|
promptPayload.promptMessages.length > 0
|
||||||
|
? String(promptPayload.systemPrompt || "")
|
||||||
|
: String(promptPayload.systemPrompt || consolidationSystemPrompt || "");
|
||||||
try {
|
try {
|
||||||
decision = await callLLMForJSON({
|
decision = await callLLMForJSON({
|
||||||
systemPrompt:
|
systemPrompt: llmSystemPrompt,
|
||||||
promptPayload.systemPrompt || consolidationSystemPrompt,
|
|
||||||
userPrompt: promptPayload.userPrompt,
|
userPrompt: promptPayload.userPrompt,
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@@ -174,10 +174,15 @@ export async function extractMemories({
|
|||||||
"请分析对话,按 JSON 格式输出操作列表。",
|
"请分析对话,按 JSON 格式输出操作列表。",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const promptPayload = resolveTaskPromptPayload(promptBuild, userPrompt);
|
const promptPayload = resolveTaskPromptPayload(promptBuild, userPrompt);
|
||||||
|
const llmSystemPrompt =
|
||||||
|
Array.isArray(promptPayload.promptMessages) &&
|
||||||
|
promptPayload.promptMessages.length > 0
|
||||||
|
? String(promptPayload.systemPrompt || "")
|
||||||
|
: String(promptPayload.systemPrompt || systemPrompt || "");
|
||||||
|
|
||||||
// 调用 LLM
|
// 调用 LLM
|
||||||
const result = await callLLMForJSON({
|
const result = await callLLMForJSON({
|
||||||
systemPrompt: promptPayload.systemPrompt || systemPrompt,
|
systemPrompt: llmSystemPrompt,
|
||||||
userPrompt: promptPayload.userPrompt,
|
userPrompt: promptPayload.userPrompt,
|
||||||
maxRetries: 2,
|
maxRetries: 2,
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
553
index.js
553
index.js
@@ -292,6 +292,8 @@ const defaultSettings = {
|
|||||||
let currentGraph = null;
|
let currentGraph = null;
|
||||||
let isExtracting = false;
|
let isExtracting = false;
|
||||||
let isRecalling = false;
|
let isRecalling = false;
|
||||||
|
let activeRecallPromise = null;
|
||||||
|
let recallRunSequence = 0;
|
||||||
let lastInjectionContent = "";
|
let lastInjectionContent = "";
|
||||||
let lastExtractedItems = []; // 最近提取的节点(面板展示用)
|
let lastExtractedItems = []; // 最近提取的节点(面板展示用)
|
||||||
let lastRecalledItems = []; // 最近召回的节点(面板展示用)
|
let lastRecalledItems = []; // 最近召回的节点(面板展示用)
|
||||||
@@ -322,6 +324,9 @@ let pendingHistoryRecoveryTrigger = "";
|
|||||||
let pendingHistoryMutationCheckTimers = [];
|
let pendingHistoryMutationCheckTimers = [];
|
||||||
let pendingGraphLoadRetryTimer = null;
|
let pendingGraphLoadRetryTimer = null;
|
||||||
let pendingGraphLoadRetryChatId = "";
|
let pendingGraphLoadRetryChatId = "";
|
||||||
|
let skipBeforeCombineRecallUntil = 0;
|
||||||
|
let lastPreGenerationRecallKey = "";
|
||||||
|
let lastPreGenerationRecallAt = 0;
|
||||||
const generationRecallTransactions = new Map();
|
const generationRecallTransactions = new Map();
|
||||||
const GENERATION_RECALL_TRANSACTION_TTL_MS = 15000;
|
const GENERATION_RECALL_TRANSACTION_TTL_MS = 15000;
|
||||||
const stageNoticeHandles = {
|
const stageNoticeHandles = {
|
||||||
@@ -696,6 +701,38 @@ function abortStage(stage) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abortRecallStageWithReason(reason = "召回已终止") {
|
||||||
|
const controller = stageAbortControllers.recall;
|
||||||
|
if (!controller || controller.signal.aborted) return false;
|
||||||
|
controller.abort(createAbortError(reason));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForActiveRecallToSettle(timeoutMs = 1800) {
|
||||||
|
const pending = activeRecallPromise;
|
||||||
|
if (!pending) {
|
||||||
|
return {
|
||||||
|
settled: !isRecalling,
|
||||||
|
timedOut: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
await Promise.race([
|
||||||
|
Promise.resolve(pending)
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
|
settled = true;
|
||||||
|
}),
|
||||||
|
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
settled: settled || !isRecalling,
|
||||||
|
timedOut: !settled && isRecalling,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildAbortStageAction(stage) {
|
function buildAbortStageAction(stage) {
|
||||||
const abortStageName = findAbortableStageForNotice(stage);
|
const abortStageName = findAbortableStageForNotice(stage);
|
||||||
if (!abortStageName) return undefined;
|
if (!abortStageName) return undefined;
|
||||||
@@ -1687,11 +1724,19 @@ function scheduleStartupGraphReconciliation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearInjectionState() {
|
function clearInjectionState(options = {}) {
|
||||||
|
const {
|
||||||
|
preserveRecallStatus = false,
|
||||||
|
preserveRuntimeStatus = preserveRecallStatus,
|
||||||
|
} = options;
|
||||||
lastInjectionContent = "";
|
lastInjectionContent = "";
|
||||||
lastRecalledItems = [];
|
lastRecalledItems = [];
|
||||||
lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
if (!preserveRecallStatus) {
|
||||||
runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
||||||
|
}
|
||||||
|
if (!preserveRuntimeStatus) {
|
||||||
|
runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
|
||||||
|
}
|
||||||
recordInjectionSnapshot("recall", {
|
recordInjectionSnapshot("recall", {
|
||||||
injectionText: "",
|
injectionText: "",
|
||||||
selectedNodeIds: [],
|
selectedNodeIds: [],
|
||||||
@@ -1703,7 +1748,7 @@ function clearInjectionState() {
|
|||||||
mode: "cleared",
|
mode: "cleared",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!isRecalling) {
|
if (!isRecalling && !preserveRecallStatus) {
|
||||||
dismissStageNotice("recall");
|
dismissStageNotice("recall");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3155,22 +3200,105 @@ function markGenerationRecallTransactionHookState(
|
|||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearGenerationRecallTransactionsForChat(
|
||||||
|
chatId = getCurrentChatId(),
|
||||||
|
{ clearAll = false } = {},
|
||||||
|
) {
|
||||||
|
let removed = 0;
|
||||||
|
const normalizedChatId = String(chatId || "");
|
||||||
|
if (clearAll || !normalizedChatId) {
|
||||||
|
removed = generationRecallTransactions.size;
|
||||||
|
generationRecallTransactions.clear();
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [transactionId, transaction] of generationRecallTransactions.entries()) {
|
||||||
|
if (String(transaction?.chatId || "") !== normalizedChatId) continue;
|
||||||
|
generationRecallTransactions.delete(transactionId);
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminalGenerationRecallHookState(state = "") {
|
||||||
|
return ["completed", "failed", "aborted", "skipped"].includes(
|
||||||
|
String(state || ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldRunRecallForTransaction(transaction, hookName) {
|
function shouldRunRecallForTransaction(transaction, hookName) {
|
||||||
if (!hookName) return true;
|
if (!hookName) return true;
|
||||||
if (!transaction) return true;
|
if (!transaction) return true;
|
||||||
const hookStates = transaction.hookStates || {};
|
const hookStates = transaction.hookStates || {};
|
||||||
if (hookStates[hookName] === "completed") {
|
if (isTerminalGenerationRecallHookState(hookStates[hookName])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
hookName === "GENERATE_BEFORE_COMBINE_PROMPTS" &&
|
hookName === "GENERATE_BEFORE_COMBINE_PROMPTS" &&
|
||||||
hookStates.GENERATION_AFTER_COMMANDS === "completed"
|
isTerminalGenerationRecallHookState(hookStates.GENERATION_AFTER_COMMANDS)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRecallRunResult(status = "completed", extra = {}) {
|
||||||
|
const normalizedStatus = String(status || "skipped").trim() || "skipped";
|
||||||
|
return {
|
||||||
|
ok: normalizedStatus === "completed",
|
||||||
|
didRecall: normalizedStatus === "completed",
|
||||||
|
status: normalizedStatus,
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGenerationRecallHookStateFromResult(result) {
|
||||||
|
const status = String(result?.status || "").trim();
|
||||||
|
switch (status) {
|
||||||
|
case "completed":
|
||||||
|
return "completed";
|
||||||
|
case "failed":
|
||||||
|
return "failed";
|
||||||
|
case "aborted":
|
||||||
|
case "superseded":
|
||||||
|
return "aborted";
|
||||||
|
default:
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更") {
|
||||||
|
const hadActiveRecall = Boolean(
|
||||||
|
isRecalling ||
|
||||||
|
(stageAbortControllers.recall &&
|
||||||
|
!stageAbortControllers.recall.signal?.aborted),
|
||||||
|
);
|
||||||
|
if (hadActiveRecall) {
|
||||||
|
abortRecallStageWithReason(`${reason},当前召回已取消`);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearGenerationRecallTransactionsForChat();
|
||||||
|
clearRecallInputTracking();
|
||||||
|
clearInjectionState({
|
||||||
|
preserveRecallStatus: hadActiveRecall,
|
||||||
|
preserveRuntimeStatus: hadActiveRecall,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hadActiveRecall) {
|
||||||
|
setLastRecallStatus(
|
||||||
|
"召回已取消",
|
||||||
|
`${reason},等待新的召回请求`,
|
||||||
|
"warning",
|
||||||
|
{
|
||||||
|
syncRuntime: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hadActiveRecall;
|
||||||
|
}
|
||||||
|
|
||||||
function createGenerationRecallContext({
|
function createGenerationRecallContext({
|
||||||
hookName,
|
hookName,
|
||||||
generationType = "normal",
|
generationType = "normal",
|
||||||
@@ -4648,7 +4776,7 @@ function applyRecallInjection(settings, recallInput, recentMessages, result) {
|
|||||||
if (now - lastRecallFallbackNoticeAt > 15000) {
|
if (now - lastRecallFallbackNoticeAt > 15000) {
|
||||||
lastRecallFallbackNoticeAt = now;
|
lastRecallFallbackNoticeAt = now;
|
||||||
toastr.warning(
|
toastr.warning(
|
||||||
llmMeta.reason || "LLM 精排未返回有效结果,已回退到评分排序",
|
llmMeta.reason || "LLM 精排未成功,已改用评分排序并继续注入记忆",
|
||||||
"ST-BME 召回提示",
|
"ST-BME 召回提示",
|
||||||
{ timeOut: 4500 },
|
{ timeOut: 4500 },
|
||||||
);
|
);
|
||||||
@@ -4662,184 +4790,240 @@ function applyRecallInjection(settings, recallInput, recentMessages, result) {
|
|||||||
* 召回管线:检索并注入记忆
|
* 召回管线:检索并注入记忆
|
||||||
*/
|
*/
|
||||||
async function runRecall(options = {}) {
|
async function runRecall(options = {}) {
|
||||||
if (isRecalling || !currentGraph) return false;
|
if (isRecalling) {
|
||||||
|
abortRecallStageWithReason("旧召回已取消,正在启动新的召回");
|
||||||
const settings = getSettings();
|
const settle = await waitForActiveRecallToSettle();
|
||||||
if (!settings.enabled || !settings.recallEnabled) return false;
|
if (!settle.settled && isRecalling) {
|
||||||
if (!isGraphReadable()) {
|
|
||||||
setLastRecallStatus(
|
|
||||||
"等待图谱加载",
|
|
||||||
getGraphMutationBlockReason("召回"),
|
|
||||||
"warning",
|
|
||||||
{ syncRuntime: true },
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isGraphMetadataWriteAllowed()) {
|
|
||||||
if (!(await recoverHistoryIfNeeded("pre-recall"))) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = getContext();
|
|
||||||
const chat = context.chat;
|
|
||||||
if (!chat || chat.length === 0) return false;
|
|
||||||
|
|
||||||
isRecalling = true;
|
|
||||||
const recallController = beginStageAbortController("recall");
|
|
||||||
const recallSignal = recallController.signal;
|
|
||||||
if (options.signal) {
|
|
||||||
if (options.signal.aborted) {
|
|
||||||
recallController.abort(
|
|
||||||
options.signal.reason || createAbortError("宿主已终止生成"),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
options.signal.addEventListener(
|
|
||||||
"abort",
|
|
||||||
() =>
|
|
||||||
recallController.abort(
|
|
||||||
options.signal.reason || createAbortError("宿主已终止生成"),
|
|
||||||
),
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ensureVectorReadyIfNeeded("pre-recall", recallSignal);
|
|
||||||
const recentContextMessageLimit = clampInt(
|
|
||||||
settings.recallLlmContextMessages,
|
|
||||||
4,
|
|
||||||
0,
|
|
||||||
20,
|
|
||||||
);
|
|
||||||
const recallInput = resolveRecallInput(
|
|
||||||
chat,
|
|
||||||
recentContextMessageLimit,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
const userMessage = recallInput.userMessage;
|
|
||||||
const recentMessages = recallInput.recentMessages;
|
|
||||||
|
|
||||||
if (!userMessage) return false;
|
|
||||||
|
|
||||||
recallInput.hookName = options.hookName || "";
|
|
||||||
|
|
||||||
console.log("[ST-BME] 开始召回", {
|
|
||||||
source: recallInput.source,
|
|
||||||
sourceLabel: recallInput.sourceLabel,
|
|
||||||
hookName: recallInput.hookName,
|
|
||||||
userMessageLength: userMessage.length,
|
|
||||||
recentMessages: recentMessages.length,
|
|
||||||
});
|
|
||||||
setLastRecallStatus(
|
|
||||||
"召回中",
|
|
||||||
[
|
|
||||||
getRecallHookLabel(recallInput.hookName),
|
|
||||||
`来源 ${recallInput.sourceLabel}`,
|
|
||||||
`上下文 ${recentMessages.length} 条`,
|
|
||||||
`当前用户消息长度 ${userMessage.length}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" · "),
|
|
||||||
"running",
|
|
||||||
{ syncRuntime: true },
|
|
||||||
);
|
|
||||||
if (recallInput.source === "send-intent") {
|
|
||||||
pendingRecallSendIntent = createRecallInputRecord();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await retrieve({
|
|
||||||
graph: currentGraph,
|
|
||||||
userMessage,
|
|
||||||
recentMessages,
|
|
||||||
embeddingConfig: getEmbeddingConfig(),
|
|
||||||
schema: getSchema(),
|
|
||||||
signal: recallSignal,
|
|
||||||
settings,
|
|
||||||
onStreamProgress: ({ previewText, receivedChars }) => {
|
|
||||||
const preview = previewText?.length > 60
|
|
||||||
? "…" + previewText.slice(-60)
|
|
||||||
: previewText || "";
|
|
||||||
setLastRecallStatus(
|
|
||||||
"AI 生成中",
|
|
||||||
`${preview} [${receivedChars}字]`,
|
|
||||||
"running",
|
|
||||||
{ syncRuntime: true, noticeMarquee: true },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
topK: settings.recallTopK,
|
|
||||||
maxRecallNodes: settings.recallMaxNodes,
|
|
||||||
enableLLMRecall: settings.recallEnableLLM,
|
|
||||||
enableVectorPrefilter: settings.recallEnableVectorPrefilter,
|
|
||||||
enableGraphDiffusion: settings.recallEnableGraphDiffusion,
|
|
||||||
diffusionTopK: settings.recallDiffusionTopK,
|
|
||||||
llmCandidatePool: settings.recallLlmCandidatePool,
|
|
||||||
recallPrompt: undefined,
|
|
||||||
weights: {
|
|
||||||
graphWeight: settings.graphWeight,
|
|
||||||
vectorWeight: settings.vectorWeight,
|
|
||||||
importanceWeight: settings.importanceWeight,
|
|
||||||
},
|
|
||||||
// v2 options
|
|
||||||
enableVisibility: settings.enableVisibility ?? false,
|
|
||||||
visibilityFilter: context.name2 || null,
|
|
||||||
enableCrossRecall: settings.enableCrossRecall ?? false,
|
|
||||||
enableProbRecall: settings.enableProbRecall ?? false,
|
|
||||||
probRecallChance: settings.probRecallChance ?? 0.15,
|
|
||||||
enableMultiIntent: settings.recallEnableMultiIntent ?? true,
|
|
||||||
multiIntentMaxSegments: settings.recallMultiIntentMaxSegments ?? 4,
|
|
||||||
teleportAlpha: settings.recallTeleportAlpha ?? 0.15,
|
|
||||||
enableTemporalLinks: settings.recallEnableTemporalLinks ?? true,
|
|
||||||
temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2,
|
|
||||||
enableDiversitySampling:
|
|
||||||
settings.recallEnableDiversitySampling ?? true,
|
|
||||||
dppCandidateMultiplier:
|
|
||||||
settings.recallDppCandidateMultiplier ?? 3,
|
|
||||||
dppQualityWeight: settings.recallDppQualityWeight ?? 1.0,
|
|
||||||
enableCooccurrenceBoost:
|
|
||||||
settings.recallEnableCooccurrenceBoost ?? false,
|
|
||||||
cooccurrenceScale: settings.recallCooccurrenceScale ?? 0.1,
|
|
||||||
cooccurrenceMaxNeighbors:
|
|
||||||
settings.recallCooccurrenceMaxNeighbors ?? 10,
|
|
||||||
enableResidualRecall:
|
|
||||||
settings.recallEnableResidualRecall ?? false,
|
|
||||||
residualBasisMaxNodes:
|
|
||||||
settings.recallResidualBasisMaxNodes ?? 24,
|
|
||||||
residualNmfTopics: settings.recallNmfTopics ?? 15,
|
|
||||||
residualNmfNoveltyThreshold:
|
|
||||||
settings.recallNmfNoveltyThreshold ?? 0.4,
|
|
||||||
residualThreshold: settings.recallResidualThreshold ?? 0.3,
|
|
||||||
residualTopK: settings.recallResidualTopK ?? 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
applyRecallInjection(settings, recallInput, recentMessages, result);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (isAbortError(e)) {
|
|
||||||
setLastRecallStatus(
|
setLastRecallStatus(
|
||||||
"召回已终止",
|
"召回忙",
|
||||||
e?.message || "已手动终止当前召回",
|
"上一轮召回仍在清理,请稍后重试",
|
||||||
"warning",
|
"warning",
|
||||||
{
|
{
|
||||||
syncRuntime: true,
|
syncRuntime: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return false;
|
return createRecallRunResult("skipped", {
|
||||||
|
reason: "上一轮召回仍在清理",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
console.error("[ST-BME] 召回失败:", e);
|
|
||||||
const message = e?.message || String(e);
|
|
||||||
setLastRecallStatus("召回失败", message, "error", {
|
|
||||||
syncRuntime: true,
|
|
||||||
toastKind: "",
|
|
||||||
});
|
|
||||||
toastr.error(`召回失败: ${message}`);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
finishStageAbortController("recall", recallController);
|
|
||||||
isRecalling = false;
|
|
||||||
refreshPanelLiveState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentGraph) {
|
||||||
|
return createRecallRunResult("skipped", {
|
||||||
|
reason: "当前无图谱",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.enabled || !settings.recallEnabled) {
|
||||||
|
return createRecallRunResult("skipped", {
|
||||||
|
reason: "召回功能未启用",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isGraphReadable()) {
|
||||||
|
const reason = getGraphMutationBlockReason("召回");
|
||||||
|
setLastRecallStatus("等待图谱加载", reason, "warning", {
|
||||||
|
syncRuntime: true,
|
||||||
|
});
|
||||||
|
return createRecallRunResult("skipped", {
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isGraphMetadataWriteAllowed()) {
|
||||||
|
if (!(await recoverHistoryIfNeeded("pre-recall"))) {
|
||||||
|
return createRecallRunResult("skipped", {
|
||||||
|
reason: "历史恢复未就绪",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
if (!chat || chat.length === 0) {
|
||||||
|
return createRecallRunResult("skipped", {
|
||||||
|
reason: "当前聊天为空",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const runId = ++recallRunSequence;
|
||||||
|
let recallPromise = null;
|
||||||
|
recallPromise = (async () => {
|
||||||
|
isRecalling = true;
|
||||||
|
const recallController = beginStageAbortController("recall");
|
||||||
|
const recallSignal = recallController.signal;
|
||||||
|
if (options.signal) {
|
||||||
|
if (options.signal.aborted) {
|
||||||
|
recallController.abort(
|
||||||
|
options.signal.reason || createAbortError("宿主已终止生成"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
options.signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() =>
|
||||||
|
recallController.abort(
|
||||||
|
options.signal.reason || createAbortError("宿主已终止生成"),
|
||||||
|
),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureVectorReadyIfNeeded("pre-recall", recallSignal);
|
||||||
|
const recentContextMessageLimit = clampInt(
|
||||||
|
settings.recallLlmContextMessages,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
const recallInput = resolveRecallInput(
|
||||||
|
chat,
|
||||||
|
recentContextMessageLimit,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
const userMessage = recallInput.userMessage;
|
||||||
|
const recentMessages = recallInput.recentMessages;
|
||||||
|
|
||||||
|
if (!userMessage) {
|
||||||
|
return createRecallRunResult("skipped", {
|
||||||
|
reason: "当前没有可用于召回的用户输入",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recallInput.hookName = options.hookName || "";
|
||||||
|
|
||||||
|
console.log("[ST-BME] 开始召回", {
|
||||||
|
source: recallInput.source,
|
||||||
|
sourceLabel: recallInput.sourceLabel,
|
||||||
|
hookName: recallInput.hookName,
|
||||||
|
userMessageLength: userMessage.length,
|
||||||
|
recentMessages: recentMessages.length,
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
setLastRecallStatus(
|
||||||
|
"召回中",
|
||||||
|
[
|
||||||
|
getRecallHookLabel(recallInput.hookName),
|
||||||
|
`来源 ${recallInput.sourceLabel}`,
|
||||||
|
`上下文 ${recentMessages.length} 条`,
|
||||||
|
`当前用户消息长度 ${userMessage.length}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · "),
|
||||||
|
"running",
|
||||||
|
{ syncRuntime: true },
|
||||||
|
);
|
||||||
|
if (recallInput.source === "send-intent") {
|
||||||
|
pendingRecallSendIntent = createRecallInputRecord();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await retrieve({
|
||||||
|
graph: currentGraph,
|
||||||
|
userMessage,
|
||||||
|
recentMessages,
|
||||||
|
embeddingConfig: getEmbeddingConfig(),
|
||||||
|
schema: getSchema(),
|
||||||
|
signal: recallSignal,
|
||||||
|
settings,
|
||||||
|
onStreamProgress: ({ previewText, receivedChars }) => {
|
||||||
|
const preview = previewText?.length > 60
|
||||||
|
? "…" + previewText.slice(-60)
|
||||||
|
: previewText || "";
|
||||||
|
setLastRecallStatus(
|
||||||
|
"AI 生成中",
|
||||||
|
`${preview} [${receivedChars}字]`,
|
||||||
|
"running",
|
||||||
|
{ syncRuntime: true, noticeMarquee: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
topK: settings.recallTopK,
|
||||||
|
maxRecallNodes: settings.recallMaxNodes,
|
||||||
|
enableLLMRecall: settings.recallEnableLLM,
|
||||||
|
enableVectorPrefilter: settings.recallEnableVectorPrefilter,
|
||||||
|
enableGraphDiffusion: settings.recallEnableGraphDiffusion,
|
||||||
|
diffusionTopK: settings.recallDiffusionTopK,
|
||||||
|
llmCandidatePool: settings.recallLlmCandidatePool,
|
||||||
|
recallPrompt: undefined,
|
||||||
|
weights: {
|
||||||
|
graphWeight: settings.graphWeight,
|
||||||
|
vectorWeight: settings.vectorWeight,
|
||||||
|
importanceWeight: settings.importanceWeight,
|
||||||
|
},
|
||||||
|
// v2 options
|
||||||
|
enableVisibility: settings.enableVisibility ?? false,
|
||||||
|
visibilityFilter: context.name2 || null,
|
||||||
|
enableCrossRecall: settings.enableCrossRecall ?? false,
|
||||||
|
enableProbRecall: settings.enableProbRecall ?? false,
|
||||||
|
probRecallChance: settings.probRecallChance ?? 0.15,
|
||||||
|
enableMultiIntent: settings.recallEnableMultiIntent ?? true,
|
||||||
|
multiIntentMaxSegments: settings.recallMultiIntentMaxSegments ?? 4,
|
||||||
|
teleportAlpha: settings.recallTeleportAlpha ?? 0.15,
|
||||||
|
enableTemporalLinks: settings.recallEnableTemporalLinks ?? true,
|
||||||
|
temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2,
|
||||||
|
enableDiversitySampling:
|
||||||
|
settings.recallEnableDiversitySampling ?? true,
|
||||||
|
dppCandidateMultiplier:
|
||||||
|
settings.recallDppCandidateMultiplier ?? 3,
|
||||||
|
dppQualityWeight: settings.recallDppQualityWeight ?? 1.0,
|
||||||
|
enableCooccurrenceBoost:
|
||||||
|
settings.recallEnableCooccurrenceBoost ?? false,
|
||||||
|
cooccurrenceScale: settings.recallCooccurrenceScale ?? 0.1,
|
||||||
|
cooccurrenceMaxNeighbors:
|
||||||
|
settings.recallCooccurrenceMaxNeighbors ?? 10,
|
||||||
|
enableResidualRecall:
|
||||||
|
settings.recallEnableResidualRecall ?? false,
|
||||||
|
residualBasisMaxNodes:
|
||||||
|
settings.recallResidualBasisMaxNodes ?? 24,
|
||||||
|
residualNmfTopics: settings.recallNmfTopics ?? 15,
|
||||||
|
residualNmfNoveltyThreshold:
|
||||||
|
settings.recallNmfNoveltyThreshold ?? 0.4,
|
||||||
|
residualThreshold: settings.recallResidualThreshold ?? 0.3,
|
||||||
|
residualTopK: settings.recallResidualTopK ?? 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyRecallInjection(settings, recallInput, recentMessages, result);
|
||||||
|
return createRecallRunResult("completed", {
|
||||||
|
reason: "召回完成",
|
||||||
|
selectedNodeIds: result.selectedNodeIds || [],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (isAbortError(e)) {
|
||||||
|
setLastRecallStatus(
|
||||||
|
"召回已终止",
|
||||||
|
e?.message || "已手动终止当前召回",
|
||||||
|
"warning",
|
||||||
|
{
|
||||||
|
syncRuntime: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return createRecallRunResult("aborted", {
|
||||||
|
reason: e?.message || "召回已终止",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error("[ST-BME] 召回失败:", e);
|
||||||
|
const message = e?.message || String(e);
|
||||||
|
setLastRecallStatus("召回失败", message, "error", {
|
||||||
|
syncRuntime: true,
|
||||||
|
toastKind: "",
|
||||||
|
});
|
||||||
|
toastr.error(`召回失败: ${message}`);
|
||||||
|
return createRecallRunResult("failed", {
|
||||||
|
reason: message,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
finishStageAbortController("recall", recallController);
|
||||||
|
isRecalling = false;
|
||||||
|
if (activeRecallPromise === recallPromise) {
|
||||||
|
activeRecallPromise = null;
|
||||||
|
}
|
||||||
|
refreshPanelLiveState();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
activeRecallPromise = recallPromise;
|
||||||
|
return await recallPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 事件钩子 ====================
|
// ==================== 事件钩子 ====================
|
||||||
@@ -4853,6 +5037,7 @@ function onChatChanged() {
|
|||||||
skipBeforeCombineRecallUntil = 0;
|
skipBeforeCombineRecallUntil = 0;
|
||||||
lastPreGenerationRecallKey = "";
|
lastPreGenerationRecallKey = "";
|
||||||
lastPreGenerationRecallAt = 0;
|
lastPreGenerationRecallAt = 0;
|
||||||
|
clearGenerationRecallTransactionsForChat("", { clearAll: true });
|
||||||
abortAllRunningStages();
|
abortAllRunningStages();
|
||||||
dismissAllStageNotices();
|
dismissAllStageNotices();
|
||||||
syncGraphLoadFromLiveContext({
|
syncGraphLoadFromLiveContext({
|
||||||
@@ -4881,7 +5066,7 @@ function onMessageSent(messageId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMessageDeleted(chatLengthOrMessageId, meta = null) {
|
function onMessageDeleted(chatLengthOrMessageId, meta = null) {
|
||||||
clearInjectionState();
|
invalidateRecallAfterHistoryMutation("消息已删除");
|
||||||
scheduleHistoryMutationRecheck(
|
scheduleHistoryMutationRecheck(
|
||||||
"message-deleted",
|
"message-deleted",
|
||||||
chatLengthOrMessageId,
|
chatLengthOrMessageId,
|
||||||
@@ -4890,12 +5075,12 @@ function onMessageDeleted(chatLengthOrMessageId, meta = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMessageEdited(messageId, meta = null) {
|
function onMessageEdited(messageId, meta = null) {
|
||||||
clearInjectionState();
|
invalidateRecallAfterHistoryMutation("消息已编辑");
|
||||||
scheduleHistoryMutationRecheck("message-edited", messageId, meta);
|
scheduleHistoryMutationRecheck("message-edited", messageId, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageSwiped(messageId, meta = null) {
|
function onMessageSwiped(messageId, meta = null) {
|
||||||
clearInjectionState();
|
invalidateRecallAfterHistoryMutation("已切换楼层 swipe");
|
||||||
scheduleHistoryMutationRecheck("message-swiped", messageId, meta);
|
scheduleHistoryMutationRecheck("message-swiped", messageId, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4925,7 +5110,7 @@ async function onGenerationAfterCommands(type, params = {}, dryRun = false) {
|
|||||||
recallContext.hookName,
|
recallContext.hookName,
|
||||||
"running",
|
"running",
|
||||||
);
|
);
|
||||||
const didRecall = await runRecall({
|
const recallResult = await runRecall({
|
||||||
...recallOptions,
|
...recallOptions,
|
||||||
recallKey: recallContext.recallKey,
|
recallKey: recallContext.recallKey,
|
||||||
hookName: recallContext.hookName,
|
hookName: recallContext.hookName,
|
||||||
@@ -4935,7 +5120,7 @@ async function onGenerationAfterCommands(type, params = {}, dryRun = false) {
|
|||||||
markGenerationRecallTransactionHookState(
|
markGenerationRecallTransactionHookState(
|
||||||
recallContext.transaction,
|
recallContext.transaction,
|
||||||
recallContext.hookName,
|
recallContext.hookName,
|
||||||
didRecall ? "completed" : "pending",
|
getGenerationRecallHookStateFromResult(recallResult),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4960,7 +5145,7 @@ async function onBeforeCombinePrompts() {
|
|||||||
recallContext.hookName,
|
recallContext.hookName,
|
||||||
"running",
|
"running",
|
||||||
);
|
);
|
||||||
const didRecall = await runRecall({
|
const recallResult = await runRecall({
|
||||||
...recallOptions,
|
...recallOptions,
|
||||||
recallKey: recallContext.recallKey,
|
recallKey: recallContext.recallKey,
|
||||||
hookName: recallContext.hookName,
|
hookName: recallContext.hookName,
|
||||||
@@ -4968,7 +5153,7 @@ async function onBeforeCombinePrompts() {
|
|||||||
markGenerationRecallTransactionHookState(
|
markGenerationRecallTransactionHookState(
|
||||||
recallContext.transaction,
|
recallContext.transaction,
|
||||||
recallContext.hookName,
|
recallContext.hookName,
|
||||||
didRecall ? "completed" : "pending",
|
getGenerationRecallHookStateFromResult(recallResult),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
llm.js
45
llm.js
@@ -1446,6 +1446,7 @@ export async function callLLMForJSON({
|
|||||||
promptMessages = [],
|
promptMessages = [],
|
||||||
debugContext = null,
|
debugContext = null,
|
||||||
onStreamProgress = null,
|
onStreamProgress = null,
|
||||||
|
returnFailureDetails = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const override = getLlmTestOverride("callLLMForJSON");
|
const override = getLlmTestOverride("callLLMForJSON");
|
||||||
if (override) {
|
if (override) {
|
||||||
@@ -1459,6 +1460,8 @@ export async function callLLMForJSON({
|
|||||||
additionalMessages,
|
additionalMessages,
|
||||||
promptMessages,
|
promptMessages,
|
||||||
debugContext,
|
debugContext,
|
||||||
|
onStreamProgress,
|
||||||
|
returnFailureDetails,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1467,6 +1470,7 @@ export async function callLLMForJSON({
|
|||||||
requestSource,
|
requestSource,
|
||||||
);
|
);
|
||||||
let lastFailureReason = "";
|
let lastFailureReason = "";
|
||||||
|
let lastFailureType = "";
|
||||||
const promptExecutionSummary = buildPromptExecutionSummary(debugContext);
|
const promptExecutionSummary = buildPromptExecutionSummary(debugContext);
|
||||||
|
|
||||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
@@ -1503,18 +1507,28 @@ export async function callLLMForJSON({
|
|||||||
if (!responseText || typeof responseText !== "string") {
|
if (!responseText || typeof responseText !== "string") {
|
||||||
console.warn(`[ST-BME] LLM 返回空响应 (尝试 ${attempt + 1})`);
|
console.warn(`[ST-BME] LLM 返回空响应 (尝试 ${attempt + 1})`);
|
||||||
lastFailureReason = "返回空响应";
|
lastFailureReason = "返回空响应";
|
||||||
|
lastFailureType = "empty-response";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试解析 JSON
|
// 尝试解析 JSON
|
||||||
const parsed = extractJSON(outputCleanup.cleanedText);
|
const parsed = extractJSON(outputCleanup.cleanedText);
|
||||||
if (parsed !== null) {
|
if (parsed !== null) {
|
||||||
return parsed;
|
return returnFailureDetails
|
||||||
|
? {
|
||||||
|
ok: true,
|
||||||
|
data: parsed,
|
||||||
|
attempts: attempt + 1,
|
||||||
|
errorType: "",
|
||||||
|
failureReason: "",
|
||||||
|
}
|
||||||
|
: parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncated =
|
const truncated =
|
||||||
response.finishReason === "length" ||
|
response.finishReason === "length" ||
|
||||||
looksLikeTruncatedJson(outputCleanup.cleanedText);
|
looksLikeTruncatedJson(outputCleanup.cleanedText);
|
||||||
|
lastFailureType = truncated ? "truncated-json" : "invalid-json";
|
||||||
lastFailureReason = truncated
|
lastFailureReason = truncated
|
||||||
? "输出因长度限制被截断,请重新输出更紧凑的完整 JSON"
|
? "输出因长度限制被截断,请重新输出更紧凑的完整 JSON"
|
||||||
: "输出不是有效 JSON,请严格返回紧凑 JSON 对象";
|
: "输出不是有效 JSON,请严格返回紧凑 JSON 对象";
|
||||||
@@ -1524,13 +1538,40 @@ export async function callLLMForJSON({
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isAbortError(e)) {
|
if (isAbortError(e)) {
|
||||||
throw e;
|
const abortMessage = e?.message || String(e) || "LLM 调用已终止";
|
||||||
|
const isTimeoutAbort =
|
||||||
|
!signal?.aborted && /超时/i.test(String(abortMessage || ""));
|
||||||
|
if (!isTimeoutAbort) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
console.error(`[ST-BME] LLM 调用超时 (尝试 ${attempt + 1}):`, e);
|
||||||
|
lastFailureReason = abortMessage;
|
||||||
|
lastFailureType = "timeout";
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
console.error(`[ST-BME] LLM 调用失败 (尝试 ${attempt + 1}):`, e);
|
console.error(`[ST-BME] LLM 调用失败 (尝试 ${attempt + 1}):`, e);
|
||||||
lastFailureReason = e?.message || String(e) || "LLM 调用失败";
|
lastFailureReason = e?.message || String(e) || "LLM 调用失败";
|
||||||
|
lastFailureType = "provider-error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (returnFailureDetails) {
|
||||||
|
const failureSnapshot = {
|
||||||
|
ok: false,
|
||||||
|
data: null,
|
||||||
|
attempts: maxRetries + 1,
|
||||||
|
errorType: lastFailureType || "unknown",
|
||||||
|
failureReason: lastFailureReason || "LLM 未返回可解析 JSON",
|
||||||
|
};
|
||||||
|
recordTaskLlmRequest(taskType || privateRequestSource, {
|
||||||
|
jsonFailure: failureSnapshot,
|
||||||
|
promptExecution: promptExecutionSummary,
|
||||||
|
}, {
|
||||||
|
merge: true,
|
||||||
|
});
|
||||||
|
return failureSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1093,7 +1093,8 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
|
|||||||
).text;
|
).text;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
systemPrompt: String(promptBuild?.systemPrompt || ""),
|
systemPrompt:
|
||||||
|
executionMessages.length > 0 ? "" : String(promptBuild?.systemPrompt || ""),
|
||||||
userPrompt: hasUserMessage ? "" : sanitizedFallbackUserPrompt,
|
userPrompt: hasUserMessage ? "" : sanitizedFallbackUserPrompt,
|
||||||
promptMessages: executionMessages,
|
promptMessages: executionMessages,
|
||||||
additionalMessages:
|
additionalMessages:
|
||||||
|
|||||||
53
retriever.js
53
retriever.js
@@ -57,6 +57,37 @@ function resolveTaskPromptPayload(promptBuild, fallbackUserPrompt = "") {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveTaskLlmSystemPrompt(promptPayload, fallbackSystemPrompt = "") {
|
||||||
|
const hasPromptMessages =
|
||||||
|
Array.isArray(promptPayload?.promptMessages) &&
|
||||||
|
promptPayload.promptMessages.length > 0;
|
||||||
|
if (hasPromptMessages) {
|
||||||
|
return String(promptPayload?.systemPrompt || "");
|
||||||
|
}
|
||||||
|
return String(promptPayload?.systemPrompt || fallbackSystemPrompt || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRecallFallbackReason(llmResult) {
|
||||||
|
const failureType = String(llmResult?.errorType || "").trim();
|
||||||
|
const failureReason = String(llmResult?.failureReason || "").trim();
|
||||||
|
switch (failureType) {
|
||||||
|
case "timeout":
|
||||||
|
return "LLM 精排请求超时,已回退到评分排序";
|
||||||
|
case "empty-response":
|
||||||
|
return "LLM 精排返回空响应,已回退到评分排序";
|
||||||
|
case "truncated-json":
|
||||||
|
return "LLM 精排输出被截断,已回退到评分排序";
|
||||||
|
case "invalid-json":
|
||||||
|
return "LLM 精排未返回有效 JSON,已回退到评分排序";
|
||||||
|
case "provider-error":
|
||||||
|
return failureReason
|
||||||
|
? `LLM 精排调用失败(${failureReason}),已回退到评分排序`
|
||||||
|
: "LLM 精排调用失败,已回退到评分排序";
|
||||||
|
default:
|
||||||
|
return failureReason || "LLM 精排未返回可用结果,已回退到评分排序";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isAbortError(error) {
|
function isAbortError(error) {
|
||||||
return error?.name === "AbortError";
|
return error?.name === "AbortError";
|
||||||
}
|
}
|
||||||
@@ -535,6 +566,7 @@ export async function retrieve({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
status: llmResult.status,
|
status: llmResult.status,
|
||||||
reason: llmResult.reason,
|
reason: llmResult.reason,
|
||||||
|
fallbackType: llmResult.fallbackType || "",
|
||||||
candidatePool: llmCandidates.length,
|
candidatePool: llmCandidates.length,
|
||||||
selectedSeedCount: llmResult.selectedNodeIds.length,
|
selectedSeedCount: llmResult.selectedNodeIds.length,
|
||||||
};
|
};
|
||||||
@@ -562,7 +594,7 @@ export async function retrieve({
|
|||||||
selectedNodeIds = reconstructSceneNodeIds(
|
selectedNodeIds = reconstructSceneNodeIds(
|
||||||
graph,
|
graph,
|
||||||
selectedNodeIds,
|
selectedNodeIds,
|
||||||
normalizedTopK + 6,
|
normalizedMaxRecallNodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 访问强化
|
// 访问强化
|
||||||
@@ -597,7 +629,10 @@ export async function retrieve({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedNodeIds = uniqueNodeIds(selectedNodeIds);
|
selectedNodeIds = uniqueNodeIds(selectedNodeIds).slice(
|
||||||
|
0,
|
||||||
|
normalizedMaxRecallNodes,
|
||||||
|
);
|
||||||
retrievalMeta.llm = llmMeta;
|
retrievalMeta.llm = llmMeta;
|
||||||
retrievalMeta.timings.total = roundMs(nowMs() - startedAt);
|
retrievalMeta.timings.total = roundMs(nowMs() - startedAt);
|
||||||
|
|
||||||
@@ -809,8 +844,8 @@ async function llmRecall(
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
const promptPayload = resolveTaskPromptPayload(recallPromptBuild, userPrompt);
|
const promptPayload = resolveTaskPromptPayload(recallPromptBuild, userPrompt);
|
||||||
|
|
||||||
const result = await callLLMForJSON({
|
const llmResult = await callLLMForJSON({
|
||||||
systemPrompt: promptPayload.systemPrompt || systemPrompt,
|
systemPrompt: resolveTaskLlmSystemPrompt(promptPayload, systemPrompt),
|
||||||
userPrompt: promptPayload.userPrompt,
|
userPrompt: promptPayload.userPrompt,
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
signal,
|
signal,
|
||||||
@@ -822,7 +857,9 @@ async function llmRecall(
|
|||||||
promptMessages: promptPayload.promptMessages,
|
promptMessages: promptPayload.promptMessages,
|
||||||
additionalMessages: promptPayload.additionalMessages,
|
additionalMessages: promptPayload.additionalMessages,
|
||||||
onStreamProgress,
|
onStreamProgress,
|
||||||
|
returnFailureDetails: true,
|
||||||
});
|
});
|
||||||
|
const result = llmResult?.ok ? llmResult.data : null;
|
||||||
|
|
||||||
if (result?.selected_ids && Array.isArray(result.selected_ids)) {
|
if (result?.selected_ids && Array.isArray(result.selected_ids)) {
|
||||||
// 校验 ID 有效性
|
// 校验 ID 有效性
|
||||||
@@ -845,10 +882,16 @@ async function llmRecall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LLM 失败时回退到纯评分排序
|
// LLM 失败时回退到纯评分排序
|
||||||
|
const fallbackReason = llmResult?.ok
|
||||||
|
? Array.isArray(result?.selected_ids)
|
||||||
|
? "LLM 返回的候选 ID 无效,已回退到评分排序"
|
||||||
|
: "LLM 返回了无法识别的 JSON 结构,已回退到评分排序"
|
||||||
|
: buildRecallFallbackReason(llmResult);
|
||||||
return {
|
return {
|
||||||
selectedNodeIds: candidates.slice(0, maxNodes).map((c) => c.nodeId),
|
selectedNodeIds: candidates.slice(0, maxNodes).map((c) => c.nodeId),
|
||||||
status: "fallback",
|
status: "fallback",
|
||||||
reason: "LLM 未返回有效 JSON 或有效候选,已回退到评分排序",
|
reason: fallbackReason,
|
||||||
|
fallbackType: llmResult?.ok ? "invalid-candidate" : llmResult?.errorType || "unknown",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function extractSnippet(startMarker, endMarker) {
|
|||||||
|
|
||||||
const persistencePrelude = extractSnippet(
|
const persistencePrelude = extractSnippet(
|
||||||
'const MODULE_NAME = "st_bme";',
|
'const MODULE_NAME = "st_bme";',
|
||||||
"function clearInjectionState() {",
|
"function clearInjectionState(options = {}) {",
|
||||||
);
|
);
|
||||||
const persistenceCore = extractSnippet(
|
const persistenceCore = extractSnippet(
|
||||||
"function loadGraphFromChat(options = {}) {",
|
"function loadGraphFromChat(options = {}) {",
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ function createGenerationRecallHarness() {
|
|||||||
);
|
);
|
||||||
context.runRecall = async (options = {}) => {
|
context.runRecall = async (options = {}) => {
|
||||||
context.runRecallCalls.push({ ...options });
|
context.runRecallCalls.push({ ...options });
|
||||||
return true;
|
return { status: "completed", didRecall: true, ok: true };
|
||||||
};
|
};
|
||||||
return context;
|
return context;
|
||||||
});
|
});
|
||||||
@@ -1296,6 +1296,31 @@ async function testGenerationRecallDifferentKeyCanRunAgain() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testGenerationRecallSkippedStateDoesNotLoopToBeforeCombine() {
|
||||||
|
const harness = await createGenerationRecallHarness();
|
||||||
|
harness.chat = [{ is_user: true, mes: "同一条但本次跳过" }];
|
||||||
|
harness.runRecall = async (options = {}) => {
|
||||||
|
harness.runRecallCalls.push({ ...options });
|
||||||
|
return {
|
||||||
|
status: "skipped",
|
||||||
|
didRecall: false,
|
||||||
|
ok: false,
|
||||||
|
reason: "测试跳过",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||||
|
await harness.result.onBeforeCombinePrompts();
|
||||||
|
|
||||||
|
assert.equal(harness.runRecallCalls.length, 1);
|
||||||
|
assert.equal(
|
||||||
|
harness.result.generationRecallTransactions.size,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const transaction = [...harness.result.generationRecallTransactions.values()][0];
|
||||||
|
assert.equal(transaction.hookStates.GENERATION_AFTER_COMMANDS, "skipped");
|
||||||
|
}
|
||||||
|
|
||||||
async function testRerollUsesBatchBoundaryRollbackAndPersistsState() {
|
async function testRerollUsesBatchBoundaryRollbackAndPersistsState() {
|
||||||
const harness = await createRerollHarness();
|
const harness = await createRerollHarness();
|
||||||
harness.chat = [
|
harness.chat = [
|
||||||
@@ -1644,6 +1669,7 @@ await testProcessedHistoryAdvanceRequiresCompleteStrongSuccess();
|
|||||||
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
|
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
|
||||||
await testGenerationRecallBeforeCombineRunsStandalone();
|
await testGenerationRecallBeforeCombineRunsStandalone();
|
||||||
await testGenerationRecallDifferentKeyCanRunAgain();
|
await testGenerationRecallDifferentKeyCanRunAgain();
|
||||||
|
await testGenerationRecallSkippedStateDoesNotLoopToBeforeCombine();
|
||||||
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
|
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
|
||||||
await testRerollRejectsMissingRecoveryPoint();
|
await testRerollRejectsMissingRecoveryPoint();
|
||||||
await testRerollFallsBackToDirectExtractForUnprocessedFloor();
|
await testRerollFallsBackToDirectExtractForUnprocessedFloor();
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const extractPromptBuild = await buildTaskPrompt(settings, "extract", {
|
|||||||
currentRange: "1 ~ 2",
|
currentRange: "1 ~ 2",
|
||||||
});
|
});
|
||||||
const extractPayload = buildTaskLlmPayload(extractPromptBuild, "fallback-user");
|
const extractPayload = buildTaskLlmPayload(extractPromptBuild, "fallback-user");
|
||||||
|
assert.equal(extractPayload.systemPrompt, "");
|
||||||
assert.equal(extractPayload.userPrompt, "");
|
assert.equal(extractPayload.userPrompt, "");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
extractPayload.promptMessages.filter((message) => message.role === "user").length,
|
extractPayload.promptMessages.filter((message) => message.role === "user").length,
|
||||||
@@ -86,6 +87,7 @@ const recallPromptBuild = await buildTaskPrompt(settings, "recall", {
|
|||||||
graphStats: "candidate_count=2",
|
graphStats: "candidate_count=2",
|
||||||
});
|
});
|
||||||
const recallPayload = buildTaskLlmPayload(recallPromptBuild, "fallback-user");
|
const recallPayload = buildTaskLlmPayload(recallPromptBuild, "fallback-user");
|
||||||
|
assert.equal(recallPayload.systemPrompt, "");
|
||||||
assert.equal(recallPayload.userPrompt, "");
|
assert.equal(recallPayload.userPrompt, "");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
recallPayload.promptMessages.filter((message) => message.role === "user").length,
|
recallPayload.promptMessages.filter((message) => message.role === "user").length,
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ try {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const payload = buildTaskLlmPayload(promptBuild, "unused fallback");
|
const payload = buildTaskLlmPayload(promptBuild, "unused fallback");
|
||||||
|
assert.equal(payload.systemPrompt, "");
|
||||||
const result = await llm.callLLMForJSON({
|
const result = await llm.callLLMForJSON({
|
||||||
systemPrompt: payload.systemPrompt,
|
systemPrompt: payload.systemPrompt,
|
||||||
userPrompt: payload.userPrompt,
|
userPrompt: payload.userPrompt,
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ const state = {
|
|||||||
diffusionCalls: [],
|
diffusionCalls: [],
|
||||||
llmCalls: [],
|
llmCalls: [],
|
||||||
llmCandidateCount: 0,
|
llmCandidateCount: 0,
|
||||||
|
llmResponse: { selected_ids: ["rule-2", "rule-1"] },
|
||||||
|
llmOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const graph = createGraph();
|
const graph = createGraph();
|
||||||
@@ -164,12 +166,26 @@ const retrieve = await loadRetrieve({
|
|||||||
{ nodeId: "rule-3", energy: 0.9 },
|
{ nodeId: "rule-3", energy: 0.9 },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
async callLLMForJSON({ userPrompt }) {
|
async callLLMForJSON(params = {}) {
|
||||||
|
const { userPrompt = "" } = params;
|
||||||
|
state.llmOptions.push({ ...params });
|
||||||
state.llmCalls.push(userPrompt);
|
state.llmCalls.push(userPrompt);
|
||||||
state.llmCandidateCount = userPrompt
|
state.llmCandidateCount = userPrompt
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.trim().startsWith("[")).length;
|
.filter((line) => line.trim().startsWith("[")).length;
|
||||||
return { selected_ids: ["rule-2", "rule-1"] };
|
if (params.returnFailureDetails) {
|
||||||
|
if (state.llmResponse?.ok === false) {
|
||||||
|
return state.llmResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: state.llmResponse,
|
||||||
|
errorType: "",
|
||||||
|
failureReason: "",
|
||||||
|
attempts: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state.llmResponse;
|
||||||
},
|
},
|
||||||
getSTContextForPrompt() {
|
getSTContextForPrompt() {
|
||||||
return {};
|
return {};
|
||||||
@@ -201,7 +217,9 @@ assert.deepEqual(Array.from(noStageResult.selectedNodeIds), ["rule-2", "rule-1"]
|
|||||||
state.vectorCalls.length = 0;
|
state.vectorCalls.length = 0;
|
||||||
state.diffusionCalls.length = 0;
|
state.diffusionCalls.length = 0;
|
||||||
state.llmCalls.length = 0;
|
state.llmCalls.length = 0;
|
||||||
|
state.llmOptions.length = 0;
|
||||||
state.llmCandidateCount = 0;
|
state.llmCandidateCount = 0;
|
||||||
|
state.llmResponse = { selected_ids: ["rule-2", "rule-1"] };
|
||||||
const llmPoolResult = await retrieve({
|
const llmPoolResult = await retrieve({
|
||||||
graph,
|
graph,
|
||||||
userMessage: "请根据规则给出结论",
|
userMessage: "请根据规则给出结论",
|
||||||
@@ -227,10 +245,12 @@ assert.equal(llmPoolResult.meta.retrieval.vectorMergedHits, 3);
|
|||||||
assert.equal(llmPoolResult.meta.retrieval.diversityApplied, true);
|
assert.equal(llmPoolResult.meta.retrieval.diversityApplied, true);
|
||||||
assert.equal(llmPoolResult.meta.retrieval.candidatePoolBeforeDpp, 3);
|
assert.equal(llmPoolResult.meta.retrieval.candidatePoolBeforeDpp, 3);
|
||||||
assert.equal(llmPoolResult.meta.retrieval.candidatePoolAfterDpp, 2);
|
assert.equal(llmPoolResult.meta.retrieval.candidatePoolAfterDpp, 2);
|
||||||
|
assert.equal(state.llmOptions[0].returnFailureDetails, true);
|
||||||
|
|
||||||
state.vectorCalls.length = 0;
|
state.vectorCalls.length = 0;
|
||||||
state.diffusionCalls.length = 0;
|
state.diffusionCalls.length = 0;
|
||||||
state.llmCalls.length = 0;
|
state.llmCalls.length = 0;
|
||||||
|
state.llmOptions.length = 0;
|
||||||
await retrieve({
|
await retrieve({
|
||||||
graph,
|
graph,
|
||||||
userMessage: "规则一和规则二有什么关联",
|
userMessage: "规则一和规则二有什么关联",
|
||||||
@@ -261,4 +281,89 @@ assert.equal(state.diffusionCalls[0].options.topK, 7);
|
|||||||
assert.equal(state.diffusionCalls[0].options.teleportAlpha, 0.15);
|
assert.equal(state.diffusionCalls[0].options.teleportAlpha, 0.15);
|
||||||
assert.equal(noStageResult.meta.retrieval.llm.status, "disabled");
|
assert.equal(noStageResult.meta.retrieval.llm.status, "disabled");
|
||||||
|
|
||||||
|
state.vectorCalls.length = 0;
|
||||||
|
state.diffusionCalls.length = 0;
|
||||||
|
state.llmCalls.length = 0;
|
||||||
|
state.llmOptions.length = 0;
|
||||||
|
state.llmResponse = {
|
||||||
|
ok: false,
|
||||||
|
errorType: "invalid-json",
|
||||||
|
failureReason: "输出不是有效 JSON,请严格返回紧凑 JSON 对象",
|
||||||
|
};
|
||||||
|
const fallbackResult = await retrieve({
|
||||||
|
graph,
|
||||||
|
userMessage: "LLM 这次会坏掉",
|
||||||
|
recentMessages: ["用户:请回忆相关规则"],
|
||||||
|
embeddingConfig: {},
|
||||||
|
schema,
|
||||||
|
options: {
|
||||||
|
topK: 4,
|
||||||
|
maxRecallNodes: 2,
|
||||||
|
enableVectorPrefilter: true,
|
||||||
|
enableGraphDiffusion: false,
|
||||||
|
enableLLMRecall: true,
|
||||||
|
llmCandidatePool: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(fallbackResult.meta.retrieval.llm.status, "fallback");
|
||||||
|
assert.match(fallbackResult.meta.retrieval.llm.reason, /有效 JSON|回退到评分排序/);
|
||||||
|
assert.equal(fallbackResult.meta.retrieval.llm.fallbackType, "invalid-json");
|
||||||
|
|
||||||
|
const sceneGraph = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "event-1",
|
||||||
|
type: "event",
|
||||||
|
importance: 10,
|
||||||
|
createdTime: 1,
|
||||||
|
archived: false,
|
||||||
|
fields: { title: "事件一" },
|
||||||
|
seqRange: [1, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "character-1",
|
||||||
|
type: "character",
|
||||||
|
importance: 6,
|
||||||
|
createdTime: 2,
|
||||||
|
archived: false,
|
||||||
|
fields: { name: "Alice" },
|
||||||
|
seqRange: [1, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "location-1",
|
||||||
|
type: "location",
|
||||||
|
importance: 5,
|
||||||
|
createdTime: 3,
|
||||||
|
archived: false,
|
||||||
|
fields: { title: "大厅" },
|
||||||
|
seqRange: [1, 1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ fromId: "event-1", toId: "character-1", relation: "mentions" },
|
||||||
|
{ fromId: "event-1", toId: "location-1", relation: "occurs_at" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const sceneSchema = [
|
||||||
|
{ id: "event", label: "事件", alwaysInject: false },
|
||||||
|
{ id: "character", label: "角色", alwaysInject: false },
|
||||||
|
{ id: "location", label: "地点", alwaysInject: false },
|
||||||
|
];
|
||||||
|
const cappedResult = await retrieve({
|
||||||
|
graph: sceneGraph,
|
||||||
|
userMessage: "只看这一个场景",
|
||||||
|
recentMessages: [],
|
||||||
|
embeddingConfig: {},
|
||||||
|
schema: sceneSchema,
|
||||||
|
options: {
|
||||||
|
topK: 3,
|
||||||
|
maxRecallNodes: 1,
|
||||||
|
enableVectorPrefilter: false,
|
||||||
|
enableGraphDiffusion: false,
|
||||||
|
enableLLMRecall: false,
|
||||||
|
enableProbRecall: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(cappedResult.selectedNodeIds.length, 1);
|
||||||
|
|
||||||
console.log("retrieval-config tests passed");
|
console.log("retrieval-config tests passed");
|
||||||
|
|||||||
Reference in New Issue
Block a user