Refine diffusion graph channels and drop legacy who compatibility

This commit is contained in:
2026-02-13 15:56:22 +08:00
parent 9ba120364c
commit 6aa1547d6f
6 changed files with 110 additions and 180 deletions

View File

@@ -3,8 +3,6 @@
//
// 设计依据:
// - BGE-M3 (BAAI, 2024): 自然语言段落检索精度最高 → semantic = 纯自然语言
// - Interpersonal Circumplex (Kiesler, 1983): 权力轴+情感轴 → dynamics 枚举
// - Labov Narrative Structure (1972): 叙事功能轴 → dynamics 枚举补充
// - TransE (Bordes, 2013): s/t/r 三元组方向性 → edges 格式
//
// 每楼层 1-2 个场景锚点非碎片原子60-100 字场景摘要
@@ -32,26 +30,6 @@ export function isBatchCancelled() {
return batchCancelled;
}
// ============================================================================
// dynamics 封闭枚举8 个标签,两轴四象限 + 叙事轴)
// ============================================================================
const VALID_DYNAMICS = new Set([
// 权力轴 (Interpersonal Circumplex: Dominance-Submission)
'支配', // 控制、命令、审视、威慑、主导
'让渡', // 顺从、服从、屈服、被动、配合
// 情感轴 (Interpersonal Circumplex: Hostility-Friendliness)
'亲密', // 温柔、关怀、依赖、信任、连接
'敌意', // 对抗、拒绝、攻击、嘲讽、排斥
// 叙事轴 (Labov Narrative Structure)
'揭示', // 真相、发现、告白、暴露、秘密
'决意', // 选择、承诺、放弃、宣言、转折
'张力', // 悬念、对峙、暗涌、不安、等待
'丧失', // 分离、死亡、破碎、遗憾、崩塌
]);
// ============================================================================
// L0 提取 Prompt
// ============================================================================
@@ -68,9 +46,7 @@ const SYSTEM_PROMPT = `你是场景摘要器。从一轮对话中提取1-2个场
{"anchors":[
{
"scene": "60-100字完整场景描述",
"who": ["角色名1","角色名2"],
"edges": [{"s":"施事方","t":"受事方","r":"互动行为"}],
"dynamics": ["标签"],
"where": "地点"
}
]}
@@ -81,20 +57,12 @@ const SYSTEM_PROMPT = `你是场景摘要器。从一轮对话中提取1-2个场
- 读者只看 scene 就能复原这一幕
- 60-100字信息密集但流畅
## who
- 参与互动的角色正式名称,不用代词或别称
- 只从正文内容中识别角色名,不要把标签名(如 user、assistant当作角色
## edges关系三元组
- s=施事方 t=受事方 r=互动行为10-15字
- s/t 必须是参与互动的角色正式名称,不用代词或别称
- 只从正文内容中识别角色名,不要把标签名(如 user、assistant当作角色
- 每个锚点 1-3 条
## dynamics封闭枚举选0-2个
权力轴:支配(控制/命令/审视) | 让渡(顺从/服从/屈服)
情感轴:亲密(温柔/信任/连接) | 敌意(对抗/拒绝/攻击)
叙事轴:揭示(真相/秘密) | 决意(选择/承诺) | 张力(对峙/不安) | 丧失(分离/破碎)
纯日常无明显模式时 dynamics 为 []
## where
- 场景地点,无明确地点时空字符串
@@ -107,7 +75,7 @@ const SYSTEM_PROMPT = `你是场景摘要器。从一轮对话中提取1-2个场
## 示例
输入:艾拉在火山口举起圣剑刺穿古龙心脏,龙血溅满她的铠甲,她跪倒在地痛哭
输出:
{"anchors":[{"scene":"火山口上艾拉举起圣剑刺穿古龙的心脏,龙血溅满铠甲,古龙轰然倒地,艾拉跪倒在滚烫的岩石上痛哭,完成了她不得不做的弑杀","who":["艾拉","古龙"],"edges":[{"s":"艾拉","t":"古龙","r":"以圣剑刺穿心脏"}],"dynamics":["决意","丧失"],"where":"火山口"}]}`;
{"anchors":[{"scene":"火山口上艾拉举起圣剑刺穿古龙的心脏,龙血溅满铠甲,古龙轰然倒地,艾拉跪倒在滚烫的岩石上痛哭,完成了她不得不做的弑杀","edges":[{"s":"艾拉","t":"古龙","r":"以圣剑刺穿心脏"}],"where":"火山口"}]}`;
const JSON_PREFILL = '{"anchors":[';
@@ -121,19 +89,6 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// 清洗与构建
// ============================================================================
/**
* 清洗 dynamics 标签,只保留合法枚举值
* @param {string[]} raw
* @returns {string[]}
*/
function sanitizeDynamics(raw) {
if (!Array.isArray(raw)) return [];
return raw
.map(d => String(d || '').trim())
.filter(d => VALID_DYNAMICS.has(d))
.slice(0, 2);
}
/**
* 清洗 edges 三元组
* @param {object[]} raw
@@ -148,26 +103,8 @@ function sanitizeEdges(raw) {
t: String(e.t || '').trim(),
r: String(e.r || '').trim().slice(0, 30),
}))
.filter(e => e.s && e.t && e.r)
.slice(0, 3);
}
/**
* 清洗 who 列表
* @param {string[]} raw
* @returns {string[]}
*/
function sanitizeWho(raw) {
if (!Array.isArray(raw)) return [];
const seen = new Set();
return raw
.map(w => String(w || '').trim())
.filter(w => {
if (!w || w.length < 1 || seen.has(w)) return false;
seen.add(w);
return true;
})
.slice(0, 6);
.filter(e => e.s && e.t && e.r)
.slice(0, 3);
}
/**
@@ -186,10 +123,7 @@ function anchorToAtom(anchor, aiFloor, idx) {
// scene 过短(< 15 字)可能是噪音
if (scene.length < 15) return null;
const who = sanitizeWho(anchor.who);
const edges = sanitizeEdges(anchor.edges);
const dynamics = sanitizeDynamics(anchor.dynamics);
const where = String(anchor.where || '').trim();
return {
@@ -201,9 +135,7 @@ function anchorToAtom(anchor, aiFloor, idx) {
semantic: scene,
// ═══ 图结构层(扩散的 key ═══
who,
edges,
dynamics,
where,
};
}
@@ -268,7 +200,7 @@ async function extractAtomsForRoundWithRetry(userMessage, aiMessage, aiFloor, op
}
// 兼容:优先 anchors回退 atoms
const rawAnchors = parsed?.anchors || parsed?.atoms;
const rawAnchors = parsed?.anchors;
if (!rawAnchors || !Array.isArray(rawAnchors)) {
if (attempt < RETRY_COUNT) {
await sleep(RETRY_DELAY);
@@ -395,3 +327,4 @@ export async function batchExtractAtoms(chat, onProgress) {
return allAtoms;
}