feat: 同批次节点默认弱关联边 + LLM 可增强/移除 + prompt 更新

- extractor: 批次操作完成后统一处理 links,再补默认弱 related 边(0.25)
- extractor: 支持 remove/delete/unlink/invalidate 语义显式移除边
- extractor: update 操作现在也能处理 links
- extractor: 边计数改为仅统计真正新增的边
- prompt: 输出格式示例加 links 字段,补 links/remove 语法说明
- prompt: 行为规则加关联边使用规范段落
- tests: 3 条回归覆盖默认弱边、显式覆盖、显式移除
This commit is contained in:
Youzini-afk
2026-04-20 20:27:15 +08:00
parent 4a95158fc1
commit d3c199fee1
3 changed files with 385 additions and 60 deletions

View File

@@ -2767,6 +2767,156 @@ async function testExtractorPropagatesLlmFailureReason() {
}
}
async function testExtractorAddsWeakDefaultRelatedEdgeForSameBatchNodes() {
const graph = createEmptyGraph();
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
operations: [
{
type: "event",
id: "evt1",
title: "钟楼初见",
summary: "两人在钟楼第一次正式见面。",
participants: "艾琳, 用户",
},
{
type: "event",
id: "evt2",
title: "钟楼密谈",
summary: "见面后立刻进入短暂密谈。",
participants: "艾琳, 用户",
},
],
};
},
},
});
try {
const result = await extractMemories({
graph,
messages: [{ seq: 10, role: "assistant", content: "测试批次默认弱连边" }],
startSeq: 10,
endSeq: 10,
schema,
embeddingConfig: null,
settings: {},
});
assert.equal(result.success, true);
assert.equal(result.newNodes, 2);
assert.equal(result.newEdges, 1);
const activeEdges = graph.edges.filter((edge) => !edge.invalidAt && !edge.expiredAt);
assert.equal(activeEdges.length, 1);
assert.equal(activeEdges[0]?.relation, "related");
assert.equal(activeEdges[0]?.strength, 0.25);
} finally {
restoreOverrides();
}
}
async function testExtractorExplicitLinksOverrideDefaultBatchWeakEdge() {
const graph = createEmptyGraph();
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
operations: [
{
type: "event",
id: "evt1",
title: "档案室发现",
summary: "发现了一份关键档案。",
participants: "艾琳",
links: [{ targetRef: "evt2", relation: "related", strength: 0.91 }],
},
{
type: "event",
id: "evt2",
title: "档案内容核对",
summary: "紧接着对档案内容进行核对。",
participants: "艾琳",
},
],
};
},
},
});
try {
const result = await extractMemories({
graph,
messages: [{ seq: 11, role: "assistant", content: "测试显式 links 覆盖默认弱边" }],
startSeq: 11,
endSeq: 11,
schema,
embeddingConfig: null,
settings: {},
});
assert.equal(result.success, true);
assert.equal(result.newNodes, 2);
assert.equal(result.newEdges, 1);
const activeEdges = graph.edges.filter((edge) => !edge.invalidAt && !edge.expiredAt);
assert.equal(activeEdges.length, 1);
assert.equal(activeEdges[0]?.relation, "related");
assert.equal(activeEdges[0]?.strength, 0.91);
} finally {
restoreOverrides();
}
}
async function testExtractorExplicitRemoveSuppressesDefaultBatchWeakEdge() {
const graph = createEmptyGraph();
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
return {
operations: [
{
type: "event",
id: "evt1",
title: "花园交错",
summary: "两条线索在花园中短暂交错。",
participants: "艾琳, 守卫",
links: [{ targetRef: "evt2", relation: "related", remove: true }],
},
{
type: "event",
id: "evt2",
title: "花园分流",
summary: "随后两条线索各自分流。",
participants: "艾琳, 守卫",
},
],
};
},
},
});
try {
const result = await extractMemories({
graph,
messages: [{ seq: 12, role: "assistant", content: "测试显式移除默认弱边" }],
startSeq: 12,
endSeq: 12,
schema,
embeddingConfig: null,
settings: {},
});
assert.equal(result.success, true);
assert.equal(result.newNodes, 2);
assert.equal(result.newEdges, 0);
const activeEdges = graph.edges.filter((edge) => !edge.invalidAt && !edge.expiredAt);
assert.equal(activeEdges.length, 0);
} finally {
restoreOverrides();
}
}
async function testConsolidatorMergeUpdatesSeqRange() {
const graph = createEmptyGraph();
const target = createNode({
@@ -7208,6 +7358,9 @@ await testExtractorFailsOnUnknownOperation();
await testExtractorNormalizesFlatCreateOperation();
await testExtractorNormalizesArrayPayloadAndPreservesScopeField();
await testExtractorPropagatesLlmFailureReason();
await testExtractorAddsWeakDefaultRelatedEdgeForSameBatchNodes();
await testExtractorExplicitLinksOverrideDefaultBatchWeakEdge();
await testExtractorExplicitRemoveSuppressesDefaultBatchWeakEdge();
await testConsolidatorMergeUpdatesSeqRange();
await testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing();
await testBatchJournalVectorDeltaCapturesRecoveryFields();