拆解 OpenClaw Dreaming 的分层记忆流水线,看 Agent 如何把会话沉淀为可检索长期上下文。
原文链接 :AI 小老六
导语
一个能长期工作的 Agent,麻烦点不在"把对话存下来"。因为聊天记录本身太粗糙:里面有临时指令、工具失败、过程性解释、重复确认,也有少量会在未来反复有用的偏好、事实和工程判断。把这些内容原封不动塞进长期记忆,只会让上下文越来越脏。
OpenClaw Dreaming 机制处理的是一类问题:怎样让一次次对话先变成可追溯素材,再经过候选筛选、稳定归纳和索引构建,最后成为未来任务可以调用的长期上下文。
这套机制更像一条后台运行的记忆生产线 。前台会话负责完成当前任务和保留原始事实;后台 Dreaming 再把这些事实加工成不同稳定程度的记忆,并通过 SQLite、全文索引和 memory_search 接回未来对话。
记忆不是一个文件:从日志、候选到索引的三层边界
OpenClaw 的记忆系统最容易被误解为"一个 Markdown 长期笔记"。实际情况更接近一套分层存储:原始会话、加工过程、长期正文、检索索引分别承担不同职责。
| 层级 | 典型位置 | 保存内容 | 核心职责 | 人工介入建议 |
|---|---|---|---|---|
| 原始输入层 | tech/sessions/*.jsonl |
用户消息、助手回复、工具事件、运行时元信息 | 留住当时发生了什么 | 不建议编辑,适合作为源日志 |
| 素材缓冲层 | memory/.dreams/session-corpus/YYYY-MM-DD.txt |
从 session 中抽取出的可读片段 | 给 Dreaming 提供当天素材 | 不建议编辑,适合排查抽取问题 |
| 运行审计层 | memory/.dreams/events.jsonl |
召回事件、阶段完成事件、写入路径 | 解释某次 Dreaming 是否运行、如何运行 | 只读排查 |
| 状态信号层 | daily-ingestion.json |
|||
、session-ingestion.json、short-term-recall.json、phase-signals.json |
游标、hash、召回次数、阶段命中 | 支持增量处理、去重和候选晋升判断 | 不建议手改 | |
| 长期正文层 | memory/YYYY-MM-DD.md |
|||
、DREAMS.md |
Light Sleep、REM Sleep、反思摘要和稳定上下文 | 供人和 Agent 阅读、审计、复用 | 可谨慎编辑,保留结构标记 | |
| 检索索引层 | ~/.openclaw/memory/tech.sqlite |
files |
||
、chunks、chunks_fts、embedding_cache |
支撑关键词检索、语义检索和增量索引 | 不应作为最终事实源 | ||
| 调用接口层 | memory_search |
|||
、memory_get |
查询和回读能力 | 让未来任务召回相关长期上下文 | 不涉及编辑 |
可以把这套结构拆成三条边界:
| 边界 | 关键判断 |
|---|---|
tech/sessions/*.jsonl |
|
| 记录事实,但不判断长期价值 | 它回答"当时发生过什么",不回答"什么值得以后记住"。 |
.dreams/* |
|
| 记录生产过程,但不是最终知识库 | 它回答"为什么这条内容被召回或处理",不适合直接当长期事实。 |
memory/*.md |
|
是可审计正文,tech.sqlite 是派生索引 |
排查时可以先从索引定位,再回到 Markdown 原文看上下文。 |
图:从会话日志到长期记忆检索回流的完整链路
这张图表达的重点是:记忆不会从当前对话直接跳到长期事实。它先经过素材化、候选化、稳定化,再进入可检索的长期上下文。
Session Ingestion:先保留原始上下文,再做延迟判断
每次用户发起请求时,系统首先要解决的是当前任务,而不是立刻"学习"。因此用户消息、助手回复、工具调用和运行元信息会先落到 session JSONL。这个阶段只保证一件事:原始上下文没有丢。
后续 Dreaming 会从这些持续追加的 session 文件里做增量抽取。session-ingestion.json 记录每个 session 文件的处理进度,例如文件大小、修改时间、内容 hash、行数以及最后处理到的内容行。这样后台任务不需要每次全量扫描,也能避免重复抽取同一段会话。
素材进入 session-corpus 后,会变成更适合阅读和回溯的行级文本:
[tech/sessions/...jsonl#L10] Assistant: 我先核对源码里的 dreaming / REM / DEEP 文件和规则,再把确认后的结论补进文档。
这一行看似普通,实际保留了三个关键信息:
| 字段 | 示例 | 作用 |
|---|---|---|
| 来源文件 | tech/sessions/...jsonl |
能定位到哪个 Agent 会话产生了素材 |
| 行号锚点 | #L10 |
支持回到原始日志查看上下文 |
| 角色与内容 | Assistant: ... |
|
、User: ... |
给后续候选判断提供语义输入 |
图:会话日志增量抽取为素材语料的过程
这一步的设计取舍很清楚:先完整留痕,再延迟判断。对话里的很多内容只对当前任务有用,过早写入长期记忆会制造噪声;但完全不留痕,又会丢掉后续可能需要归纳的证据。
Light 与 REM:用稳定性区分候选记忆和长期判断
memory/YYYY-MM-DD.md 是 Dreaming 输出最直观的位置。它不是单纯按时间堆内容,而是在文件内部用阶段标记区分不同稳定程度的信息。
| 区块 | 典型标记 | 内容形态 | 语义定位 | 使用方式 |
|---|---|---|---|---|
| Light Sleep | `` | |||
到 light:end |
Candidate、confidence、evidence、recalls、status | 候选记忆,表示"可能有用,但还要观察" | 适合近期续接、路径线索、任务偏好补全 | |
| REM Sleep | `` | |||
到 rem:end |
Reflections、Possible Lasting Truths | 更稳定的长期归纳 | 更适合影响后续回答、工具选择和写作规范 | |
| 会话补充 | 普通 Markdown 小节 | 文档链接、用户偏好、任务结果、实现锚点 | 人工或系统补写的稳定上下文 | 适合明确记录可复用事实 |
Light 和 REM 的差别不在格式,而在可信度和使用方式。
| 对比维度 | Light Sleep | REM Sleep |
|---|---|---|
| 目标 | 捕捉近期可能有价值的线索 | 提炼可长期复用的事实、偏好、模式和约束 |
| 粒度 | 较细,常带路径、原话、任务片段和证据位置 | 较粗,通常压缩成稳定判断 |
| 可信度表达 | 常见 confidence、status=staged、recalls |
通常来自多次 evidence 或更高置信归纳 |
| 行为影响 | 应谨慎使用,主要补全上下文 | 更容易影响后续回答和任务判断 |
| 典型用途 | 近期任务续接、排查线索保留 | 用户偏好、长期规则、重复问题模式 |
图:候选记忆如何经过证据判断沉淀为 REM 归纳
实际阅读日期记忆时,比较稳的顺序是:先看 REM 得到稳定判断,再回到 Light 看证据和来源。这样可以避免把候选层的低置信线索误当成长期事实。
图:Light 候选更像暂存信号,REM 归纳更接近可长期复用的稳定判断
Recall State 与 Phase Signal:候选为什么会被晋升
如果只有 Light 和 REM 两个正文区块,系统仍然难以回答一个调试问题:为什么这条内容会被保留,为什么它会从候选晋升为稳定记忆?
OpenClaw 用两类状态文件补齐这条证据链。
| 文件 | 处理对象 | 常见字段 | 粒度 | 解决的问题 |
|---|---|---|---|---|
daily-ingestion.json |
memory/YYYY-MM-DD.md |
|||
| 等日期记忆文件 | mtimeMs |
|||
、size、path |
文件级 | 判断每日记忆文件是否变化 | ||
session-ingestion.json |
tech/sessions/*.jsonl |
mtimeMs |
||
、size、contentHash、lineCount、lastContentLine |
文件级 + 行级 + hash | 支持持续追加日志的增量抽取 | ||
short-term-recall.json |
被检索命中的记忆片段 | recallCount |
||
、recallDays、queryHashes、conceptTags |
片段级 | 判断哪些片段近期经常被搜索到 | ||
phase-signals.json |
Light / REM 阶段出现的 memory key | lightHits |
||
、remHits、更新时间 |
阶段信号级 | 判断候选是否具备晋升趋势 |
**short-term-recall.json 关注的是"它是不是经常被查到"** ;phase-signals.json 关注的是"它是不是反复被 Dreaming 认为重要"。两者结合,候选晋升才更有解释力。
图:召回热度和阶段信号共同影响候选晋升
事件日志则提供运行过程的时间线。典型事件会像这样出现:
json
{"type":"memory.recall.recorded","query":"dreaming_sessions:2026-05-09","resultCount":105}
{"type":"memory.dream.completed","phase":"light","inlinePath":".../memory/2026-05-10.md"}
{"type":"memory.dream.completed","phase":"rem","inlinePath":".../memory/2026-05-10.md"}
这些事件不适合直接用来回答用户问题,但很适合排查:某个日期有没有被 Dreaming 处理,处理到了 Light 还是 REM,召回结果数量是否异常,最终写入了哪个文件。
DREAMS.md 与 tech.sqlite:一个讲述变化,一个服务检索
长期记忆里还有两个容易混淆的对象:DREAMS.md 和 tech.sqlite。
DREAMS.md 更像系统近期状态的叙事层。它会用更自然的方式记录反复出现的主题、工作流变化、阻塞点和观察结论。它有助于理解"最近系统在关注什么",但不一定适合作为严格事实源。
**tech.sqlite 则完全是另一类东西** 。它是从 Markdown 记忆派生出来的检索加速层,服务 memory_search,不应该替代 Markdown 原文做人工审计。
| 对象 | 更像什么 | 主要读者 | 稳定性 | 适合用途 |
|---|---|---|---|---|
DREAMS.md |
叙事型反思日志 | 人类和 Agent | 偏解释性 | 理解近期主题、工作流变化、未解决问题 |
memory/YYYY-MM-DD.md |
结构化长期记忆正文 | Agent、检索系统、人类 | 更适合长期复用 | 作为长期记忆主路径和可追溯证据 |
tech.sqlite |
派生索引 | memory_search |
||
| 、索引器 | 依赖 Markdown 同步 | 快速定位相关片段 |
tech.sqlite 的表结构大致承担这些职责:
| 表 | 作用 | 与文件系统的关系 |
|---|---|---|
meta |
保存索引版本、配置等元信息 | 帮助判断索引结构和运行参数 |
files |
记录被索引文件的路径、hash、mtime、size | 判断哪些 Markdown 文件需要同步 |
chunks |
保存按行切分后的记忆片段、文本和 embedding 信息 | 把长 Markdown 拆成可检索单元 |
chunks_fts |
FTS5 全文索引 | 支持关键词、路径、中文片段等全文检索 |
embedding_cache |
缓存 embedding 结果 | 避免同一文本重复生成 embedding |
图:Markdown 记忆被切分、索引并服务检索
这条链路解释了一个关键原则:检索结果只是入口,Markdown 原文才是审计依据 。排查记忆内容时,应先用索引找到路径和行号,再回到 memory/*.md 看完整上下文。
一次真实查询如何穿过记忆系统
当用户提出一个与历史任务、偏好或技术结论有关的问题时,OpenClaw 并不是只依赖当前上下文。它会在当前任务和历史记忆之间建立一条回路。
| 步骤 | 参与文件或接口 | 发生的事情 | 输出 |
|---|---|---|---|
| 理解当前问题 | 当前用户消息、会话上下文 | 判断问题是否需要历史偏好、任务结果或技术结论 | 明确检索意图 |
| 召回历史 | memory_search |
||
、tech.sqlite |
从已索引记忆中找相关片段 | 获得候选历史上下文 | |
| 回读证据 | memory_get |
||
、memory/*.md |
按路径和行号读取原始 Markdown | 避免只凭索引摘要判断 | |
| 查证过程 | .dreams/*.json/jsonl |
||
、phase-signals.json |
读取事件、游标、阶段信号 | 判断某条记忆来源是否可靠 | |
| 组织回答 | DREAMS.md |
||
、session-corpus、日期记忆 |
区分叙事层、素材层、候选层、稳定层 | 形成可读解释 | |
| 写回结果 | 文档、Markdown 或新的 session | 输出本次结论 | 成为未来 Dreaming 的潜在素材 |
图:一次查询如何通过索引回读可审计原文
Dreaming 的后台属性也体现在这里:当前对话产生素材,后台筛选素材,长期记忆被索引,未来对话再把它召回。系统不追求记住一切,它反复压缩真正有复用价值的上下文。
图:长期记忆通过索引被召回,再参与下一次 Agent 回答与任务执行
为什么不能退回单文件记忆
把所有长期信息放进一个 memory.md 文件,看起来简单,实际会很快暴露工程问题。
| 工程问题 | 单文件记忆的风险 | 分层机制的处理方式 |
|---|---|---|
| 原始会话太噪 | 问候、失败重试、过程说明混进长期事实 | session-corpus |
| 先作为素材层,等待筛选 | ||
| 候选事实不稳定 | 未验证事实直接污染长期记忆 | Light Sleep 用 confidence、evidence、status 暂存 |
| 长期偏好需要压缩 | 历史上下文越积越长,检索结果越来越散 | REM Sleep 将重复模式压缩为稳定判断 |
| 检索需要速度 | 每次查询都扫描全量 Markdown,成本高且不稳定 | SQLite chunk + FTS + embedding 提供快速检索 |
| 调试需要证据链 | 不知道某条记忆从哪里来、为什么出现 | events、ingestion、recall、phase signals 保留过程证据 |
| 多阶段处理需要去重 | 每次运行都可能重复写入同一素材 | ingestion 游标和 recall key 支持增量处理与去重 |
图:单文件记忆与 Dreaming 分层机制的工程差异
分层不是为了把系统做复杂,而是为了让不同稳定程度的信息拥有不同出口。越靠近原始会话,越强调完整和可追溯;越靠近 REM 和索引,越强调稳定、压缩和复用。
排查 Dreaming:先看结果,再追过程,最后查索引
分析 Dreaming 输出时,不建议一上来就打开 SQLite,也不建议直接修改 .dreams 里的状态文件。更稳的路径是按"结果、过程、索引"逐层排查。
| 顺序 | 文件 | 先看它的原因 | 重点关注 |
|---|---|---|---|
| 1 | memory/YYYY-MM-DD.md |
先确认最终有没有沉淀 | Light / REM 是否存在,内容是否符合预期 |
| 2 | memory/.dreams/events.jsonl |
确认 Dreaming 是否完成、完成哪个阶段 | memory.dream.completed |
、phase、inlinePath |
|||
| 3 | memory/.dreams/session-corpus/YYYY-MM-DD.txt |
查看当天原始会话素材是否被抽取 | 是否有目标会话、来源行号是否正确 |
| 4 | short-term-recall.json |
判断哪些片段近期被频繁召回 | recallCount |
、recallDays、conceptTags |
|||
| 5 | phase-signals.json |
判断哪些片段被 Light / REM 反复命中 | lightHits |
、remHits |
|||
| 6 | daily-ingestion.json |
||
、session-ingestion.json |
排查为什么没有新增素材或重复处理 | lineCount |
|
、contentHash、mtimeMs |
|||
| 7 | tech.sqlite |
最后确认索引是否同步 | files |
、chunks、chunks_fts |
图:按结果、过程、索引逐层定位记忆问题
这条路径能减少误判:先确认"最终有没有写入",再确认"过程有没有运行",最后确认"索引有没有跟上"。
几个容易踩错的理解
| 常见理解 | 更准确的说法 | 为什么重要 |
|---|---|---|
| Dreaming 就是把聊天记录写进 memory | 它会先抽取素材,再通过候选、召回和阶段信号筛选 | 避免把所有临时对话都当成长期事实 |
.dreams |
||
| 目录就是长期记忆 | .dreams |
|
更偏中间状态和运行审计,长期记忆主要看 memory/*.md |
避免把状态文件误当正文知识库 | |
tech.sqlite |
||
| 是最终事实源 | 它是派生索引,Markdown 原文才更适合人工审计 | 排查时应回到 path 和 line 的原文 |
| Light Sleep 已经是稳定事实 | Light 只是候选,常带 status=staged 和较低置信度 |
后续回答不应过度依赖候选层 |
| REM Sleep 一定完全正确 | REM 更稳定,但仍应看 evidence 和上下文 | 长期记忆仍可能过期或受历史上下文影响 |
| 文件存在就一定能被搜到 | 需要被索引器同步进 tech.sqlite 才能被快速检索 |
能解释 memory 文件和 search 结果不一致的问题 |
用五个子系统重新理解 Dreaming
从工程架构看,Dreaming 可以拆成五个子系统,而不是一堆零散文件:
| 子系统 | 输入 | 输出 | 关键文件 |
|---|---|---|---|
| 输入记录 | 当前对话、工具事件、运行元信息 | 原始 session 日志 | tech/sessions/*.jsonl |
| 素材抽取 | 新增 session 行 | 按日期聚合的可读素材 | session-corpus/YYYY-MM-DD.txt |
、session-ingestion.json |
|||
| 候选判断 | 素材、历史召回、阶段信号 | Light Candidate | short-term-recall.json |
、phase-signals.json |
|||
| 长期沉淀 | 多次命中或高价值候选 | REM Reflection、日期记忆 | memory/YYYY-MM-DD.md |
、DREAMS.md |
|||
| 检索回流 | Markdown 记忆正文 | chunk、FTS、embedding、未来召回 | tech.sqlite |
、memory_search、memory_get |
图:Dreaming 可拆解为输入、抽取、候选、沉淀和回流五个子系统
这个拆法对排查很有帮助:素材缺失就看输入记录和抽取;候选不合理就看召回和阶段信号;回答没有复用历史记忆,就看索引回流和 memory_search。
结语
OpenClaw Dreaming 的价值,不在于让 Agent 机械地记住更多内容。它给长期记忆补上了一套生产过程。
原始会话先被保留下来,随后进入素材层;素材被召回、聚合、打分,形成 Light 候选;重复出现或价值明确的内容被压缩成 REM 判断;最终 Markdown 正文进入 SQLite 索引,再通过 memory_search 和 memory_get 回到未来任务。
这样形成的长期记忆不是黑盒摘要,也不是无限膨胀的聊天记录。它有来源、有阶段、有证据、有索引,排查时还能回到原文验证。对一个需要长期协作的 Agent 来说,盲目记住所有事情没有意义;能把可复用的上下文沉淀下来,并在下一次任务里解释清楚为什么召回它,才更有用。OpenClaw Dreaming 记忆系统拆解:Agent 长期记忆如何从会话日志流向检索回流