两层记忆:近期压缩 + Dream 长期反思
这一章讲 nanobot 怎么「记事」。它没有向量数据库——记忆就是工作区里的几个 Markdown 文件 + 一个
history.jsonl。短期记忆靠预算触发的压缩,长期记忆靠一个会做梦的 agent。
4.0 心智模型:两层,各管一段时间
| 层 | 存哪 | 谁维护 | 类比 |
|---|---|---|---|
| 近期会话 | 会话 JSONL(<workspace>/sessions/) | Consolidator | 工作记忆 / 草稿纸 |
| 长期记忆 | MEMORY.md、SOUL.md、USER.md + history.jsonl | Dream(LLM) | 长期记忆 / 日记 |
把会话历史当「内存」、把工作区文件当「磁盘」:内存满了要先 spill 到 history.jsonl(Consolidator),磁盘上的东西隔段时间被「整理归档」成更精炼的记忆(Dream)。
4.1 Consolidator:按 token 预算压缩
它要解决的小问题
会话越聊越长,迟早超模型窗口。不能简单截断(丢上下文),也不能全留(撑爆)。Consolidator 的做法:当会话 token 超过预算,把最老的一批消息用 LLM 摘要成一段「归档摘要」写进 history.jsonl,再从活跃历史里移除。
它由两处触发:BUILD 前的 maybe_consolidate_by_tokens(loop.py:1452-1455)和 SAVE 后的后台压缩(loop.py:1560-1565)。consolidation_ratio(默认 0.5)决定一次压掉多少。摘要失败时有兜底:raw_archive 直接把原始消息不经 LLM地 dump 进 history.jsonl 并告警(memory.py:571-588)——宁可粗糙存档,也不丢数据。
4.2 history.jsonl 与 cursor:Dream 的输入流
history.jsonl 是一条只追加的事件流,每条带一个单调递增的 cursor(memory.py:339-367)。两个消费者各持一个游标:
- 近期历史喂进系统提示:
read_recent_history_for_prompt(since_cursor=...)(memory.py:380、context.py:100-112)。 - Dream读未处理部分:
read_unprocessed_history(since_cursor=last_dream_cursor)(memory.py:367)。
Dream 处理完一批就把 last_dream_cursor 往前推(set_last_dream_cursor),保证每条历史只被反思一次,且 Dream 已沉淀的内容不再重复出现在近期历史里(context.py:101-105 用的就是同一个游标)。这就是 README 里说的「两阶段记忆 / line-age memory」的机制底座。
4.3 Dream:一个会做梦的受限 agent
思路
Dream 不是一段固定脚本,而是另起一个临时(ephemeral)agent 轮,给它一段「反思提示 + 待处理的对话历史」,让它自己决定往 MEMORY.md 里记什么、要不要把某个反复出现的流程沉淀成一个技能。
怎么搭这一轮
- 提示:
build_dream_prompt(memory.py:485-507)取未处理历史(默认最多 20 条、每条截断到 500 字符),拼上agent/dream.md模板,并把内置的skill-creator技能路径塞进去,引导它学会造技能。 - 工具(故意很少):
build_dream_tools(memory.py:509-549)只给一个受限工具集——只读访问工作区、只能写skills/目录和MEMORY.md/SOUL.md/USER.md这三个文件。Dream 不能跑 shell、不能上网。这是有意的权限收缩:做梦只该整理记忆,不该有外部副作用。 - 会话键:
dream:YYYYMMDD-HHMMSS(memory.py:594-597),与用户会话隔离。 - 判完成:只有 ephemeral 轮
_stop_reason == "completed"才算 Dream 成功(memory.py:551-555),失败不推游标,下次重试。
落盘 + git
记忆文件改完后,Dream 用 build_dream_commit_message 生成提交信息并通过 GitStore 自动提交(memory.py:74、599-604)——记忆变更有版本历史,可回溯、可 diff。旧的 Dream 会话文件由 prune_dream_sessions 只保留最近 N 个(memory.py:608)。
4.4 一条记忆的完整旅程(把上面串起来)
你和 agent 对话
| 每轮 append 到会话 JSONL
v
会话 token 超预算 --Consolidator--> 老消息摘要写入 history.jsonl(带 cursor)
| |
| 近期历史(read_recent, since=dream_cursor)|
v v
系统提示「# Recent History」 Dream 定时取 unprocessed(since=dream_cursor)
| ephemeral 受限 agent 反思
v
写 MEMORY.md / 造 skill,git 提交
推进 dream_cursor(该批不再重复处理)
5. 巧妙之处
- Dream 是 agent,不是脚本。 复用同一套 runner,只是换了受限工具集和提示——记忆维护本身也享受工具调用与自纠(
build_dream_tools)。 - 游标驱动、各读各的。 一条
history.jsonl两个游标,既不重复反思、又不让已沉淀内容重复占近期窗口(memory.py的 cursor 体系)。 - 记忆进 git。 反思结果自动提交,记忆演化可审计、可回滚(
memory.py:74、599-604)。 - 降级不丢数据。 LLM 摘要失败就
raw_archive原样存档并告警(memory.py:571-588)。
6. 边界与局限
- 无语义检索。 记忆是 Markdown + 顺序事件流;「想起相关的事」靠把近期历史塞进提示,不是按相似度召回。超出窗口的旧记忆要靠 Dream 提前沉淀进
MEMORY.md才进得了提示。 - Dream 质量取决于模型。 反思、造技能都由 LLM 自由决定,可能记错或造出低质技能;失败轮会重试但不保证收敛。
- 压缩是有损的。 Consolidator 摘要会丢细节;
consolidation_ratio越大,单次丢得越多。
7. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 记忆文件读写 | nanobot/agent/memory.py | MemoryStore.read_memory/write_memory、get_memory_context |
| 事件流与游标 | nanobot/agent/memory.py | append_history、read_unprocessed_history、get_last_dream_cursor |
| 近期历史进提示 | nanobot/agent/memory.py、context.py | read_recent_history_for_prompt、ContextBuilder.build_system_prompt |
| Dream 提示/工具 | nanobot/agent/memory.py | build_dream_prompt、build_dream_tools、dream_run_completed |
| Dream 提交/清理 | nanobot/agent/memory.py | build_dream_commit_message、prune_dream_sessions、git |
| 近期压缩 | nanobot/agent/memory.py | Consolidator、raw_archive |