nanobot 的记忆机制:它为什么能记住你的习惯和喜好?
Posted on Sun 15 March 2026 in AI • 3 min read
nanobot 的记忆机制:它为什么能记住你的习惯和喜好?
用过 ChatGPT 的人大概都有这个体验:聊了半天,关掉窗口,下次再开——它把你忘得一干二净。你得重新自我介绍,重新解释项目背景,重新说一遍"我喜欢简洁的回答"。
就像每次去同一家理发店,Tony 老师都问你"今天想剪什么样的"。哥们儿,上次不是说了吗?
这个问题的根源不复杂:LLM 本身是无状态的。每次对话都是一张白纸。所谓的"上下文",不过是把之前的聊天记录塞进 prompt 里重新发一遍。窗口一满,早期的对话就被截断了。
nanobot 是一个超轻量的开源 AI 助手框架,核心代码只有约 4000 行 Python。但它解决了这个问题——它能跨会话记住你是谁、你在做什么、你喜欢什么样的回答风格。我用了一段时间,发现它确实"认识"我:知道我的项目目录在哪,知道我写博客喜欢用中文,知道我偏好"质量优先、速度其次"。
这篇文章就来拆解一下,它是怎么做到的。
一、核心矛盾:LLM 的"金鱼记忆"
先说清楚问题。LLM 面临三个互相矛盾的约束:
- 上下文窗口有限——对话越长,塞进去的历史越多,直到撑爆
- Prompt Cache 很脆弱——改动前面的消息,整个缓存前缀失效,推理成本翻倍
- 用户期望长期记忆——"我上周跟你说过的那个项目,还记得吗?"
传统做法要么截断历史(丢信息),要么全量塞入(撑爆窗口),要么做 RAG 检索(复杂度飙升)。
nanobot 的方案很朴素:两层记忆 + 后台整理。
二、双层记忆架构
nanobot 的记忆系统由两个 Markdown 文件组成。没错,就是两个纯文本文件。
第一层:MEMORY.md —— "我知道什么"
路径:~/.nanobot/workspace/memory/MEMORY.md
这个文件存的是长期事实:用户偏好、项目上下文、人际关系、配置信息。每次对话开始时,它的全部内容会被注入到 system prompt 里。
打个比方,这就是一个人的"知识库"——你知道自己叫什么名字、住在哪里、在做什么工作。你不需要每天早上重新学习这些。
我的 MEMORY.md 长这样(节选):
## User Information
- Name: Walter Fan
- Location: Related to Hefei (合肥), China
- Communicates in both English and Chinese
## Preferences
- Quality first, speed second
- Prefers detailed, actionable technical content with code examples
- Likes Chinese language for book/blog content
更新方式是全量覆写——整理记忆时,LLM 会把旧内容和新发现的事实合并,重新生成整个文件。这样能自动去重,保持文档结构清晰。
第二层:HISTORY.md —— "发生过什么"
路径:~/.nanobot/workspace/memory/HISTORY.md
这个文件存的是事件日志:每次记忆整理后,会追加一段带时间戳的摘要。
[2026-03-10 14:30] User asked about configuring Telegram bot. Discussed
bot token setup, allowFrom whitelist, and proxy configuration. User chose
to use SOCKS5 proxy at 127.0.0.1:1080.
[2026-03-12 09:15] Debugged a session corruption issue. The problem was
orphaned tool_call_id references after a partial consolidation.
这个文件不会被加载到 prompt 里——太大了,会吃掉宝贵的上下文窗口。需要回忆过去的事情时,Agent 会用 grep 去搜索:
grep -i "telegram" memory/HISTORY.md
打个比方,MEMORY.md 是你脑子里的知识,HISTORY.md 是你的日记本。你不会每天把日记从头读一遍,但需要的时候可以翻出来查。
两层的对比:
| 维度 | MEMORY.md | HISTORY.md |
|---|---|---|
| 类比 | 一个人的知识/信念 | 一个人的日记 |
| 在 prompt 里? | 是(每次都加载) | 否(太大) |
| 怎么查? | LLM 直接看到 | 用 grep 搜索 |
| 更新方式 | 全量覆写(合并去重) | 追加(只增不改) |
| 增长速度 | 慢(事实会合并) | 线性增长 |
三、会话模型:只追加,不修改
理解了两层记忆,再来看会话(Session)的设计。
nanobot 的 Session 有一条铁律:消息只追加,不修改,不删除。
@dataclass
class Session:
key: str # "channel:chat_id"
messages: list[dict[str, Any]] # 只追加
last_consolidated: int = 0 # 整理指针
为什么?因为 LLM 的 Prompt Cache。大多数 LLM 提供商会缓存 prompt 的前缀——如果你改了前面的消息,整个缓存就废了,下一次推理要重新计算所有 token。只追加的设计保证了缓存前缀永远有效。
这里有个关键字段:last_consolidated。它是一个整数索引,标记"整理到哪了":
消息列表: [m0, m1, m2, ..., m14, m15, ..., m49]
↑ ↑ ↑
0 15 49
│ │
└── 已整理 ────────────┘
│ │
└── 未整理 ────┘
last_consolidated = 15
get_history() 方法只返回 last_consolidated 之后的消息——已经整理过的部分不再发给 LLM,它们的精华已经沉淀到 MEMORY.md 和 HISTORY.md 里了。
四、记忆整理:让 LLM 自己做笔记
最有意思的部分来了。nanobot 的记忆整理不是用规则引擎或者关键词提取,而是让另一个 LLM 来做。
触发条件
当未整理的消息数量达到 memory_window(默认 100 条)时,后台自动触发整理:
unconsolidated = len(session.messages) - session.last_consolidated
if unconsolidated >= self.memory_window:
# 启动后台整理任务
asyncio.create_task(_consolidate_and_unlock())
注意,这是一个异步后台任务——不会阻塞用户的当前对话。你继续聊你的,整理在后台默默进行。
整理过程
整理的核心逻辑在 MemoryStore.consolidate() 里,大约 60 行代码。流程是这样的:
-
选取范围:取
messages[last_consolidated:-keep_count],其中keep_count = memory_window // 2。也就是说,默认保留最近 50 条不动,把更早的消息拿去整理。 -
格式化:把消息格式化成文本,带上时间戳和角色:
[2026-03-15 14:30] USER: 帮我看看这个 Go 代码的 lint 问题 [2026-03-15 14:31] ASSISTANT [tools: exec, read_file]: 发现 36 个 P0 级别的 lint 问题... -
调用 LLM:发起一个独立的 LLM 调用,system prompt 是"你是一个记忆整理 Agent",给它当前的 MEMORY.md 内容和待整理的对话,让它调用
save_memory工具。 -
save_memory 工具:这是一个专门定义的工具,有两个参数:
history_entry:2-5 句话的事件摘要,带时间戳,追加到 HISTORY.md-
memory_update:更新后的完整 MEMORY.md 内容(旧事实 + 新事实) -
推进指针:整理成功后,
last_consolidated前进到新位置。
用代码说话,save_memory 工具的 schema 定义:
_SAVE_MEMORY_TOOL = [{
"type": "function",
"function": {
"name": "save_memory",
"parameters": {
"type": "object",
"properties": {
"history_entry": {
"type": "string",
"description": "A paragraph (2-5 sentences) summarizing key events. "
"Start with [YYYY-MM-DD HH:MM]."
},
"memory_update": {
"type": "string",
"description": "Full updated long-term memory as markdown. "
"Include all existing facts plus new ones."
}
},
"required": ["history_entry", "memory_update"]
}
}
}]
这个设计很巧妙——让 LLM 自己决定什么是"长期事实"、什么是"事件记录"。比人工写规则灵活得多。
容错设计
整理过程有几层保护:
- LLM 没调用工具? 返回 False,指针不前进,下次重试
- 整理过程抛异常? 捕获并记录日志,指针不前进,不丢数据
- 并发冲突? 每个 session 有独立的
asyncio.Lock,加上_consolidating集合防止重复触发
# 并发保护
self._consolidating: set[str] = set() # 防重复
self._consolidation_locks: WeakValueDictionary # 串行化
self._consolidation_tasks: set[Task] # 防 GC
一句话:整理失败不会丢数据,最多是下次再整理一遍。
五、上下文组装:每次对话的"开场白"
记忆存好了,怎么用?
每次用户发消息,ContextBuilder 会组装一个完整的 system prompt:
def build_system_prompt(self):
parts = [self._get_identity()] # 1. 身份:我是 nanobot
parts.append(self._load_bootstrap_files()) # 2. 用户自定义文件
memory = self.memory.get_memory_context()
if memory:
parts.append(f"# Memory\n\n{memory}") # 3. MEMORY.md 全文
parts.append(skills_summary) # 4. 技能列表
return "\n\n---\n\n".join(parts)
注意第 3 步——MEMORY.md 的全部内容被塞进了 system prompt。这就是为什么 nanobot "认识"你:每次对话开始,它都会"复习"一遍关于你的所有已知事实。
然后,build_messages() 把 system prompt、历史消息(只有未整理的部分)和当前用户消息拼在一起,发给 LLM:
return [
{"role": "system", "content": system_prompt}, # 含 MEMORY.md
*history, # 未整理的历史
{"role": "user", "content": current_message}, # 当前消息
]
六、/new 命令:优雅地"翻篇"
有时候你想开始一个全新的话题,不想被之前的上下文干扰。nanobot 提供了 /new 命令:
- 等待正在进行的整理任务完成
- 归档所有未整理的消息(
archive_all=True) - 清空会话消息,重置
last_consolidated为 0 - 保存空会话到磁盘
如果归档失败,会话不会被清空——宁可保留旧消息,也不丢数据。
if not await self._consolidate_memory(temp, archive_all=True):
return "Memory archival failed, session not cleared. Please try again."
七、Agent 也能主动记笔记
除了自动整理,nanobot 的 Agent 还能主动更新 MEMORY.md。
memory skill 的说明文件(SKILL.md)里写得很清楚:
Write important facts immediately using
edit_fileorwrite_file: - User preferences ("I prefer dark mode") - Project context ("The API uses OAuth2") - Relationships ("Alice is the project lead")
也就是说,当你在对话中提到"我喜欢简洁的回答"或者"这个项目用的是 FastAPI",Agent 可以当场用 edit_file 工具把这个信息写进 MEMORY.md,不用等到自动整理。
这就像一个好助手,听到老板说了什么偏好,立刻记在本子上,而不是等到下班才回忆。
八、设计取舍
任何工程方案都有取舍。nanobot 的记忆系统也不例外:
| 决策 | 好处 | 代价 |
|---|---|---|
| 消息只追加不修改 | Prompt Cache 高效;不丢数据 | 内存中消息列表持续增长 |
| 用 LLM 做整理 | 摘要质量高;能提取隐含事实 | 每次整理多一次 API 调用 |
| MEMORY.md 全量覆写 | 自动去重;文档结构清晰 | LLM 可能遗漏已有事实 |
| HISTORY.md 不进 prompt | 节省上下文窗口 | Agent 需要主动 grep,可能漏掉相关历史 |
| 后台异步整理 | 不阻塞用户对话 | 需要并发控制 |
其中"LLM 可能遗漏已有事实"这一点值得注意。因为 MEMORY.md 是全量覆写的,如果整理 LLM 在生成新版本时漏掉了某些旧事实,那些信息就丢了。不过实际使用中,这种情况比较少见——整理 prompt 里明确要求"include all existing facts plus new ones"。
九、和其他方案的对比
市面上 AI Agent 的记忆方案大致有几种:
1. 纯上下文窗口(ChatGPT 默认模式) - 优点:简单 - 缺点:窗口满了就忘,跨会话无记忆
2. 向量数据库 RAG(LangChain Memory、Mem0 等) - 优点:能存海量信息,语义检索 - 缺点:复杂度高,需要额外基础设施,检索质量不稳定
3. 结构化知识图谱 - 优点:关系明确,推理能力强 - 缺点:构建和维护成本高
4. nanobot 的双层 Markdown - 优点:极简,可读,可手动编辑,不依赖外部服务 - 缺点:MEMORY.md 大小受 prompt 窗口限制,不适合海量知识
nanobot 的方案胜在简单和透明。两个 Markdown 文件,你随时可以打开看看 AI 记住了什么,甚至手动修改。不需要 Pinecone,不需要 ChromaDB,不需要任何额外的数据库。
对于个人助手这个场景,这个方案够用了。你的偏好、项目上下文、常用配置——这些信息加起来也就几 KB,远远塞得进一个 prompt。
十、自己动手试试
如果你想体验 nanobot 的记忆系统,三步就够:
pip install nanobot-ai
nanobot onboard # 初始化
nanobot agent # 开始聊天
聊几轮之后,打开 ~/.nanobot/workspace/memory/MEMORY.md 看看——你会发现 AI 已经默默记下了关于你的信息。
或者直接告诉它:"记住,我喜欢用中文回答,代码注释用英文。" 然后开一个新会话,看看它是不是还记得。
nanobot 的记忆机制不算复杂,甚至可以说有点"土"——两个 Markdown 文件,一个 LLM 调用,一个整数指针。但它管用。就像老话说的,能解决问题的方案就是好方案。
4000 行代码,能做到跨会话记忆、自动整理、主动记录、优雅归档。这大概就是"小而美"的工程哲学:不追求大而全的架构,而是用最简单的方式解决最核心的问题。