Dream 两阶段记忆巩固
Dream 两阶段记忆巩固
学习目标
理解 Nanobot 的 Dream 系统如何通过两阶段 LLM 调用实现自动化记忆管理,以及 Git 行级年龄标注的创新设计。
前置知识
- 记忆系统设计 — 短期/长期记忆分层
- Dream 记忆巩固 — 定时记忆巩固模式
下文假设你已理解上述概念,直接聚焦 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文件,fsync后os.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 中文件被截断预览),实现精确编辑。