From 300ed2798f08ec05018edbdbf9b601d873e8b25f Mon Sep 17 00:00:00 2001 From: bielie Date: Sat, 14 Feb 2026 21:30:57 +0800 Subject: [PATCH] refactor(prompt): structure constraints people/world and harden formatting chore(diffusion): raise DIFFUSION_CAP to 100 --- modules/story-summary/generate/prompt.js | 147 ++++++++++++++---- .../vector/retrieval/diffusion.js | 2 +- 2 files changed, 117 insertions(+), 32 deletions(-) diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js index aa971cd..86beb03 100644 --- a/modules/story-summary/generate/prompt.js +++ b/modules/story-summary/generate/prompt.js @@ -303,26 +303,103 @@ function filterConstraintsByRelevance(facts, focusEntities, knownCharacters) { } /** - * 格式化 constraints 用于注入 - * @param {object[]} facts - 所有 facts - * @param {string[]} focusEntities - 焦点实体 - * @param {Set} knownCharacters - 已知角色 - * @returns {string[]} 格式化后的行 + * Build people dictionary for constraints display. + * Primary source: selected event participants; fallback: focus entities. + * + * @param {object|null} recallResult + * @param {string[]} focusEntities + * @returns {Map} normalize(name) -> display name */ -function formatConstraintsForInjection(facts, focusEntities, knownCharacters) { - const filtered = filterConstraintsByRelevance(facts, focusEntities, knownCharacters); +function buildConstraintPeopleDict(recallResult, focusEntities = []) { + const dict = new Map(); + const add = (raw) => { + const display = String(raw || '').trim(); + const key = normalize(display); + if (!display || !key) return; + if (!dict.has(key)) dict.set(key, display); + }; - if (!filtered.length) return []; + const selectedEvents = recallResult?.events || []; + for (const item of selectedEvents) { + const participants = item?.event?.participants || []; + for (const p of participants) add(p); + } - return filtered - .sort((a, b) => (b.since || 0) - (a.since || 0)) - .map(f => { - const since = f.since ? ` (#${f.since + 1})` : ''; - if (isRelationFact(f) && f.trend) { - return `- ${f.s} ${f.p}: ${f.o} [${f.trend}]${since}`; + if (dict.size === 0) { + for (const f of (focusEntities || [])) add(f); + } + + return dict; +} + +/** + * Group filtered constraints into people/world buckets. + * @param {object[]} facts + * @param {Map} peopleDict + * @returns {{ people: Map, world: object[] }} + */ +function groupConstraintsForDisplay(facts, peopleDict) { + const people = new Map(); + const world = []; + + for (const f of (facts || [])) { + const subjectNorm = normalize(f?.s); + const displayName = peopleDict.get(subjectNorm); + if (displayName) { + if (!people.has(displayName)) people.set(displayName, []); + people.get(displayName).push(f); + } else { + world.push(f); + } + } + + return { people, world }; +} + +function formatConstraintLine(f, includeSubject = false) { + const subject = String(f?.s || '').trim(); + const predicate = String(f?.p || '').trim(); + const object = String(f?.o || '').trim(); + const trendRaw = String(f?.trend || '').trim(); + const hasSince = f?.since !== undefined && f?.since !== null; + const since = hasSince ? ` (#${f.since + 1})` : ''; + const trend = isRelationFact(f) && trendRaw ? ` [${trendRaw}]` : ''; + if (includeSubject) { + return `- ${subject} ${predicate}: ${object}${trend}${since}`; + } + return `- ${predicate}: ${object}${trend}${since}`; +} + +/** + * Render grouped constraints into structured human-readable lines. + * @param {{ people: Map, world: object[] }} grouped + * @returns {string[]} + */ +function formatConstraintsStructured(grouped) { + const lines = []; + const people = grouped?.people || new Map(); + const world = grouped?.world || []; + + if (people.size > 0) { + lines.push('people:'); + for (const [name, facts] of people.entries()) { + lines.push(` ${name}:`); + const sorted = [...facts].sort((a, b) => (b.since || 0) - (a.since || 0)); + for (const f of sorted) { + lines.push(` ${formatConstraintLine(f, false)}`); } - return `- ${f.s}的${f.p}: ${f.o}${since}`; - }); + } + } + + if (world.length > 0) { + lines.push('world:'); + const sortedWorld = [...world].sort((a, b) => (b.since || 0) - (a.since || 0)); + for (const f of sortedWorld) { + lines.push(` ${formatConstraintLine(f, true)}`); + } + } + + return lines; } // ───────────────────────────────────────────────────────────────────────────── @@ -610,18 +687,23 @@ function buildNonVectorPrompt(store) { const data = store.json || {}; const sections = []; - // [Constraints] L3 Facts - const allFacts = getFacts(); - const constraintLines = allFacts - .filter(f => !f.retracted) - .sort((a, b) => (b.since || 0) - (a.since || 0)) - .map(f => { - const since = f.since ? ` (#${f.since + 1})` : ''; - if (isRelationFact(f) && f.trend) { - return `- ${f.s} ${f.p}: ${f.o} [${f.trend}]${since}`; - } - return `- ${f.s}的${f.p}: ${f.o}${since}`; - }); + // [Constraints] L3 Facts (structured: people/world) + const allFacts = getFacts().filter(f => !f.retracted); + const nonVectorPeopleDict = buildConstraintPeopleDict( + { events: data.events || [] }, + [] + ); + const nonVectorFocus = nonVectorPeopleDict.size > 0 + ? [...nonVectorPeopleDict.values()] + : [...getKnownCharacters(store)]; + const nonVectorKnownCharacters = getKnownCharacters(store); + const filteredConstraints = filterConstraintsByRelevance( + allFacts, + nonVectorFocus, + nonVectorKnownCharacters + ); + const groupedConstraints = groupConstraintsForDisplay(filteredConstraints, nonVectorPeopleDict); + const constraintLines = formatConstraintsStructured(groupedConstraints); if (constraintLines.length) { sections.push(`[定了的事] 已确立的事实\n${constraintLines.join("\n")}`); @@ -743,11 +825,14 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities, const allFacts = getFacts(); const knownCharacters = getKnownCharacters(store); - const constraintLines = formatConstraintsForInjection(allFacts, focusEntities, knownCharacters); + const filteredConstraints = filterConstraintsByRelevance(allFacts, focusEntities, knownCharacters); + const constraintPeopleDict = buildConstraintPeopleDict(recallResult, focusEntities); + const groupedConstraints = groupConstraintsForDisplay(filteredConstraints, constraintPeopleDict); + const constraintLines = formatConstraintsStructured(groupedConstraints); if (metrics) { metrics.constraint.total = allFacts.length; - metrics.constraint.filtered = allFacts.length - constraintLines.length; + metrics.constraint.filtered = allFacts.length - filteredConstraints.length; } if (constraintLines.length) { @@ -759,7 +844,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities, total.used += constraintBudget.used; injectionStats.constraint.count = assembled.constraints.lines.length; injectionStats.constraint.tokens = constraintBudget.used; - injectionStats.constraint.filtered = allFacts.length - constraintLines.length; + injectionStats.constraint.filtered = allFacts.length - filteredConstraints.length; if (metrics) { metrics.constraint.injected = assembled.constraints.lines.length; diff --git a/modules/story-summary/vector/retrieval/diffusion.js b/modules/story-summary/vector/retrieval/diffusion.js index 6136dbe..d963c08 100644 --- a/modules/story-summary/vector/retrieval/diffusion.js +++ b/modules/story-summary/vector/retrieval/diffusion.js @@ -63,7 +63,7 @@ const CONFIG = { // Post-verification (Cosine Gate) COSINE_GATE: 0.48, // min cosine(queryVector, stateVector) SCORE_FLOOR: 0.12, // min finalScore = PPR_normalized × cosine - DIFFUSION_CAP: 80, // max diffused nodes (excluding seeds) + DIFFUSION_CAP: 100, // max diffused nodes (excluding seeds) }; // ═══════════════════════════════════════════════════════════════════════════