跳到主要内容

数据与心智模型:文档 / 记忆 / 画像 / 项目

30 秒导读: 本章确立 Supermemory 全书的词汇表和 API 边界。你会先分清四个核心名词——文档 / 记忆 / 画像 / 项目——分别是什么、字段长什么样;再看它们如何被一层叫 SupermemoryClient 的"HTTP 门面"暴露成 6 个方法。后面每一章(MCP 服务器、中间件、Memory Tool、图可视化)都建立在这套词汇之上。

本章只讲**"数据是什么、API 长什么样"。它不讲** MCP 工具怎么注册(见 02)、不讲框架中间件(见 03)、不讲图怎么渲染(见 05)。


1. 先建立心智模型:四个名词

Supermemory 的对外数据只有四类东西。先用一句话各自点破,再逐个深入。

名词白话一句话
document(文档)你喂进去的原始内容一篇 PDF / 网页 / 一段文本,进来后被处理成可检索的东西
memory entry(记忆)从文档里抽取出的一条事实"用户偏好 TypeScript"——是文档的产物,不是文档本身
profile(画像)把记忆聚合成的用户速写两串字符串:稳定偏好 + 近期活动
project(项目)一个隔离的记忆空间用 container tag 圈起来,A 项目看不到 B 项目

一句话直觉:document 当"原始档案"、memory 当"从档案里划出来的重点句"。你存的是档案,但 AI 助手日常检索、拼进上下文的,是那些重点句。

三者是包含关系——一个文档"生出"多条记忆:

document(原始内容)
└── memoryEntries: [
memory entry #1 ← 从内容抽出的一条事实
memory entry #2
memory entry #3
]

这个包含关系在类型里是直接写死的:DocumentWithMemories 接口就带一个 memoryEntries: DocumentMemoryEntry[] 字段(apps/mcp/src/client.ts:86-94,接口 DocumentWithMemories)。


2. 数据的三层形态

上面的四个名词里,document 和 memory 之间还有更细的层次。理解这三层,是读懂后面所有 API 返回值的前提。

2.1 三层是什么

第 1 层 document 一份原始内容(有 title / summary / type)

│ 处理管线抽取

第 2 层 memory entry 一条事实(有 version / parentMemoryId / isLatest …)

│ 被检索时命中

第 3 层 chunk / result 搜索时返回的片段(memory 或 chunk 二选一)
  • document = 原始内容的容器,本身不参与相似度检索,只作为"这条记忆从哪来"的出处。
  • memory entry = 真正被 AI 用的知识单元,带版本链(见 §3)。
  • 搜索结果(Memory) = 检索时命中的东西,可能是一条完整记忆(memory 字段),也可能是文档的一个切片(chunk 字段)——二者互斥。

2.2 搜索结果为什么是"联合类型"

Memory 类型是一个 union(联合类型):要么带 memory 字段,要么带 chunk 字段,不会同时有。

// apps/mcp/src/client.ts:14-28 type Memory
export type Memory =
| ({ id; memory: string; similarity;} & MemoryRichFields) // 命中一条记忆
| ({ id; chunk: string; similarity;} & MemoryRichFields) // 命中一个文档切片

代码里到处用 "memory" in m 来区分这两支。取文本的辅助函数就是这么写的:

// apps/mcp/src/client.ts:106-108 getMemoryText
export function getMemoryText(m: Memory): string {
return "memory" in m ? m.memory : m.chunk
}

为什么重要: 后面 forgetMemory 的语义匹配里,有一句"只有真记忆能被遗忘、chunk 不能"——靠的正是这个 "memory" in r 判别(apps/mcp/src/client.ts:214,searchResult.results.find((r) => "memory" in r))。

2.3 关键字段速查

搜索结果里每条记忆的字段(apps/mcp/src/client.ts:6-28,Memory + MemoryRichFields):

字段含义
id记忆/切片的唯一 ID
memorychunk文本内容(二选一)
similarity与查询的相似度分数
title / content标题 / 原文
metadata附加元数据
updatedAt更新时间
documents关联的文档
isAggregated是否为聚合结果

一个安全细节: 所有文本在返回前都被 limitByChars 截断到 MAX_CHARS = 200000(约 5 万 token),超长补 ...(apps/mcp/src/client.ts:3:110-112,limitByChars)。这是防止一条巨型文档撑爆调用方上下文窗口的护栏。


3. 记忆的版本链(memory 最独特的地方)

这是 Supermemory 区别于普通向量库的核心:记忆不是一次写死,而是带版本、能被新版本取代、能被"遗忘"。

3.1 要解决的问题

用户的偏好会变。今天"喜欢 React",下个月"改用 Next.js"。普通向量库只会堆两条互相矛盾的记录;Supermemory 用一条版本链把它们串起来,并标记谁是最新。

memory v1 "偏好 Vue" isLatest=false
│ parentMemoryId 指向上一版

memory v2 "偏好 React" isLatest=false


memory v3 "偏好 React + TS" isLatest=true ← 默认只返回这一条

(版本演化示意,事实佐证见 skills/supermemory/references/architecture.md 的 "Memory Versioning" 一节——该文件作为被研究的数据引用。)

3.2 版本链字段(核对源码)

MCP 侧的记忆条目类型 DocumentMemoryEntry 把版本链字段全列了出来(apps/mcp/src/client.ts:70-84):

字段类型作用
versionnumber第几版
parentMemoryIdstring | null指向上一版(链的"前驱指针")
rootMemoryIdstring | null指向链的源头(第一版)
isLatestboolean是否为当前最新版
isForgottenboolean是否已被遗忘
isStaticboolean是稳定事实还是动态信息(见 §4)
forgetAfterstring | null定时遗忘的时间点
forgetReasonstring | null遗忘原因
spaceIdstring所属空间

3.3 图可视化侧的同一套字段 + 关系边

图渲染包 memory-graph 用的 GraphApiMemory几乎同构的一套字段,额外多了"记忆之间怎么连"的关系边信息(packages/memory-graph/src/types.ts:5-27,GraphApiMemory):

// packages/memory-graph/src/types.ts:5-24 GraphApiMemory(节选)
export interface GraphApiMemory {
version: number
parentMemoryId: string | null
rootMemoryId: string | null
isLatest: boolean
isForgotten: boolean
spaceId: string
relation?: MemoryRelation | null // 这条记忆和别人的关系
memoryRelations?: Record<string, MemoryRelation> | null
nextVersionId?: string | null // 版本链的"后继指针"
// …
}

关系类型 MemoryRelation 只有三种取值(packages/memory-graph/src/api-types.ts,type MemoryRelation):

edgeType白话例子
updates新版取代旧版"偏好 React 18" 取代 "偏好 React 17"
extends补充、丰富"完成了 TS 进阶课" 丰富 "喜欢 TS"
derives推断出的新联系从"每天读 ML 论文"推出"是 ML 工程师"

一条边就是 { source, target, edgeType }(packages/memory-graph/src/types.ts:39-43,GraphApiEdge)。

边界提醒: 版本链字段在本章讲透即可;这些字段怎么被画成力导向图上的连线05-memory-graph.md 的事,本章不展开。

3.4 两处文档类型的差异(别混淆)

仓库里有两个 DocumentWithMemories,字段名略有不同,别搞混:

来源文件记忆数组字段名文档类型字段名
apps/mcp/src/client.ts:86memoryEntriestype
packages/memory-graph/src/api-types.tsmemoriesdocumentType

前者是 MCP 门面的形状,后者是图渲染的形状。它们描述同一个概念,只是各自适配自己那层的消费方。


4. 用户画像:static + dynamic 两串字符串

画像是把散落的记忆聚合成的"用户速写"。它的数据结构极简——就是两个字符串数组

4.1 结构

// apps/mcp/src/client.ts:49-57
export interface Profile {
static: string[] // 稳定偏好:名字、职业、长期习惯
dynamic: string[] // 近期活动:最近在做什么、刚问过什么
}
export interface ProfileResponse {
profile: Profile
searchResults?: SearchResult // 可选:附带与 query 相关的检索结果
}
  • static ← 从 isStatic: true 的记忆聚合(稳定、高优先级)。
  • dynamic ← 从近期记忆聚合(时效性、会被更新)。

这两条线正好对应 §3.2 里记忆条目上的 isStatic 字段——画像不过是把记忆按这个标志分成两堆。

4.2 一次调用就拿到

不需要自己拉记忆再聚合;getProfile 一次调用直接返回成品(apps/mcp/src/client.ts:291-329,getProfile):

// apps/mcp/src/client.ts:291-303(节选)
async getProfile(query?: string): Promise<ProfileResponse> {
const result = await this.client.profile({
containerTag: this.containerTag, // 限定在当前项目空间
q: query, // 可选:同时做一次相关检索
})
return {
profile: {
static: result.profile?.static || [],
dynamic: result.profile?.dynamic || [],
},
// 若传了 q,还会带回 searchResults
}
}

要点: 传了 query 就顺带返回 searchResults(把画像 + 相关记忆一次性给你);不传就只回画像。


5. 项目(container tag):记忆的隔离墙

同一个 Supermemory 后端要服务很多用户、很多组织、很多上下文。container tag 就是把它们隔开的机制。

5.1 概念

一个 container tag = 一个独立空间。不同 tag 之间的记忆互相不可见。

container tag = "user_123" container tag = "org_acme"
┌───────────────────────┐ ┌───────────────────────┐
│ 记忆:偏好深色模式 │ │ 记忆:使用 AWS │
│ 记忆:使用 TypeScript │ ✗ │ 记忆:50 名员工 │
└───────────────────────┘ 互不 └───────────────────────┘
可见

好处:隐私隔离、多租户、把个人 / 工作 / 某个项目的上下文分开。(隔离语义佐证见 skills/supermemory/references/architecture.md 的 "Container Tag Isolation" 一节,作为数据引用。)

5.2 客户端怎么锁定 tag

SupermemoryClient 在构造时就把 container tag 定死;没传就用默认值 sm_project_default(apps/mcp/src/client.ts:4:147):

// apps/mcp/src/client.ts:135-148(节选)
constructor(bearerToken, containerTag?, apiUrl = "https://api.supermemory.ai") {
// …
this.containerTag = containerTag || DEFAULT_PROJECT_ID // "sm_project_default"
}

之后每一个方法调用都会自动带上 this.containerTag——createMemory、search、getProfile、forgetMemory 无一例外。这保证了调用方永远只在自己那面墙内读写。

5.3 tag 从哪来:x-sm-project HTTP 头

在 MCP 服务器里,container tag 是从请求头 x-sm-project 读出来的(apps/mcp/src/index.ts:122):

// apps/mcp/src/index.ts:122
const containerTag = c.req.header("x-sm-project")

它被塞进执行上下文的 props(apps/mcp/src/index.ts:180-188),最终传给 SupermemoryClient 的构造函数(apps/mcp/src/server.ts:532-536,new SupermemoryClient(apiKey, containerTag || mcpRootContainerTag, apiUrl))。

一句话链路:

HTTP 头 x-sm-project → props.containerTag → new SupermemoryClient(…, tag) → 每次 API 调用都带 tag

x-sm-project 也被显式列进 CORS 的允许头里(apps/mcp/src/index.ts:44:115),说明它是设计上要跨域透传的一等公民。


6. 搜索模式:memories / hybrid / documents

检索是所有"记忆型 AI"的核心动作。Supermemory 的搜索有三种模式,由 SearchOptions 指定。

6.1 三种模式

// apps/mcp/src/client.ts:36-47 SearchOptions(节选)
export interface SearchOptions {
searchMode?: "memories" | "hybrid" | "documents"
rerank?: boolean
rewriteQuery?: boolean
include?: {
documents?; relatedMemories?; summaries?; chunks?; forgottenMemories?
}
}
searchMode检索对象直觉
memories只搜抽取出的记忆(事实)要"结论",干净
documents只搜原始文档切片要"原文出处"
hybrid记忆 + 文档一起搜两者兼顾(默认)

6.2 hybrid 是默认

search 方法里,若调用方没指定模式,就落到 "hybrid"(apps/mcp/src/client.ts:254):

// apps/mcp/src/client.ts:250-259(节选)
const result = await this.client.search.memories({
q: query,
limit,
containerTag: this.containerTag,
searchMode: options?.searchMode ?? "hybrid", // ← 默认 hybrid
threshold,
rerank: options?.rerank,
rewriteQuery: options?.rewriteQuery,
include: options?.include,
})

hybrid 的含义: 同时在"记忆层"和"文档层"检索,所以返回的 Memory[] 里会混着 memory 结果和 chunk 结果——这正是 §2.2 那个联合类型存在的原因。include 里还能额外要求带上关联文档、相关记忆、摘要,甚至被遗忘的记忆(forgottenMemories)。


7. SupermemoryClient:引擎的 HTTP 门面

前面所有数据形态,最终都通过一个类对外暴露:SupermemoryClient。它是"记忆引擎"的HTTP 门面(facade)——把底层 SDK 和裸 fetch 包成 6 个语义清晰的方法。

7.1 全景:6 个方法打到哪

SupermemoryClient(apps/mcp/src/client.ts:129-458)

├── createMemory ──► SDK client.add ─┐
├── forgetMemory ──► SDK memories.forget ├─ 走 supermemory SDK(baseURL=apiUrl)
├── search ──► SDK search.memories │
├── getProfile ──► SDK client.profile ─┘

├── getProjects ──► fetch GET /v3/projects ─┐ 裸 fetch
└── getDocuments ──► fetch POST /v3/documents/documents ─┘

有意思的分工:四个走官方 SDK,两个走裸 fetch SDK 方法(add/forget/search/profile)由 supermemory 包封装;而列项目、列文档这两个是直接 fetch/v3 端点的。

7.2 逐方法速查

方法底层调用HTTP 端点关键入参返回
createMemoryclient.add(SDK 内部,佐证为 POST /v3/documents)content + tag + metadata.sm_source="mcp"{ id, status:"queued", containerTag }
forgetMemorymemories.forget(SDK 内部)contentid + tag{ success, message, containerTag }
searchsearch.memories(SDK 内部,佐证为 /v4/search)q, limit, threshold, SearchOptionsSearchResult
getProfileclient.profile(SDK 内部)可选 qProfileResponse
getProjectsfetchGET /v3/projectsstring[](各项目的 containerTag)
getDocumentsfetchPOST /v3/documents/documentspage, limit, containerTagsDocumentsApiResponse

(SDK 内部) 的端点在 client.ts 里看不到具体路径——它们被 supermemory npm 包封装了。/v3/documents/v4/search 这两个具体路径来自 skills/supermemory/references/architecture.md 的 "Data Flow" 图(作为数据佐证,非本仓源码直述,故标 (inferred))。裸 fetch 的两个端点则是源码里写死的字符串,可直接核对(apps/mcp/src/client.ts:334:366)。

7.3 请求 / 响应长什么样

createMemory —— 存一条记忆,立即返回队列态(apps/mcp/src/client.ts:151-170):

// 请求
await this.client.add({
content,
containerTag: this.containerTag,
metadata: { sm_source: "mcp" }, // 标记来源是 MCP
})
// 响应
{ id: "…", status: "queued", containerTag: "…" }

注意 status 恒为 "queued"——写入是异步的,方法返回时内容还在处理管线里(extract → chunk → embed → index),并未就绪。

getDocuments —— 分页拉文档(连带其记忆条目)(apps/mcp/src/client.ts:360-390):

// 请求体
{ page: 1, limit: 10, sort: "createdAt", order: "desc", containerTags }
// 响应
{
documents: DocumentWithMemories[], // 每个含 memoryEntries[]
pagination: { currentPage, limit, totalItems, totalPages }
}

响应形状就是 §1 那个"文档包含记忆"的结构,再加分页信息(DocumentsApiResponse,apps/mcp/src/client.ts:96-104)。

7.4 错误处理:统一收敛到 handleError

每个方法的 catch 都调 this.handleError(error)。它的签名返回 never——永不正常返回,只抛出一个措辞友好的 Error(apps/mcp/src/client.ts:392-457,handleError)。

它把各种失败归一化成给终端用户看的人话:

触发条件抛出的消息(节选)
AbortError / TimeoutError"Request timed out after 30 seconds…"
TypeError 且含 "fetch"/"network""Network error. Please check your connection…"
HTTP 401"Authentication failed. Please re-authenticate."
HTTP 402"Memory limit reached. Upgrade at supermemory.ai"
HTTP 404"Memory not found. It may have been deleted."
HTTP 429"Rate limit exceeded…"
HTTP ≥ 500"Server error…"

一个诚实的更正: 本方法是抛异常(throw-based)风格,不是 neverthrowResult 风格——handleError 的返回类型就是 never,内部全是 throw new Error(...)(apps/mcp/src/client.ts:392)。SDKResult(apps/mcp/src/client.ts:115-127)只是一个给 SDK 原始返回做类型标注的接口,与错误处理无关。代码里看不出任何 neverthrow 的使用。

forgetMemory 里有一处局部的容错逻辑值得一提:它先试精确内容匹配,若 SDK 抛的是 404 就吞掉、转而走语义搜索兜底(阈值 0.85),否则原样重抛(apps/mcp/src/client.ts:189-228)。这是"删记忆"这个动作特有的降级链,不是全局的错误模型。


8. 巧妙之处(可带走的设计)

  • 文档 / 记忆分层。 存的是文档,用的是记忆——检索命中的最小单元是"事实"而非"整篇",既省上下文又可溯源(apps/mcp/src/client.ts:86-94)。
  • 版本链而非覆盖。parentMemoryId + isLatest 把"偏好变了"表达成一条链,默认只回最新版,需要时可回溯历史(apps/mcp/src/client.ts:70-84)。
  • 画像即两串字符串。 复杂的聚合逻辑藏在后端,对外只暴露 static[] + dynamic[],一次调用即得(apps/mcp/src/client.ts:49-52:291)。
  • 构造即绑定空间。 container tag 在 new SupermemoryClient 时锁定,之后每次调用自动携带,调用方无法越界(apps/mcp/src/client.ts:147)。
  • 200k 字符护栏。 所有返回文本统一截断,防止单条巨型内容撑爆下游上下文(apps/mcp/src/client.ts:110-112)。

9. 边界与本章不覆盖的

  • SDK 端点不透明。 createMemory/forgetMemory/search/getProfile 的确切 HTTP 路径封装在 supermemory npm 包内,本仓源码看不到;§7.2 里标 (inferred) 的路径来自 architecture.md(数据佐证),非源码直述。
  • 写入是异步的。 createMemory 返回 "queued" 即结束,内容尚未可检索;处理管线(extract/chunk/embed/index)的细节不在本仓 MCP 代码里。
  • 本章不讲: MCP 工具注册与 schema(→ 02)、框架中间件 withSupermemory(→ 03)、Claude Memory Tool 桥接(→ 04)、版本链/关系边的图渲染(→ 05)。

10. 代码地图(导航索引)

主题文件路径符号名
搜索结果联合类型apps/mcp/src/client.tsMemory / MemoryRichFields
取结果文本apps/mcp/src/client.tsgetMemoryText
搜索结果封装apps/mcp/src/client.tsSearchResult
搜索模式与选项apps/mcp/src/client.tsSearchOptions
画像结构apps/mcp/src/client.tsProfile / ProfileResponse
项目结构apps/mcp/src/client.tsProject
记忆条目(带版本链)apps/mcp/src/client.tsDocumentMemoryEntry
文档含记忆apps/mcp/src/client.tsDocumentWithMemories
文档 API 响应apps/mcp/src/client.tsDocumentsApiResponse
HTTP 门面类apps/mcp/src/client.tsSupermemoryClient
存记忆apps/mcp/src/client.tsSupermemoryClient.createMemory
遗忘记忆(带语义兜底)apps/mcp/src/client.tsSupermemoryClient.forgetMemory
搜索(默认 hybrid)apps/mcp/src/client.tsSupermemoryClient.search
取画像apps/mcp/src/client.tsSupermemoryClient.getProfile
列项目apps/mcp/src/client.tsSupermemoryClient.getProjects
列文档apps/mcp/src/client.tsSupermemoryClient.getDocuments
错误归一化apps/mcp/src/client.tsSupermemoryClient.handleError
字符截断护栏apps/mcp/src/client.tslimitByChars / MAX_CHARS
图侧记忆类型(含关系边)packages/memory-graph/src/types.tsGraphApiMemory / GraphApiEdge
图侧文档类型packages/memory-graph/src/types.tsGraphApiDocument
关系类型三取值packages/memory-graph/src/api-types.tsMemoryRelation
x-sm-project 读取apps/mcp/src/index.tshandleMcpRequest(containerTag 一行)
客户端构造绑定 tagapps/mcp/src/server.tsnew SupermemoryClient(…)