摘要

1) 一句话总结 Clawdbot 是一款开源的本地 AI 助手,其核心特色是采用基于纯 Markdown 文件和 SQLite 混合搜索的双层持久化记忆系统,实现了低成本、无限制且完全由用户掌控的长期上下文留存。

2) 关键要点

  • 本地化与集成:采用 MIT 许可,在本地机器运行,可与 Discord、WhatsApp、Telegram 等现有聊天平台集成,自主处理现实世界任务。
  • 上下文与记忆分离:明确区分了短暂、受限且昂贵的“上下文”(单次 API 请求内容)与持久、无限制且廉价的“记忆”(本地磁盘文件)。
  • 双层 Markdown 记忆系统:记忆以纯文本形式存储在智能体工作区。第 1 层为每日追加日志(memory/YYYY-MM-DD.md),第 2 层为长期精选知识(MEMORY.md)。
  • 自动化索引:后台监听文件更改,将文本分割为 400 tokens(重叠 80 tokens)的块,并向量化存储到本地 SQLite 数据库中。
  • 混合搜索机制:智能体通过 memory_search 工具检索记忆,采用 70% 向量语义搜索(依赖 sqlite-vec)和 30% BM25 关键词搜索(依赖 FTS5)的加权组合,默认最低得分阈值为 0.35。
  • 多智能体隔离:支持多智能体运行,每个智能体拥有独立的工作区(Markdown 源文件)和状态目录(SQLite 索引),实现记忆隔离。
  • 上下文压缩 (Compaction):当逼近模型上下文上限(如 200K 或 1M tokens)时,系统会自动或手动将早期对话总结为紧凑条目并持久化到 JSONL 记录中。
  • 压缩前刷新 (Memory Flush):在执行压缩前,系统会触发静默轮次,让智能体将关键决策和事实写入本地 Markdown 文件,防止摘要过程丢失重要信息。
  • 输出修剪 (Pruning):自动裁剪过大的工具输出日志(结合 Anthropic 的 5 分钟缓存 TTL 机制),以降低 API 成本并节省上下文空间。
  • 会话生命周期:会话具有边界,可配置为每天(默认凌晨 4 点)或空闲 N 分钟后自动重置,并可通过钩子在开启新会话时自动保存上下文摘要。

3) 风险与不足(基于原文明确提及)

  • 多智能体数据越权风险:默认情况下,多智能体工作区只是一个“软沙盒”,除非明确启用严格的沙盒模式,否则智能体理论上可以通过绝对路径访问其他工作区的数据。
  • 压缩导致的信息丢失:基于 LLM 的上下文压缩是一个有损过程,重要信息可能会在总结中被省略并永久丢失(系统依赖“压缩前刷新”来缓解此风险)。
  • 修剪导致的细节丢失:对长工具输出(如大型日志)的修剪是一个有损过程,发送给模型的旧输出一旦被裁剪便无法在上下文中恢复(尽管完整日志仍保留在本地 JSONL 文件中)。

正文

Clawdbot 是一个由 Peter Steinberger 创建的开源个人 AI 助手(采用 MIT 许可)。在撰写本文时,它已迅速获得广泛关注,在 GitHub 上拥有超过 32,600 颗星。与在云端运行的 ChatGPT 或 Claude 不同,Clawdbot 在你的本地机器上运行,并能与你已经在使用的聊天平台(如 Discord、WhatsApp、Telegram 等)进行集成。

让 Clawdbot 脱颖而出的是它自主处理现实世界任务的能力:管理电子邮件、安排日历事件、处理航班值机,以及按计划运行后台任务。但真正引起我注意的是它的持久化记忆系统,该系统能够保持 24/7 的上下文留存,记住对话并无限期地在之前的交互基础上进行构建。

如果你读过我之前关于 ChatGPT 记忆Claude 记忆 的文章,你就会知道我对不同的 AI 产品如何处理记忆非常着迷。Clawdbot 采取了一种根本不同的方法:它没有采用基于云的、由公司控制的记忆,而是将一切都保存在本地,让用户完全拥有自己的上下文和技能。

让我们深入了解它的工作原理。

上下文是如何构建的

在深入探讨记忆之前,让我们先了解模型在每次请求中会看到什么:

[0] 系统提示词 (System Prompt)(静态 + 条件指令)
[1] 项目上下文 (Project Context)(引导文件:AGENTS.md、SOUL.md 等)
[2] 对话历史 (Conversation History)(消息、工具调用、压缩摘要)
[3] 当前消息 (Current Message)

系统提示词定义了智能体的能力和可用工具。与记忆相关的是“项目上下文”,它包含了注入到每次请求中的、用户可编辑的 Markdown 文件:

文件用途
AGENTS.md智能体指令,包括记忆指南
SOUL.md个性与基调
USER.md关于用户的信息
TOOLS.md外部工具的使用指南

这些文件与记忆文件一起存放在智能体的工作区中,使得整个智能体的配置透明且可编辑。

上下文 vs 记忆

理解**上下文(Context)记忆(Memory)**之间的区别,是理解 Clawdbot 的基础。

上下文是模型在单次请求中看到的所有内容:

上下文 = 系统提示词 + 对话历史 + 工具结果 + 附件

上下文是:

  • 短暂的(Ephemeral) - 仅存在于本次请求中
  • 受限的(Bounded) - 受模型上下文窗口限制(例如 200K tokens)
  • 昂贵的(Expensive) - 每个 token 都会消耗 API 成本并影响速度

记忆是存储在磁盘上的内容:

记忆 = MEMORY.md + memory/*.md + 会话记录 (Session Transcripts)

记忆是:

  • 持久化的(Persistent) - 在重启后依然存在,可保存数天、数月
  • 无限制的(Unbounded) - 可以无限增长
  • 廉价的(Cheap) - 存储没有 API 成本
  • 可搜索的(Searchable) - 为语义检索建立了索引

记忆工具

智能体通过两个专门的工具来访问记忆:

用途:在所有文件中查找相关的记忆

{
  "name": "memory_search",
  "description": "强制性的回忆步骤:在回答有关先前工作、决策、日期、人员、偏好或待办事项的问题之前,对 MEMORY.md + memory/*.md 进行语义搜索",
  "parameters": {
    "query": "关于 API 我们决定了什么?",
    "maxResults": 6,
    "minScore": 0.35
  }
}

返回结果

{
  "results": [
    {
      "path": "memory/2026-01-20.md",
      "startLine": 45,
      "endLine": 52,
      "score": 0.87,
      "snippet": "## API 讨论\n决定使用 REST 而不是 GraphQL,因为更简单...",
      "source": "memory"
    }
  ],
  "provider": "openai",
  "model": "text-embedding-3-small"
}

memory_get

用途:在找到特定内容后读取它

{
  "name": "memory_get",
  "description": "在执行 memory_search 后,从记忆文件中读取特定的行",
  "parameters": {
    "path": "memory/2026-01-20.md",
    "from": 45,
    "lines": 15
  }
}

返回结果

{
  "path": "memory/2026-01-20.md",
  "text": "## API 讨论\n\n与团队开会讨论了 API 架构。\n\n### 决策\n我们选择 REST 而不是 GraphQL,原因如下:\n1. 实现更简单\n2. 更好的缓存\n3. 团队更熟悉\n\n### 接口端点\n- GET /users\n- POST /auth/login\n- GET /projects/:id"
}

写入记忆

没有专门的 memory_write 工具。智能体使用标准的 writeedit 工具来写入记忆,就像它处理任何文件一样。由于记忆只是 Markdown 格式,你也可以手动编辑这些文件(它们会被自动重新索引)。

写入位置由 AGENTS.md 中的提示词驱动:

触发条件目标位置
日常笔记、“记住这个”memory/YYYY-MM-DD.md
持久的事实、偏好、决策MEMORY.md
经验教训AGENTS.mdTOOLS.md

自动写入也会在压缩前刷新(pre-compaction flush)和会话结束时发生(将在后面的章节中介绍)。

记忆存储

Clawdbot 的记忆系统建立在“记忆就是智能体工作区中的纯 Markdown 文件”这一原则之上。

双层记忆系统

记忆存放在智能体的工作区中(默认:~/clawd/):

~/clawd/
├── MEMORY.md              - 第 2 层:长期精选知识
└── memory/
    ├── 2026-01-26.md      - 第 1 层:今天的笔记
    ├── 2026-01-25.md      - 昨天的笔记
    ├── 2026-01-24.md      - ...以此类推
    └── ...

第 1 层:每日日志 (memory/YYYY-MM-DD.md)

这些是仅限追加的每日笔记,智能体全天都会在这里写入内容。当智能体想要记住某事或被明确告知要记住某事时,它会写入此文件。

# 2026-01-26
 
## 上午 10:30 - API 讨论
与用户讨论了 REST vs GraphQL。决策:为了简单起见使用 REST。
关键端点:/users, /auth, /projects。
 
## 下午 2:15 - 部署
将 v2.3.0 部署到生产环境。没有问题。
 
## 下午 4:00 - 用户偏好
用户提到他们更喜欢 TypeScript 而不是 JavaScript。

第 2 层:长期记忆 (MEMORY.md)

这是经过整理的持久化知识。当发生重大事件、产生想法、做出决策、形成观点和吸取教训时,智能体会写入此文件。

# 长期记忆
 
## 用户偏好
- 喜欢 TypeScript 胜过 JavaScript
- 喜欢简洁的解释
- 正在开发项目 "Acme Dashboard"
 
## 重要决策
- 2026-01-15: 选择 PostgreSQL 作为数据库
- 2026-01-20: 采用 REST 而不是 GraphQL
- 2026-01-26: 使用 Tailwind CSS 进行样式设计
 
## 关键联系人
- Alice (alice@acme.com) - 设计主管
- Bob (bob@acme.com) - 后端工程师

智能体如何知道读取记忆

自动加载的 AGENTS.md 文件包含以下指令:

## 每次会话
 
在做任何其他事情之前:
1. 读取 SOUL.md - 这是你的身份
2. 读取 USER.md - 这是你正在帮助的人
3. 读取 memory/YYYY-MM-DD.md(今天和昨天)以获取最近的上下文
4. 如果在主会话(MAIN SESSION,与人类的直接聊天)中,还要读取 MEMORY.md
 
不要请求许可,直接执行。

记忆是如何建立索引的

当你保存一个记忆文件时,后台会发生以下情况:

┌─────────────────────────────────────────────────────────────┐
│  1. 文件已保存                                              │
│     ~/clawd/memory/2026-01-26.md                            │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  2. 文件监听器检测到更改                                    │
│     Chokidar 监控 MEMORY.md + memory/**/*.md                │
│     防抖 1.5 秒以批量处理快速写入                           │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  3. 分块 (Chunking)                                         │
│     分割成约 400 token 的块,并带有 80 token 的重叠         │
│                                                             │
│     ┌────────────────┐                                      │
│     │ 块 1           │                                      │
│     │ 第 1-15 行     │──────┐                               │
│     └────────────────┘      │                               │
│     ┌────────────────┐      │ (80 token 重叠)               │
│     │ 块 2           │◄─────┘                               │
│     │ 第 12-28 行    │──────┐                               │
│     └────────────────┘      │                               │
│     ┌────────────────┐      │                               │
│     │ 块 3           │◄─────┘                               │
│     │ 第 25-40 行    │                                      │
│     └────────────────┘                                      │
│                                                             │
│     为什么是 400/80?为了平衡语义连贯性与粒度。             │
│     重叠确保跨越块边界的事实能在两边都被捕获。              │
│     这两个值都是可配置的。                                  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  4. 向量化 (Embedding)                                      │
│     每个块 -> 向量化提供商 -> 向量                          │
│                                                             │
│     "讨论了 REST vs GraphQL" ->                             │
│         OpenAI/Gemini/Local ->                              │
│         [0.12, -0.34, 0.56, ...]  (1536 维)                 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  5. 存储                                                    │
│     ~/.clawdbot/memory/<agentId>.sqlite                     │
│                                                             │
│     数据表:                                                 │
│     - chunks (id, path, start_line, end_line, text, hash)   │
│     - chunks_vec (id, embedding)      -> sqlite-vec         │
│     - chunks_fts (text)               -> FTS5 全文搜索      │
│     - embedding_cache (hash, vector)  -> 避免重复向量化     │
└─────────────────────────────────────────────────────────────┘

sqlite-vec 是一个 SQLite 扩展,它允许直接在 SQLite 中进行向量相似度搜索,无需外部向量数据库。

FTS5 是 SQLite 内置的全文搜索引擎,为 BM25 关键词匹配提供支持。它们结合在一起,使得 Clawdbot 能够从单个轻量级数据库文件中运行混合搜索(语义 + 关键词)。

记忆是如何被搜索的

当你搜索记忆时,Clawdbot 会并行运行两种搜索策略。向量搜索(语义)查找含义相同的内容,而 BM25 搜索(关键词)查找包含精确 token 的内容。

结果通过加权评分进行组合:

最终得分 = (0.7 * 向量得分) + (0.3 * 文本得分)

为什么是 70/30?语义相似度是记忆召回的主要信号,但 BM25 关键词匹配可以捕捉到向量可能遗漏的精确术语(名称、ID、日期)。低于 minScore 阈值(默认 0.35)的结果将被过滤掉。所有这些值都是可配置的。

这确保了无论你是搜索概念(“那个数据库的东西”)还是具体细节(“POSTGRES_URL”),都能获得良好的结果。

多智能体记忆

Clawdbot 支持多个智能体,每个智能体都具有完全的记忆隔离

~/.clawdbot/memory/              # 状态目录(索引)
├── main.sqlite                  # "main" 智能体的向量索引
└── work.sqlite                  # "work" 智能体的向量索引
 
~/clawd/                         # "main" 智能体工作区(源文件)
├── MEMORY.md
└── memory/
    └── 2026-01-26.md
 
~/clawd-work/                    # "work" 智能体工作区(源文件)
├── MEMORY.md
└── memory/
    └── 2026-01-26.md

Markdown 文件(事实来源)存放在每个工作区中,而 SQLite 索引(派生数据)存放在状态目录中。每个智能体都有自己的工作区和索引。记忆管理器由 agentId + workspaceDir 作为键,因此不会自动发生跨智能体的记忆搜索。

智能体能读取彼此的记忆吗? 默认情况下不能。每个智能体只能看到自己的工作区。然而,工作区是一个软沙盒(默认工作目录),而不是硬边界。除非你启用严格的沙盒模式,否则智能体理论上可以使用绝对路径访问另一个工作区。

这种隔离对于分离上下文非常有用。例如,一个用于 WhatsApp 的“个人”智能体和一个用于 Slack 的“工作”智能体,各自拥有不同的记忆和个性。

压缩 (Compaction)

每个 AI 模型都有上下文窗口限制。Claude 有 200K tokens,GPT-5.1 有 1M。长时间的对话最终会触及这个上限。

当这种情况发生时,Clawdbot 会使用压缩:将较早的对话总结为一个紧凑的条目,同时保持最近的消息完整。

┌─────────────────────────────────────────────────────────────┐
│  压缩前                                                     │
│  上下文: 180,000 / 200,000 tokens                           │
│                                                             │
│  [轮次 1] 用户: "我们来构建一个 API 吧"                     │
│  [轮次 2] 智能体: "好的!你需要什么端点?"                  │
│  [轮次 3] 用户: "用户和认证"                                │
│  [轮次 4] 智能体: *创建了 500 行的 schema*                  │
│  [轮次 5] 用户: "添加速率限制"                              │
│  [轮次 6] 智能体: *修改代码*                                │
│  ... (还有 100 个轮次) ...                                  │
│  [轮次 150] 用户: "现在状态如何?"                          │
│                                                             │
│  ⚠️ 接近限制                                                │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  触发压缩                                                   │
│                                                             │
│  1. 将轮次 1-140 总结为一个紧凑的摘要                       │
│  2. 保持轮次 141-150 完整(最近的上下文)                   │
│  3. 将摘要持久化到 JSONL 会话记录中                         │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  压缩后                                                     │
│  上下文: 45,000 / 200,000 tokens                            │
│                                                             │
│  [摘要] "构建了带有 /users, /auth 端点的 REST API。         │
│   实现了 JWT 认证、速率限制(100 请求/分钟)、              │
│   PostgreSQL 数据库。已部署到预发布环境 v2.4.0。            │
│   当前重点:生产环境部署准备。"                             │
│                                                             │
│  [轮次 141-150 原样保留]                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

自动压缩 vs 手动压缩

自动:在接近上下文限制时触发

  • 你会在详细模式下看到:🧹 Auto-compaction complete(自动压缩完成)
  • 原始请求将使用压缩后的上下文进行重试

手动:使用 /compact 命令

/compact 重点关注决策和未解决的问题

与某些优化不同,压缩会持久化到磁盘。摘要会被写入会话的 JSONL 记录文件中,因此未来的会话将从压缩后的历史记录开始。

记忆刷新 (The Memory Flush)

基于 LLM 的压缩是一个有损过程。重要的信息可能会在总结中被省略并潜在地丢失。为了应对这个问题,Clawdbot 使用了压缩前记忆刷新(pre-compaction memory flush)。

┌─────────────────────────────────────────────────────────────┐
│  上下文接近限制                                             │
│                                                             │
│  ████████████████████████████░░░░░░░░  上下文的 75%         │
│                              ↑                              │
│                    跨越软阈值                               │
│                    (contextWindow - reserve - softThreshold)│
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  静默记忆刷新轮次                                           │
│                                                             │
│  系统: "压缩前记忆刷新。现在存储持久记忆                    │
│        (使用 memory/YYYY-MM-DD.md)。                        │
│        如果没有要存储的内容,请回复 NO_REPLY。"             │
│                                                             │
│  智能体: 回顾对话寻找重要信息                               │
│          将关键决策/事实写入记忆文件                        │
│          -> NO_REPLY (用户什么也看不到)                     │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  安全进行压缩                                               │
│                                                             │
│  重要信息现在已保存在磁盘上                                 │
│  压缩可以继续进行而不会丢失知识                             │
└─────────────────────────────────────────────────────────────┘

记忆刷新可以在 clawdbot.yamlclawdbot.json 文件中进行配置。

{
  "agents": {
    "defaults": {
      "compaction": {
        "reserveTokensFloor": 20000,
        "memoryFlush": {
          "enabled": true,
          "softThresholdTokens": 4000,
          "systemPrompt": "会话即将压缩。现在存储持久记忆。",
          "prompt": "将持久的笔记写入 memory/YYYY-MM-DD.md;如果没有要存储的内容,请回复 NO_REPLY。"
        }
      }
    }
  }
}

修剪 (Pruning)

工具的返回结果可能非常庞大。单个 exec 命令可能会输出 50,000 个字符的日志。修剪会裁剪这些旧的输出,而不会重写历史记录。这是一个有损过程,旧的输出无法恢复。

┌─────────────────────────────────────────────────────────────┐
│  修剪前(内存中)                                           │
│                                                             │
│  工具结果 (exec): [50,000 字符的 npm install 输出]          │
│  工具结果 (read): [大型配置文件, 10,000 字符]               │
│  工具结果 (exec): [构建日志, 30,000 字符]                   │
│  用户: "构建成功了吗?"                                     │
└─────────────────────────────────────────────────────────────┘

                              ▼ (软裁剪 + 硬清除)
┌─────────────────────────────────────────────────────────────┐
│  修剪后(发送给模型)                                       │
│                                                             │
│  工具结果 (exec): "npm WARN deprecated...[已截断]           │
│                       ...Successfully installed."           │
│  工具结果 (read): "[旧的工具结果内容已清除]"                │
│  工具结果 (exec): [保留 - 太新了,不予修剪]                 │
│  用户: "构建成功了吗?"                                     │
└─────────────────────────────────────────────────────────────┘
 
磁盘上的 JSONL 文件: 未更改(完整的输出仍然存在)

缓存 TTL 修剪

Anthropic 会将提示词前缀缓存长达 5 分钟,以减少重复调用的延迟和成本。当在 TTL(生存时间)窗口内发送相同的提示词前缀时,缓存的 token 成本会降低约 90%。TTL 过期后,下一个请求必须重新缓存整个提示词。

问题在于:如果会话空闲时间超过了 TTL,下一个请求就会丢失缓存,并且必须以全额的“缓存写入”价格重新缓存完整的对话历史记录。

缓存 TTL 修剪通过检测缓存何时过期并在下一个请求之前裁剪旧的工具结果来解决这个问题。需要重新缓存的提示词越小,意味着成本越低:

{
  "agent": {
    "contextPruning": {
      "mode": "cache-ttl",      // 仅在缓存过期后修剪
      "ttl": "600",             // 匹配你的 cacheControlTtl
      "keepLastAssistants": 3,  // 保护最近的工具结果
      "softTrim": {
        "maxChars": 4000,
        "headChars": 1500,
        "tailChars": 1500
      },
      "hardClear": {
        "enabled": true,
        "placeholder": "[旧的工具结果内容已清除]"
      }
    }
  }
}

会话生命周期

会话不会永远持续下去。它们会根据可配置的规则进行重置,从而为记忆创造自然的边界。默认行为是每天重置。但也有其他可用的模式。

模式行为
daily在固定时间重置(默认:当地时间凌晨 4 点)
idle在 N 分钟不活动后重置
daily+idle以先到期者为准

会话记忆钩子

当你运行 /new 开始一个新的会话时,会话记忆钩子可以自动保存上下文:

/new


┌─────────────────────────────────────────────────────────────┐
│  触发会话记忆钩子                                           │
│                                                             │
│  1. 从结束的会话中提取最后 15 条消息                        │
│  2. 通过 LLM 生成描述性的 slug (简短标识)                   │
│  3. 保存到 ~/clawd/memory/2026-01-26-api-design.md          │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  新会话开始                                                 │
│                                                             │
│  以前的上下文现在可以通过 memory_search 进行搜索            │
└─────────────────────────────────────────────────────────────┘

结论

Clawdbot 的记忆系统之所以成功,是因为它秉持了几个关键原则:

1. 透明度优于黑盒

记忆是纯 Markdown 文件。你可以阅读它、编辑它、对它进行版本控制。没有不透明的数据库或专有格式。

2. 搜索优于注入

智能体不是将所有内容都塞进上下文中,而是搜索相关的内容。这保持了上下文的专注并降低了成本。

3. 持久化优于会话

重要信息保存在磁盘上的文件中,而不仅仅是在对话历史记录中。压缩无法破坏已经保存的内容。

4. 混合搜索优于单一搜索

仅靠向量搜索会遗漏精确匹配。仅靠关键词搜索会遗漏语义。混合搜索让你两者兼得。

参考资料


如果你觉得这篇文章很有趣,我很乐意听听你的想法。请在 TwitterLinkedIn 上分享它,或者通过 guptaamanthan01[at]gmail[dot]com 与我联系。

关联主题