Improve story summary logging
This commit is contained in:
@@ -36,7 +36,7 @@ const RECENT_ORPHAN_MAX = 5000; // [待整理] 独立预算
|
||||
const TOTAL_BUDGET_MAX = 15000; // 总预算(用于日志显示)
|
||||
const L3_MAX = 2000;
|
||||
const ARCS_MAX = 1500;
|
||||
const TOP_N_STAR = 5; // 相似度前N条加⭐
|
||||
const TOP_N_STAR = 5; // 匹配度前N条加⭐
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 工具函数
|
||||
@@ -179,84 +179,64 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
||||
const pct = (n, d) => (d > 0 ? Math.round((n / d) * 100) : 0);
|
||||
|
||||
const lines = [
|
||||
"",
|
||||
"╔══════════════════════════════════════════════════════════════╗",
|
||||
"║ Prompt 装配报告 ║",
|
||||
"╠══════════════════════════════════════════════════════════════╣",
|
||||
`║ 总预算: ${stats.budget.max} tokens`,
|
||||
`║ 已使用: ${stats.budget.used} tokens (${pct(stats.budget.used, stats.budget.max)}%)`,
|
||||
`║ 剩余: ${stats.budget.max - stats.budget.used} tokens`,
|
||||
"╚══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
'',
|
||||
'\u250c' + '\u2500'.repeat(61) + '\u2510',
|
||||
'\u2502 \u3010\u88c5\u914d\u7edf\u8ba1\u3011 \u2502',
|
||||
'\u2514' + '\u2500'.repeat(61) + '\u2518',
|
||||
` \u603b\u9884\u7b97: ${stats.budget.max} tokens | \u5df2\u4f7f\u7528: ${stats.budget.used} tokens (${pct(stats.budget.used, stats.budget.max)}%)`,
|
||||
'',
|
||||
];
|
||||
|
||||
// 世界状态
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [1] 世界约束 (上限 2000) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${stats.world.count} 条 | ${stats.world.tokens} tokens`);
|
||||
lines.push("");
|
||||
// [1] World constraints
|
||||
lines.push(' [1] \u4e16\u754c\u7ea6\u675f (\u4e0a\u9650 2000)');
|
||||
lines.push(` \u9009\u5165: ${stats.world.count} \u6761 | \u6d88\u8017: ${stats.world.tokens} tokens`);
|
||||
lines.push('');
|
||||
|
||||
// 核心经历 + 过往背景
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [2] 核心经历 + 过往背景(含证据) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 选入: ${stats.events.selected} 条 | 事件本体: ${stats.events.tokens} tokens`);
|
||||
lines.push(` 挂载证据: ${stats.evidence.attached} 条 | 证据: ${stats.evidence.tokens} tokens`);
|
||||
lines.push(` 核心: ${details.directCount || 0} | 过往: ${details.similarCount || 0}`);
|
||||
if (details.eventList?.length) {
|
||||
lines.push(" ────────────────────────────────────────");
|
||||
details.eventList.slice(0, 20).forEach((ev, i) => {
|
||||
const type = ev.isDirect ? "核心" : "过往";
|
||||
const hasE = ev.hasEvidence ? " +E" : "";
|
||||
const title = (ev.title || "(无标题)").slice(0, 32);
|
||||
lines.push(` ${String(i + 1).padStart(2)}. [${type}${hasE}] ${title} (${ev.tokens}tok)`);
|
||||
});
|
||||
if (details.eventList.length > 20) lines.push(` ... 还有 ${details.eventList.length - 20} 条`);
|
||||
}
|
||||
lines.push("");
|
||||
// [2] Core + background events
|
||||
lines.push(' [2] \u6838\u5fc3\u7ecf\u5386 + \u8fc7\u5f80\u80cc\u666f');
|
||||
lines.push(` \u4e8b\u4ef6: ${stats.events.selected} \u6761 | \u6d88\u8017: ${stats.events.tokens} tokens`);
|
||||
|
||||
// 远期片段
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [3] 远期片段(已总结范围) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${stats.orphans.injected} 条 | ${stats.orphans.tokens} tokens`);
|
||||
lines.push("");
|
||||
const l0EvidenceCount = details.eventList?.filter(e => e.hasL0Evidence)?.length || 0;
|
||||
const l1EvidenceCount = (stats.evidence.attached || 0) - l0EvidenceCount;
|
||||
lines.push(` \u8bc1\u636e: ${stats.evidence.attached} \u6761 (L0: ${l0EvidenceCount}, L1: ${l1EvidenceCount}) | \u6d88\u8017: ${stats.evidence.tokens} tokens`);
|
||||
lines.push(` \u6838\u5fc3: ${details.directCount || 0} \u6761 | \u8fc7\u5f80: ${details.similarCount || 0} \u6761`);
|
||||
lines.push('');
|
||||
|
||||
// 待整理
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [4] 待整理(未总结范围,独立预算 5000) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${recentOrphanStats?.injected || 0} 条 | ${recentOrphanStats?.tokens || 0} tokens`);
|
||||
lines.push(` 楼层范围: ${recentOrphanStats?.floorRange || "N/A"}`);
|
||||
lines.push("");
|
||||
// [3] Long-term chunks
|
||||
const l0OrphanCount = stats.orphans.l0Count || 0;
|
||||
const l1OrphanCount = (stats.orphans.injected || 0) - l0OrphanCount;
|
||||
lines.push(' [3] \u8fdc\u671f\u7247\u6bb5 (\u5df2\u603b\u7ed3\u8303\u56f4)');
|
||||
lines.push(` \u9009\u5165: ${stats.orphans.injected} \u6761 (L0: ${l0OrphanCount}, L1: ${l1OrphanCount}) | \u6d88\u8017: ${stats.orphans.tokens} tokens`);
|
||||
lines.push('');
|
||||
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ [5] 人物弧光(上限 1500) │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
lines.push(` 注入: ${stats.arcs.count} 条 | ${stats.arcs.tokens} tokens`);
|
||||
lines.push("");
|
||||
// [4] Recent orphans
|
||||
lines.push(' [4] \u5f85\u6574\u7406 (\u72ec\u7acb\u9884\u7b97 5000)');
|
||||
lines.push(` \u9009\u5165: ${recentOrphanStats?.injected || 0} \u6761 | \u6d88\u8017: ${recentOrphanStats?.tokens || 0} tokens`);
|
||||
lines.push(` \u697c\u5c42: ${recentOrphanStats?.floorRange || 'N/A'}`);
|
||||
lines.push('');
|
||||
|
||||
// 预算条形图
|
||||
lines.push("┌─────────────────────────────────────────────────────────────┐");
|
||||
lines.push("│ 【预算分布】 │");
|
||||
lines.push("└─────────────────────────────────────────────────────────────┘");
|
||||
// [5] Arcs
|
||||
lines.push(' [5] \u4eba\u7269\u5f27\u5149 (\u4e0a\u9650 1500)');
|
||||
lines.push(` \u9009\u5165: ${stats.arcs.count} \u6761 | \u6d88\u8017: ${stats.arcs.tokens} tokens`);
|
||||
lines.push('');
|
||||
|
||||
// Budget bar
|
||||
lines.push(' \u3010\u9884\u7b97\u5206\u5e03\u3011');
|
||||
const total = stats.budget.max;
|
||||
const bar = (tokens, label) => {
|
||||
const width = Math.round((tokens / total) * 40);
|
||||
const pctStr = pct(tokens, total) + "%";
|
||||
return ` ${label.padEnd(6)} ${"█".repeat(width).padEnd(40)} ${String(tokens).padStart(5)} (${pctStr})`;
|
||||
const width = Math.round((tokens / total) * 30);
|
||||
const pctStr = pct(tokens, total) + '%';
|
||||
return ` ${label.padEnd(6)} ${'\u2588'.repeat(width).padEnd(30)} ${String(tokens).padStart(5)} (${pctStr})`;
|
||||
};
|
||||
lines.push(bar(stats.world.tokens, "约束"));
|
||||
lines.push(bar(stats.events.tokens, "经历"));
|
||||
lines.push(bar(stats.evidence.tokens, "证据"));
|
||||
lines.push(bar(stats.orphans.tokens, "远期"));
|
||||
lines.push(bar(recentOrphanStats?.tokens || 0, "待整理"));
|
||||
lines.push(bar(stats.arcs.tokens, "弧光"));
|
||||
lines.push(bar(stats.budget.max - stats.budget.used, "剩余"));
|
||||
lines.push("");
|
||||
lines.push(bar(stats.world.tokens, '\u7ea6\u675f'));
|
||||
lines.push(bar(stats.events.tokens + stats.evidence.tokens, '\u7ecf\u5386'));
|
||||
lines.push(bar(stats.orphans.tokens, '\u8fdc\u671f'));
|
||||
lines.push(bar(recentOrphanStats?.tokens || 0, '\u5f85\u6574\u7406'));
|
||||
lines.push(bar(stats.arcs.tokens, '\u5f27\u5149'));
|
||||
lines.push(bar(stats.budget.max - stats.budget.used, '\u5269\u4f59'));
|
||||
lines.push('');
|
||||
|
||||
return lines.join("\n");
|
||||
return lines.join('\n');
|
||||
}
|
||||
// 重写事件文本里的序号前缀:把 “{idx}. ” 或 “{idx}.【...】” 的 idx 替换
|
||||
function renumberEventText(text, newIndex) {
|
||||
@@ -468,7 +448,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// 候选按相似度从高到低(保证高分优先拥有证据)
|
||||
// 候选按匹配度从高到低(保证高分优先拥有证据)
|
||||
const candidates = [...recalledEvents].sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
|
||||
|
||||
const selectedDirect = []; // { event, text, tokens, chunk, hasEvidence }
|
||||
|
||||
@@ -2677,3 +2677,30 @@ h1 span {
|
||||
font-size: .8125rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 调试日志区域手机适配 */
|
||||
@media (max-width: 768px) {
|
||||
#recall-log-content {
|
||||
font-size: 10px;
|
||||
padding: 8px;
|
||||
overflow-x: hidden; /* 禁止横向滚动 */
|
||||
word-break: break-all; /* 强制换行 */
|
||||
white-space: pre-wrap; /* 保留换行但允许自动换行 */
|
||||
}
|
||||
|
||||
.debug-log-viewer {
|
||||
font-size: 10px;
|
||||
overflow-x: hidden;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#recall-log-content,
|
||||
.debug-log-viewer {
|
||||
font-size: 9px;
|
||||
line-height: 1.4;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,26 @@ let vectorAbortController = null;
|
||||
let lastSentUserMessage = null;
|
||||
let lastSentTimestamp = 0;
|
||||
|
||||
function captureUserInput() {
|
||||
const text = $("#send_textarea").val();
|
||||
if (text?.trim()) {
|
||||
lastSentUserMessage = text.trim();
|
||||
lastSentTimestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
function onSendPointerdown(e) {
|
||||
if (e.target?.closest?.("#send_but")) {
|
||||
captureUserInput();
|
||||
}
|
||||
}
|
||||
|
||||
function onSendKeydown(e) {
|
||||
if (e.key === "Enter" && !e.shiftKey && e.target?.closest?.("#send_textarea")) {
|
||||
captureUserInput();
|
||||
}
|
||||
}
|
||||
|
||||
let hideApplyTimer = null;
|
||||
const HIDE_APPLY_DEBOUNCE_MS = 250;
|
||||
|
||||
@@ -1483,6 +1503,10 @@ function registerEvents() {
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
|
||||
|
||||
// 用户输入捕获(原生捕获阶段)
|
||||
document.addEventListener("pointerdown", onSendPointerdown, true);
|
||||
document.addEventListener("keydown", onSendKeydown, true);
|
||||
|
||||
// 注入链路
|
||||
eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
|
||||
eventSource.on(event_types.GENERATION_STOPPED, clearExtensionPrompt);
|
||||
@@ -1497,6 +1521,9 @@ function unregisterEvents() {
|
||||
hideOverlay();
|
||||
|
||||
clearExtensionPrompt();
|
||||
|
||||
document.removeEventListener("pointerdown", onSendPointerdown, true);
|
||||
document.removeEventListener("keydown", onSendKeydown, true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -180,6 +180,8 @@ function buildExpDecayWeights(n, beta) {
|
||||
function buildQuerySegments(chat, count, excludeLastAi, pendingUserMessage = null) {
|
||||
if (!chat?.length) return [];
|
||||
|
||||
const { name1 } = getContext();
|
||||
|
||||
let messages = chat;
|
||||
if (excludeLastAi && messages.length > 0 && !messages[messages.length - 1]?.is_user) {
|
||||
messages = messages.slice(0, -1);
|
||||
@@ -193,12 +195,12 @@ function buildQuerySegments(chat, count, excludeLastAi, pendingUserMessage = nul
|
||||
|
||||
// 避免重复(如果 chat 已包含该消息则不追加)
|
||||
if (lastMsgText !== pendingText) {
|
||||
messages = [...messages, { is_user: true, name: "用户", mes: pendingUserMessage }];
|
||||
messages = [...messages, { is_user: true, name: name1 || "用户", mes: pendingUserMessage }];
|
||||
}
|
||||
}
|
||||
|
||||
return messages.slice(-count).map((m, idx, arr) => {
|
||||
const speaker = m.name || (m.is_user ? '用户' : '角色');
|
||||
const speaker = m.name || (m.is_user ? (name1 || "用户") : "角色");
|
||||
const clean = cleanForRecall(m.mes);
|
||||
if (!clean) return '';
|
||||
const limit = idx === arr.length - 1 ? CONFIG.QUERY_MAX_CHARS : CONFIG.QUERY_CONTEXT_CHARS;
|
||||
@@ -387,7 +389,8 @@ async function searchChunks(queryVector, vectorConfig, l0FloorBonus = new Map())
|
||||
c => c.similarity
|
||||
);
|
||||
|
||||
|
||||
|
||||
// floor 稀疏去重:每个楼层只保留该楼层匹配度最高的那条
|
||||
const bestByFloor = new Map();
|
||||
for (const s of selected) {
|
||||
const prev = bestByFloor.get(s.floor);
|
||||
@@ -396,7 +399,7 @@ async function searchChunks(queryVector, vectorConfig, l0FloorBonus = new Map())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 最终结果按匹配度降序
|
||||
const sparse = Array.from(bestByFloor.values()).sort((a, b) => b.similarity - a.similarity);
|
||||
|
||||
const floors = [...new Set(sparse.map(c => c.floor))];
|
||||
@@ -444,7 +447,8 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
||||
if (!vectorMap.size) return [];
|
||||
|
||||
const userName = normalize(name1);
|
||||
const userName = normalize(name1);
|
||||
const queryNormList = (queryEntities || []).map(normalize).filter(Boolean);
|
||||
const querySet = new Set(queryNormList);
|
||||
|
||||
// 只取硬约束类的 world topic
|
||||
const worldTopics = (store?.json?.world || [])
|
||||
@@ -461,16 +465,19 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
||||
|
||||
// participants 命中
|
||||
const participants = (event.participants || []).map(normalize).filter(Boolean);
|
||||
const participants = (event.participants || []).map(normalize).filter(Boolean);
|
||||
if (participants.some(p => p !== userName && querySet.has(p))) {
|
||||
bonus += CONFIG.BONUS_PARTICIPANT_HIT;
|
||||
const hitCount = participants.filter(p => p !== userName && querySet.has(p)).length;
|
||||
const hasParticipantHit = hitCount > 0;
|
||||
if (hasParticipantHit) {
|
||||
bonus += CONFIG.BONUS_PARTICIPANT_HIT * Math.log2(hitCount + 1);
|
||||
reasons.push(hitCount > 1 ? `participant×${hitCount}` : 'participant');
|
||||
}
|
||||
|
||||
// text 命中
|
||||
const text = normalize(`${event.title || ''} ${event.summary || ''}`);
|
||||
const text = normalize(`${event.title || ''} ${event.summary || ''}`);
|
||||
if ((queryEntities || []).some(e => text.includes(normalize(e)))) {
|
||||
bonus += CONFIG.BONUS_TEXT_HIT;
|
||||
const textHitCount = queryNormList.filter(e => text.includes(e)).length;
|
||||
if (textHitCount > 0) {
|
||||
bonus += CONFIG.BONUS_TEXT_HIT * Math.log2(textHitCount + 1);
|
||||
reasons.push(textHitCount > 1 ? `text×${textHitCount}` : 'text');
|
||||
}
|
||||
|
||||
// world topic 命中
|
||||
@@ -499,7 +506,7 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
||||
bonus,
|
||||
finalScore: sim + bonus,
|
||||
reasons,
|
||||
reasons,
|
||||
isDirect: hasParticipantHit,
|
||||
vector: v,
|
||||
};
|
||||
});
|
||||
@@ -548,65 +555,37 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
||||
// 日志:因果树格式化
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
function formatCausalTree(causalEvents, recalledEvents) {
|
||||
if (!causalEvents?.length) return '';
|
||||
|
||||
const lines = [
|
||||
'',
|
||||
'┌─────────────────────────────────────────────────────────────┐',
|
||||
'│ 【因果链追溯】 │',
|
||||
'└─────────────────────────────────────────────────────────────┘',
|
||||
];
|
||||
|
||||
// 按 chainFrom 分组展示
|
||||
const bySource = new Map();
|
||||
for (const c of causalEvents) {
|
||||
for (const src of c.chainFrom || []) {
|
||||
if (!bySource.has(src)) bySource.set(src, []);
|
||||
bySource.get(src).push(c);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [sourceId, ancestors] of bySource) {
|
||||
const sourceEvent = recalledEvents.find(e => e.event?.id === sourceId);
|
||||
const sourceTitle = sourceEvent?.event?.title || sourceId;
|
||||
lines.push(` ${sourceId} "${sourceTitle}" 的前因链:`);
|
||||
|
||||
// 按深度排序
|
||||
ancestors.sort((a, b) => a.depth - b.depth);
|
||||
|
||||
for (const c of ancestors) {
|
||||
const indent = ' ' + ' '.repeat(c.depth - 1);
|
||||
const ev = c.event;
|
||||
const title = ev.title || '(无标题)';
|
||||
const refs = c.chainFrom.length > 1 ? ` [被${c.chainFrom.length}条链引用]` : '';
|
||||
lines.push(`${indent}└─ [depth=${c.depth}] ${ev.id} "${title}"${refs}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 日志:主报告
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
function formatRecallLog({
|
||||
elapsed,
|
||||
segments,
|
||||
weights,
|
||||
chunkResults,
|
||||
eventResults,
|
||||
allEvents,
|
||||
queryEntities,
|
||||
causalEvents = [],
|
||||
chunkPreFilterStats = null,
|
||||
l0Results = [],
|
||||
l0PreFilterStats = null,
|
||||
}) {
|
||||
const lines = [
|
||||
const lines = [
|
||||
'╔══════════════════════════════════════════════════════════════╗',
|
||||
'║ 记忆召回报告 ║',
|
||||
'╠══════════════════════════════════════════════════════════════╣',
|
||||
`║ 耗时: ${elapsed}ms`,
|
||||
'\u2554' + '\u2550'.repeat(62) + '\u2557',
|
||||
'\u2551 \u8bb0\u5fc6\u53ec\u56de\u62a5\u544a \u2551',
|
||||
'\u2560' + '\u2550'.repeat(62) + '\u2563',
|
||||
`\u2551 \u8017\u65f6: ${elapsed}ms`,
|
||||
'\u255a' + '\u2550'.repeat(62) + '\u255d',
|
||||
'',
|
||||
'',
|
||||
'┌─────────────────────────────────────────────────────────────┐',
|
||||
'│ 【查询构建】最近 5 条消息,指数衰减加权 (β=0.7) │',
|
||||
'│ 权重越高 = 对召回方向影响越大 │',
|
||||
'\u250c' + '\u2500'.repeat(61) + '\u2510',
|
||||
'\u2502 \u3010\u67e5\u8be2\u6784\u5efa\u3011\u6700\u8fd1 5 \u6761\u6d88\u606f\uff0c\u6307\u6570\u8870\u51cf\u52a0\u6743 (\u03b2=0.7) \u2502',
|
||||
'\u2514' + '\u2500'.repeat(61) + '\u2518',
|
||||
];
|
||||
|
||||
|
||||
// Keep query previews only (the only place to keep raw text)
|
||||
const segmentsSorted = segments.map((s, i) => ({
|
||||
idx: i + 1,
|
||||
weight: weights?.[i] ?? 0,
|
||||
@@ -614,105 +593,79 @@ function formatRecallLog({ elapsed, segments, weights, chunkResults, eventResult
|
||||
})).sort((a, b) => b.weight - a.weight);
|
||||
|
||||
segmentsSorted.forEach((s, rank) => {
|
||||
segmentsSorted.forEach((s, rank) => {
|
||||
const bar = '\u2588'.repeat(Math.round(s.weight * 20));
|
||||
const preview = s.text.length > 60 ? s.text.slice(0, 60) + '...' : s.text;
|
||||
const preview = s.text.length > 60 ? s.text.slice(0, 60) + '...' : s.text;
|
||||
const marker = rank === 0 ? ' \u25c0 \u4e3b\u5bfc' : '';
|
||||
lines.push(` ${(s.weight * 100).toFixed(1).padStart(5)}% ${bar.padEnd(12)} ${preview}${marker}`);
|
||||
});
|
||||
|
||||
lines.push('');
|
||||
lines.push('');
|
||||
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||
lines.push('│ 【提取实体】用于判断"亲身经历"(DIRECT) │');
|
||||
lines.push('└─────────────────────────────────────────────────────────────┘');
|
||||
lines.push('\u250c' + '\u2500'.repeat(61) + '\u2510');
|
||||
lines.push('\u2502 \u3010\u63d0\u53d6\u5b9e\u4f53\u3011 \u2502');
|
||||
lines.push('\u2514' + '\u2500'.repeat(61) + '\u2518');
|
||||
lines.push(` ${queryEntities?.length ? queryEntities.join('\u3001') : '(\u65e0)'}`);
|
||||
|
||||
// Recall stats (numbers only)
|
||||
lines.push('');
|
||||
lines.push('');
|
||||
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||
lines.push('│ 【L0 语义锚点】状态变更加权信号 │');
|
||||
lines.push('\u250c' + '\u2500'.repeat(61) + '\u2510');
|
||||
lines.push('\u2502 \u3010\u53ec\u56de\u7edf\u8ba1\u3011 \u2502');
|
||||
lines.push('\u2514' + '\u2500'.repeat(61) + '\u2518');
|
||||
|
||||
|
||||
// L0
|
||||
const l0Floors = [...new Set(l0Results.map(r => r.floor))].sort((a, b) => a - b);
|
||||
const l0Floors = [...new Set(l0Results.map(r => r.floor))].sort((a, b) => a - b);
|
||||
lines.push(` 召回: ${l0Results.length} 条`);
|
||||
lines.push(` 影响楼层: ${l0Floors.join(', ')}(L1/L2 候选在这些楼层获得 +${CONFIG.L0_FLOOR_BONUS_FACTOR} 加分)`);
|
||||
lines.push('');
|
||||
|
||||
l0Results.slice(0, 10).forEach((r, i) => {
|
||||
lines.push(` ${String(i + 1).padStart(2)}. #${r.floor} ${r.atom.semantic.slice(0, 50)}${r.atom.semantic.length > 50 ? '...' : ''}`);
|
||||
lines.push(` 相似度: ${r.similarity.toFixed(3)}`);
|
||||
});
|
||||
|
||||
if (l0Results.length > 10) {
|
||||
lines.push(` ... 还有 ${l0Results.length - 10} 条`);
|
||||
lines.push(' L0 \u8bed\u4e49\u951a\u70b9:');
|
||||
if (l0Results.length) {
|
||||
const l0Dist = {
|
||||
'0.8+': l0Results.filter(r => r.similarity >= 0.8).length,
|
||||
'0.7-0.8': l0Results.filter(r => r.similarity >= 0.7 && r.similarity < 0.8).length,
|
||||
'0.6-0.7': l0Results.filter(r => r.similarity >= 0.6 && r.similarity < 0.7).length,
|
||||
'0.55-0.6': l0Results.filter(r => r.similarity >= 0.55 && r.similarity < 0.6).length,
|
||||
};
|
||||
lines.push(` \u9009\u5165: ${l0Results.length} \u6761 | \u5f71\u54cd\u697c\u5c42: ${l0Floors.join(', ')} (+${CONFIG.L0_FLOOR_BONUS_FACTOR} \u52a0\u6743)`);
|
||||
lines.push(` \u5339\u914d\u5ea6: 0.8+: ${l0Dist['0.8+']} | 0.7-0.8: ${l0Dist['0.7-0.8']} | 0.6-0.7: ${l0Dist['0.6-0.7']} | 0.55-0.6: ${l0Dist['0.55-0.6']}`);
|
||||
} else {
|
||||
} else {
|
||||
lines.push(' (\u65e0\u6570\u636e)');
|
||||
}
|
||||
|
||||
// L1
|
||||
lines.push('');
|
||||
lines.push('');
|
||||
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||
lines.push('│ 【L1 原文片段】 │');
|
||||
lines.push('└─────────────────────────────────────────────────────────────┘');
|
||||
lines.push(' L1 \u539f\u6587\u7247\u6bb5:');
|
||||
if (chunkPreFilterStats) {
|
||||
const dist = chunkPreFilterStats.distribution || {};
|
||||
const dist = chunkPreFilterStats.distribution || {};
|
||||
lines.push(` 过滤前: ${chunkPreFilterStats.total} 条`);
|
||||
lines.push(' 相似度分布:');
|
||||
lines.push(` 0.8+: ${dist['0.8+'] || 0} | 0.7-0.8: ${dist['0.7-0.8'] || 0} | 0.6-0.7: ${dist['0.6-0.7'] || 0}`);
|
||||
lines.push(` 0.55-0.6: ${dist['0.55-0.6'] || 0} | <0.55: ${dist['<0.55'] || 0}`);
|
||||
lines.push(` 通过阈值(>=${chunkPreFilterStats.threshold}): ${chunkPreFilterStats.passThreshold} 条`);
|
||||
lines.push(` \u5168\u91cf: ${chunkPreFilterStats.total} \u6761 | \u901a\u8fc7\u9608\u503c(\u2265${chunkPreFilterStats.threshold}): ${chunkPreFilterStats.passThreshold} \u6761 | \u6700\u7ec8: ${chunkResults.length} \u6761`);
|
||||
lines.push(` \u5339\u914d\u5ea6: 0.8+: ${dist['0.8+'] || 0} | 0.7-0.8: ${dist['0.7-0.8'] || 0} | 0.6-0.7: ${dist['0.6-0.7'] || 0} | <0.6: ${(dist['0.55-0.6'] || 0) + (dist['<0.55'] || 0)}`);
|
||||
|
||||
const floorCounts = new Map();
|
||||
chunkResults.forEach(c => floorCounts.set(c.floor, (floorCounts.get(c.floor) || 0) + 1));
|
||||
const floorStats = `\u8986\u76d6 ${floorCounts.size} \u4e2a\u697c\u5c42`;
|
||||
lines.push(` ${floorStats}`);
|
||||
} else {
|
||||
} else {
|
||||
lines.push(` \u9009\u5165: ${chunkResults.length} \u6761`);
|
||||
}
|
||||
|
||||
|
||||
chunkResults.slice(0, 15).forEach((c, i) => {
|
||||
const preview = c.text.length > 50 ? c.text.slice(0, 50) + '...' : c.text;
|
||||
lines.push(` ${String(i + 1).padStart(2)}. #${String(c.floor).padStart(3)} [${c.speaker}] ${preview}`);
|
||||
lines.push(` 相似度: ${c.similarity.toFixed(3)}`);
|
||||
});
|
||||
|
||||
if (chunkResults.length > 15) {
|
||||
lines.push(` ... 还有 ${chunkResults.length - 15} 条`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||
lines.push('│ 【L2 事件记忆】 │');
|
||||
lines.push('│ DIRECT=亲身经历 SIMILAR=相关背景 │');
|
||||
lines.push('└─────────────────────────────────────────────────────────────┘');
|
||||
|
||||
eventResults.forEach((e, i) => {
|
||||
const type = e._recallType === 'DIRECT' ? '★ DIRECT ' : ' SIMILAR';
|
||||
const title = e.event.title || '(无标题)';
|
||||
lines.push(` ${String(i + 1).padStart(2)}. ${type} ${title}`);
|
||||
lines.push(` 相似度: ${e.similarity.toFixed(3)} | 原因: ${e._recallReason}`);
|
||||
});
|
||||
|
||||
// L2
|
||||
const preFilterDist = eventResults[0]?._preFilterDistribution || {};
|
||||
const directCount = eventResults.filter(e => e._recallType === 'DIRECT').length;
|
||||
const similarCount = eventResults.filter(e => e._recallType === 'SIMILAR').length;
|
||||
const similarCount = eventResults.filter(e => e._recallType === 'SIMILAR').length;
|
||||
|
||||
lines.push('');
|
||||
lines.push('');
|
||||
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||
lines.push('│ 【统计】 │');
|
||||
lines.push('└─────────────────────────────────────────────────────────────┘');
|
||||
lines.push(` L1 片段: ${chunkResults.length} 条`);
|
||||
lines.push(' L2 \u4e8b\u4ef6\u8bb0\u5fc6:');
|
||||
lines.push(` \u603b\u4e8b\u4ef6: ${allEvents.length} \u6761 | \u901a\u8fc7\u9608\u503c(\u2265${preFilterDist.threshold || 0.65}): ${preFilterDist.passThreshold || 0} \u6761 | \u6700\u7ec8: ${eventResults.length} \u6761`);
|
||||
if (preFilterDist.total) {
|
||||
if (preFilterDist.total) {
|
||||
lines.push(` L2 过滤前分布(${preFilterDist.total} 条,含bonus):`);
|
||||
lines.push(` 0.85+: ${preFilterDist['0.85+'] || 0} | 0.7-0.85: ${preFilterDist['0.7-0.85'] || 0} | 0.6-0.7: ${preFilterDist['0.6-0.7'] || 0}`);
|
||||
lines.push(` 0.5-0.6: ${preFilterDist['0.5-0.6'] || 0} | <0.5: ${preFilterDist['<0.5'] || 0}`);
|
||||
lines.push(` \u5339\u914d\u5ea6: 0.85+: ${preFilterDist['0.85+'] || 0} | 0.7-0.85: ${preFilterDist['0.7-0.85'] || 0} | 0.6-0.7: ${preFilterDist['0.6-0.7'] || 0} | <0.6: ${(preFilterDist['0.5-0.6'] || 0) + (preFilterDist['<0.5'] || 0)}`);
|
||||
}
|
||||
}
|
||||
lines.push(` 实体命中: ${queryEntities?.length || 0} 个`);
|
||||
lines.push(` \u7c7b\u578b: DIRECT ${directCount} \u6761 | SIMILAR ${similarCount} \u6761`);
|
||||
|
||||
// Causal chains
|
||||
if (causalEvents.length) {
|
||||
const maxRefs = Math.max(...causalEvents.map(c => c.chainFrom?.length || 0));
|
||||
const maxDepth = Math.max(...causalEvents.map(c => c.depth || 0));
|
||||
lines.push('');
|
||||
lines.push(' \u56e0\u679c\u94fe\u8ffd\u6eaf:');
|
||||
lines.push(` \u8ffd\u6eaf: ${causalEvents.length} \u6761 | \u6700\u5927\u88ab\u5f15: ${maxRefs} \u6b21 | \u6700\u5927\u6df1\u5ea6: ${maxDepth}`);
|
||||
}
|
||||
|
||||
|
||||
// 追加因果树详情
|
||||
lines.push(formatCausalTree(causalEvents, eventResults));
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -747,7 +700,7 @@ export async function recallMemory(queryText, allEvents, vectorConfig, options =
|
||||
}
|
||||
|
||||
const lexicon = buildEntityLexicon(store, allEvents);
|
||||
const lexicon = buildEntityLexicon(store, allEvents);
|
||||
const queryEntities = extractEntities(segments.join('\n'), lexicon);
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// L0 召回
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function searchStateAtoms(queryVector, vectorConfig) {
|
||||
const atoms = getStateAtoms();
|
||||
const atomMap = new Map(atoms.map(a => [a.atomId, a]));
|
||||
|
||||
// 计算相似度
|
||||
// 计算匹配度
|
||||
const scored = stateVectors
|
||||
.map(sv => {
|
||||
const atom = atomMap.get(sv.atomId);
|
||||
@@ -92,8 +92,8 @@ export function buildL0FloorBonus(l0Results, bonusFactor = 0.10) {
|
||||
const floorBonus = new Map();
|
||||
|
||||
for (const r of l0Results || []) {
|
||||
// 每个楼层只加一次,取最高相似度对应的 bonus
|
||||
// 简化处理:统一加 bonusFactor,不区分相似度高低
|
||||
// 每个楼层只加一次,取最高匹配度对应的 bonus
|
||||
// 简化处理:统一加 bonusFactor,不区分匹配度高低
|
||||
if (!floorBonus.has(r.floor)) {
|
||||
floorBonus.set(r.floor, bonusFactor);
|
||||
}
|
||||
@@ -132,13 +132,13 @@ export function stateToVirtualChunks(l0Results) {
|
||||
|
||||
/**
|
||||
* 合并 L0 和 L1 chunks,每楼层最多保留 limit 条
|
||||
* @param {Array} l0Chunks - 虚拟 chunks(已按相似度排序)
|
||||
* @param {Array} l1Chunks - 真实 chunks(已按相似度排序)
|
||||
* @param {Array} l0Chunks - 虚拟 chunks(已按匹配度排序)
|
||||
* @param {Array} l1Chunks - 真实 chunks(已按匹配度排序)
|
||||
* @param {number} limit - 每楼层上限
|
||||
* @returns {Array} 合并后的 chunks
|
||||
*/
|
||||
export function mergeAndSparsify(l0Chunks, l1Chunks, limit = 2) {
|
||||
// 合并并按相似度排序
|
||||
// 合并并按匹配度排序
|
||||
const all = [...(l0Chunks || []), ...(l1Chunks || [])]
|
||||
.sort((a, b) => b.similarity - a.similarity);
|
||||
|
||||
@@ -153,7 +153,7 @@ export function mergeAndSparsify(l0Chunks, l1Chunks, limit = 2) {
|
||||
}
|
||||
}
|
||||
|
||||
// 扁平化并保持相似度排序
|
||||
// 扁平化并保持匹配度排序
|
||||
return Array.from(byFloor.values())
|
||||
.flat()
|
||||
.sort((a, b) => b.similarity - a.similarity);
|
||||
|
||||
Reference in New Issue
Block a user