feat(recall): add diffusion stage and improve retrieval metrics
This commit is contained in:
@@ -43,18 +43,22 @@ function canNotifyRecallFail() {
|
||||
// 预算常量
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MAIN_BUDGET_MAX = 10000;
|
||||
const DISTANT_EVIDENCE_MAX = 2500;
|
||||
const RECENT_EVIDENCE_MAX = 5000;
|
||||
const TOTAL_BUDGET_MAX = 15000;
|
||||
const SHARED_POOL_MAX = 10000;
|
||||
const CONSTRAINT_MAX = 2000;
|
||||
const ARCS_MAX = 1500;
|
||||
const EVENT_BUDGET_MAX = 5000;
|
||||
const RELATED_EVENT_MAX = 1000;
|
||||
const SUMMARIZED_EVIDENCE_MAX = 1500;
|
||||
const UNSUMMARIZED_EVIDENCE_MAX = 5000;
|
||||
const TOP_N_STAR = 5;
|
||||
|
||||
// 邻近补挂:未被事件消费的 L0,如果距最近事件 ≤ 此值则补挂
|
||||
const NEARBY_FLOOR_TOLERANCE = 2;
|
||||
|
||||
// L0 显示文本:分号拼接 vs 多行模式的阈值
|
||||
const L0_JOINED_MAX_LENGTH = 120;
|
||||
// 背景证据实体过滤旁通阈值(与事件过滤策略一致)
|
||||
const EVIDENCE_ENTITY_BYPASS_SIM = 0.80;
|
||||
// 背景证据:无实体匹配时保留的最低相似度(与 recall.js CONFIG.EVENT_ENTITY_BYPASS_SIM 保持一致)
|
||||
const EVIDENCE_ENTITY_BYPASS_SIM = 0.70;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 工具函数
|
||||
@@ -157,7 +161,7 @@ function collectL0Entities(l0) {
|
||||
* 背景证据是否保留(按焦点实体过滤)
|
||||
* 规则:
|
||||
* 1) 无焦点实体:保留
|
||||
* 2) similarity >= 0.80:保留(旁通)
|
||||
* 2) similarity >= 0.70:保留(旁通)
|
||||
* 3) who/edges 命中焦点实体:保留
|
||||
* 4) 兼容旧数据:semantic 文本包含焦点实体:保留
|
||||
* 否则过滤。
|
||||
@@ -361,7 +365,7 @@ function formatArcLine(arc) {
|
||||
*/
|
||||
function buildL0DisplayText(l0) {
|
||||
const atom = l0.atom || {};
|
||||
return String(atom.scene || atom.semantic || l0.text || '').trim() || '(未知锚点)';
|
||||
return String(atom.semantic || l0.text || '').trim() || '(未知锚点)';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,15 +478,15 @@ function buildEvidenceGroup(floor, l0AtomsForFloor, l1ByFloor) {
|
||||
* 格式化一个证据组为文本行数组
|
||||
*
|
||||
* 短行模式(拼接后 ≤ 120 字):
|
||||
* › #500 [📌] 薇薇保持跪趴姿势;薇薇展示细节;薇薇与蓝袖之间:被审视
|
||||
* ┌ #499 [蓝袖] ...
|
||||
* › #500 [📌] 小林整理会议记录;小周补充行动项;两人确认下周安排
|
||||
* ┌ #499 [小周] ...
|
||||
* › #500 [角色] ...
|
||||
*
|
||||
* 长行模式(拼接后 > 120 字):
|
||||
* › #500 [📌] 薇薇保持跪趴姿势 在书房
|
||||
* │ 薇薇展示肛周细节 在书房
|
||||
* │ 薇薇与蓝袖之间:身体被审视 在书房
|
||||
* ┌ #499 [蓝袖] ...
|
||||
* › #500 [📌] 小林在图书馆归档旧资料
|
||||
* │ 小周核对目录并修正编号
|
||||
* │ 两人讨论借阅规则并更新说明
|
||||
* ┌ #499 [小周] ...
|
||||
* › #500 [角色] ...
|
||||
*
|
||||
* @param {EvidenceGroup} group - 证据组
|
||||
@@ -705,7 +709,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
const T_Start = performance.now();
|
||||
|
||||
const data = store.json || {};
|
||||
const total = { used: 0, max: MAIN_BUDGET_MAX };
|
||||
const total = { used: 0, max: SHARED_POOL_MAX };
|
||||
|
||||
// 从 recallResult 解构
|
||||
const l0Selected = recallResult?.l0Selected || [];
|
||||
@@ -723,7 +727,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
|
||||
// 注入统计
|
||||
const injectionStats = {
|
||||
budget: { max: TOTAL_BUDGET_MAX, used: 0 },
|
||||
budget: { max: SHARED_POOL_MAX + UNSUMMARIZED_EVIDENCE_MAX, used: 0 },
|
||||
constraint: { count: 0, tokens: 0, filtered: 0 },
|
||||
arc: { count: 0, tokens: 0 },
|
||||
event: { selected: 0, tokens: 0 },
|
||||
@@ -817,6 +821,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
const eventHits = (recallResult?.events || []).filter(e => e?.event?.summary);
|
||||
|
||||
const candidates = [...eventHits].sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
|
||||
const eventBudget = { used: 0, max: Math.min(EVENT_BUDGET_MAX, total.max - total.used) };
|
||||
const relatedBudget = { used: 0, max: RELATED_EVENT_MAX };
|
||||
|
||||
const selectedDirect = [];
|
||||
const selectedRelated = [];
|
||||
@@ -825,8 +831,10 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
const e = candidates[candidateRank];
|
||||
|
||||
if (total.used >= total.max) break;
|
||||
if (eventBudget.used >= eventBudget.max) break;
|
||||
|
||||
const isDirect = e._recallType === "DIRECT";
|
||||
if (!isDirect && relatedBudget.used >= relatedBudget.max) continue;
|
||||
|
||||
// 收集该事件范围内的 EvidenceGroup(per-floor)
|
||||
const evidenceGroups = collectEvidenceGroupsForEvent(e.event, l0Selected, l1ByFloor, usedL0Ids);
|
||||
@@ -873,6 +881,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
injectionStats.event.selected++;
|
||||
injectionStats.event.tokens += costNoEvidence;
|
||||
total.used += costNoEvidence;
|
||||
eventBudget.used += costNoEvidence;
|
||||
if (!isDirect) relatedBudget.used += costNoEvidence;
|
||||
|
||||
eventDetails.list.push({
|
||||
title: e.event?.title || e.event?.id,
|
||||
@@ -912,6 +922,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
injectionStats.evidence.l0InEvents += l0Count;
|
||||
injectionStats.evidence.l1InEvents += l1FloorCount;
|
||||
total.used += cost;
|
||||
eventBudget.used += cost;
|
||||
if (!isDirect) relatedBudget.used += cost;
|
||||
|
||||
eventDetails.list.push({
|
||||
title: e.event?.title || e.event?.id,
|
||||
@@ -928,6 +940,73 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
selectedDirect.sort((a, b) => getEventSortKey(a.event) - getEventSortKey(b.event));
|
||||
selectedRelated.sort((a, b) => getEventSortKey(a.event) - getEventSortKey(b.event));
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 邻近补挂:未被事件消费的 L0,距最近已选事件 ≤ 2 楼则补挂
|
||||
// 每个 L0 只挂最近的一个事件,不扩展事件范围,不产生重叠
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
const allSelectedItems = [...selectedDirect, ...selectedRelated];
|
||||
const nearbyByItem = new Map();
|
||||
|
||||
for (const l0 of l0Selected) {
|
||||
if (usedL0Ids.has(l0.id)) continue;
|
||||
|
||||
let bestItem = null;
|
||||
let bestDistance = Infinity;
|
||||
|
||||
for (const item of allSelectedItems) {
|
||||
const range = parseFloorRange(item.event?.summary);
|
||||
if (!range) continue;
|
||||
|
||||
let distance;
|
||||
if (l0.floor < range.start) distance = range.start - l0.floor;
|
||||
else if (l0.floor > range.end) distance = l0.floor - range.end;
|
||||
else continue;
|
||||
|
||||
if (distance <= NEARBY_FLOOR_TOLERANCE && distance <= bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestItem) {
|
||||
if (!nearbyByItem.has(bestItem)) nearbyByItem.set(bestItem, []);
|
||||
nearbyByItem.get(bestItem).push(l0);
|
||||
usedL0Ids.add(l0.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [item, nearbyL0s] of nearbyByItem) {
|
||||
const floorMap = groupL0ByFloor(nearbyL0s);
|
||||
|
||||
for (const [floor, l0s] of floorMap) {
|
||||
const group = buildEvidenceGroup(floor, l0s, l1ByFloor);
|
||||
item.evidenceGroups.push(group);
|
||||
}
|
||||
|
||||
item.evidenceGroups.sort((a, b) => a.floor - b.floor);
|
||||
|
||||
const newText = formatEventWithEvidence(item.event, 0, item.evidenceGroups, causalById);
|
||||
const newTokens = estimateTokens(newText);
|
||||
const delta = newTokens - item.tokens;
|
||||
|
||||
if (total.used + delta > total.max) {
|
||||
for (const l0 of nearbyL0s) usedL0Ids.delete(l0.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
total.used += delta;
|
||||
eventBudget.used += delta;
|
||||
|
||||
const isDirect = selectedDirect.includes(item);
|
||||
if (!isDirect) relatedBudget.used += delta;
|
||||
|
||||
injectionStats.evidence.l0InEvents += nearbyL0s.length;
|
||||
item.text = newText;
|
||||
item.tokens = newTokens;
|
||||
injectionStats.event.tokens += delta;
|
||||
}
|
||||
|
||||
// 重新编号 + 星标
|
||||
const directEventTexts = selectedDirect.map((it, i) => {
|
||||
const numbered = renumberEventText(it.text, i + 1);
|
||||
@@ -964,7 +1043,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
const distantL0 = remainingL0.filter(l0 => l0.floor <= lastSummarized);
|
||||
|
||||
if (distantL0.length && total.used < total.max) {
|
||||
const distantBudget = { used: 0, max: Math.min(DISTANT_EVIDENCE_MAX, total.max - total.used) };
|
||||
const distantBudget = { used: 0, max: Math.min(SUMMARIZED_EVIDENCE_MAX, total.max - total.used) };
|
||||
|
||||
// 按楼层排序(时间顺序)后分组
|
||||
distantL0.sort((a, b) => a.floor - b.floor);
|
||||
@@ -1006,7 +1085,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
.filter(l0 => l0.floor >= recentStart && l0.floor <= recentEnd);
|
||||
|
||||
if (recentL0.length) {
|
||||
const recentBudget = { used: 0, max: RECENT_EVIDENCE_MAX };
|
||||
const recentBudget = { used: 0, max: UNSUMMARIZED_EVIDENCE_MAX };
|
||||
|
||||
// 按楼层排序后分组
|
||||
recentL0.sort((a, b) => a.floor - b.floor);
|
||||
@@ -1051,10 +1130,10 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
sections.push(`[好像有关的事] 听说过或有点模糊\n\n${assembled.relatedEvents.lines.join("\n\n")}`);
|
||||
}
|
||||
if (assembled.distantEvidence.lines.length) {
|
||||
sections.push(`[更早以前] 记忆里残留的老画面\n${assembled.distantEvidence.lines.join("\n")}`);
|
||||
sections.push(`[零散记忆] 没归入事件的片段\n${assembled.distantEvidence.lines.join("\n")}`);
|
||||
}
|
||||
if (assembled.recentEvidence.lines.length) {
|
||||
sections.push(`[近期] 清晰但还没整理\n${assembled.recentEvidence.lines.join("\n")}`);
|
||||
sections.push(`[新鲜记忆] 还没总结的部分\n${assembled.recentEvidence.lines.join("\n")}`);
|
||||
}
|
||||
if (assembled.arcs.lines.length) {
|
||||
sections.push(`[这些人] 他们的弧光\n${assembled.arcs.lines.join("\n")}`);
|
||||
@@ -1085,9 +1164,11 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
metrics.formatting.time = Math.round(performance.now() - T_Format_Start);
|
||||
metrics.timing.formatting = metrics.formatting.time;
|
||||
|
||||
metrics.budget.total = total.used + (assembled.recentEvidence.tokens || 0);
|
||||
metrics.budget.limit = TOTAL_BUDGET_MAX;
|
||||
metrics.budget.utilization = Math.round(metrics.budget.total / TOTAL_BUDGET_MAX * 100);
|
||||
const effectiveTotal = total.used + (assembled.recentEvidence.tokens || 0);
|
||||
const effectiveLimit = SHARED_POOL_MAX + UNSUMMARIZED_EVIDENCE_MAX;
|
||||
metrics.budget.total = effectiveTotal;
|
||||
metrics.budget.limit = effectiveLimit;
|
||||
metrics.budget.utilization = Math.round(effectiveTotal / effectiveLimit * 100);
|
||||
metrics.budget.breakdown = {
|
||||
constraints: assembled.constraints.tokens,
|
||||
events: injectionStats.event.tokens,
|
||||
|
||||
Reference in New Issue
Block a user