跳到主要内容

CORE — 架构与原理

30 秒导读: CORE 是一个开源、自托管的「个人 AI 操作系统」。本组文档只讲它最硬核的那块——记忆引擎(temporal knowledge graph,带时间维度的知识图谱):它把你跟 AI 的每段对话、每份文档,变成一张会随时间演化、能精准回忆的个人记忆网,让下一个任务一开始就「已经知道上次发生了什么」。

1. 这是什么(零基础也能懂)

一句话定义: CORE 的记忆引擎,是一台把「自然语言片段」持续熔炼成「结构化、带时间戳、可检索的个人知识图谱」的流水线。

解决什么问题 / 给谁用:

假设你让 AI 助手帮你干活。普通聊天机器人每次都「失忆」——你上周说过「我住旧金山」「提醒我发 PR 前先跑测试」,这周它全忘了。CORE 的目标:把这些散落在对话里的事实长期记住,而且记对

难点不在「存一句话」,而在三件事:

  • 同一件事会被反复说、换着说 —— 「John 在 Google 工作」和「John 受雇于 Google」是同一个事实,不能存两遍。
  • 事实会变 —— 你今天说「我住旧金山」,明年搬到纽约。旧记忆不能一删了之(你可能问「我以前住哪」),但也不能继续当成「现在」。
  • 回忆要准 —— 问「John 的电话是多少」和问「上周 CORE 项目有什么进展」,要走完全不同的取数路子。

它能做什么(功能):

  • 把一段对话/一份文档(CORE 里叫一个 episode,情节)拆成原子事实并落进图谱。
  • 自动去重(同义事实合并)与矛盾消解(新事实让旧事实「失效」而非删除)。
  • 维护双时间轴:事实「何时在现实中成立」与「何时被系统记录」分开存。
  • 区分**「世界事实」(谁是谁、发生了什么)和「用户心声」**(规则、偏好、目标),分别用最合适的方式存。
  • 检索时先用 LLM 给你的问题分类,再分流到 6 种不同的取数策略。

用起来什么样: 对外其实就两个动词——写入一个 episode按意图检索。写入的核心入口是 KnowledgeGraphService.addEpisode:

// 示意,非源码:对外的两个核心动作
// 1) 写入:把一段对话变成记忆
await kg.addEpisode({
episodeBody: "我住旧金山,在 Google 做后端", // 原始文本
referenceTime: "2026-06-30T10:00:00Z", // 这句话「何时成立」
source: "slack",
sessionId: "sess-123",
}, prisma);

// 2) 检索:按自然语言意图回忆
const result = await search("John 的电话是多少?", userId, workspaceId);
// → 路由判定这是 entity_lookup/attribute 查询,直奔实体属性,不去翻对话

一句话直觉/类比: 把它想成**「会自己做笔记的大脑」**——你只管说话,它在背后把每句话抄成卡片、把重复卡片合并、给过时卡片划掉(但不撕掉)、并按主题归档,等你来问的时候按问题类型从对应抽屉里取。

2. 顶层全景(它大概怎么转)

记忆引擎分两条命脉:写入(ingestion)读出(retrieval)。中间是双存储:Neo4j 图谱存「世界事实」,Postgres 的 Aspects Store 存「用户心声」,向量库存所有嵌入用于相似度搜索。

┌──────────────── 写入(本组文档 §1–3)────────────────┐
│ │
一段对话/文档 ──► ① 预处理 ──► ② 摄取(同步) ──► ③ 图解析(异步)
(episode) 切块/版本/diff 清洗→抽取→分类→落库 去重/合并/矛盾失效
│ │ │ │
│ ▼ ▼ ▼
│ (chunkEpisode) ┌─ 世界事实 → Neo4j 图谱(具体化三元组)
│ └─ 用户心声 → Aspects Store(整句)
│ 所有文本 → 向量库(嵌入)

└──────────────── 读出(本组文档 §4)──────────────────┐

自然语言查询 ──► ④ 意图路由(LLM)──► ⑤ 6 种处理器之一 ──► ⑥ 压缩+重排 ──► 结果
分类+抽 aspect/实体 按查询类型分流取数 合并会话/Cohere

怎么读这张图: 上半是写入(从左到右三阶段:预处理→摄取→图解析),下半是读出。两半通过中间的「双存储 + 向量库」相连。

部件一句话职责:

部件干什么在哪个文件
预处理 Preprocess决定要不要切块;文档做版本号 + diff(只摄取改动)apps/webapp/app/jobs/ingest/preprocess-episode.logic.ts
摄取 Ingestion单个 episode 的同步主线:清洗→抽取→分类→落库apps/webapp/app/services/knowledgeGraph.server.ts(addEpisode)
图解析 Graph Resolution异步后处理:实体合并、重复语句删除、矛盾语句失效apps/webapp/app/jobs/ingest/graph-resolution.logic.ts
图存储 Neo4j Provider具体化三元组的 Cypher 实现(SPO + 时间戳)packages/providers/src/graph/neo4j/domains/triple.ts
Aspects Store用户心声(规则/偏好/目标)整句存 Postgres + 向量apps/webapp/app/services/aspectStore.server.ts
意图路由 Router用 LLM 把查询分类、抽 aspect/实体/时间窗apps/webapp/app/services/search-v2/router.ts
查询处理器 Handlers6 种查询类型各自的取数逻辑apps/webapp/app/services/search-v2/handlers.ts

主线走一遍(高层):

  1. 一个 episode 进来 → 预处理判断长度,长就切块;文档还会算版本号、跟上一版做 diff,只把「改动」喂给 LLM(省 token)。
  2. 每个 chunk → 摄取:LLM 先清洗成干净叙述,再并行抽取出「世界事实(graph_facts)」和「用户心声(voice_facts)」,各自分类成 aspect,落库。
  3. 落库后触发异步图解析:把这次抽出的实体/语句,跟图里已有的做去重矛盾检测——重复的删掉并把出处搬到旧节点,矛盾的把旧语句标记 invalidAt(失效,但保留)。
  4. 查询时:路由先用 LLM 判断「这是哪类问题」,再交给对应处理器多路取数,最后压缩 + 重排返回。

3. 阅读地图(按这个顺序读)

建议顺序(由浅入深):

  1. 01-ingestion-pipeline.md — 写入主线:一段话怎么被切、洗、抽、分类、落库。先读这章建立全局。
  2. 02-temporal-graph-model.md — 数据模型:为什么用「具体化三元组」、双时间轴、失效语义。这是 CORE 最核心的设计。
  3. 03-graph-resolution.md — 异步消解:实体合并 + 重复/矛盾处理,记忆怎么保持一致与演化。
  4. 04-retrieval.md — 读出主线:意图路由 + 6 种处理器 + 压缩重排。

4. 巧妙之处(先睹为快)

带走这几条精华(每条在对应章节有 file:line 出处):

  • 双存储,各按本性存。 「世界」是关系网(谁连谁)→ 分解成原子三元组进图谱;「心声」是整句指令(「永远先跑测试」)→ 拆开就没意义,整句进 Aspects Store。一刀切会两头不讨好。
  • 失效而非删除(bi-temporal)。 旧事实标 invalidAt 留在图里,既能回答「现在如何」也能回答「以前如何」,还留下了「谁让它失效的」(invalidatedBy)。
  • 文档更新只摄取 diff。 第 2 版文档来了,跟上一版做 git 风格 diff,只把改动喂 LLM——日志里直接打印 token 节省百分比。
  • 同步抽取 + 异步消解分离。 摄取主线只管「快速落库」,昂贵的全局去重/矛盾检测丢到后台队列,写入延迟低。
  • 检索先分类再分流。 不是所有查询都走同一套向量搜索——LLM 路由把问题分成 6 类(实体查属性 / 关系 / 时间窗 / 主题枚举…),各走最优路径。

各章详述。