mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
recall: sectioned recentMessages with context/target split for LLM prompt
- prompt-builder.js: add RECALL_TARGET_CONTENT_HEADER, update splitSectionedTranscriptPayloadMessage to recognize recall-specific target header - retriever.js: add buildRecallSectionedTranscript helper, format recentMessages as sectioned transcript with context-review and recall-target headers for prompt building while keeping flat string[] for ranking - p0-regressions.mjs: add testRecallUsesSectionedPromptMessagesForContextAndTarget regression asserting two system messages with correct transcriptSection and headers
This commit is contained in:
@@ -328,10 +328,12 @@ function isPromptMessageArray(value) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXTRACTION_CONTEXT_REVIEW_HEADER =
|
export const EXTRACTION_CONTEXT_REVIEW_HEADER =
|
||||||
"--- 以下是上下文回顾(已提取过),仅供理解剧情 ---";
|
"--- 以下是上下文回顾(已提取过),仅供理解剧情 ---";
|
||||||
const EXTRACTION_TARGET_CONTENT_HEADER =
|
export const EXTRACTION_TARGET_CONTENT_HEADER =
|
||||||
"--- 以下是本次需要提取记忆的新对话内容 ---";
|
"--- 以下是本次需要提取记忆的新对话内容 ---";
|
||||||
|
export const RECALL_TARGET_CONTENT_HEADER =
|
||||||
|
"--- 以下是本次需要召回记忆的新对话内容 ---";
|
||||||
|
|
||||||
function getPromptMessageContextGroup(value) {
|
function getPromptMessageContextGroup(value) {
|
||||||
const descriptor = getPromptMessageLikeDescriptor(value);
|
const descriptor = getPromptMessageLikeDescriptor(value);
|
||||||
@@ -1927,11 +1929,16 @@ function splitSectionedTranscriptPayloadMessage(message = {}) {
|
|||||||
const normalizedRole = normalizeRole(message?.role);
|
const normalizedRole = normalizeRole(message?.role);
|
||||||
const sourceKey = String(message?.sourceKey || "").trim();
|
const sourceKey = String(message?.sourceKey || "").trim();
|
||||||
const content = String(message?.content || "").trim();
|
const content = String(message?.content || "").trim();
|
||||||
|
const targetSectionHeader = content.includes(RECALL_TARGET_CONTENT_HEADER)
|
||||||
|
? RECALL_TARGET_CONTENT_HEADER
|
||||||
|
: content.includes(EXTRACTION_TARGET_CONTENT_HEADER)
|
||||||
|
? EXTRACTION_TARGET_CONTENT_HEADER
|
||||||
|
: "";
|
||||||
if (
|
if (
|
||||||
normalizedRole !== "system" ||
|
normalizedRole !== "system" ||
|
||||||
!["recentMessages", "dialogueText"].includes(sourceKey) ||
|
!["recentMessages", "dialogueText"].includes(sourceKey) ||
|
||||||
!content.includes(EXTRACTION_CONTEXT_REVIEW_HEADER) ||
|
!content.includes(EXTRACTION_CONTEXT_REVIEW_HEADER) ||
|
||||||
!content.includes(EXTRACTION_TARGET_CONTENT_HEADER)
|
!targetSectionHeader
|
||||||
) {
|
) {
|
||||||
return [message];
|
return [message];
|
||||||
}
|
}
|
||||||
@@ -1943,10 +1950,9 @@ function splitSectionedTranscriptPayloadMessage(message = {}) {
|
|||||||
EXTRACTION_CONTEXT_REVIEW_HEADER,
|
EXTRACTION_CONTEXT_REVIEW_HEADER,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
);
|
);
|
||||||
const targetIndex = content.indexOf(
|
const targetIndex = targetSectionHeader
|
||||||
EXTRACTION_TARGET_CONTENT_HEADER,
|
? content.indexOf(targetSectionHeader, searchIndex)
|
||||||
searchIndex,
|
: -1;
|
||||||
);
|
|
||||||
let nextIndex = -1;
|
let nextIndex = -1;
|
||||||
let nextHeader = "";
|
let nextHeader = "";
|
||||||
if (contextIndex >= 0 && (targetIndex < 0 || contextIndex <= targetIndex)) {
|
if (contextIndex >= 0 && (targetIndex < 0 || contextIndex <= targetIndex)) {
|
||||||
@@ -1954,7 +1960,7 @@ function splitSectionedTranscriptPayloadMessage(message = {}) {
|
|||||||
nextHeader = EXTRACTION_CONTEXT_REVIEW_HEADER;
|
nextHeader = EXTRACTION_CONTEXT_REVIEW_HEADER;
|
||||||
} else if (targetIndex >= 0) {
|
} else if (targetIndex >= 0) {
|
||||||
nextIndex = targetIndex;
|
nextIndex = targetIndex;
|
||||||
nextHeader = EXTRACTION_TARGET_CONTENT_HEADER;
|
nextHeader = targetSectionHeader;
|
||||||
}
|
}
|
||||||
if (nextIndex < 0 || !nextHeader) {
|
if (nextIndex < 0 || !nextHeader) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
buildTaskExecutionDebugContext,
|
buildTaskExecutionDebugContext,
|
||||||
buildTaskLlmPayload,
|
buildTaskLlmPayload,
|
||||||
buildTaskPrompt,
|
buildTaskPrompt,
|
||||||
|
EXTRACTION_CONTEXT_REVIEW_HEADER,
|
||||||
|
RECALL_TARGET_CONTENT_HEADER,
|
||||||
} from "../prompting/prompt-builder.js";
|
} from "../prompting/prompt-builder.js";
|
||||||
import {
|
import {
|
||||||
applyCooccurrenceBoost,
|
applyCooccurrenceBoost,
|
||||||
@@ -93,6 +95,32 @@ function resolveTaskLlmSystemPrompt(promptPayload, fallbackSystemPrompt = "") {
|
|||||||
return String(promptPayload?.systemPrompt || fallbackSystemPrompt || "");
|
return String(promptPayload?.systemPrompt || fallbackSystemPrompt || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRecallSectionedTranscript(recentMessages = []) {
|
||||||
|
const lines = (Array.isArray(recentMessages) ? recentMessages : [])
|
||||||
|
.map((line) => String(line || "").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetLines = [lines[lines.length - 1]].filter(Boolean);
|
||||||
|
const contextLines = lines.slice(0, -1).filter(Boolean);
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
if (contextLines.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
`${EXTRACTION_CONTEXT_REVIEW_HEADER}\n\n${contextLines.join("\n---\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (targetLines.length > 0) {
|
||||||
|
sections.push(
|
||||||
|
`${RECALL_TARGET_CONTENT_HEADER}\n\n${targetLines.join("\n---\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
function buildRecallFallbackReason(llmResult) {
|
function buildRecallFallbackReason(llmResult) {
|
||||||
const failureType = String(llmResult?.errorType || "").trim();
|
const failureType = String(llmResult?.errorType || "").trim();
|
||||||
const failureReason = String(llmResult?.failureReason || "").trim();
|
const failureReason = String(llmResult?.failureReason || "").trim();
|
||||||
@@ -2153,6 +2181,8 @@ async function llmRecall(
|
|||||||
) {
|
) {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
const contextStr = recentMessages.join("\n---\n");
|
const contextStr = recentMessages.join("\n---\n");
|
||||||
|
const sectionedContextStr =
|
||||||
|
buildRecallSectionedTranscript(recentMessages) || contextStr;
|
||||||
const sceneOwnerCandidateText = buildSceneOwnerCandidateText(sceneOwnerCandidates);
|
const sceneOwnerCandidateText = buildSceneOwnerCandidateText(sceneOwnerCandidates);
|
||||||
const {
|
const {
|
||||||
candidateKeyToNodeId,
|
candidateKeyToNodeId,
|
||||||
@@ -2177,7 +2207,7 @@ async function llmRecall(
|
|||||||
|
|
||||||
const recallPromptBuild = await buildTaskPrompt(settings, "recall", {
|
const recallPromptBuild = await buildTaskPrompt(settings, "recall", {
|
||||||
taskName: "recall",
|
taskName: "recall",
|
||||||
recentMessages: contextStr || "(无)",
|
recentMessages: sectionedContextStr || "(无)",
|
||||||
userMessage,
|
userMessage,
|
||||||
candidateNodes: candidateDescriptions,
|
candidateNodes: candidateDescriptions,
|
||||||
candidateText: candidateDescriptions,
|
candidateText: candidateDescriptions,
|
||||||
@@ -2212,7 +2242,7 @@ async function llmRecall(
|
|||||||
activeStoryTimeLabel || "(未确定)",
|
activeStoryTimeLabel || "(未确定)",
|
||||||
"",
|
"",
|
||||||
"## 最近对话上下文",
|
"## 最近对话上下文",
|
||||||
contextStr || "(无)",
|
sectionedContextStr || contextStr || "(无)",
|
||||||
"",
|
"",
|
||||||
"## 用户最新输入",
|
"## 用户最新输入",
|
||||||
userMessage,
|
userMessage,
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ const {
|
|||||||
generateSynopsis,
|
generateSynopsis,
|
||||||
} = await import("../maintenance/extractor.js");
|
} = await import("../maintenance/extractor.js");
|
||||||
const { consolidateMemories } = await import("../maintenance/consolidator.js");
|
const { consolidateMemories } = await import("../maintenance/consolidator.js");
|
||||||
|
const { retrieve } = await import("../retrieval/retriever.js");
|
||||||
const {
|
const {
|
||||||
createBatchJournalEntry,
|
createBatchJournalEntry,
|
||||||
buildReverseJournalRecoveryPlan,
|
buildReverseJournalRecoveryPlan,
|
||||||
@@ -169,6 +170,10 @@ const {
|
|||||||
rollbackBatch,
|
rollbackBatch,
|
||||||
} = await import("../runtime/runtime-state.js");
|
} = await import("../runtime/runtime-state.js");
|
||||||
const { createDefaultTaskProfiles } = await import("../prompting/prompt-profiles.js");
|
const { createDefaultTaskProfiles } = await import("../prompting/prompt-profiles.js");
|
||||||
|
const {
|
||||||
|
EXTRACTION_CONTEXT_REVIEW_HEADER,
|
||||||
|
RECALL_TARGET_CONTENT_HEADER,
|
||||||
|
} = await import("../prompting/prompt-builder.js");
|
||||||
const extensionsApi = await import("../../../../extensions.js");
|
const extensionsApi = await import("../../../../extensions.js");
|
||||||
const llm = await import("../llm/llm.js");
|
const llm = await import("../llm/llm.js");
|
||||||
const embedding = await import("../vector/embedding.js");
|
const embedding = await import("../vector/embedding.js");
|
||||||
@@ -6238,6 +6243,84 @@ async function testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testRecallUsesSectionedPromptMessagesForContextAndTarget() {
|
||||||
|
const graph = createEmptyGraph();
|
||||||
|
addNode(graph, makeEvent(1, "仓库争执"));
|
||||||
|
addNode(graph, makeEvent(2, "走廊追问"));
|
||||||
|
|
||||||
|
const captured = [];
|
||||||
|
const restoreOverrides = pushTestOverrides({
|
||||||
|
llm: {
|
||||||
|
async callLLMForJSON(params = {}) {
|
||||||
|
captured.push(params);
|
||||||
|
return {
|
||||||
|
selected_keys: ["R1"],
|
||||||
|
reason: "R1: 与当前追问直接相关",
|
||||||
|
active_owner_keys: [],
|
||||||
|
active_owner_scores: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await retrieve({
|
||||||
|
graph,
|
||||||
|
userMessage: "她为什么突然改口?",
|
||||||
|
recentMessages: [
|
||||||
|
"[assistant]: 她先否认自己去过仓库。",
|
||||||
|
"[user]: 我记得她当时很紧张。",
|
||||||
|
"[user]: 她为什么突然改口?",
|
||||||
|
],
|
||||||
|
embeddingConfig: null,
|
||||||
|
schema,
|
||||||
|
settings: {
|
||||||
|
taskProfilesVersion: 3,
|
||||||
|
taskProfiles: createDefaultTaskProfiles(),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
topK: 4,
|
||||||
|
maxRecallNodes: 2,
|
||||||
|
enableLLMRecall: true,
|
||||||
|
enableVectorPrefilter: false,
|
||||||
|
enableGraphDiffusion: false,
|
||||||
|
llmCandidatePool: 2,
|
||||||
|
enableScopedMemory: false,
|
||||||
|
enablePovMemory: false,
|
||||||
|
enableRegionScopedObjective: false,
|
||||||
|
enableCognitiveMemory: false,
|
||||||
|
enableSpatialAdjacency: false,
|
||||||
|
enableStoryTimeline: false,
|
||||||
|
injectStoryTimeLabel: false,
|
||||||
|
injectUserPovMemory: false,
|
||||||
|
injectObjectiveGlobalMemory: false,
|
||||||
|
enableContextQueryBlend: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(Array.isArray(result?.selectedNodeIds));
|
||||||
|
assert.equal(captured.length, 1);
|
||||||
|
const promptMessages = Array.isArray(captured[0].promptMessages)
|
||||||
|
? captured[0].promptMessages
|
||||||
|
: [];
|
||||||
|
const recentMessageSections = promptMessages.filter(
|
||||||
|
(message) => message.sourceKey === "recentMessages",
|
||||||
|
);
|
||||||
|
assert.equal(recentMessageSections.length, 2);
|
||||||
|
assert.equal(recentMessageSections[0].role, "system");
|
||||||
|
assert.equal(recentMessageSections[1].role, "system");
|
||||||
|
assert.equal(recentMessageSections[0].transcriptSection, "context");
|
||||||
|
assert.equal(recentMessageSections[1].transcriptSection, "target");
|
||||||
|
assert.match(recentMessageSections[0].content, new RegExp(EXTRACTION_CONTEXT_REVIEW_HEADER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||||
|
assert.match(recentMessageSections[1].content, new RegExp(RECALL_TARGET_CONTENT_HEADER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||||
|
assert.match(recentMessageSections[0].content, /她先否认自己去过仓库/);
|
||||||
|
assert.match(recentMessageSections[0].content, /我记得她当时很紧张/);
|
||||||
|
assert.match(recentMessageSections[1].content, /她为什么突然改口/);
|
||||||
|
} finally {
|
||||||
|
restoreOverrides();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt() {
|
async function testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt() {
|
||||||
const graph = createEmptyGraph();
|
const graph = createEmptyGraph();
|
||||||
addNode(
|
addNode(
|
||||||
@@ -6736,6 +6819,7 @@ await testLlmDebugSnapshotRedactsSecretsBeforeStorage();
|
|||||||
await testEmbeddingUsesConfigTimeoutInsteadOfDefault();
|
await testEmbeddingUsesConfigTimeoutInsteadOfDefault();
|
||||||
await testLlmOutputRegexCleansResponseBeforeJsonParse();
|
await testLlmOutputRegexCleansResponseBeforeJsonParse();
|
||||||
await testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt();
|
await testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt();
|
||||||
|
await testRecallUsesSectionedPromptMessagesForContextAndTarget();
|
||||||
await testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt();
|
await testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt();
|
||||||
await testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan();
|
await testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan();
|
||||||
await testManualCompressUsesForcedCompressionAndPersistsRealMutation();
|
await testManualCompressUsesForcedCompressionAndPersistsRealMutation();
|
||||||
|
|||||||
Reference in New Issue
Block a user