很多 Agent 项目都会遇到同一个问题日志越堆越多长期记忆文件却越来越像垃圾场。你让它“记住重要的事”它要么把所有原始对话一股脑塞进MEMORY.md要么用 LLM 总结出一堆似是而非的“洞察”。前者让长期记忆被噪声淹没后者让长期记忆失真——LLM 的转述一旦写进去你就再也找不到原始上下文了。OpenClaw 的 Dream 机制给了一个很不一样的答案不判断信息本身重不重要而是让信息在真实对话中证明自己被需要再巩固下来。https://github.com/openclaw/openclaw/blob/main/docs/concepts/dreaming.md这篇文章把 Dream 机制的完整实现路线拆开讲清楚包含数据格式、评分公式、门控逻辑和六周落地计划。零基础也能跟。先破除 8 个新手误解在讲实现之前先把最容易卡住的地方拆掉。这些误解几乎每个人第一次接触 Dream 时都会有。误解 1以为MEMORY.md每次对话都全量塞进上下文。不是。MEMORY.md是长期记忆库通过按需召回被片段化地读取不会整篇塞进每次对话。误解 2以为用户画像存在MEMORY.md里。不是。用户画像姓名、偏好、沟通风格存在独立的USER.md会话启动时注入系统提示词。MEMORY.md存的是“经过策展的长期事实”。误解 3以为暂存区存的是用户的 Query 原文。不是。暂存区存的是日志片段的信号画像。Query 只以哈希形式参与“查询多样性”这一个维度原文不落盘。误解 4以为 REM 阶段调用 LLM 提取“新事实”。不是。REM 不提取新事实。新事实的来源是 Light 阶段从日志里切出的片段。REM 的 LLM 只做主题反思输出一个温和的rem_hits信号。误解 5以为 Deep 写入MEMORY.md的是 LLM 提炼的“事实”。不是。写入的是原始日志片段的逐字原文带来源注释。LLM 的转述不会进入MEMORY.md。误解 6以为新事实没有召回次数就永远无法晋升。它确实初始recall_count 0无法立刻晋升。它需要在后续真实对话中被反复召回次数才会增长。这是设计意图不是缺陷一次性出现的信息本来就不该轻易进入长期记忆。误解 7以为日期日志是按 Session 分的。不是。memory/YYYY-MM-DD.md是按天汇总的 append-only 文件同一天所有会话、所有轮次都追加到同一个文件里。误解 8以为按需召回会把整篇日志或完整交互轨迹注入上下文。不是。召回返回的是片段snippet即被切块后命中的那个行范围默认最多注入 3 条。一、整体架构两条管线在“召回痕迹”处交汇Dream 不是一个独立跑的程序它由两条管线组成实时管线清醒时运行用户发消息 → 向量语义搜索相关片段 → 记录这次召回 → 把片段注入上下文给模型参考。后台管线空闲或夜间运行按 Light → REM → Deep 顺序执行。Light 读日志切块暂存REM 对活跃片段做 LLM 主题反思Deep 评分门控后把合格片段追加到MEMORY.md。实时管线用户消息 → 向量召回 → 记录痕迹 → 注入上下文 ↓ 写入 recall-traces / candidates.json ↓ 读取 后台管线Light 切块暂存 → REM 主题反思 → Deep 评分门控 → MEMORY.md这里有四条铁律新手务必先记住只有 Deep 能写MEMORY.md。Light 和 REM 只写信号不写长期记忆。recall_count真实用户召回和daily_count日志扫描摄入严格分开。门控只认recall_count。MEMORY.md只追加不替换。现有内容永远保留。Deep 写入的是原始片段不是 LLM 提炼的事实。二、存储布局纯文件系统每个文件都有明确职责不用向量数据库不用消息队列纯文件系统就能跑起来。workspace/ ├── memory/ │ ├── 2026-03-01.md # 每日日志按天汇总所有会话只追加 │ ├── 2026-03-02.md │ └── .dreams/ # Dream 中间态 │ ├── candidates.json # 暂存区片段 信号画像 │ ├── phase-signals.json # Light/REM 信号 │ ├── recall-traces/ │ │ └── 2026-03-01.jsonl # 按天分文件的召回痕迹 │ ├── index/ │ │ ├── vectors.npy # 向量矩阵 │ │ ├── keys.json # 行号到片段 key 的映射 │ │ └── meta.json # 模型名、维度、数量 │ └── runs/ │ ├── light-2026-03-04.json │ ├── rem-2026-03-04.json │ └── deep-2026-03-04.json ├── MEMORY.md # 长期记忆只由 Deep 追加 ├── USER.md # 用户画像启动时注入系统提示词 └── DREAMS.md # 人类可读审计日志关键原则memory/*.md是唯一原始来源永远 append-only。.dreams/下所有文件都是可重建的派生状态——删掉.dreams/跑一次 Light 就能从日志和召回痕迹重建。各文件格式召回痕迹recall-traces/YYYY-MM-DD.jsonl每行一条召回事件。{key:memory/2026-03-01.md:10-12,query:项目代号叫什么,query_hash:a1b2c3d4,score:0.87,recalled_at:2026-03-01T10:05:00Z}暂存区candidates.json单个 JSON 对象key 是片段标识value 是该片段的全部信号。{memory/2026-03-01.md:10-12:{source_path:memory/2026-03-01.md,start_line:10,end_line:12,snippet:项目代号是 Falcon。,content_hash:3a7bd3e2...,recall_count:2,daily_count:1,query_hashes:[a1b2c3d4,e5f6a7b8],total_score:1.69,last_recalled_at:2026-03-03T14:20:00Z,light_hits:1,rem_hits:0,promoted:false}}为什么daily_count和recall_count必须分开daily_count是 Light 阶段“日志扫描又看到它”的次数每次运行都会自动增长不需要用户真实使用。如果把它混进recall_count用于门控任何旧片段都会因为“每天都在日志里”而自动满足召回次数要求门控就失效了。门控只认recall_count。三、前置模块向量语义召回Dream 的评分原料来自真实召回所以必须先有一个能工作的召回模块。嵌入模型推荐BAAI/bge-m31024 维中文检索性能最优CPU 可跑内存紧张可用 ONNX int8 版558MB。英文为主可用all-MiniLM-L6-v2384 维极轻但中文差。索引维护Light 阶段为每个新增或内容变更的片段调用upsert_vector()计算嵌入并写入vectors.npy。已存在片段替换对应行新片段追加。检索函数把用户 query 编码成向量与全量向量矩阵做点积双方都已 L2 归一化点积即余弦相似度取 top-K默认 3。对每个命中结果调用record_recall()。defrecord_recall(candidate_key:str,query:str,score:float):qhhashlib.sha256(query.encode()).hexdigest()[:16]nowdatetime.utcnow().isoformat()# 1. 追加 JSONL 一行# 2. 更新 candidates.json:# recall_count 1# total_score score# query_hashes 去重追加# last_recalled_at 更新关键细节同一个 query 反复命中同一片段时recall_count增长但query_hashes不重复增长。这保证“频率”和“多样性”是两个独立维度。四、Light 阶段切块暂存输入最近 3 天的每日日志文件。切块策略按空行或段落边界切分每块目标 100–300 字符块间保留 20% 重叠防止边界信息丢失。每块生成稳定 key、行号范围、原文 snippet、内容哈希。去重与暂存遍历切块结果。若候选已存在且内容哈希一致仅递增daily_count若不存在或内容已变upsert 到candidates.json初始化所有信号为 0并加入待建向量列表。Light 信号扫描近期召回痕迹找出有召回记录的片段 key对每个递增light_hits同时更新phase-signals.json。关键约束Light 阶段不调用 LLM纯机械操作。五、REM 阶段LLM 主题反思很多人以为 REM 是让 LLM 去日志里提取“值得记的新事实”。不是。输入筛选只读取phase-signals.json中light_hits 0且尚未晋升的候选。绝不重扫全量候选——否则旧的高召回片段会持续霸榜新片段永远没有机会。LLM 调用将筛选后的片段单次上限 50 条送入低成本模型要求输出 JSONthemes主题、associations关联、highlights值得保留的 key 列表。信号回写对highlights中的每个 key递增rem_hits。人类可读输出将themes和associations追加到DREAMS.md的## REM Sleepblock。这只是给你看的审计记录不是晋升依据。关键澄清REM 的 LLM 输出不产生新事实不进入MEMORY.md。它的“判断”只体现为rem_hits这个温和的评分加成信号。新事实的真正来源始终是 Light 阶段从日志切出的片段。六、Deep 阶段评分门控与晋升核心这是整个机制最关键的部分分五步执行。第一步六维加权评分新手最容易卡在这里因为原始信号数值差异极大召回次数 0~50相似度 0~1直接相加没有意义。所以中间必须有一层归一化——把每个维度都变成 0~1 的分再加权求和。维度 1Relevance相关性— 权重 0.30衡量“这条记忆被召回时平均有多准”。公式relevance total_score / recall_count截断到 1.0。例子片段被召回 3 次相似度分别 0.85、0.80、0.90则total_score 2.55relevance 0.85。注意分母是这条片段自己的召回次数不是全局的。维度 2Frequency频率— 权重 0.24衡量“这条记忆被用了多少次”但用对数压缩高频条目的优势。公式frequency ln(1 recall_count) / ln(11)。召回次数频率分1 次0.2893 次0.5785 次0.74710 次1.00050 次1.640从 1 次到 3 次分数翻了近一倍从 10 次到 50 次只从 1.0 涨到 1.64。前几次召回最有价值旧条目无法单凭频率碾压新候选。维度 3Diversity查询多样性— 权重 0.15公式diversity len(query_hashes) / 5截断到 1.0。含义命中 5 个不同查询就认为通用性满分。用户用“项目代号”“那个鸟名字的项目”“之前说的代号”三种方式都能召回它显然比只用一种问法更值得保留。维度 4Recency新近度— 权重 0.15公式recency exp(-days_since_last_recall / 14)半衰期约 14 天。含义14 天没被召回分数衰减到约 0.3728 天没被召回衰减到约 0.14。维度 5Consolidation巩固度— 权重 0.10公式consolidation min(1.0, light_hits * 0.1 rem_hits * 0.15)。含义Dream 内部产生的“睡眠期间二次关注”信号只作为温和加成。维度 6Conceptual Richness概念丰富度— 权重 0.06公式conceptual min(1.0, len(snippet) / 300)。含义权重最低只微弱偏好内容充实的片段避免极短的“嗯”“好的”混入。把六项拼起来base_score 0.30 * relevance 0.24 * frequency 0.15 * diversity 0.15 * recency 0.10 * consolidation 0.06 * conceptual六项之和就是base_score范围 0~1。阶段加成在 base_score 之上叠加phase_bonus min(0.05, light_hits * 0.01) * recency min(0.08, rem_hits * 0.015) * recency final_score min(1.0, base_score phase_bonus)阶段加成上限很小Light 最多 0.05REM 最多 0.08并且要乘以recency——很久没被召回的记忆即使被阶段标记过加成也会被时间衰减打折。阶段信号是锦上添花不是雪中送炭。第二步三重门控评分算出一个final_score后并不会直接拿分数决定一切。门控是三道硬性下限必须同时满足recall_count 3真实用户召回次数len(query_hashes) 2不同查询数保证通用性final_score 0.75综合分下限候选片段 → [recall_count ≥ 3] → [unique_queries ≥ 2] → [score ≥ 0.75] → 晋升列表 ↓ 不过 ↓ 不过 ↓ 不过 拒绝·记录原因 拒绝·记录原因 拒绝·记录原因门控和评分的分工门控负责否决明显不合格的候选没被用过、只用过一种问法、综合分太低评分负责在合格者内部排序。一个候选即使final_score高达 0.95如果recall_count 0依然会被minRecallCount直接拦下。新手最常问如果事实刚被提取出来它没有召回次数怎么办答案是——它确实初始recall_count 0需要在后续真实对话中被反复搜索命中次数才会增长。零召回的“一次性事实”会被门控拦截这是设计意图不是缺陷。第三步重新水合通过门控的候选不能直接写入。从源日志文件重新读取对应行范围计算内容哈希并与content_hash比对。若源文件不存在、行号越界或哈希不匹配源被编辑或删除跳过该候选记录“源已失效”。这防止把陈旧内容晋升到长期记忆。第四步追加写入MEMORY.md通过水合的候选按分数降序排列以## Promoted From Short-Term Memory ({日期})为标题追加到MEMORY.md末尾。## Promoted From Short-Term Memory (2026-04-15) - 项目代号是 Falcon。 !-- source: memory/2026-03-01.md:10-12 score: 0.812 recalls: 3 --新手常见误解以为 Deep 会替换整个MEMORY.md。不是。只追加新的 block现有内容完全保留。写入的是原始片段逐字内容不是 LLM 提炼的事实。第五步审计与 dry-run将本次运行的输入数、晋升数、拒绝数、晋升列表、拒绝样本及原因写入runs/deep-{date}.json并在DREAMS.md追加## Deep Sleep摘要。Dry-run 是必选项不是可选项。首次部署时以 dry-run 运行只输出“将会晋升什么、拒绝什么、原因”人工审查评分排序合理性后再开启实际写入。直接开启实际写入可能导致低质量片段污染MEMORY.md。七、调度与运维调度方式cron 夜间执行如0 3 * * *为主空闲检测最近 N 分钟无交互为兜底显式/dream指令用于手动调试。三者调用同一个run_dream_sweep(dry_run)函数。配置外置dreaming:schedule:0 3 * * *light:lookback_days:3chunk_size:300deep:dry_run:falsethresholds:min_score:0.75min_recall_count:3min_unique_queries:2并发安全candidates.json的读-改-写必须用文件锁fcntl.flock包裹或确保 Dream sweep 是单进程。召回管线的record_recall()与 Dream 管线可能并发必须加锁。更换嵌入模型的代价嵌入模型与维度必须全局一致。中途更换模型旧向量与新查询向量不在同一语义空间距离计算毫无意义。必须删除index/整个目录从candidates.json全量重建。八、六周落地路线周任务验收标准第 1 周召回痕迹管线向量嵌入、索引读写、search_memory()、record_recall()recall-traces/下能看到新 JSONL 行recall_count和query_hashes正确增长第 2 周Light 阶段切块、去重暂存、light_hits递增、向量同步candidates.json有合理片段vectors.npy行数与候选数一致第 3 周Deep 阶段 dry-run评分函数、三重门控、重新水合只输出不写文件用历史数据回放人工检查评分排序是否合理第 4 周Deep 实际写入与审计MEMORY.md追加、DREAMS.md审计MEMORY.md末尾正确追加现有内容保留源失效片段被跳过第 5 周REM 阶段LLM 主题反思、rem_hits递增rem_hits合理增长确认 REM 输出未进入MEMORY.md第 6 周调度与调参接入 cron 和空闲检测跑 1–2 周真实数据观察晋升条目质量调整阈值与权重MVP 路径如果想最快验证核心闭环只做第 1、2、3 周加 Deep 的实际写入跳过 REM即可跑通“召回痕迹 → 切块暂存 → 评分门控 → 追加审计”主线。REM 提供的只是温和加成不是晋升的决定性因素。九、九个必须绕开的坑坑后果正确做法把daily_count混入recall_count用于门控日志扫描信号冒充用户召回门控失效两字段分离门控只用recall_countREM 重扫全量候选旧高召回条目霸榜新候选永远进不来REM 只读light_hits 0的候选不做重新水合源被编辑后MEMORY.md出现陈旧内容写入前校验content_hashLight/REM 写MEMORY.md噪声污染长期记忆代码强制只有 Deep 有写权限MEMORY.md整篇替换丢失历史长期记忆只以追加模式写入以为 Deep 写入的是 LLM 提炼的事实实际写入原始 snippet设计预期需对齐接受“原始片段 使用频率门控”的巩固模型更换嵌入模型后不重建索引新旧向量维度或语义空间不一致删除index/整个目录全量重建向量索引与候选集不同步新片段有候选无向量召回中永远不可见Light 阶段保存候选后立即建向量首次部署直接开启实际写入低质量片段污染MEMORY.md先以 dry-run 运行至少一周十、最终形态一条记忆的晋升之路系统稳定运行后一条记忆的完整轨迹是这样的3月1日 日志首次出现「项目代号是 Falcon」 recall_count 0 3月3日 用户问「项目代号」被召回 recall_count 1 3月5日 用户问「Falcon 是哪个项目」 recall_count 2, unique 2 3月8日 用户问「之前说的代号」 recall_count 3, unique 3 3月9日 Deep 运行评分 0.81三门全过 → 追加到 MEMORY.md 之后 每次被召回recall_count 继续增长 长期记忆持续被使用新事实不是第一次出现就晋升而是用真实使用数据证明自己。这套设计的本质是用行为数据召回频率与多样性而非语义判断来决定记忆的价值。LLM 在 REM 阶段只提供辅助信号不掌握晋升决定权。这保证了长期记忆的质量下限同时保持了整个管线的人类可读性和可调参性。最后一个想和你讨论的问题Deep 阶段写入MEMORY.md的是原始日志片段的逐字原文而不是 LLM 提炼后的摘要。OpenClaw 当前就是这么实现的。但是使用LLM可以借鉴Mem0的形式做一个自动化的记忆更新。你更倾向哪一种原始片段还是 LLM 摘要欢迎在评论区聊聊。如果这篇文章帮你理清了 Dream 机制的实现思路欢迎点赞收藏。
阅读完成 · 觉得有帮助?