最近先后读了 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 记忆文件建了索引:

  1. Markdown 文件分块(400 tokens 一块,相邻重叠 80 tokens)
  2. 每个块同时生成向量 Embedding文本 Token
  3. 分别存入 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 的理解能力上限。这在当前阶段是一个不可避免的递归依赖——也许以后会有更好的方案,但目前来看,这已经是工程上最务实的选择了。


参考:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐