AI Agent 怎么“记住“你说过的话?从 nanobot 和 OpenClaw 的源码看两种记忆方案
最近先后读了 nanobot 和 OpenClaw 两个开源 AI Agent 的记忆模式,发现它们在"记忆"这件事上走了两条不同的路线。一个追求极简,一个做得更重,但底层面对的问题是一样的:LLM 的上下文窗口是有限的,怎么让 Agent 跨会话地"记住"用户的信息?接下来我们一起来看看
文章目录
先说共识:为什么 Agent 需要记忆系统
在聊具体实现之前,先对齐一下问题。
大模型本身是无状态的。你跟 ChatGPT 聊了 100 轮,它"知道"前面说了什么,不是因为它"记住"了,而是因为每次请求都把之前的对话历史塞进了 prompt 里。这就是所谓的"上下文窗口"。
问题在于,上下文窗口有几个硬伤:
- 有上限:Claude 200K tokens,GPT-4 128K tokens,看着挺多,但实际上一个下午的工作对话就能填满
- 越长越贵:token 是按量计费的,历史消息越多,每轮对话的成本越高
- 不跨会话:关了窗口再打开,什么都不记得
所以但凡要做一个"长期可用"的 AI 助手,都绕不开一个问题:怎么把记忆从上下文里剥离出来,变成一个独立的、持久化的、可管理的系统。
nanobot 和 OpenClaw 都给出了自己的方案。
nanobot 的记忆方案:三个文件各管一摊
nanobot(港大 HKUDS 实验室,GitHub 35k+ star,Python 写的)的记忆系统用三个东西来存储:
Session JSONL:原始对话的忠实录像
每个对话按 channel:chat_id 建一个 JSONL 文件,每轮交互追加一行:
{"role":"user","content":"北京天气怎么样","timestamp":"2026-03-25T10:00:00"}
{"role":"assistant","content":"","tool_calls":[{"id":"call_1","function":{"name":"web_search",...}}]}
{"role":"tool","tool_call_id":"call_1","content":"北京今天晴 22°C..."}
{"role":"assistant","content":"北京今天晴朗,气温22度左右。","timestamp":"2026-03-25T10:00:05"}
user、assistant、tool result 全都存,只追加不修改。这个设计是刻意的——源码注释写得很明确,append-only 是为了保持跟 LLM provider 侧 KV cache 的前缀一致性。你改了历史消息,provider 的缓存就失效了,等于白花之前的钱。
Session 上有一个关键字段:last_consolidated,记录"前面多少条消息已经被整理过了"。发给 LLM 的历史只取 last_consolidated 之后的部分。
MEMORY.md:浓缩的长期记忆
一个全局的 Markdown 文件,存的是整理后的关键事实:
## 用户信息
- 叫小明,在字节做后端
- 偏好简洁的代码风格
## 技术偏好
- 主力语言 Python 和 Go
- 习惯用 VS Code
每次 LLM 调用时,这个文件的内容会被完整地塞进 system prompt 里。所以它需要保持精简——太长了就本末倒置了。
它的更新方式是覆盖写。每次记忆整理时,LLM 会把旧的 MEMORY.md 内容和新的对话合在一起,输出一个更新后的版本,直接覆盖原文件。
HISTORY.md:只增不改的日志
另一个 Markdown 文件,存的是每次记忆整理后的对话摘要,只追加不覆盖:
[2026-03-22 14:30] 讨论了 API 设计方案,用户倾向 RESTful 风格,决定用 FastAPI。
[2026-03-23 09:00] 用户要求设置每天 8 点的新闻提醒,已创建 cron job。
这个文件不会出现在每次的 prompt 里,更像一个归档。如果将来需要回忆"之前聊过什么",可以去翻或者 grep。
三者的协作关系
用一张图来描述:
Session.messages: [msg0, msg1, msg2, ..., msg39, msg40, ..., msg99]
|----- 已整理 ------|------- 未整理 --------|
↓ ↓
MEMORY.md (覆盖写) 发给 LLM 作为历史
HISTORY.md (追加写)
last_consolidated 就像一个滑动的指针:指针左边的消息已经被"压缩"进了 MEMORY.md 和 HISTORY.md,不再发给 LLM;指针右边的消息原样保留在上下文里。
OpenClaw 的记忆方案:双源分层 + 搜索引擎
OpenClaw(功能类似 Claude Code 的个人 AI 助手,TypeScript 写的)的记忆系统更重一些,它把记忆分成"动态记忆"和"静态记忆"两大类。
动态记忆:也是 JSONL
跟 nanobot 一样,OpenClaw 的对话也是用 JSONL 格式实时记录。这部分两边几乎一模一样,没什么可说的。
静态记忆:比 nanobot 多一层
OpenClaw 的静态记忆分两个存储位置:
MEMORY.md:全局核心记忆,以手动维护为主memory/YYYY-MM-DD-{slug}.md:按日期 + 语义主题命名的会话记忆文件
跟 nanobot 只有一个 MEMORY.md 不同,OpenClaw 会把每次会话的要点存成独立的、按日期组织的 Markdown 文件。比如今天讨论了 API 设计,就会生成一个 memory/2026-03-25-api-design.md。
静态记忆有三种产生方式:
1. 用户直接编辑 MEMORY.md。 手动写"你要叫我老板"之类的,跟 nanobot 一样。
2. 会话重置时的 session-memory Hook。 用户执行 /new 重置会话时,系统自动从 JSONL 里取最近 15 条消息,让 LLM 生成语义化文件名,写成 memory/YYYY-MM-DD-{slug}.md。nanobot 没有这个机制。
3. Memory Flush(记忆刷新)。 这是跟 nanobot 差别最大的地方。当上下文快满的时候,OpenClaw 不是直接压缩,而是先触发一个额外的 Agent 回合,让 Agent 自己判断哪些信息值得长期保存:
"Pre-compaction memory flush."
"Store durable memories now (use memory/YYYY-MM-DD.md)."
"If nothing to store, reply with SILENT_REPLY_TOKEN."
也就是说,OpenClaw 在压缩之前多了一个"抢救"步骤——先让 Agent 把重要信息存到 memory 文件里,然后再做有损压缩。代价是多一次完整的 LLM 调用。
压缩策略的差异
这是两个项目差别最大的地方之一。
nanobot 的压缩:把一段对话交给 LLM,通过 forced tool_choice 强制 LLM 调用 save_memory 工具,输出两个字段——memory_update(覆盖写 MEMORY.md)和 history_entry(追加写 HISTORY.md)。整理后 last_consolidated 指针右移,被整理过的消息从 LLM 的上下文里消失,但在 Session 文件里保留原样。
# nanobot 的做法:一次 LLM 调用,同时更新两个文件
forced = {"type": "function", "function": {"name": "save_memory"}}
response = await provider.chat_with_retry(
messages=chat_messages,
tools=_SAVE_MEMORY_TOOL,
tool_choice=forced, # 强制调用,确保输出格式可控
)
# memory_update → 覆盖 MEMORY.md
# history_entry → 追加 HISTORY.md
OpenClaw 的压缩:先跑 Memory Flush(一次完整 LLM 调用,让 Agent 存储 durable memories),然后再用 LLM 对历史消息做有损摘要。摘要的指令很明确:
"Merge these partial summaries into a single cohesive summary.
Preserve decisions, TODOs, open questions, and any constraints."
只保留决策、待办、未决问题和约束条件,具体数值和时间点会被丢弃。压缩完之后,摘要直接替换掉历史消息放在上下文里。
两者最大的区别在于压缩后对上下文的处理方式:
- nanobot:压缩后的信息进 MEMORY.md(塞在 system prompt 里)+ HISTORY.md(归档),原始消息从上下文窗口移除但保留在 Session 文件中
- OpenClaw:压缩后的摘要直接替换原始历史,留在上下文窗口里继续参与对话
记忆检索:一个"全带上",一个"按需搜"
这可能是两个项目在记忆系统设计上最本质的分歧。
nanobot:全量注入
nanobot 的做法非常直接——每次 LLM 调用时,把 MEMORY.md 的完整内容塞进 system prompt:
# context.py
def build_system_prompt(self):
memory = self.memory.get_memory_context()
if memory:
parts.append(f"# Memory\n\n{memory}")
没有索引,没有搜索,没有 embedding。全带上,让 LLM 自己去判断哪些记忆跟当前对话相关。
好处是实现极简,零延迟,不会漏掉任何已记录的信息。坏处也很明显——MEMORY.md 不能太大,否则每次请求的固定 token 开销就很高。这也倒逼了 MEMORY.md 必须保持精简,只存最核心的事实。
OpenClaw:混合搜索 + 按需召回
OpenClaw 的做法重得多。它给所有的 Markdown 记忆文件建了索引:
- Markdown 文件分块(400 tokens 一块,相邻重叠 80 tokens)
- 每个块同时生成向量 Embedding 和文本 Token
- 分别存入 SQLite 的
sqlite-vec(向量检索)和FTS5(全文检索)两个扩展
检索时用混合策略,向量搜索占 70% 权重,BM25 全文搜索占 30%:
// 混合搜索,加权合并结果
const vectorResults = await vectorSearch(query, { weight: 0.7 });
const bm25Results = await fts5Search(query, { weight: 0.3 });
return merged.toSorted((a, b) => b.score - a.score);
文章提到这个 70/30 的比例来自内部测试:纯向量召回率 76%,纯 BM25 召回率 68%,混合方案达到 89%。
Agent 通过两个工具跟记忆交互:
memory_search:语义搜索,返回匹配的文件片段和行号memory_get:根据路径和行号精确读取内容
也就是说,OpenClaw 的记忆不是"每次全带上",而是 Agent 觉得需要的时候才去搜。这让它能承载大得多的记忆量,但引入了额外的复杂度和延迟——每次检索要跑 embedding + 查 SQLite,搜出来的结果还要占上下文。
失败兜底:工程上的务实
两个项目在"万一 LLM 整理记忆失败了怎么办"这件事上,处理方式不一样但都比较务实。
nanobot:三次机会 + 降级存原文
nanobot 有两层降级:
第一层,如果 forced tool_choice 报错(有些模型不支持),自动降级到 tool_choice="auto" 重试。
第二层,如果连续失败 3 次,直接把原始消息格式化存进 HISTORY.md,打上 [RAW] 标记:
def _raw_archive(self, messages):
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
self.append_history(
f"[{ts}] [RAW] {len(messages)} messages\n"
f"{self._format_messages(messages)}"
)
不优雅但管用——数据不丢,以后可以手动整理或者重新用 LLM 处理。
OpenClaw:Memory Flush 本身就是一道保险
OpenClaw 的 Memory Flush 机制天然带了一层兜底——在压缩前先让 Agent 主动存储重要信息。即使后续的有损摘要丢了某些细节,关键信息已经被写到了 memory/*.md 文件里,可以通过搜索找回来。
不过这也意味着每次压缩要多花一次 LLM 调用的钱。
触发时机:什么时候开始整理
nanobot:按 token 数触发
nanobot 的 MemoryConsolidator 在每条消息处理前检查 token 数:
budget = context_window_tokens - max_completion_tokens - 1024 # 安全余量
target = budget // 2 # 目标压到一半
estimated = estimate_session_prompt_tokens(session)
if estimated < budget:
return # 还没超,不管
# 超了,开始整理,最多循环 5 轮
for round_num in range(5):
if estimated <= target:
return
# 找一个 user 消息边界,切一段出来整理
boundary = pick_consolidation_boundary(session, estimated - target)
chunk = session.messages[last_consolidated:boundary]
await consolidate_messages(chunk)
session.last_consolidated = boundary
注意它的 target 是 budget // 2,不是刚好压到线以下就停。这是为了留出足够的空间,避免聊两句又触发整理。
整理边界的选择也有讲究——只在 user 消息的位置切分,确保不会把一轮完整的 user → assistant → tool → assistant 交互劈开。
OpenClaw:按字符数触发
OpenClaw 用的是字符数阈值,默认 20000 字符。到了阈值先跑 Memory Flush,再做 Compaction。
两种方式各有取舍。按 token 数更精确(毕竟 LLM 的限制就是 token),但需要调用 tokenizer 来估算;按字符数简单粗暴,有一定的误差但实现成本低。
Session 的持久化格式:都选了 JSONL
这一点两者几乎完全一致——都用 JSONL(一行一个 JSON 对象)来存 session。
nanobot 的 JSONL 第一行是 metadata(包含 last_consolidated、创建时间等),后面每行一条消息。
OpenClaw 的 JSONL 格式类似,也是逐条追加。
JSONL 在这个场景下确实是个好选择:append-only 写入性能好(直接追加一行),读取也简单(逐行 parse),不需要读完整个文件就能处理,比整个 JSON 文件或 SQLite 更轻量。
成本差异
这是实际使用时最直观的差别。
nanobot 每次整理的额外成本:一次 LLM 调用(用于记忆浓缩)。
OpenClaw 每次压缩的额外成本:两次 LLM 调用(Memory Flush 一次 + Compaction 摘要一次),外加日常使用中 memory_search 的 embedding 计算和检索成本。
不过 OpenClaw 的检索式记忆能支撑更大的记忆量——理论上 memory/ 目录下可以有几百个文件,Agent 按需搜索,不用全塞进 prompt。nanobot 的全量注入方式则要求 MEMORY.md 必须控制在合理大小内。
对比总结
把关键差异列一下:
| 维度 | nanobot | OpenClaw |
|---|---|---|
| 语言 | Python | TypeScript |
| 动态记忆 | Session JSONL (append-only) | Session JSONL (类似) |
| 长期记忆存储 | 单一 MEMORY.md | MEMORY.md + memory/日期-主题.md |
| 历史日志 | HISTORY.md (只追加) | 无单独日志,memory/*.md 承担类似角色 |
| 压缩触发 | token 数超阈值 | 字符数超阈值 (默认 20000) |
| 压缩前处理 | 无 | Memory Flush (多一次 LLM 调用抢救重要信息) |
| 压缩方式 | LLM + forced tool_choice → 覆盖 MEMORY.md + 追加 HISTORY.md | LLM 有损摘要 → 替换上下文历史 |
| 压缩后的上下文 | 移除已整理消息,MEMORY.md 在 system prompt 里 | 摘要替换历史,memory/*.md 可搜索 |
| 记忆检索 | 全量注入 system prompt,无搜索 | 向量 + BM25 混合搜索 (70/30),按需召回 |
| 索引系统 | 无 | SQLite (sqlite-vec + FTS5) |
| 失败兜底 | 3 次失败后 raw dump 原始消息 | Memory Flush 天然兜底 |
| 每次整理成本 | 1 次 LLM 调用 | 2 次 LLM 调用 |
| 记忆容量上限 | 受 MEMORY.md 大小限制 | 理论上无限 (按需搜索) |
哪种更好?
说实话,这个问题没有标准答案,取决于使用场景。
nanobot 的方案适合:个人助手场景,对话频率不高,需要记住的信息量有限。MEMORY.md 里存几十条关键事实就够了。优势是实现极简、延迟低、成本可控。
OpenClaw 的方案适合:高频使用、信息密集的场景(比如日常开发助手)。每天可能产生几十个 memory 文件,需要靠搜索来找回之前的信息。优势是记忆容量大、检索精度高。
两者也不是完全互斥的。你完全可以在 nanobot 的方案上加一个 memory 搜索层,或者在 OpenClaw 的方案里精简掉 Memory Flush 步骤。记忆系统的设计本质上是在信息完整性、系统复杂度、token 成本三者之间找平衡点,没有银弹。
最后说一句,不管是 nanobot 的"一个 MEMORY.md 走天下",还是 OpenClaw 的"索引 + 搜索",它们都绕不开同一个本质操作:用 LLM 来理解和压缩 LLM 自己的对话。记忆整理的质量上限,就是 LLM 的理解能力上限。这在当前阶段是一个不可避免的递归依赖——也许以后会有更好的方案,但目前来看,这已经是工程上最务实的选择了。
参考:
- nanobot 源码:https://github.com/HKUDS/nanobot(v0.1.4,2026年3月)
- OpenClaw 记忆系统分析:从架构到代码:深入理解 OpenClaw 的双源记忆系统
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)