refactor(prompt): structure constraints people/world and harden formatting

chore(diffusion): raise DIFFUSION_CAP to 100
This commit is contained in:
2026-02-14 21:30:57 +08:00
parent e8383570e0
commit 300ed2798f
2 changed files with 117 additions and 32 deletions

View File

@@ -303,26 +303,103 @@ function filterConstraintsByRelevance(facts, focusEntities, knownCharacters) {
}
/**
* 格式化 constraints 用于注入
* @param {object[]} facts - 所有 facts
* @param {string[]} focusEntities - 焦点实体
* @param {Set<string>} 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<string, string>} 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 [];
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}`;
const selectedEvents = recallResult?.events || [];
for (const item of selectedEvents) {
const participants = item?.event?.participants || [];
for (const p of participants) add(p);
}
return `- ${f.s}${f.p}: ${f.o}${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<string, string>} peopleDict
* @returns {{ people: Map<string, object[]>, 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<string, object[]>, 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)}`);
}
}
}
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;

View File

@@ -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)
};
// ═══════════════════════════════════════════════════════════════════════════