ReMe/docs/zh/memory_as_file.md
jinliyl 15d12be6b6
fix(index): tolerate invalid text encoding (#490)
* fix(index): tolerate invalid text encoding

* fix(index): preserve text chunker compatibility
2026-08-26 16:16:26 +08:00

369 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Memory as File
ReMe 的核心思想是:**Memory as File, File as Memory**。
<p align="center">
<img src="../figure/memory-as-file.svg" alt="ReMe Memory as File 文件化记忆模型" width="92%">
</p>
**Memory as File**:长期记忆不是藏在黑盒数据库里,原始材料和可读记忆都落在 workspace 内由用户拥有的文件中。用户和 Agent
可以直接读、写、移动、删除它们;`metadata/` 里的索引和快照是可重建的派生状态。
**File as Memory**每个文件不只是普通文本也是一个可索引、可链接、可演化的记忆节点。ReMe 会从文件中解析 frontmatter、正文
chunk、wikilink 边,并把它们组织成检索和图谱。
换句话说,文件是人的可读界面,也是 Agent 的操作接口目录结构负责承载记忆分层Markdown 语法负责表达内容、元数据和关系。
## 设计目标
ReMe 把记忆设计成文件,不只是为了“方便存储”,而是为了让长期记忆具备几个基本性质:
| 目标 | 含义 |
|--------|----------------------------------------------------------------------------------------------------|
| 可读 | 用户可以直接打开 workspace像读普通笔记一样读 daily、digest 和原始材料。 |
| 可编辑 | 用户和 Agent 都能用文件操作修正、补充、移动或删除记忆,不必依赖专用数据库客户端。 |
| 可追溯 | digest 中的长期结论可以通过 Sources 章节回到 daily、resource 或 session 原文。 |
| 可迁移 | workspace 是普通目录Markdown、JSONL、YAML 和资源文件可以被备份、同步、版本管理或迁移到其他工具。 |
| 可索引 | 文件虽然是普通文本,但 ReMe 会解析 frontmatter、chunk、wikilink构建检索索引和文件图谱。 |
| 可协作 | 人负责判断和修正Agent 负责整理、链接和检索;二者看到和操作的是同一套文件。 |
因此ReMe 的记忆不是“数据库里的一条隐藏记录”,也不是“只给 LLM 看的 prompt 片段”。它首先是用户拥有的文件,其次才被系统索引成可召回的记忆。
## 记忆分层
ReMe 的 workspace 把记忆分成四层:
```text
source records -> session/ + resource/
working memory -> daily/
long memory -> digest/
system state -> metadata/
```
这四层解决的是不同问题。
`session/``resource/` 保存来源记录。`resource/` 文件保持原路径和原内容;标准 Auto Memory 保留对话消息,但会有意排除
tool-result 和 base64 data block防止召回结果和二进制负载被误当成用户证据。Agent 运行时生成状态则放在 `mem_session/`
`daily/` 是浅加工层。它把当天发生的对话和资源整理成更适合阅读的 daily note什么事情发生了、有哪些结论、留下了哪些后续任务、对应原文在哪里。
daily 不追求最终抽象,它更像当天工作台。
`digest/` 是深加工层。这里保存的是可以长期复用的记忆节点例如用户偏好、项目背景、流程经验、概念知识、决策先例。digest 不应该只是复制
daily而应该把多次出现的事实、方法和关系合并成更稳定的表述。
`metadata/` 是系统索引层。它保存 file catalog、chunk 索引、图谱快照等运行状态。用户通常不需要手写这里的内容;真正的人工编辑入口是
`daily/``digest/` 和必要时的 `resource/`
这个分层让 ReMe 可以同时保留“现场”和“抽象”daily 负责还原当时发生了什么digest 负责回答以后还能复用什么。
## 目录结构
ReMe 用目录表达记忆组织和记忆分层。原始材料先进入 `resource/``session/`,再沉淀到 `daily/`,最后由 `auto_dream`
整合到 `digest/`
对应的自动流程分别是 [Auto Memory](./auto_memory.md)、[Auto Resource](./auto_resource.md)
和 [Auto Dream](./auto_dream.md)。检索这些文件时使用 [Memory Search](./memory_search.md)。
```text
<workspace_dir>/
├── metadata/ # 系统索引层ReMe 索引、图谱、catalog 等持久状态,不作为人工编辑入口
├── session/ # 来源记录层;对话来源记录
│ ├── dialog/
│ │ └── <session_id>.jsonl # auto_memory 保存的来源消息
│ └── claude_code/
│ └── <session_id>.jsonl # auto_memory_cc 使用的 ReMe 副本
├── mem_session/ # Agent wrapper 生成的 session/配置,不是用户记忆
│ ├── agentscope/
│ ├── claude_config/
│ └── codex/
├── resource/ # 来源记录层;外部原始材料
│ ├── <resource>.<ext> # 根目录文件使用今天日期
│ └── YYYY-MM-DD/
│ └── <resource>.<ext> # 按目录日期进入 daily
├── daily/ # 浅加工层;按日期组织当天事实、对话摘要、资源解读
│ ├── YYYY-MM-DD.md # 当天索引页
│ └── YYYY-MM-DD/
│ ├── <generated_name>.md # 按主题命名的对话或资源卡片
│ └── interests.yaml # auto_dream 产出的主动兴趣主题
└── digest/ # 深加工层;可长期复用的个人事实、流程经验、知识节点
├── personal/
│ └── <memory>.md # 用户画像、偏好、长期个人事实
├── procedure/
│ └── <memory>.md # 流程、方法论、操作经验
└── wiki/
└── <memory>.md # 通用知识、概念、决策先例
```
典型流转如下:
```text
对话
-> session/dialog/<session_id>.jsonl
-> daily/YYYY-MM-DD/<generated_name>.md
-> digest/personal | digest/procedure | digest/wiki
外部资料
-> resource/[YYYY-MM-DD/]<resource>.<ext>
-> daily/YYYY-MM-DD/<generated_name>.md
-> digest/wiki | digest/procedure
```
前两步偏向记录和整理,最后一步偏向长期沉淀。`auto_memory``auto_resource` 负责从原始输入生成 daily`auto_dream`
负责从 daily 抽取并整合 digest。daily 文件名来自经校验的 frontmatter `name``session_id``source_conversation`
`source_resource` 负责稳定追溯与定位,不用来强制决定文件名。
## Markdown 格式
ReMe 优先使用 Markdown 表达记忆因为它同时适合人读、Agent 编辑和程序解析。
一个典型记忆文件:
```markdown
---
name: 光伏产业链研究
description: 从硅料到组件的全链条梳理
tags: [新能源, 光伏]
---
# 结论
光伏产业链可以拆成 [[digest/wiki/硅料.md]]、硅片、电池片和组件。
主要生产商包括 [[digest/wiki/隆基绿能.md|隆基]]。
```
### Frontmatter
Frontmatter 是文件开头的 YAML 块,用 `---` 包住:
```markdown
---
name: 文档名
description: 文档描述
source_conversation: [[session/dialog/abc.jsonl]]
---
```
当前代码固定识别 `name``description`,其他字段会作为额外 metadata 保留。写入接口会把 `name``description`
`metadata` 合并成 frontmatter。
推荐把 frontmatter 当作“节点级摘要”,把正文当作“证据、解释和关系”。例如:
```markdown
---
name: 用户偏好:文档说明风格
description: 用户偏好直接、工程化、有上下文但不冗长的中文技术说明。
kind: preference
confidence: observed
---
用户多次要求文档补充动机、边界和例子,但避免营销式表述。
执行 [[digest/procedure/技术文档写作.md]] 时应用这个偏好。
## Sources
该偏好记录于 [[daily/2026-06-20/文档说明风格.md]],其中保留了用户多次提出的指导。
```
这样做有三个好处:
1. `name``description` 可以在列表、召回结果和 Agent 判断中作为轻量摘要。
2. 正文可以承载更完整的事实、条件、反例和来源。
3. 普通 Wikilink 可以被图谱解析,后续移动文件时也能被维护。
Frontmatter 适合放稳定、短小、结构化的字段;正文适合放需要人读的解释。不要把大段正文塞进 YAML 字段。
### Wikilink
Wikilink 用 `[[...]]` 表达文件之间的关系:
```text
[[daily/2026-06-20/session.md]]
[[notes/example.md#L9]]
[[notes/example.md#L9-L10]]
[[notes/example.md#L9-L10,L15-L20]]
```
ReMe 的 wikilink 是 **字面路径语义**
```text
[[X]] -> target_path = "X"
```
它不会自动补 `.md`,不会按文件名搜索,也不会自动解析 folder note。推荐写完整的 workspace 相对路径,并带上扩展名。
`[label](../wiki/example.md)` 这类普通 Markdown 链接不会建立 `FileLink`move 或 retarget 操作也不会改写它们。
`#L9``#L9-L10``#L9-L10,L15-L20` 这类锚点会作为普通 `target_anchor` 字符串保存在图谱中。图谱解析器不会校验行号锚点,因此
`#L0``#L10-L9``#L9,` 也会被保存。`read` 不会解析追加在 `path` 后的锚点;读取指定范围时需要分别传入从 1 开始、首尾均包含的
`start_line``end_line`,例如
`read(path="digest/wiki/光伏.md", start_line=9, end_line=10)`
Wikilink 的作用:
```text
正文链接 -> 建立 FileLink
move 文件 -> 默认改写入边中的 [[旧路径]]
delete 文件 -> 返回仍存在的入边,提示清理引用
search 命中 -> 可展开出入链,帮助理解上下文
```
解析结果:
```text
FileLink
source_path = 当前文件
target_path = notes/example.md
target_anchor = L9-L10,L15-L20
```
旧文档中的 `related:: [[path]]``- related:: [[path]]`
`[related:: [[path]]]` 仍然可以读取。ReMe 会忽略外围文本,把内部 `[[path]]`
作为普通链接建立索引。从曾存储 typed link 的版本升级后,应执行一次 `reme reindex`,用源文件重建不含旧关系字段的派生图索引。
### 来源和关系
ReMe 里最重要的两类链接是来源链接和概念关系链接。
Sources 章节说明“这条长期记忆从哪里来”:
```markdown
## Sources
该偏好观察自 [[daily/2026-06-20/文档说明风格.md]],支撑它的报告证据保留在
[[resource/2026-06-20/report.pdf]] 中。
```
概念关系链接说明“这个节点和哪些长期记忆有关”,并自然织入正文:
```markdown
这份分析扩展了 [[digest/wiki/光伏产业链.md]],遵循
[[digest/procedure/调研报告拆解流程.md]],并与
[[digest/wiki/集中式逆变器.md]] 对比。
```
## 人工编辑和 Agent 编辑
因为记忆就是文件,用户可以直接在编辑器里改 workspaceAgent 也可以通过 ReMe 的文件工具读写同一批文件。两者遵守同一套约定:
| 操作 | 建议 |
|------------|-------------------------------------------------------------------------------------|
| 新增记忆 | 写入合适目录Markdown 使用 frontmatter并尽量写完整 workspace-relative wikilink。 |
| 修改正文 | 保留已有来源和关键 wikilink如果是修正旧结论在正文里说明新材料如何改变旧判断。 |
| 移动文件 | 使用 ReMe 的 move 工具时会默认改写入边中的旧路径;手工移动后建议重新检查入链。 |
| 删除文件 | 删除前检查入链ReMe 的 delete 会返回仍然指向目标的来源文件,方便清理悬空引用。 |
| 修改元数据 | 用 frontmatter 表达短字段;正文发生实质变化时同步更新 `description`。 |
一个实用规则是:**可以让 Agent 重写表达,但不要让它丢掉证据边**。尤其是 digest 节点中的 Sources 条目和已有
digest-to-digest Wikilink是长期记忆可追溯和可扩展的基础。
## 路径语义
所有文件工具和 wikilink 都以 workspace-relative path 为基本单位:
```text
digest/wiki/光伏.md
daily/2026-06-20/文档说明风格.md
resource/2026-06-20/report.pdf
```
这带来一个明确边界ReMe 不把 `[[光伏]]` 当作全库标题搜索,也不假设 Obsidian 式的同名解析。`[[digest/wiki/光伏.md]]`
就是指向这个具体路径。
推荐习惯:
1. 链接 Markdown 文件时带上 `.md`
2. 从 digest 指向 daily 或 resource 时写完整来源路径。
3. 文件重命名或移动尽量通过 ReMe 的 move 工具完成,避免留下旧路径。
4. 对外部资源使用 `resource/YYYY-MM-DD/...`,对长期抽象使用 `digest/...`,不要把原始资料直接塞进 digest。
这种显式路径语义牺牲了一点手写便利性,但换来的是可预测、可迁移和可自动维护。
## Memory Chunking
Memory chunking 是把一个文件拆成可检索片段的过程。ReMe 不是直接按固定长度切 Markdown而是尽量保持语义结构。
本节说明文件如何被切成检索 chunk索引更新、BM25、向量召回和链接展开流程见 [Memory Search](./memory_search.md)。
传统 RAG 常见做法是固定窗口切分:
```text
Document
|
| every N tokens + overlap
v
chunk 1 | chunk 2 | chunk 3 | ...
```
这种方式简单,但容易把标题、表格、代码块、列表和 `[[wikilink]]` 从中间切开。检索命中后Agent
往往只看到一段孤立文本,不知道它属于哪个章节,也不清楚它和其他记忆节点的关系。
ReMe 的 chunking 更接近“按文件结构切记忆”:
```text
Markdown file
|
| frontmatter + headings + blocks + wikilinks
v
semantic chunks with document skeleton
```
对比:
```text
传统 RAG chunk
= 固定长度文本片段 + overlap
ReMe memory chunk
= 章节结构 + 正文片段 + 行号范围 + wikilink 关系上下文
```
Markdown 文件使用 `MarkdownFileChunker`
```text
Markdown
|
| mistletoe AST
v
Document
└─ H1 section
├─ paragraph / list / table / code
└─ H2 section
└─ ...
|
v
FileChunk[]
```
分块规则:
```text
1. 先解析 frontmatter正文单独进入 chunker。
2. 按标题层级构建章节树。
3. 优先让一个完整章节成为一个 chunk。
4. 章节过长时,向下递归拆子章节和正文块。
5. 表格拆分时重复表头。
6. 代码块拆分时重复 fence。
7. 列表按 item 打包。
8. 最后才按行贪心拆分,并添加 [Part X/N]。
```
每个 chunk 默认会带上标题骨架:
```text
# 一级标题
## 当前章节
命中的正文片段
## 后续章节标题
```
这样检索命中时Agent 不只看到孤立段落,还能看到它在原文件中的结构位置。
非 Markdown 默认走 `DefaultFileChunker`:按字节大小切分,并保留少量 overlap对 Markdown 则会避免把 `[[wikilink]]` 从中间切开。
`DefaultFileChunker``MarkdownFileChunker` 使用各自配置的 `encoding` 解码文件,并在索引前将平台换行符统一为
LF。默认的 `invalid_encoding_policy: replace` 会在源文件含无效字节时保留其中可解码的内容用于检索,但不会修改源
文件;如需拒绝此类文件,可在 chunker 组件上设置 `invalid_encoding_policy: strict`