数据截至 (上游 commit 549301e79f1d)
CORE 时序知识图谱模型 — 具体化三元组 + 双时间轴 + 失效
本章讲什么: CORE 记忆的「数据结构」。为什么一个事实在 CORE 里不是一条边,而是一个独立节点?为什么删除要靠「划掉」而不是真删?这是整个项目最值得借鉴的设计。
1. 先讲直觉:为什么普通三元组不够用
经典知识图谱里,一个事实「John 在 Google 工作」就是一条边:
(John) ──works_at──► (Google) ← 边上挂不下太多东西
问题来了:
- 这条事实何时成立?何时失效?哪段对话说的?有多确信?——边上塞这些属性既别扭又难查。
- 事实变了怎么办? ——删边?那「以前在 Google」的历史就没了。
CORE 的答案:把「事实」本身提升为一等公民节点,叫 Statement(语句)节点。这叫具体化(reification,把关系变成节点)。
2. 具体化三元组:事实是一个节点
在 CORE 里,「John 在 Google 工作」长这样:
(Episode: 「我在 Google 做后端」) ← 出处/证据
│ HAS_PROVENANCE
▼
(Entity:John) ◄─HAS_SUBJECT─ (Statement) ─HAS_OBJECT─► (Entity:Google)
│
HAS_PREDICATE
▼
(Entity: "works_at", type=Predicate)
怎么读这张图: 中间的 Statement 是事实本体,它伸出四只手——指向 subject / predicate / object 三个 Entity,外加一条 HAS_PROVENANCE 连回「说出这事的 Episode」。所有时间戳、aspect、确信度都挂在 Statement 节点上。
注意:连谓词(predicate)也是一个 Entity 节点(type="Predicate"),不是普通字符串。这样「works_at」这个关系本身也能被检索、被合并。
2.1 真实的 Cypher
落库的 Cypher 把这五个关系一次 MERGE 出来(packages/providers/src/graph/neo4j/domains/triple.ts:31-48):
MERGE (episode)-[:HAS_PROVENANCE]->(statement)
MERGE (statement)-[:HAS_SUBJECT]->(subject)
MERGE (statement)-[:HAS_PREDICATE]->(predicate)
MERGE (statement)-[:HAS_OBJECT]->(object)
saveTriple 先存 Statement 和三个 Entity 节点,再建这四条关系(triple.ts:12-63)。读回来时反向走:getTriplesForEpisode 从 Episode 沿 HAS_PROVENANCE 找 Statement,再各找 subject/predicate/object(triple.ts:65-96)。
2.2 Statement 节点上有什么
类型见 packages/types/src/graph/graph.entity.ts:261-276:
| 字段 | 含义 |
|---|---|
fact | 人类可读的事实句子 |
validAt | 事实在现实中何时开始成立 |
invalidAt | 何时失效(null = 仍然有效) |
invalidatedBy | 哪个 Episode 让它失效的(UUID) |
aspect | 分类(Identity/Event/Relationship/…) |
createdAt | 系统何时记录的(与 validAt 分开!) |
3. 双时间轴(bi-temporal):两个「时间」不是一回事
CORE 严格区分两条时间线:
- valid time(有效时间) —— 事实在现实世界里的生命周期:
validAt→invalidAt。 - transaction time(记录时间) —— 系统何时知道这件事:
createdAt。
为什么要分开? 举例:你 6 月 30 日才告诉 AI「我去年 1 月就搬到纽约了」。
validAt= 2025-01(事实从去年起成立)createdAt= 2026-06-30(系统今天才记录)
只有把两者分开,才能正确回答「我去年住哪」和「你什么时候知道的」这类问题。validAt 来自 episode 的 referenceTime,在织三元组时赋值(knowledgeGraph.server.ts:696)。
4. 失效而非删除:记忆怎么「演化」
这是 CORE 最重要的语义。当新事实和旧事实矛盾(「John 从 Google 跳槽去了 Meta」),CORE 不删旧语句,而是:
旧:(John works_at Google) validAt=2024 invalidAt=null ──┐
│ 新 episode 说「John 现在在 Meta」
▼
旧:(John works_at Google) validAt=2024 invalidAt=2026 invalidatedBy=<新episode>
新:(John works_at Meta) validAt=2026 invalidAt=null
旧语句被打上 invalidAt 时间戳和 invalidatedBy(谁干的),留在图里。好处:
- 回答「John 现在在哪」→ 只看
invalidAt IS NULL的。 - 回答「John 以前在哪」→ 历史还在。
- 审计「这个记忆怎么变的」→ 顺着
invalidatedBy能追溯到具体那段对话。
实现:invalidateStatement(apps/webapp/app/services/graphModels/statement.ts:157-177)调底层 provider 的 invalidateStatement,写入 invalidatedBy 和 invalidAt。批量版 invalidateStatements(:179-195)。
谁来触发失效? 不是摄取主线——是异步图解析在做矛盾检测后调用,见 03-graph-resolution.md。
4.1 「有效」语句的查询长这样
几乎所有读取都带 invalidAt IS NULL 这个过滤。例如取某 episode 当前有效的、属于特定 aspect 的语句(statement.ts:342-363):
MATCH (e:Episode {uuid: $episodeUuid})-[:HAS_PROVENANCE]->(s:Statement)
WHERE s.invalidAt IS NULL -- 只要仍然有效的
AND s.aspect IN $aspects
RETURN s
反过来,「被某 episode 失效掉的语句」也能查(getInvalidatedStatementsForEpisode,statement.ts:375-396,WHERE s.invalidatedBy = $episodeUuid)——persona 生成会用它当「墓碑(tombstone)」信号。
5. 两类知识、两个存储(再强调)
图谱只存世界事实(可分解成 SPO 的客观事实)。用户心声(整句指令/偏好)不进图谱,进 Aspects Store(Postgres VoiceAspect 表 + 向量),类型见 graph.entity.ts:244-255。它也有 validAt/invalidAt/invalidatedBy,同样走「失效而非删除」,但不分解——因为「永远在发 PR 前跑测试」拆成三元组就废了。
一段内容
├─ 世界事实「John 在 Google 工作」 ─► Neo4j:拆成 SPO 三元组
└─ 用户心声「永远先跑测试」 ─► Aspects Store:整句存,不拆
6. 巧妙之处
- 谓词也是实体节点(
triple.ts:42)→ 关系本身可被语义检索和合并,「works_at」和「employed_by」能在图解析时归并。 - 出处即证据链(
HAS_PROVENANCE)→ 每个事实都能追回「哪段对话说的」,删 episode 能级联清理(graphModels/episode.ts:92-159)。 - 同一物理结构服务历史与现状 → 一个
invalidAt字段,既支持「时间旅行查询」又支持「只看当前」,不用维护两套表。
7. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 具体化三元组落库(Cypher) | packages/providers/src/graph/neo4j/domains/triple.ts | saveTriple |
| 读回三元组 | packages/providers/src/graph/neo4j/domains/triple.ts | getTriplesForEpisode / getTriplesForStatementsBatch |
| Statement / Triple 类型 | packages/types/src/graph/graph.entity.ts | StatementNode / Triple |
| 失效(单/批) | apps/webapp/app/services/graphModels/statement.ts | invalidateStatement / invalidateStatements |
| 有效语句查询 | apps/webapp/app/services/graphModels/statement.ts | getStatementsForEpisodeByAspects |
| 被失效语句查询(墓碑) | apps/webapp/app/services/graphModels/statement.ts | getInvalidatedStatementsForEpisode |
| 心声类型(整句存) | packages/types/src/graph/graph.entity.ts | VoiceAspectNode / VOICE_ASPECTS |