1. 模块概述
记忆系统(nanobot/agent/memory.py)由两个核心类组成:
- **MemoryStore**:纯文件 I/O 层,管理
MEMORY.md、history.jsonl、SOUL.md、USER.md的读写 - **Consolidator**:基于 Token 预算的对话历史自动摘要引擎
📍 源码:
nanobot/nanobot/agent/memory.py#L1-L1050
1.1 记忆文件体系
| 文件 | 用途 | 写入方式 |
|---|---|---|
memory/MEMORY.md |
长期记忆(事实、偏好) | Dream 巩固自动更新 |
memory/history.jsonl |
对话摘要归档 | Consolidator.archive() 追加 |
SOUL.md |
Agent 身份/性格定义 | 用户编辑或 Dream 更新 |
USER.md |
用户信息/偏好 | 用户编辑或 Dream 更新 |
memory/.cursor |
history.jsonl 写入游标 | append_history() 自增 |
memory/.dream_cursor |
Dream 处理进度游标 | Dream 完成后更新 |
1.2 在系统中的位置
graph TB
subgraph "记忆文件"
MEM[memory/MEMORY.md]
HIST[memory/history.jsonl]
SOUL[SOUL.md]
USER[USER.md]
end
subgraph "MemoryStore"
RM[read_memory/write_memory]
RS[read_soul/write_soul]
RU[read_user/write_user]
AH[append_history]
RUH[read_unprocessed_history]
BDP[build_dream_prompt]
end
subgraph "Consolidator"
MC[maybe_consolidate_by_tokens]
AR[archive]
ES[estimate_session_prompt_tokens]
PC[pick_consolidation_boundary]
end
subgraph "消费者"
CB[ContextBuilder]
AL[AgentLoop]
AC[AutoCompact]
end
RM --> MEM
RS --> SOUL
RU --> USER
AH --> HIST
CB --> RM
CB --> RUH
AL --> MC
AC --> MC
MC --> ES
MC --> AR
AR --> AH
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 操作对象 | 操作 | 含义 |
|---|---|---|---|
read_ |
memory |
— | 读取 MEMORY.md |
write_ |
memory |
— | 写入 MEMORY.md |
read_ |
soul |
— | 读取 SOUL.md |
read_ |
user |
— | 读取 USER.md |
append_ |
history |
— | 追加到 history.jsonl |
read_ |
unprocessed_history |
— | 读取未处理的历史条目 |
read_ |
recent_history_for_prompt |
— | 读取用于提示注入的历史 |
build_ |
dream_prompt |
— | 构建 Dream 巩固提示 |
build_ |
dream_tools |
— | 构建 Dream 受限工具集 |
maybe_ |
consolidate_by_tokens |
— | 按 Token 预算决定是否巩固 |
pick_ |
consolidation_boundary |
— | 选择安全的巩固边界 |
estimate_ |
session_prompt_tokens |
— | 估算会话的提示 Token 数 |
compact_ |
idle_session |
— | 硬截断空闲会话 |
2.2 易混淆函数对比表
| 对比维度 | archive |
raw_archive |
|---|---|---|
| LLM 调用 | ✅ 通过 LLM 生成摘要 | ❌ 直接格式化原始消息 |
| 输出质量 | 结构化的自然语言摘要 | [RAW] N messages 前缀 + 格式化文本 |
| 字符上限 | _ARCHIVE_SUMMARY_MAX_CHARS (8,000) |
_RAW_ARCHIVE_MAX_CHARS (16,000) |
| 使用场景 | 正常巩固流程 | LLM 调用失败时的降级路径 |
| 一句话区分 | archive 是正常路径(LLM 摘要) |
raw_archive 是降级路径(保留原始数据) |
| 对比维度 | append_history |
raw_archive |
|---|---|---|
| 输入 | 单条文本 | 消息列表 |
| 格式化 | 直接写入 | 先通过 _format_messages 格式化 |
| 字符限制 | _HISTORY_ENTRY_HARD_CAP (64,000) |
_RAW_ARCHIVE_MAX_CHARS (16,000) |
| 一句话区分 | append_history 是底层原语 |
raw_archive 是高层便捷方法 |
3. API Signatures
graph TB
subgraph "MemoryStore 公共接口"
RM2[read_memory/write_memory]
RS2[read_soul/write_soul]
RU2[read_user/write_user]
AH2[append_history]
GC[get_memory_context]
RUH2[read_unprocessed_history]
RRH[read_recent_history_for_prompt]
BDP2[build_dream_prompt]
BDT[build_dream_tools]
RA[raw_archive]
end
subgraph "Consolidator 公共接口"
MC2[maybe_consolidate_by_tokens]
CI[compact_idle_session]
AR2[archive]
ES2[estimate_session_prompt_tokens]
end
MemoryStore
1 | class MemoryStore: |
Consolidator
1 | class Consolidator: |
4. 数据结构深度解析
4.1 MemoryStore
4.a 结构体存在的理由
MemoryStore 是 nanobot 持久化记忆的唯一入口。它将文件 I/O、游标管理、历史迁移和 Git 版本控制集中在一个类中,确保所有记忆操作通过统一接口进行。如果没有它,记忆文件可能被多个组件以不一致的方式写入,导致数据损坏。
4.b 字段三层分析表
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
_append_lock |
确保 cursor 分配和文件写入是原子操作 | 并发写入导致 cursor 重复,history.jsonl 中出现重复条目 | 可以用文件锁,但 threading.Lock 更轻量 |
_cursor_file |
持久化自增游标,重启后不丢失 | 每次启动从 1 开始,cursor 与已有条目冲突 | 可以用文件尾的最大 cursor,但扫描大文件开销大 |
_dream_cursor_file |
跟踪 Dream 已处理的进度 | Dream 重复处理相同的历史条目 | 可以存在内存中,但重启丢失进度 |
_git |
自动 Git 提交 MEMORY/SOUL/USER 的变更 | 记忆变更无法追溯和回滚 | 可以用文件备份,但 Git 提供完整的版本历史 |
_corruption_logged |
限速非整数 cursor 的警告日志 | 每次读取都打印警告,日志泛滥 | 可以用计数器限速,但 boolean 标志最简单 |
4.2 Consolidator
4.a 结构体存在的理由
Consolidator 解决了”上下文窗口有限但对话无限增长”的核心矛盾。它通过 Token 预算估算决定何时需要归档旧消息,并在安全的 user-turn 边界处切割消息,通过 LLM 生成摘要后写入 history.jsonl。这确保 Agent 始终在上下文窗口内运行,同时不丢失重要历史信息。
4.b 字段三层分析表
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
consolidation_ratio |
控制归档目标(预算的 50%),留出增长空间 | 每次归档到刚好低于预算,频繁触发归档 | 可以用绝对阈值,但比例适应不同窗口大小 |
_MAX_CONSOLIDATION_ROUNDS |
限制单次调用的归档轮数,避免死循环 | LLM 持续返回超短摘要导致无限循环 | 可以用超时,但轮数限制更确定 |
_locks |
每个 session 独立的异步锁,防止并发归档冲突 | 多个 turn 同时归档同一会话导致消息丢失 | 可以用全局锁,但降低并发性 |
5. 关键算法剖析
5.1 Token 预算巩固算法
flowchart TD
A[maybe_consolidate_by_tokens] --> B{context_window_tokens > 0?}
B -->|No| C[跳过]
B -->|Yes| D[获取 session 锁]
D --> E[估算 prompt tokens]
E --> F{estimated < budget?}
F -->|Yes| G[记录日志,跳过]
F -->|No| H[循环: round 0..MAX_ROUNDS]
H --> I{estimated <= target?}
I -->|Yes| G
I -->|No| J[pick_consolidation_boundary]
J --> K{找到安全边界?}
K -->|No| L[记录日志,退出循环]
K -->|Yes| M[archive chunk via LLM]
M --> N[更新 last_consolidated]
N --> O[重新估算 prompt tokens]
O --> H
L --> G
G --> P[persist last_summary]
关键参数:
budget = context_window_tokens - max_completion_tokens - 1024(安全缓冲)target = budget * consolidation_ratio(默认 50%,留出增长空间)_MAX_CONSOLIDATION_ROUNDS = 5(最多 5 轮归档)
5.2 巩固边界选择
pick_consolidation_boundary 只在 user-turn 边界处切割:
1 | 消息序列: [user1, assistant1, tool1, tool2, user2, assistant2, user3, ...] |
这确保了归档后的历史仍然从 user 消息开始(Provider 要求),且不会破坏 tool_call/tool_result 的配对关系。
6. 设计决策分析
6.1 为什么 Dream 使用受限工具集?
决策:Dream Agent 只能读写 MEMORY.md、SOUL.md、USER.md 和 skills/ 目录。
权衡:
- ✅ 防止 Dream Agent 意外修改用户代码或执行危险命令
- ✅ 缩小了工具定义空间,LLM 更快收敛
- ❌ Dream Agent 无法通过 shell 命令获取额外上下文
6.2 为什么 history.jsonl 使用追加模式?
决策:append_history 始终追加,compact_history 在条目超限时重写整个文件。
权衡:
- ✅ 追加模式性能最优(O(1) 写入)
- ✅ 原子性通过
_append_lock保证 - ❌ 需要定期压缩清理旧条目
- ❌ 大文件读取时需要全量扫描
6.3 为什么记忆文件使用 Git 版本控制?
决策:MemoryStore 通过 GitStore 自动提交 MEMORY.md、SOUL.md、USER.md 的变更。
权衡:
- ✅ 记忆变更可追溯、可回滚
- ✅ Dream Agent 的错误修改可以恢复
- ❌ 增加 I/O 开销(git add + commit)
- ❌ 需要 workspace 是一个 Git 仓库
7. 学习检查点
📝 本章小结
- MemoryStore 是文件 I/O 层,管理 6 个记忆文件,提供原子追加和游标管理
- Consolidator 解决上下文窗口限制,通过 Token 预算估算和 LLM 摘要自动归档旧消息
- 巩固边界选择确保只在 user-turn 边界切割,保护 tool_call/tool_result 配对
- Dream 是周期性巩固任务,使用受限工具集更新长期记忆文件
- Git 版本控制提供记忆变更的可追溯性
🤔 思考题
为什么
pick_consolidation_boundary只在 user-turn 边界切割,而不能在 assistant 或 tool 消息之间切割?参考答案
在
nanobot/nanobot/agent/memory.py#L673-L693,切割必须在 user 消息前,因为:1) Provider 要求消息序列以 user 消息开始(非 system),切割到 assistant 或 tool 会产生非法序列;2) 切割在 tool_call 和 tool_result 之间会破坏配对,导致后续的_drop_orphan_tool_results误删有效的 tool 结果。user-turn 边界天然保证了这两个约束。raw_archive的字符上限(16,000)为什么比archive的摘要上限(8,000)大?参考答案
raw_archive是 LLM 调用失败时的降级路径(nanobot/nanobot/agent/memory.py#L554-L571),它直接存储原始格式化文本而不是 LLM 生成的摘要。原始文本信息密度低(包含时间戳、角色标记等),需要更大空间才能保留有意义的信息。16,000 字符约能保存 10-15 条格式化的对话消息,而 8,000 字符的 LLM 摘要可以覆盖同样的信息量。