nanobot 的记忆机制:它为什么能记住你的习惯和喜好?

Posted on Sun 15 March 2026 in AI • 3 min read

nanobot 的记忆机制:它为什么能记住你的习惯和喜好?

用过 ChatGPT 的人大概都有这个体验:聊了半天,关掉窗口,下次再开——它把你忘得一干二净。你得重新自我介绍,重新解释项目背景,重新说一遍"我喜欢简洁的回答"。

就像每次去同一家理发店,Tony 老师都问你"今天想剪什么样的"。哥们儿,上次不是说了吗?

这个问题的根源不复杂:LLM 本身是无状态的。每次对话都是一张白纸。所谓的"上下文",不过是把之前的聊天记录塞进 prompt 里重新发一遍。窗口一满,早期的对话就被截断了。

nanobot 是一个超轻量的开源 AI 助手框架,核心代码只有约 4000 行 Python。但它解决了这个问题——它能跨会话记住你是谁、你在做什么、你喜欢什么样的回答风格。我用了一段时间,发现它确实"认识"我:知道我的项目目录在哪,知道我写博客喜欢用中文,知道我偏好"质量优先、速度其次"。

这篇文章就来拆解一下,它是怎么做到的。

一、核心矛盾:LLM 的"金鱼记忆"

先说清楚问题。LLM 面临三个互相矛盾的约束:

  1. 上下文窗口有限——对话越长,塞进去的历史越多,直到撑爆
  2. Prompt Cache 很脆弱——改动前面的消息,整个缓存前缀失效,推理成本翻倍
  3. 用户期望长期记忆——"我上周跟你说过的那个项目,还记得吗?"

传统做法要么截断历史(丢信息),要么全量塞入(撑爆窗口),要么做 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 行代码。流程是这样的:

  1. 选取范围:取 messages[last_consolidated:-keep_count],其中 keep_count = memory_window // 2。也就是说,默认保留最近 50 条不动,把更早的消息拿去整理。

  2. 格式化:把消息格式化成文本,带上时间戳和角色: [2026-03-15 14:30] USER: 帮我看看这个 Go 代码的 lint 问题 [2026-03-15 14:31] ASSISTANT [tools: exec, read_file]: 发现 36 个 P0 级别的 lint 问题...

  3. 调用 LLM:发起一个独立的 LLM 调用,system prompt 是"你是一个记忆整理 Agent",给它当前的 MEMORY.md 内容和待整理的对话,让它调用 save_memory 工具。

  4. save_memory 工具:这是一个专门定义的工具,有两个参数:

  5. history_entry:2-5 句话的事件摘要,带时间戳,追加到 HISTORY.md
  6. memory_update:更新后的完整 MEMORY.md 内容(旧事实 + 新事实)

  7. 推进指针:整理成功后,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 命令:

  1. 等待正在进行的整理任务完成
  2. 归档所有未整理的消息(archive_all=True
  3. 清空会话消息,重置 last_consolidated 为 0
  4. 保存空会话到磁盘

如果归档失败,会话不会被清空——宁可保留旧消息,也不丢数据。

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_file or write_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 行代码,能做到跨会话记忆、自动整理、主动记录、优雅归档。这大概就是"小而美"的工程哲学:不追求大而全的架构,而是用最简单的方式解决最核心的问题。

项目地址:github.com/HKUDS/nanobot