feat(summary): update prompt display, metrics lexical gate, and edge sanitization
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Story Summary - Recall Engine (v8 - Weighted Query Vectors + Floor Aggregation)
|
||||
// Story Summary - Recall Engine (v9 - Dense-Gated Lexical + Entity Bypass Tuning)
|
||||
//
|
||||
// 命名规范:
|
||||
// - 存储层用 L0/L1/L2/L3(StateAtom/Chunk/Event/Fact)
|
||||
// - 召回层用语义名称:anchor/evidence/event/constraint
|
||||
//
|
||||
// v7 → v8 变更:
|
||||
// - Query 取 3 条消息(对齐 L0 对结构),加权向量合成替代文本拼接
|
||||
// - R1 权重 [0.15, 0.30, 0.55](焦点 > 近上下文 > 远上下文)
|
||||
// - R2 复用 R1 向量 + embed hints 1 条,权重 [0.10, 0.20, 0.45, 0.25]
|
||||
// - Dense floor 聚合:max → maxSim×0.6 + meanSim×0.4
|
||||
// - Lexical floor 聚合:max → maxScore × (1 + 0.3×log₂(hitCount))
|
||||
// v8 → v9 变更:
|
||||
// - recallEvents() 返回 { events, vectorMap },暴露 event 向量映射
|
||||
// - Lexical Event 合并前验 dense similarity ≥ 0.50(CONFIG.LEXICAL_EVENT_DENSE_MIN)
|
||||
// - Lexical Floor 进入融合前验 dense similarity ≥ 0.50(CONFIG.LEXICAL_FLOOR_DENSE_MIN)
|
||||
// - Entity Bypass 阈值 0.85 → 0.80(CONFIG.EVENT_ENTITY_BYPASS_SIM)
|
||||
// - metrics 新增 lexical.eventFilteredByDense / lexical.floorFilteredByDense
|
||||
//
|
||||
// 架构:
|
||||
// 阶段 1: Query Build(确定性,无 LLM)
|
||||
// 阶段 2: Round 1 Dense Retrieval(batch embed 3 段 → 加权平均)
|
||||
// 阶段 3: Query Refinement(用已命中记忆产出 hints 段)
|
||||
// 阶段 4: Round 2 Dense Retrieval(复用 R1 vec + embed hints → 加权平均)
|
||||
// 阶段 5: Lexical Retrieval
|
||||
// 阶段 5: Lexical Retrieval + Dense-Gated Event Merge
|
||||
// 阶段 6: Floor W-RRF Fusion + Rerank + L1 配对
|
||||
// 阶段 7: L1 配对组装(L0 → top-1 AI L1 + top-1 USER L1)
|
||||
// 阶段 8: Causation Trace
|
||||
@@ -47,9 +47,9 @@ const MODULE_ID = 'recall';
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const CONFIG = {
|
||||
// 窗口:取 3 条消息(对齐 L0 USER+AI 对结构)
|
||||
// 窗口:取 3 条消息(对齐 L0 对结构),pending 存在时取 2 条上下文
|
||||
LAST_MESSAGES_K: 3,
|
||||
LAST_MESSAGES_K_WITH_PENDING: 2, // pending 存在时只取 2 条上下文,避免形成 4 段
|
||||
LAST_MESSAGES_K_WITH_PENDING: 2,
|
||||
|
||||
// Anchor (L0 StateAtoms)
|
||||
ANCHOR_MIN_SIMILARITY: 0.58,
|
||||
@@ -59,6 +59,11 @@ const CONFIG = {
|
||||
EVENT_SELECT_MAX: 50,
|
||||
EVENT_MIN_SIMILARITY: 0.55,
|
||||
EVENT_MMR_LAMBDA: 0.72,
|
||||
EVENT_ENTITY_BYPASS_SIM: 0.80,
|
||||
|
||||
// Lexical Dense 门槛
|
||||
LEXICAL_EVENT_DENSE_MIN: 0.50,
|
||||
LEXICAL_FLOOR_DENSE_MIN: 0.50,
|
||||
|
||||
// W-RRF 融合(L0-only)
|
||||
RRF_K: 60,
|
||||
@@ -86,9 +91,6 @@ const CONFIG = {
|
||||
// 工具函数
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 计算余弦相似度
|
||||
*/
|
||||
function cosineSimilarity(a, b) {
|
||||
if (!a?.length || !b?.length || a.length !== b.length) return 0;
|
||||
let dot = 0, nA = 0, nB = 0;
|
||||
@@ -100,9 +102,6 @@ function cosineSimilarity(a, b) {
|
||||
return nA && nB ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化字符串
|
||||
*/
|
||||
function normalize(s) {
|
||||
return String(s || '')
|
||||
.normalize('NFKC')
|
||||
@@ -111,9 +110,6 @@ function normalize(s) {
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近消息
|
||||
*/
|
||||
function getLastMessages(chat, count = 3, excludeLastAi = false) {
|
||||
if (!chat?.length) return [];
|
||||
let messages = [...chat];
|
||||
@@ -127,13 +123,6 @@ function getLastMessages(chat, count = 3, excludeLastAi = false) {
|
||||
// 加权向量工具
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 多向量加权平均
|
||||
*
|
||||
* @param {number[][]} vectors - 向量数组
|
||||
* @param {number[]} weights - 归一化后的权重(sum = 1)
|
||||
* @returns {number[]|null}
|
||||
*/
|
||||
function weightedAverageVectors(vectors, weights) {
|
||||
if (!vectors?.length || !weights?.length || vectors.length !== weights.length) return null;
|
||||
|
||||
@@ -152,14 +141,6 @@ function weightedAverageVectors(vectors, weights) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对归一化权重做“目标位最小占比”硬保底
|
||||
*
|
||||
* @param {number[]} weights - 已归一化权重(sum≈1)
|
||||
* @param {number} targetIdx - 目标位置(focus 段索引)
|
||||
* @param {number} minWeight - 最小占比(0~1)
|
||||
* @returns {number[]} 调整后的归一化权重
|
||||
*/
|
||||
function clampMinNormalizedWeight(weights, targetIdx, minWeight) {
|
||||
if (!weights?.length) return [];
|
||||
if (targetIdx < 0 || targetIdx >= weights.length) return weights;
|
||||
@@ -178,18 +159,11 @@ function clampMinNormalizedWeight(weights, targetIdx, minWeight) {
|
||||
const scale = remain / otherSum;
|
||||
|
||||
const out = weights.map((w, i) => (i === targetIdx ? minWeight : w * scale));
|
||||
// 数值稳定性:消除浮点误差
|
||||
const drift = 1 - out.reduce((a, b) => a + b, 0);
|
||||
out[targetIdx] += drift;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 R1 段权重(baseWeight × lengthFactor,归一化)
|
||||
*
|
||||
* @param {object[]} segments - QuerySegment[]
|
||||
* @returns {number[]} 归一化后的权重
|
||||
*/
|
||||
function computeSegmentWeights(segments) {
|
||||
if (!segments?.length) return [];
|
||||
|
||||
@@ -199,22 +173,13 @@ function computeSegmentWeights(segments) {
|
||||
? segments.map(() => 1 / segments.length)
|
||||
: adjusted.map(w => w / sum);
|
||||
|
||||
// focus 段始终在末尾
|
||||
const focusIdx = segments.length - 1;
|
||||
return clampMinNormalizedWeight(normalized, focusIdx, FOCUS_MIN_NORMALIZED_WEIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 R2 权重(R1 段用 R2 基础权重 + hints 段,归一化)
|
||||
*
|
||||
* @param {object[]} segments - QuerySegment[](与 R1 相同的段)
|
||||
* @param {object|null} hintsSegment - { text, baseWeight, charCount }
|
||||
* @returns {number[]} 归一化后的权重(长度 = segments.length + (hints ? 1 : 0))
|
||||
*/
|
||||
function computeR2Weights(segments, hintsSegment) {
|
||||
if (!segments?.length) return [];
|
||||
|
||||
// 为 R1 段分配 R2 基础权重(尾部对齐)
|
||||
const contextCount = segments.length - 1;
|
||||
const r2Base = [];
|
||||
for (let i = 0; i < contextCount; i++) {
|
||||
@@ -223,21 +188,17 @@ function computeR2Weights(segments, hintsSegment) {
|
||||
}
|
||||
r2Base.push(FOCUS_BASE_WEIGHT_R2);
|
||||
|
||||
// 应用 lengthFactor
|
||||
const adjusted = r2Base.map((w, i) => w * computeLengthFactor(segments[i].charCount));
|
||||
|
||||
// 追加 hints
|
||||
if (hintsSegment) {
|
||||
adjusted.push(hintsSegment.baseWeight * computeLengthFactor(hintsSegment.charCount));
|
||||
}
|
||||
|
||||
// 归一化
|
||||
const sum = adjusted.reduce((a, b) => a + b, 0);
|
||||
const normalized = sum <= 0
|
||||
? adjusted.map(() => 1 / adjusted.length)
|
||||
: adjusted.map(w => w / sum);
|
||||
|
||||
// R2 中 focus 位置固定为“segments 最后一个”
|
||||
const focusIdx = segments.length - 1;
|
||||
return clampMinNormalizedWeight(normalized, focusIdx, FOCUS_MIN_NORMALIZED_WEIGHT);
|
||||
}
|
||||
@@ -342,26 +303,27 @@ async function recallAnchors(queryVector, vectorConfig, metrics) {
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// [Events] L2 Events 检索
|
||||
// 返回 { events, vectorMap }
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function recallEvents(queryVector, allEvents, vectorConfig, focusEntities, metrics) {
|
||||
const { chatId } = getContext();
|
||||
if (!chatId || !queryVector?.length || !allEvents?.length) {
|
||||
return [];
|
||||
return { events: [], vectorMap: new Map() };
|
||||
}
|
||||
|
||||
const meta = await getMeta(chatId);
|
||||
const fp = getEngineFingerprint(vectorConfig);
|
||||
if (meta.fingerprint && meta.fingerprint !== fp) {
|
||||
xbLog.warn(MODULE_ID, 'Event fingerprint 不匹配');
|
||||
return [];
|
||||
return { events: [], vectorMap: new Map() };
|
||||
}
|
||||
|
||||
const eventVectors = await getAllEventVectors(chatId);
|
||||
const vectorMap = new Map(eventVectors.map(v => [v.eventId, v.vector]));
|
||||
|
||||
if (!vectorMap.size) {
|
||||
return [];
|
||||
return { events: [], vectorMap };
|
||||
}
|
||||
|
||||
const focusSet = new Set((focusEntities || []).map(normalize));
|
||||
@@ -400,7 +362,7 @@ async function recallEvents(queryVector, allEvents, vectorConfig, focusEntities,
|
||||
const beforeFilter = candidates.length;
|
||||
|
||||
candidates = candidates.filter(c => {
|
||||
if (c.similarity >= 0.85) return true;
|
||||
if (c.similarity >= CONFIG.EVENT_ENTITY_BYPASS_SIM) return true;
|
||||
return c._hasEntityMatch;
|
||||
});
|
||||
|
||||
@@ -444,7 +406,7 @@ async function recallEvents(queryVector, allEvents, vectorConfig, focusEntities,
|
||||
metrics.event.similarityDistribution = calcSimilarityStats(results.map(r => r.similarity));
|
||||
}
|
||||
|
||||
return results;
|
||||
return { events: results, vectorMap };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -576,12 +538,14 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 6b. Lexical floor rank(密度加成:maxScore × (1 + 0.3×log₂(hitCount)))
|
||||
// 6b. Lexical floor rank(密度加成 + Dense 门槛过滤)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const atomFloorSet = new Set(getStateAtoms().map(a => a.floor));
|
||||
|
||||
const lexFloorAgg = new Map();
|
||||
let lexFloorFilteredByDense = 0;
|
||||
|
||||
for (const { chunkId, score } of (lexicalResult?.chunkScores || [])) {
|
||||
const match = chunkId?.match(/^c-(\d+)-/);
|
||||
if (!match) continue;
|
||||
@@ -600,6 +564,13 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
// 预过滤:必须有 L0 atoms
|
||||
if (!atomFloorSet.has(floor)) continue;
|
||||
|
||||
// Dense 门槛:lexical floor 必须有最低 dense 相关性
|
||||
const denseInfo = denseFloorAgg.get(floor);
|
||||
if (!denseInfo || denseInfo.maxSim < CONFIG.LEXICAL_FLOOR_DENSE_MIN) {
|
||||
lexFloorFilteredByDense++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const cur = lexFloorAgg.get(floor);
|
||||
if (!cur) {
|
||||
lexFloorAgg.set(floor, { maxScore: score, hitCount: 1 });
|
||||
@@ -616,6 +587,10 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (metrics) {
|
||||
metrics.lexical.floorFilteredByDense = lexFloorFilteredByDense;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 6c. Floor W-RRF 融合
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -756,7 +731,6 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
atomsByFloor.get(atom.floor).push(atom);
|
||||
}
|
||||
|
||||
// 重建 denseFloorMap 以获取每层 max cosine(用于 L0 similarity 标注)
|
||||
const denseFloorMaxMap = new Map();
|
||||
for (const a of (anchorHits || [])) {
|
||||
const cur = denseFloorMaxMap.get(a.floor) || 0;
|
||||
@@ -772,7 +746,6 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
const rerankScore = item._rerankScore || 0;
|
||||
const denseSim = denseFloorMaxMap.get(floor) || 0;
|
||||
|
||||
// 收集该 floor 所有 L0 atoms
|
||||
const floorAtoms = atomsByFloor.get(floor) || [];
|
||||
for (const atom of floorAtoms) {
|
||||
l0Selected.push({
|
||||
@@ -786,7 +759,6 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
});
|
||||
}
|
||||
|
||||
// L1 top-1 配对(cosine 最高)
|
||||
const aiChunks = l1ScoredByFloor.get(floor) || [];
|
||||
const userFloor = floor - 1;
|
||||
const userChunks = (userFloor >= 0 && chat?.[userFloor]?.is_user)
|
||||
@@ -804,10 +776,6 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
l1ByFloor.set(floor, { aiTop1, userTop1 });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 6h. Metrics
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
if (metrics) {
|
||||
metrics.evidence.floorsSelected = reranked.length;
|
||||
metrics.evidence.l0Collected = l0Selected.length;
|
||||
@@ -827,7 +795,7 @@ async function locateAndPullEvidence(anchorHits, queryVector, rerankQuery, lexic
|
||||
}
|
||||
|
||||
xbLog.info(MODULE_ID,
|
||||
`Evidence: ${denseFloorRank.length} dense floors + ${lexFloorRank.length} lex floors → fusion=${fusedFloors.length} → rerank=${reranked.length} floors → L0=${l0Selected.length} L1 attached=${metrics?.evidence?.l1Attached || 0} (${totalTime}ms)`
|
||||
`Evidence: ${denseFloorRank.length} dense floors + ${lexFloorRank.length} lex floors (${lexFloorFilteredByDense} lex filtered by dense) → fusion=${fusedFloors.length} → rerank=${reranked.length} floors → L0=${l0Selected.length} L1 attached=${metrics?.evidence?.l1Attached || 0} (${totalTime}ms)`
|
||||
);
|
||||
|
||||
return { l0Selected, l1ByFloor };
|
||||
@@ -1031,7 +999,7 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) {
|
||||
const r1AnchorTime = Math.round(performance.now() - T_R1_Anchor_Start);
|
||||
|
||||
const T_R1_Event_Start = performance.now();
|
||||
const eventHits_v0 = await recallEvents(queryVector_v0, allEvents, vectorConfig, bundle.focusEntities, null);
|
||||
const { events: eventHits_v0 } = await recallEvents(queryVector_v0, allEvents, vectorConfig, bundle.focusEntities, null);
|
||||
const r1EventTime = Math.round(performance.now() - T_R1_Event_Start);
|
||||
|
||||
xbLog.info(MODULE_ID,
|
||||
@@ -1048,7 +1016,6 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) {
|
||||
|
||||
metrics.query.refineTime = Math.round(performance.now() - T_Refine_Start);
|
||||
|
||||
// 更新 v1 长度指标
|
||||
if (metrics.query?.lengths && bundle.hintsSegment) {
|
||||
metrics.query.lengths.v1Chars = metrics.query.lengths.v0Chars + bundle.hintsSegment.text.length;
|
||||
}
|
||||
@@ -1094,7 +1061,7 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) {
|
||||
metrics.timing.anchorSearch = Math.round(performance.now() - T_R2_Anchor_Start);
|
||||
|
||||
const T_R2_Event_Start = performance.now();
|
||||
let eventHits = await recallEvents(queryVector_v1, allEvents, vectorConfig, bundle.focusEntities, metrics);
|
||||
let { events: eventHits, vectorMap: eventVectorMap } = await recallEvents(queryVector_v1, allEvents, vectorConfig, bundle.focusEntities, metrics);
|
||||
metrics.timing.eventRetrieval = Math.round(performance.now() - T_R2_Event_Start);
|
||||
|
||||
xbLog.info(MODULE_ID,
|
||||
@@ -1102,7 +1069,7 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) {
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 阶段 5: Lexical Retrieval
|
||||
// 阶段 5: Lexical Retrieval + Dense-Gated Event Merge
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
const T_Lex_Start = performance.now();
|
||||
@@ -1133,32 +1100,53 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) {
|
||||
}
|
||||
|
||||
// 合并 L2 events(lexical 命中但 dense 未命中的 events)
|
||||
// ★ Dense 门槛:验证 event 向量与 queryVector_v1 的 cosine similarity
|
||||
const existingEventIds = new Set(eventHits.map(e => e.event?.id).filter(Boolean));
|
||||
const eventIndex = buildEventIndex(allEvents);
|
||||
let lexicalEventCount = 0;
|
||||
let lexicalEventFilteredByDense = 0;
|
||||
|
||||
for (const eid of lexicalResult.eventIds) {
|
||||
if (!existingEventIds.has(eid)) {
|
||||
const ev = eventIndex.get(eid);
|
||||
if (ev) {
|
||||
eventHits.push({
|
||||
event: ev,
|
||||
similarity: 0,
|
||||
_recallType: 'LEXICAL',
|
||||
});
|
||||
existingEventIds.add(eid);
|
||||
lexicalEventCount++;
|
||||
}
|
||||
if (existingEventIds.has(eid)) continue;
|
||||
|
||||
const ev = eventIndex.get(eid);
|
||||
if (!ev) continue;
|
||||
|
||||
// Dense gate: 验证 event 向量与 query 的语义相关性
|
||||
const evVec = eventVectorMap.get(eid);
|
||||
if (!evVec?.length) {
|
||||
// 无向量无法验证相关性,丢弃
|
||||
lexicalEventFilteredByDense++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const sim = cosineSimilarity(queryVector_v1, evVec);
|
||||
if (sim < CONFIG.LEXICAL_EVENT_DENSE_MIN) {
|
||||
lexicalEventFilteredByDense++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 通过门槛,使用实际 dense similarity(而非硬编码 0)
|
||||
eventHits.push({
|
||||
event: ev,
|
||||
similarity: sim,
|
||||
_recallType: 'LEXICAL',
|
||||
});
|
||||
existingEventIds.add(eid);
|
||||
lexicalEventCount++;
|
||||
}
|
||||
|
||||
if (metrics) {
|
||||
metrics.lexical.eventFilteredByDense = lexicalEventFilteredByDense;
|
||||
|
||||
if (lexicalEventCount > 0) {
|
||||
metrics.event.byRecallType.lexical = lexicalEventCount;
|
||||
metrics.event.selected += lexicalEventCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics && lexicalEventCount > 0) {
|
||||
metrics.event.byRecallType.lexical = lexicalEventCount;
|
||||
metrics.event.selected += lexicalEventCount;
|
||||
}
|
||||
|
||||
xbLog.info(MODULE_ID,
|
||||
`Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} mergedEvents=+${lexicalEventCount} (${lexTime}ms)`
|
||||
`Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} mergedEvents=+${lexicalEventCount} filteredByDense=${lexicalEventFilteredByDense} (${lexTime}ms)`
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -1204,13 +1192,13 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) {
|
||||
metrics.event.entityNames = bundle.focusEntities;
|
||||
metrics.event.entitiesUsed = bundle.focusEntities.length;
|
||||
|
||||
console.group('%c[Recall v8]', 'color: #7c3aed; font-weight: bold');
|
||||
console.group('%c[Recall v9]', 'color: #7c3aed; font-weight: bold');
|
||||
console.log(`Total: ${metrics.timing.total}ms`);
|
||||
console.log(`Query Build: ${metrics.query.buildTime}ms | Refine: ${metrics.query.refineTime}ms`);
|
||||
console.log(`R1 weights: [${r1Weights.map(w => w.toFixed(2)).join(', ')}]`);
|
||||
console.log(`Focus: [${bundle.focusEntities.join(', ')}]`);
|
||||
console.log(`Round 2 Anchors: ${anchorHits.length} hits → ${anchorFloors_dense.size} floors`);
|
||||
console.log(`Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length}`);
|
||||
console.log(`Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} evtMerged=+${lexicalEventCount} evtFiltered=${lexicalEventFilteredByDense} floorFiltered=${metrics.lexical.floorFilteredByDense || 0}`);
|
||||
console.log(`Fusion (floor, weighted): dense=${metrics.fusion.denseFloors} lex=${metrics.fusion.lexFloors} → cap=${metrics.fusion.afterCap} (${metrics.fusion.time}ms)`);
|
||||
console.log(`Floor Rerank: ${metrics.evidence.beforeRerank || 0} → ${metrics.evidence.floorsSelected || 0} floors → L0=${metrics.evidence.l0Collected || 0} (${metrics.evidence.rerankTime || 0}ms)`);
|
||||
console.log(`L1: ${metrics.evidence.l1Pulled || 0} pulled → ${metrics.evidence.l1Attached || 0} attached (${metrics.evidence.l1CosineTime || 0}ms)`);
|
||||
|
||||
Reference in New Issue
Block a user