Refactor L0 quality and diffusion graph gating for balanced recall
This commit is contained in:
@@ -63,6 +63,8 @@ const SYSTEM_PROMPT = `你是场景摘要器。从一轮对话中提取1-2个场
|
|||||||
- 只从正文内容中识别角色名,不要把标签名(如 user、assistant)当作角色
|
- 只从正文内容中识别角色名,不要把标签名(如 user、assistant)当作角色
|
||||||
- r 使用动作模板短语:“动作+对象/结果”(例:“提出交易条件”、“拒绝对方请求”、“当众揭露秘密”、“安抚对方情绪”)
|
- r 使用动作模板短语:“动作+对象/结果”(例:“提出交易条件”、“拒绝对方请求”、“当众揭露秘密”、“安抚对方情绪”)
|
||||||
- r 不要写人名,不要复述整句,不要写心理描写或评价词
|
- r 不要写人名,不要复述整句,不要写心理描写或评价词
|
||||||
|
- r 正例(合格):提出交易条件、拒绝对方请求、当众揭露秘密、安抚对方情绪、强行打断发言、转移谈话焦点
|
||||||
|
- r 反例(不合格):我觉得她现在很害怕、他突然非常生气地大喊起来、user开始说话、assistant解释了很多细节
|
||||||
- 每个锚点 1-3 条
|
- 每个锚点 1-3 条
|
||||||
|
|
||||||
## where
|
## where
|
||||||
@@ -87,6 +89,46 @@ const JSON_PREFILL = '{"anchors":[';
|
|||||||
|
|
||||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
const ACTION_STRIP_WORDS = [
|
||||||
|
'突然', '非常', '有些', '有点', '轻轻', '悄悄', '缓缓', '立刻',
|
||||||
|
'马上', '然后', '并且', '而且', '开始', '继续', '再次', '正在',
|
||||||
|
];
|
||||||
|
|
||||||
|
function clamp(v, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeActionPhrase(raw) {
|
||||||
|
let text = String(raw || '')
|
||||||
|
.normalize('NFKC')
|
||||||
|
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||||
|
.trim();
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
text = text
|
||||||
|
.replace(/[,。!?、;:,.!?;:"'“”‘’()()[\]{}<>《》]/g, '')
|
||||||
|
.replace(/\s+/g, '');
|
||||||
|
|
||||||
|
for (const word of ACTION_STRIP_WORDS) {
|
||||||
|
text = text.replaceAll(word, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replace(/(地|得|了|着|过)+$/g, '');
|
||||||
|
|
||||||
|
if (text.length < 2) return '';
|
||||||
|
if (text.length > 12) text = text.slice(0, 12);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcAtomQuality(scene, edges, where) {
|
||||||
|
const sceneLen = String(scene || '').length;
|
||||||
|
const sceneScore = clamp(sceneLen / 80, 0, 1);
|
||||||
|
const edgeScore = clamp((edges?.length || 0) / 3, 0, 1);
|
||||||
|
const whereScore = where ? 1 : 0;
|
||||||
|
const quality = 0.55 * sceneScore + 0.35 * edgeScore + 0.10 * whereScore;
|
||||||
|
return Number(quality.toFixed(3));
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 清洗与构建
|
// 清洗与构建
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -103,7 +145,7 @@ function sanitizeEdges(raw) {
|
|||||||
.map(e => ({
|
.map(e => ({
|
||||||
s: String(e.s || '').trim(),
|
s: String(e.s || '').trim(),
|
||||||
t: String(e.t || '').trim(),
|
t: String(e.t || '').trim(),
|
||||||
r: String(e.r || '').trim().slice(0, 30),
|
r: sanitizeActionPhrase(e.r),
|
||||||
}))
|
}))
|
||||||
.filter(e => e.s && e.t && e.r)
|
.filter(e => e.s && e.t && e.r)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
@@ -127,6 +169,7 @@ function anchorToAtom(anchor, aiFloor, idx) {
|
|||||||
if (scene.length < 15) return null;
|
if (scene.length < 15) return null;
|
||||||
const edges = sanitizeEdges(anchor.edges);
|
const edges = sanitizeEdges(anchor.edges);
|
||||||
const where = String(anchor.where || '').trim();
|
const where = String(anchor.where || '').trim();
|
||||||
|
const quality = calcAtomQuality(scene, edges, where);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
atomId: `atom-${aiFloor}-${idx}`,
|
atomId: `atom-${aiFloor}-${idx}`,
|
||||||
@@ -139,6 +182,7 @@ function anchorToAtom(anchor, aiFloor, idx) {
|
|||||||
// ═══ 图结构层(扩散的 key) ═══
|
// ═══ 图结构层(扩散的 key) ═══
|
||||||
edges,
|
edges,
|
||||||
where,
|
where,
|
||||||
|
quality,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,20 +47,23 @@ const CONFIG = {
|
|||||||
MAX_ITER: 50, // hard iteration cap (typically converges in 15-25)
|
MAX_ITER: 50, // hard iteration cap (typically converges in 15-25)
|
||||||
|
|
||||||
// Edge weight channel coefficients
|
// Edge weight channel coefficients
|
||||||
// No standalone WHO channel: rely on interaction/action/location only.
|
// Candidate generation uses WHAT/HOW only.
|
||||||
|
// WHO/WHERE are reweight-only signals.
|
||||||
GAMMA: {
|
GAMMA: {
|
||||||
what: 0.55, // interaction pair overlap — Szymkiewicz-Simpson
|
what: 0.45, // interaction pair overlap — Szymkiewicz-Simpson
|
||||||
where: 0.15, // location exact match — binary
|
|
||||||
how: 0.30, // action-term co-occurrence — Jaccard
|
how: 0.30, // action-term co-occurrence — Jaccard
|
||||||
|
who: 0.15, // endpoint entity overlap — Jaccard (reweight-only)
|
||||||
|
where: 0.10, // location exact match — damped (reweight-only)
|
||||||
},
|
},
|
||||||
WHERE_MAX_GROUP_SIZE: 16, // skip location-only pair expansion for over-common places
|
WHERE_MAX_GROUP_SIZE: 16, // skip location-only pair expansion for over-common places
|
||||||
WHERE_FREQ_DAMP_PIVOT: 6, // location freq <= pivot keeps full WHERE score
|
WHERE_FREQ_DAMP_PIVOT: 6, // location freq <= pivot keeps full WHERE score
|
||||||
WHERE_FREQ_DAMP_MIN: 0.20, // lower bound for damped WHERE contribution
|
WHERE_FREQ_DAMP_MIN: 0.20, // lower bound for damped WHERE contribution
|
||||||
|
HOW_MAX_GROUP_SIZE: 24, // skip ultra-common action terms to avoid dense pair explosion
|
||||||
|
|
||||||
// Post-verification (Cosine Gate)
|
// Post-verification (Cosine Gate)
|
||||||
COSINE_GATE: 0.45, // min cosine(queryVector, stateVector)
|
COSINE_GATE: 0.48, // min cosine(queryVector, stateVector)
|
||||||
SCORE_FLOOR: 0.10, // min finalScore = PPR_normalized × cosine
|
SCORE_FLOOR: 0.12, // min finalScore = PPR_normalized × cosine
|
||||||
DIFFUSION_CAP: 100, // max diffused nodes (excluding seeds)
|
DIFFUSION_CAP: 80, // max diffused nodes (excluding seeds)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -226,25 +229,27 @@ function extractAllFeatures(allAtoms, excludeEntities = new Set()) {
|
|||||||
/**
|
/**
|
||||||
* Build inverted index: value → list of atom indices
|
* Build inverted index: value → list of atom indices
|
||||||
* @param {object[]} features
|
* @param {object[]} features
|
||||||
* @returns {{ entityIndex: Map, locationIndex: Map }}
|
* @returns {{ whatIndex: Map, howIndex: Map, locationFreq: Map }}
|
||||||
*/
|
*/
|
||||||
function buildInvertedIndices(features) {
|
function buildInvertedIndices(features) {
|
||||||
const entityIndex = new Map();
|
const whatIndex = new Map();
|
||||||
const locationIndex = new Map();
|
const howIndex = new Map();
|
||||||
|
const locationFreq = new Map();
|
||||||
|
|
||||||
for (let i = 0; i < features.length; i++) {
|
for (let i = 0; i < features.length; i++) {
|
||||||
for (const e of features[i].entities) {
|
for (const pair of features[i].interactionPairs) {
|
||||||
if (!entityIndex.has(e)) entityIndex.set(e, []);
|
if (!whatIndex.has(pair)) whatIndex.set(pair, []);
|
||||||
entityIndex.get(e).push(i);
|
whatIndex.get(pair).push(i);
|
||||||
|
}
|
||||||
|
for (const action of features[i].actionTerms) {
|
||||||
|
if (!howIndex.has(action)) howIndex.set(action, []);
|
||||||
|
howIndex.get(action).push(i);
|
||||||
}
|
}
|
||||||
const loc = features[i].location;
|
const loc = features[i].location;
|
||||||
if (loc) {
|
if (loc) locationFreq.set(loc, (locationFreq.get(loc) || 0) + 1);
|
||||||
if (!locationIndex.has(loc)) locationIndex.set(loc, []);
|
|
||||||
locationIndex.get(loc).push(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { entityIndex, locationIndex };
|
return { whatIndex, howIndex, locationFreq };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -277,30 +282,32 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
|
|||||||
const T0 = performance.now();
|
const T0 = performance.now();
|
||||||
|
|
||||||
const features = extractAllFeatures(allAtoms, excludeEntities);
|
const features = extractAllFeatures(allAtoms, excludeEntities);
|
||||||
const { entityIndex, locationIndex } = buildInvertedIndices(features);
|
const { whatIndex, howIndex, locationFreq } = buildInvertedIndices(features);
|
||||||
const locationFreq = new Map();
|
|
||||||
for (const [loc, indices] of locationIndex.entries()) {
|
|
||||||
locationFreq.set(loc, indices.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Candidate pairs: share ≥1 entity or same location
|
// Candidate pairs: only WHAT/HOW can create edges
|
||||||
|
const pairSetByWhat = new Set();
|
||||||
|
const pairSetByHow = new Set();
|
||||||
const pairSet = new Set();
|
const pairSet = new Set();
|
||||||
collectPairsFromIndex(entityIndex, pairSet, N);
|
collectPairsFromIndex(whatIndex, pairSetByWhat, N);
|
||||||
let skippedLocationGroups = 0;
|
let skippedHowGroups = 0;
|
||||||
for (const [loc, indices] of locationIndex.entries()) {
|
for (const [term, indices] of howIndex.entries()) {
|
||||||
if (!loc) continue;
|
if (!term) continue;
|
||||||
if (indices.length > CONFIG.WHERE_MAX_GROUP_SIZE) {
|
if (indices.length > CONFIG.HOW_MAX_GROUP_SIZE) {
|
||||||
skippedLocationGroups++;
|
skippedHowGroups++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const oneLocMap = new Map([[loc, indices]]);
|
const oneHowMap = new Map([[term, indices]]);
|
||||||
collectPairsFromIndex(oneLocMap, pairSet, N);
|
collectPairsFromIndex(oneHowMap, pairSetByHow, N);
|
||||||
}
|
}
|
||||||
|
for (const p of pairSetByWhat) pairSet.add(p);
|
||||||
|
for (const p of pairSetByHow) pairSet.add(p);
|
||||||
|
|
||||||
// Compute three-channel edge weights for all candidates
|
// Compute edge weights for all candidates
|
||||||
const neighbors = Array.from({ length: N }, () => []);
|
const neighbors = Array.from({ length: N }, () => []);
|
||||||
let edgeCount = 0;
|
let edgeCount = 0;
|
||||||
const channelStats = { what: 0, where: 0, how: 0 };
|
const channelStats = { what: 0, where: 0, how: 0, who: 0 };
|
||||||
|
let reweightWhoUsed = 0;
|
||||||
|
let reweightWhereUsed = 0;
|
||||||
|
|
||||||
for (const packed of pairSet) {
|
for (const packed of pairSet) {
|
||||||
const i = Math.floor(packed / N);
|
const i = Math.floor(packed / N);
|
||||||
@@ -310,6 +317,8 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
|
|||||||
const fj = features[j];
|
const fj = features[j];
|
||||||
|
|
||||||
const wWhat = overlapCoefficient(fi.interactionPairs, fj.interactionPairs);
|
const wWhat = overlapCoefficient(fi.interactionPairs, fj.interactionPairs);
|
||||||
|
const wHow = jaccard(fi.actionTerms, fj.actionTerms);
|
||||||
|
const wWho = jaccard(fi.entities, fj.entities);
|
||||||
let wWhere = 0.0;
|
let wWhere = 0.0;
|
||||||
if (fi.location && fi.location === fj.location) {
|
if (fi.location && fi.location === fj.location) {
|
||||||
const freq = locationFreq.get(fi.location) || 1;
|
const freq = locationFreq.get(fi.location) || 1;
|
||||||
@@ -319,12 +328,12 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
|
|||||||
);
|
);
|
||||||
wWhere = damp;
|
wWhere = damp;
|
||||||
}
|
}
|
||||||
const wHow = jaccard(fi.actionTerms, fj.actionTerms);
|
|
||||||
|
|
||||||
const weight =
|
const weight =
|
||||||
CONFIG.GAMMA.what * wWhat +
|
CONFIG.GAMMA.what * wWhat +
|
||||||
CONFIG.GAMMA.where * wWhere +
|
CONFIG.GAMMA.how * wHow +
|
||||||
CONFIG.GAMMA.how * wHow;
|
CONFIG.GAMMA.who * wWho +
|
||||||
|
CONFIG.GAMMA.where * wWhere;
|
||||||
|
|
||||||
if (weight > 0) {
|
if (weight > 0) {
|
||||||
neighbors[i].push({ target: j, weight });
|
neighbors[i].push({ target: j, weight });
|
||||||
@@ -332,8 +341,11 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
|
|||||||
edgeCount++;
|
edgeCount++;
|
||||||
|
|
||||||
if (wWhat > 0) channelStats.what++;
|
if (wWhat > 0) channelStats.what++;
|
||||||
if (wWhere > 0) channelStats.where++;
|
|
||||||
if (wHow > 0) channelStats.how++;
|
if (wHow > 0) channelStats.how++;
|
||||||
|
if (wWho > 0) channelStats.who++;
|
||||||
|
if (wWhere > 0) channelStats.where++;
|
||||||
|
if (wWho > 0) reweightWhoUsed++;
|
||||||
|
if (wWhere > 0) reweightWhereUsed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,12 +353,28 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
|
|||||||
|
|
||||||
xbLog.info(MODULE_ID,
|
xbLog.info(MODULE_ID,
|
||||||
`Graph: ${N} nodes, ${edgeCount} edges ` +
|
`Graph: ${N} nodes, ${edgeCount} edges ` +
|
||||||
`(what=${channelStats.what} where=${channelStats.where} how=${channelStats.how}) ` +
|
`(candidate_by_what=${pairSetByWhat.size} candidate_by_how=${pairSetByHow.size}) ` +
|
||||||
`(whereSkippedGroups=${skippedLocationGroups}) ` +
|
`(what=${channelStats.what} how=${channelStats.how} who=${channelStats.who} where=${channelStats.where}) ` +
|
||||||
|
`(reweight_who_used=${reweightWhoUsed} reweight_where_used=${reweightWhereUsed}) ` +
|
||||||
|
`(howSkippedGroups=${skippedHowGroups}) ` +
|
||||||
`(${buildTime}ms)`
|
`(${buildTime}ms)`
|
||||||
);
|
);
|
||||||
|
|
||||||
return { neighbors, edgeCount, channelStats, buildTime };
|
const totalPairs = N > 1 ? (N * (N - 1)) / 2 : 0;
|
||||||
|
const edgeDensity = totalPairs > 0 ? Number((edgeCount / totalPairs * 100).toFixed(2)) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
neighbors,
|
||||||
|
edgeCount,
|
||||||
|
channelStats,
|
||||||
|
buildTime,
|
||||||
|
candidatePairs: pairSet.size,
|
||||||
|
pairsFromWhat: pairSetByWhat.size,
|
||||||
|
pairsFromHow: pairSetByHow.size,
|
||||||
|
reweightWhoUsed,
|
||||||
|
reweightWhereUsed,
|
||||||
|
edgeDensity,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -673,6 +701,12 @@ export function diffuseFromSeeds(seeds, allAtoms, stateVectors, queryVector, met
|
|||||||
graphNodes: N,
|
graphNodes: N,
|
||||||
graphEdges: 0,
|
graphEdges: 0,
|
||||||
channelStats: graph.channelStats,
|
channelStats: graph.channelStats,
|
||||||
|
candidatePairs: graph.candidatePairs,
|
||||||
|
pairsFromWhat: graph.pairsFromWhat,
|
||||||
|
pairsFromHow: graph.pairsFromHow,
|
||||||
|
edgeDensity: graph.edgeDensity,
|
||||||
|
reweightWhoUsed: graph.reweightWhoUsed,
|
||||||
|
reweightWhereUsed: graph.reweightWhereUsed,
|
||||||
time: graph.buildTime,
|
time: graph.buildTime,
|
||||||
});
|
});
|
||||||
xbLog.info(MODULE_ID, 'No graph edges — skipping diffusion');
|
xbLog.info(MODULE_ID, 'No graph edges — skipping diffusion');
|
||||||
@@ -719,6 +753,12 @@ export function diffuseFromSeeds(seeds, allAtoms, stateVectors, queryVector, met
|
|||||||
graphNodes: N,
|
graphNodes: N,
|
||||||
graphEdges: graph.edgeCount,
|
graphEdges: graph.edgeCount,
|
||||||
channelStats: graph.channelStats,
|
channelStats: graph.channelStats,
|
||||||
|
candidatePairs: graph.candidatePairs,
|
||||||
|
pairsFromWhat: graph.pairsFromWhat,
|
||||||
|
pairsFromHow: graph.pairsFromHow,
|
||||||
|
edgeDensity: graph.edgeDensity,
|
||||||
|
reweightWhoUsed: graph.reweightWhoUsed,
|
||||||
|
reweightWhereUsed: graph.reweightWhereUsed,
|
||||||
buildTime: graph.buildTime,
|
buildTime: graph.buildTime,
|
||||||
iterations,
|
iterations,
|
||||||
convergenceError: finalError,
|
convergenceError: finalError,
|
||||||
@@ -726,6 +766,9 @@ export function diffuseFromSeeds(seeds, allAtoms, stateVectors, queryVector, met
|
|||||||
cosineGatePassed: gateStats.passed,
|
cosineGatePassed: gateStats.passed,
|
||||||
cosineGateFiltered: gateStats.filtered,
|
cosineGateFiltered: gateStats.filtered,
|
||||||
cosineGateNoVector: gateStats.noVector,
|
cosineGateNoVector: gateStats.noVector,
|
||||||
|
postGatePassRate: pprActivated > 0
|
||||||
|
? Math.round((gateStats.passed / pprActivated) * 100)
|
||||||
|
: 0,
|
||||||
finalCount: diffused.length,
|
finalCount: diffused.length,
|
||||||
scoreDistribution: diffused.length > 0
|
scoreDistribution: diffused.length > 0
|
||||||
? calcScoreStats(diffused.map(d => d.finalScore))
|
? calcScoreStats(diffused.map(d => d.finalScore))
|
||||||
@@ -783,7 +826,14 @@ function fillMetricsEmpty(metrics) {
|
|||||||
cosineGateNoVector: 0,
|
cosineGateNoVector: 0,
|
||||||
finalCount: 0,
|
finalCount: 0,
|
||||||
scoreDistribution: { min: 0, max: 0, mean: 0 },
|
scoreDistribution: { min: 0, max: 0, mean: 0 },
|
||||||
byChannel: { what: 0, where: 0, how: 0 },
|
byChannel: { what: 0, where: 0, how: 0, who: 0 },
|
||||||
|
candidatePairs: 0,
|
||||||
|
pairsFromWhat: 0,
|
||||||
|
pairsFromHow: 0,
|
||||||
|
edgeDensity: 0,
|
||||||
|
reweightWhoUsed: 0,
|
||||||
|
reweightWhereUsed: 0,
|
||||||
|
postGatePassRate: 0,
|
||||||
time: 0,
|
time: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -803,9 +853,16 @@ function fillMetrics(metrics, data) {
|
|||||||
cosineGatePassed: data.cosineGatePassed || 0,
|
cosineGatePassed: data.cosineGatePassed || 0,
|
||||||
cosineGateFiltered: data.cosineGateFiltered || 0,
|
cosineGateFiltered: data.cosineGateFiltered || 0,
|
||||||
cosineGateNoVector: data.cosineGateNoVector || 0,
|
cosineGateNoVector: data.cosineGateNoVector || 0,
|
||||||
|
postGatePassRate: data.postGatePassRate || 0,
|
||||||
finalCount: data.finalCount || 0,
|
finalCount: data.finalCount || 0,
|
||||||
scoreDistribution: data.scoreDistribution || { min: 0, max: 0, mean: 0 },
|
scoreDistribution: data.scoreDistribution || { min: 0, max: 0, mean: 0 },
|
||||||
byChannel: data.channelStats || { what: 0, where: 0, how: 0 },
|
byChannel: data.channelStats || { what: 0, where: 0, how: 0, who: 0 },
|
||||||
|
candidatePairs: data.candidatePairs || 0,
|
||||||
|
pairsFromWhat: data.pairsFromWhat || 0,
|
||||||
|
pairsFromHow: data.pairsFromHow || 0,
|
||||||
|
edgeDensity: data.edgeDensity || 0,
|
||||||
|
reweightWhoUsed: data.reweightWhoUsed || 0,
|
||||||
|
reweightWhereUsed: data.reweightWhereUsed || 0,
|
||||||
time: data.time || 0,
|
time: data.time || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,15 +118,22 @@ export function createMetrics() {
|
|||||||
seedCount: 0,
|
seedCount: 0,
|
||||||
graphNodes: 0,
|
graphNodes: 0,
|
||||||
graphEdges: 0,
|
graphEdges: 0,
|
||||||
|
candidatePairs: 0,
|
||||||
|
pairsFromWhat: 0,
|
||||||
|
pairsFromHow: 0,
|
||||||
|
edgeDensity: 0,
|
||||||
|
reweightWhoUsed: 0,
|
||||||
|
reweightWhereUsed: 0,
|
||||||
iterations: 0,
|
iterations: 0,
|
||||||
convergenceError: 0,
|
convergenceError: 0,
|
||||||
pprActivated: 0,
|
pprActivated: 0,
|
||||||
cosineGatePassed: 0,
|
cosineGatePassed: 0,
|
||||||
cosineGateFiltered: 0,
|
cosineGateFiltered: 0,
|
||||||
cosineGateNoVector: 0,
|
cosineGateNoVector: 0,
|
||||||
|
postGatePassRate: 0,
|
||||||
finalCount: 0,
|
finalCount: 0,
|
||||||
scoreDistribution: { min: 0, max: 0, mean: 0 },
|
scoreDistribution: { min: 0, max: 0, mean: 0 },
|
||||||
byChannel: { what: 0, where: 0, how: 0 },
|
byChannel: { what: 0, where: 0, how: 0, who: 0 },
|
||||||
time: 0,
|
time: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -169,6 +176,7 @@ export function createMetrics() {
|
|||||||
eventPrecisionProxy: 0,
|
eventPrecisionProxy: 0,
|
||||||
l1AttachRate: 0,
|
l1AttachRate: 0,
|
||||||
rerankRetentionRate: 0,
|
rerankRetentionRate: 0,
|
||||||
|
diffusionEffectiveRate: 0,
|
||||||
potentialIssues: [],
|
potentialIssues: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -368,9 +376,12 @@ export function formatMetricsLog(metrics) {
|
|||||||
lines.push('[Diffusion] PPR Spreading Activation');
|
lines.push('[Diffusion] PPR Spreading Activation');
|
||||||
lines.push(`├─ seeds: ${m.diffusion.seedCount}`);
|
lines.push(`├─ seeds: ${m.diffusion.seedCount}`);
|
||||||
lines.push(`├─ graph: ${m.diffusion.graphNodes} nodes, ${m.diffusion.graphEdges} edges`);
|
lines.push(`├─ graph: ${m.diffusion.graphNodes} nodes, ${m.diffusion.graphEdges} edges`);
|
||||||
|
lines.push(`├─ candidate_pairs: ${m.diffusion.candidatePairs || 0} (what=${m.diffusion.pairsFromWhat || 0}, how=${m.diffusion.pairsFromHow || 0})`);
|
||||||
|
lines.push(`├─ edge_density: ${m.diffusion.edgeDensity || 0}%`);
|
||||||
if (m.diffusion.graphEdges > 0) {
|
if (m.diffusion.graphEdges > 0) {
|
||||||
const ch = m.diffusion.byChannel || {};
|
const ch = m.diffusion.byChannel || {};
|
||||||
lines.push(`│ └─ by_channel: what=${ch.what || 0}, where=${ch.where || 0}, how=${ch.how || 0}`);
|
lines.push(`│ ├─ by_channel: what=${ch.what || 0}, how=${ch.how || 0}, who=${ch.who || 0}, where=${ch.where || 0}`);
|
||||||
|
lines.push(`│ └─ reweight_used: who=${m.diffusion.reweightWhoUsed || 0}, where=${m.diffusion.reweightWhereUsed || 0}`);
|
||||||
}
|
}
|
||||||
if (m.diffusion.iterations > 0) {
|
if (m.diffusion.iterations > 0) {
|
||||||
lines.push(`├─ ppr: ${m.diffusion.iterations} iterations, ε=${Number(m.diffusion.convergenceError).toExponential(1)}`);
|
lines.push(`├─ ppr: ${m.diffusion.iterations} iterations, ε=${Number(m.diffusion.convergenceError).toExponential(1)}`);
|
||||||
@@ -378,8 +389,10 @@ export function formatMetricsLog(metrics) {
|
|||||||
lines.push(`├─ activated (excl seeds): ${m.diffusion.pprActivated}`);
|
lines.push(`├─ activated (excl seeds): ${m.diffusion.pprActivated}`);
|
||||||
if (m.diffusion.pprActivated > 0) {
|
if (m.diffusion.pprActivated > 0) {
|
||||||
lines.push(`├─ cosine_gate: ${m.diffusion.cosineGatePassed} passed, ${m.diffusion.cosineGateFiltered} filtered`);
|
lines.push(`├─ cosine_gate: ${m.diffusion.cosineGatePassed} passed, ${m.diffusion.cosineGateFiltered} filtered`);
|
||||||
|
const passPrefix = m.diffusion.cosineGateNoVector > 0 ? '│ ├─' : '│ └─';
|
||||||
|
lines.push(`${passPrefix} pass_rate: ${m.diffusion.postGatePassRate || 0}%`);
|
||||||
if (m.diffusion.cosineGateNoVector > 0) {
|
if (m.diffusion.cosineGateNoVector > 0) {
|
||||||
lines.push(`│ └─ no_vector: ${m.diffusion.cosineGateNoVector}`);
|
lines.push(`│ ├─ no_vector: ${m.diffusion.cosineGateNoVector}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push(`├─ final_injected: ${m.diffusion.finalCount}`);
|
lines.push(`├─ final_injected: ${m.diffusion.finalCount}`);
|
||||||
@@ -435,6 +448,7 @@ export function formatMetricsLog(metrics) {
|
|||||||
lines.push(`├─ event_precision_proxy: ${m.quality.eventPrecisionProxy}`);
|
lines.push(`├─ event_precision_proxy: ${m.quality.eventPrecisionProxy}`);
|
||||||
lines.push(`├─ l1_attach_rate: ${m.quality.l1AttachRate}%`);
|
lines.push(`├─ l1_attach_rate: ${m.quality.l1AttachRate}%`);
|
||||||
lines.push(`├─ rerank_retention_rate: ${m.quality.rerankRetentionRate}%`);
|
lines.push(`├─ rerank_retention_rate: ${m.quality.rerankRetentionRate}%`);
|
||||||
|
lines.push(`├─ diffusion_effective_rate: ${m.quality.diffusionEffectiveRate}%`);
|
||||||
|
|
||||||
if (m.quality.potentialIssues && m.quality.potentialIssues.length > 0) {
|
if (m.quality.potentialIssues && m.quality.potentialIssues.length > 0) {
|
||||||
lines.push(`└─ potential_issues:`);
|
lines.push(`└─ potential_issues:`);
|
||||||
@@ -642,6 +656,10 @@ export function detectIssues(metrics) {
|
|||||||
issues.push('All PPR-activated nodes failed cosine gate - graph structure diverged from query semantics');
|
issues.push('All PPR-activated nodes failed cosine gate - graph structure diverged from query semantics');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.quality.diffusionEffectiveRate = m.diffusion.pprActivated > 0
|
||||||
|
? Math.round((m.diffusion.finalCount / m.diffusion.pprActivated) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
if (m.diffusion.cosineGateNoVector > 5) {
|
if (m.diffusion.cosineGateNoVector > 5) {
|
||||||
issues.push(`${m.diffusion.cosineGateNoVector} PPR nodes missing vectors - L0 vectorization may be incomplete`);
|
issues.push(`${m.diffusion.cosineGateNoVector} PPR nodes missing vectors - L0 vectorization may be incomplete`);
|
||||||
}
|
}
|
||||||
@@ -650,5 +668,9 @@ export function detectIssues(metrics) {
|
|||||||
issues.push(`Slow diffusion (${m.diffusion.time}ms) - graph may be too dense`);
|
issues.push(`Slow diffusion (${m.diffusion.time}ms) - graph may be too dense`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m.diffusion.pprActivated > 0 && (m.diffusion.postGatePassRate < 20 || m.diffusion.postGatePassRate > 60)) {
|
||||||
|
issues.push(`Diffusion post-gate pass rate out of target (${m.diffusion.postGatePassRate}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user