第 3 章:记忆与上下文¶
让 Agent 有记忆、有个性、能管理上下文窗口。
这一章一次解决 3 个问题¶
如果你觉得这一章信息量突然变大,这是正常的。因为它不是只补一个点,而是同时补齐了 3 个在上一章已经暴露出来的缺口:
- 对话重启后会失忆
- system prompt 太薄,Bot 没有稳定个性
- 历史消息会不断增长,最终撑爆上下文窗口
所以读这一章时,最好始终问自己:当前这一段代码是在解决“记不住”、 “不像自己”,还是“放不下”这三个问题里的哪一个?
三个问题¶
上一章的 Agent 有三个硬伤:
- 没有持久记忆——重启后什么都不记得
- 没有个性——system prompt 就一句话
- 上下文会爆——对话越长,messages 越大,最终超出 LLM 窗口
nanobot 用三个机制解决这些问题:Session 持久化、Context Builder、Memory / Dream 整合。
第一步:Session 持久化¶
对应 v0.2.2 的 Session / SessionManager。核心思路:每个对话存为一个 JSONL 文件,每行一条消息。
现在重启后对话历史不会丢了。nanobot 的实现还支持 metadata 行、legacy 路径迁移等,但核心就是 JSONL 持久化。
第二步:Context Builder——组装 System Prompt¶
这是 nanobot 最精巧的设计之一。v0.2.2 的 ContextBuilder不会把 System Prompt 写死,而是按当前 turn 动态组装:
为什么不把 system prompt 写死?¶
因为用户要能定制 Bot 的行为。通过 SOUL.md 改性格、AGENTS.md 改规则、USER.md 告诉 Bot 你是谁——全部是 Markdown 文件,改完下次对话自动生效。工具约束来自 nanobot 内置 Tool Contract;TOOLS.md 不是 v0.2.2 自动加载的 Bootstrap 文件。
build_messages 每次调用都重新读取这些文件,所以用户编辑后不需要重启。
Agent 工作区与项目工作区¶
默认 CLI/Channel 没有另选项目时,同一个配置工作区同时承担 Agent 状态和工具工作目录。WebUI 选择项目目录后,两者才需要明确区分:
| 状态 | 所有者与位置 | 生命周期 |
|---|---|---|
默认 AGENTS.md、SOUL.md、USER.md、memory/、skills/ |
配置的 Agent 工作区 | 实例的持久状态;Dream 只整理这里的 SOUL.md、USER.md 和 MEMORY.md,不改 AGENTS.md |
项目级 AGENTS.md、SOUL.md、USER.md |
WebUI 当前选择的项目工作区 | 选择项目后,ContextBuilder 从项目根读取这三个文件,不再自动叠加 Agent 工作区同名文件;Dream 不管理这些项目文件 |
sessions/*.jsonl 与会话 metadata |
Agent 工作区的 SessionManager | 按 session key 持久化;WebUI 项目路径作为会话 scope metadata 保存 |
| 项目源码和产物 | 当前项目工作区 | 由文件/命令工具在当前访问模式下操作,不属于长期记忆文件 |
不要让 Dream 把项目源码事实写进 SOUL.md,也不要把个人偏好散落到每个项目的 AGENTS.md。长期项目事实可以进入 MEMORY.md,但应写清适用项目和过期条件。
第三步:持久化记忆¶
对应 v0.2.2 的 MemoryStore 与 Consolidator。当前实现是分层设计:
| 层 | 文件或状态 | 特点 |
|---|---|---|
| 原始会话 | sessions/<key>.jsonl |
保存消息和 metadata;是否重写取决于压缩路径 |
| 摘要归档 | memory/history.jsonl |
Consolidator 追加旧对话摘要;失败时追加受限长度的 [RAW] 归档 |
| 长期记忆 | memory/MEMORY.md |
Dream 可整理的重要事实;只有非空且不再等于初始模板时才注入 |
| 游标 | memory/.cursor / memory/.dream_cursor |
前者分配摘要序号,后者记录 Dream 已成功处理到的位置 |
| 记忆版本 | Agent 工作区根的 .git/(可选) |
只在能初始化独立版本库时跟踪 SOUL.md、USER.md、MEMORY.md 和 Dream cursor |
两层生命周期:Consolidator 归档,Dream 整理¶
真实 nanobot 不让一次脆弱的 JSON 输出同时决定“删哪些对话”和“长期相信什么”。它把生命周期拆成两层:
flowchart LR
S[Session 消息] --> C[Consolidator]
C --> H[memory/history.jsonl]
H --> R[Recent History]
H --> D[Dream]
D --> F[SOUL / USER / MEMORY]
D --> G[Dream cursor + 可选 Git 版本]
第一层:Consolidator 只做摘要归档¶
v0.2.2 的 Consolidator有两条压缩路径:
| 触发方式 | 对 Session 做什么 | 共同结果 |
|---|---|---|
| token 压力触发的软整合 | 选择完整对话轮次作为边界,推进 last_consolidated;原始 Session 文件仍保留 |
把被移出当前上下文的内容总结到 history.jsonl |
| 空闲会话自动压缩 | 保留最近的合法消息后缀,重写 Session,并保存归档摘要 metadata | 同样把被移除内容总结到 history.jsonl |
Consolidator 不会直接改 SOUL.md、USER.md 或 MEMORY.md。如果摘要模型失败,它会写入受限长度的 [RAW] 归档,避免因为一次整理失败中断正常对话。
下面的教学函数只演示“旧消息 → 摘要归档”这一层。它直接裁掉旧消息是为了缩短代码;真实 token 软整合会推进游标而保留原始 Session:
第二层:Dream 整理持久文件¶
MemoryStore.build_dream_prompt()只读取 .dream_cursor 之后的新归档。Gateway 的定时任务或 /dream 会启动一次临时 Dream Session,让模型用受限文件工具审阅这些摘要,并在确有必要时编辑 Agent 工作区中的 SOUL.md、USER.md 和 memory/MEMORY.md。
只有 Dream 正常完成,.dream_cursor 才前移;失败或中止会留下待处理摘要,供下次重试。若 Agent 工作区能够初始化独立 Git 仓库,Dream 还会提交这些持久文件的变化。它不会整理 AGENTS.md,也不会修改 WebUI 当前选择的项目级 Bootstrap 文件。
每轮究竟会注入什么¶
ContextBuilder会区分四种上下文,不要把它们都叫作“长期记忆”:
| 内容 | 注入条件 |
|---|---|
| 当前 Session 历史 | 从未被软整合的消息开始,并受当前历史窗口限制 |
| Session 归档摘要 | 当前 Session metadata 中已有摘要时,以 Archived Context Summary 注入 |
| recent history(Recent History) | 仅非临时运行启用;读取 Dream cursor 之后的待处理归档,最多取最近 50 条并受 token 上限限制;普通模式只取当前 session key,统一会话模式还会合并其他非内部会话 |
MEMORY.md |
文件非空,且内容不再等于初始模板时才注入 |
因此,短对话还没触发 Consolidator 时,/dream 可能提示没有新历史;Dream 成功消费摘要后,同一批内容也不会继续以 Recent History 重复注入。临时 Dream 运行本身不会再套入这段 pending history。
Dream 命令¶
v0.2.2 的 builtin command router提供以下入口:
| 命令 | 行为 |
|---|---|
/dream |
异步启动一次整理;没有 cursor 之后的新归档时不会改文件 |
/dream-log、/dream-log <sha> |
查看最近一次或指定 Dream 提交的 diff;需要 Agent 工作区已启用版本记录 |
/dream-restore |
列出最近可恢复的 Dream 版本 |
/dream-restore <sha> |
撤销该提交引入的变化,并创建一条新的安全恢复提交 |
/dream-prompt |
v0.2.2 不支持,固定对照的 main@b189a376也未注册此命令;Dream prompt 是内部模板,不能把这项写成可执行步骤 |
先别把这些概念混在一起¶
| 概念 | 它解决什么 | 它保存或处理什么 |
|---|---|---|
Session |
当前会话历史别丢 | 原始对话消息与 metadata |
Context Builder |
决定每轮发给模型什么 | Bootstrap、工具约束、历史、摘要、长期记忆与运行时信息 |
Consolidator |
上下文放不下时归档旧内容 | Session 边界与 history.jsonl 摘要 |
Dream |
定期清理和沉淀 Agent 状态 | 根据新摘要谨慎修改 SOUL、USER、MEMORY,并推进 Dream cursor |
Memory |
跨会话保留经过筛选的事实 | MEMORY.md;不是全部原始对话的副本 |
很多初学者会把“重启后还记得”和“长期记忆已经整合”混成一件事。实际上这是两层不同机制。
本章你真正学到的抽象¶
这一章真正引入了 4 个长期有效的设计点:
Session Persistence:把对话状态从内存搬到磁盘Context Builder:把静态配置、动态记忆、运行时信息拼成统一上下文Consolidator:把离开当前上下文的旧消息变成可追踪摘要Dream:异步筛选摘要,再谨慎维护长期 Agent 状态
从这里开始,Agent 不再只是“会调用工具的聊天循环”,而是一个有稳定人格、跨轮状态和上下文预算意识的系统。
最小验证步骤¶
建议按顺序做下面 5 个验证:
- 跑一次程序并完成几轮对话,确认会生成
sessions/和memory/相关文件 - 重启程序,再问一个延续上一轮的问题,确认至少 session 历史仍然存在
- 修改
SOUL.md或AGENTS.md,确认下一次对话的风格或流程发生变化 - 构造足够长的对话,观察 Consolidator 向
memory/history.jsonl追加摘要;短对话没有摘要是正常现象 - 在真实 nanobot 中发送
/dream,完成后用/dream-log检查 diff;恢复前先用/dream-restore查看版本列表和目标 SHA
常见失败点¶
- 重启后“没有记忆”:先区分是 session 历史没保存,还是长期记忆没写入,这是两套机制
- 改了
SOUL.md没效果:通常是 build_messages 没有重新读取文件,或 system prompt 没重新构建 /dream没有可处理内容:先确认history.jsonl中存在.dream_cursor之后的新摘要;短对话可能尚未触发 Consolidator/dream-log没有版本:Agent 工作区可能无法初始化独立 Git 仓库,或 Dream 还没有产生文件变化- 上下文仍然爆掉:说明你只有“保存历史”,没有真正限制传给模型的历史窗口
完整代码¶
在看完整代码前,先记住本章新增的职责切分:
SessionManager:负责把会话历史保存和读回来ContextBuilder:负责把静态文件、记忆和运行时信息拼成完整上下文MemoryStore:负责长期记忆与摘要归档的文件读写;前面的archive_old_messages()只演示 Consolidator 的归档职责
带着这 3 个职责去看代码,会比直接从头扫到尾轻松很多。为保持示例可独立阅读,下面的教学 Agent 只实现 Session、Context Builder 和 MemoryStore;生产版的异步 Dream 生命周期应继续交给 nanobot。
下面的合并代码只用于对照职责
它复用了第 2 章的宽权限 Shell/文件工具,没有生产沙箱。不要在真实工作区运行这个代码块;实际练习请使用加固后的配套示例,它默认创建临时工作区并拒绝路径越界与危险命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | |
试一试¶
编辑 ${NANOBOT_HERO_WORKSPACE}/SOUL.md 改成任何你想要的风格,下次对话自动生效。该目录只应放教学数据,练习完成后自行清理。
关键对比¶
| 概念 | 我们的代码 | nanobot 的代码 |
|---|---|---|
| Session 持久化 | JSONL 简单序列化 | JSONL + metadata 行 + 缓存 + legacy 迁移 |
| Context Builder | 拼接 3 个 Bootstrap 文件 + 非空 Memory | 同上 + 内置 Tool Contract + Skills + Runtime Context + 条件式 Recent History |
| 记忆生命周期 | 片段只演示摘要归档 | Consolidator 写入 history.jsonl;Dream 再整理 SOUL.md / USER.md / MEMORY.md |
| Bootstrap 文件 | AGENTS.md、SOUL.md、USER.md |
同样 3 个,并支持模板同步;工具约束由内置 contract 提供 |
还缺什么?¶
Agent 现在有记忆、有个性,但它只能在终端里用。如果想让它在 Telegram / Discord 上工作呢?
下一章:消息总线——解耦 Agent 和 I/O。