跳转到内容

Dream 两阶段记忆巩固

Dream 两阶段记忆巩固

学习目标

理解 Nanobot 的 Dream 系统如何通过两阶段 LLM 调用实现自动化记忆管理,以及 Git 行级年龄标注的创新设计。

前置知识

下文假设你已理解上述概念,直接聚焦 Nanobot 的具体实现。

项目实践

两阶段处理管线

Dream 是 Nanobot 的”后台记忆整理引擎”,以定时任务(默认每 2 小时)运行,将未处理的对话历史转化为结构化记忆。

Phase 1 — 分析

  • 将未处理的对话历史 + 当前 MEMORY.md/SOUL.md/USER.md 内容一起发送给 LLM
  • LLM 分析哪些信息值得记录,哪些已过时需要淘汰
  • 输出纯文本分析结果

Phase 2 — 执行

  • 将 Phase 1 的分析结果作为指令
  • 通过 AgentRunner 驱动一个带 read_file/edit_file/write_file 工具的最小 Agent
  • Agent 自主编辑 MEMORY.md、SOUL.md、USER.md 和创建新技能

Git 行级年龄标注

Dream 在 Phase 1 中会为 MEMORY.md 的每一行附加年龄后缀,帮助 LLM 判断信息的时效性:

用户喜欢用 Python 编写项目 ← 45d
项目使用 FastAPI 框架 ← 2d

实现方式:

  • 使用 git blame 获取每行的最后修改时间
  • 超过 14 天(_STALE_THRESHOLD_DAYS = 14)的行附加 ← Nd 后缀
  • 如果 HEAD blob 的行数与当前文件内容不一致,跳过标注(避免错误标记)

这种设计的独特价值:LLM 可以基于年龄判断”这条记忆已经很久未更新,可能已过期”,从而主动淘汰过时信息。

Dream Cursor 机制

使用单调递增的 cursor 追踪哪些历史条目已被 Dream 处理:

  • .cursor 文件:记录 history.jsonl 的最新条目序号
  • .dream_cursor 文件:记录 Dream 已处理到的序号

只有当 Phase 2 成功完成(stop_reason == "completed")时才推进 Dream Cursor,确保未完成的工作在下一轮重试。

存储层设计

MemoryStore 使用 JSONL 格式(history.jsonl)存储短期历史,而非数据库:

  • 每条记录:{"cursor": N, "timestamp": "...", "content": "..."}
  • 原子写入:先写入 .tmp 文件,fsyncos.replace 原子替换
  • 目录 fsync:确保元数据持久化(Windows 跳过)
  • 遗留迁移:自动将旧版 HISTORY.md 迁移为 JSONL 格式

问题与规避

行数不一致导致标注跳过

问题:MEMORY.md 被 Dream 以外的写入者修改后,git blame 行数与实际行数不一致。

规避:行数不一致时完全跳过标注,返回未注释的原始内容,避免 LLM 收到错误的年龄信息。

Dream 失败不推进 Cursor

问题:如果 Phase 2 异常退出,不推进 Cursor 会导致下次重复处理相同条目。

规避:这是设计意图 —— 未完成的工作应该重试。同时 Dream Cursor 只在 stop_reason == "completed" 时推进。

历史条目污染

问题:外部写入者可能写入非整型 cursor 的值,污染 JSONL。

规避_valid_cursor() 拒绝非整型值(包括 bool,因为 isinstance(True, int) 在 Python 中为 True),污染条目被静默丢弃并记录一次警告。

设计取舍

为什么用 JSONL 而非数据库

优势

  • 文件可人工检查、调试和版本控制
  • 无需额外依赖
  • Git 可以追踪变更历史

代价

  • 大文件查询效率不如数据库
  • 缺乏事务和索引

替代方案:SQLite。Nanobot 选择 JSONL 的原因是”研究就绪”定位 —— 代码库应易于学习和修改。

为什么用两阶段而非单次 LLM 调用

Phase 1 分析、Phase 2 执行的分离避免了单次调用既要思考又要操作的认知过载。Phase 2 通过工具可以读取完整文件(Phase 1 中文件被截断预览),实现精确编辑。

参考来源