Clawdbot 是一个开源的个人 AI 助手(MIT 许可),由 Peter Steinberger 创建,已经在 GitHub 上获得了 32,600 多个星标。与 ChatGPT 或 Claude 不同,后者运行在云端,而 Clawdbot 直接在您的机器上运行,并与您已经使用的聊天平台(如 Discord、WhatsApp、Telegram 等)集成。
Clawdbot 的独特之处在于其能够自主处理现实世界的任务:管理电子邮件、安排日历事件、处理航班值机、以及按照计划运行后台作业。但是,吸引我注意的是其 持久内存系统,该系统维持 24/7 上下文保留,记住对话并在之前的交互基础上进行构建。
如果您阅读过我之前关于 ChatGPT 内存 和 Claude 内存 的文章,您就会知道我对不同 AI 产品如何处理内存非常感兴趣。Clawdbot 采取了一个根本不同的方法:与其基于云端、由公司控制的内存,Clawdbot 将所有内容保存在本地,赋予用户对其上下文和技能的完全所有权。
让我们深入了解其工作原理。
在深入内存之前,让我们了解一下模型在每个请求中看到的内容:
[0] 系统提示(静态+条件指令) [1] 项目上下文(引导文件:AGENTS.md、SOUL.md 等) [2] 对话历史(消息、工具调用、压缩摘要) [3] 当前消息
系统提示定义了代理的能力和可用的工具。与内存相关的重要内容是项目上下文,其中包括用户可编辑的 Markdown 文件,这些文件被注入到每个请求中:
| 文件 | 目的 |
|---|---|
AGENTS.md | 代理指令,包括内存指南 |
SOUL.md | 个性和语气 |
USER.md | 关于用户的信息 |
TOOLS.md | 外部工具的使用指南 |
这些文件与内存文件一起存储在代理的工作空间中,使整个代理配置透明且可编辑。
理解 上下文 和 内存 之间的区别对于理解 Clawdbot 至关重要。
上下文 是模型在单个请求中看到的所有内容:
上下文 = 系统提示 + 对话历史 + 工具结果 + 附件
上下文是:
内存 是存储在磁盘上的内容:
内存 = MEMORY.md + 内存/*.md + 会话转录
内存是:
代理通过两个专用工具访问内存:
memory_search目的:在所有文件中查找相关记忆
{
"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 工具。代理使用标准的 write 和 edit 工具(它用于任何文件)写入内存。由于内存只是 Markdown,您也可以手动编辑这些文件(它们将自动重新索引)。
写入位置的决定是通过 AGENTS.md 驱动的:
| 触发器 | 目标 |
|---|---|
| 日常笔记,“记住这个” | memory/YYYY-MM-DD.md |
| 持久事实、偏好、决策 | MEMORY.md |
| 经验教训 | AGENTS.md 或 TOOLS.md |
自动写入也会在预压缩刷新和会话结束时发生(在后面的部分中介绍)。
Clawdbot 的内存系统建立在“内存是代理工作空间中的普通 Markdown”这一原则之上。
内存存储在代理的工作空间中(默认:~/clawd/):
~/clawd/
├── MEMORY.md - 第 2 层:长期策划的知识
└── memory/
├── 2026-01-26.md - 第 1 层:今天的笔记
├── 2026-01-25.md - 昨天的笔记
├── 2026-01-24.md - ... 等等
└── ...
memory/YYYY-MM-DD.md)这些是 仅追加的每日笔记,代理在一天中写入这些笔记。代理在想要记住某些内容或被明确指示记住某些内容时写入这些笔记。
# 2026-01-26
## 10:30 AM - API 讨论
与用户讨论了 REST 与 GraphQL。决定:使用 REST 以实现简单性。
关键端点:/users、/auth、/projects。
## 2:15 PM - 部署
将 v2.3.0 部署到生产环境。没有问题。
## 4:00 PM - 用户偏好
用户提到他们更喜欢 TypeScript 而不是 JavaScript。
MEMORY.md)这是 策划的、持久的知识。代理在发生重大事件、思考、决策、意见和经验教训时写入此处。
# 长期内存
## 用户偏好
- 更喜欢 TypeScript 而不是 JavaScript
- 喜欢简洁的解释
- 正在处理“Acme 仪表盘”项目
## 重要决策
- 2026-01-15:选择 PostgreSQL 作为数据库
- 2026-01-20:采用 REST 而不是 GraphQL
- 2026-01-26:使用 Tailwind CSS 进行样式设计
## 关键联系人
- Alice ([email protected]) - 设计负责人
- Bob ([email protected]) - 后端工程师
AGENTS.md 文件(自动加载)包含指令:
## 每次会话
在做任何其他事情之前:
1. 读取 SOUL.md - 这是你是谁
2. 读取 USER.md - 这是你正在帮助的人
3. 读取 memory/YYYY-MM-DD.md(今天和昨天)以获取最近的上下文
4. 如果在主会话中(直接与人类聊天),也读取 MEMORY.md
不需要请求许可,只需执行即可。
当您保存内存文件时,幕后会发生以下情况:
┌─────────────────────────────────────────────────────────────┐
│ 1. 文件已保存 │
│ ~/clawd/memory/2026-01-26.md │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 文件监视器检测更改 │
│ Chokidar 监视 MEMORY.md + memory/**/*.md │
│ 1.5 秒的去抖动以批量快速写入 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 分块 │
│ 将文件分成约 400 个标记的块,重叠 80 个标记 │
│ │
│ ┌────────────────┐ │
│ │ 块 1 │ │
│ │ 行 1-15 │──────┐ │
│ └────────────────┘ │ │
│ ┌────────────────┐ │ (80 个标记的重叠) │
│ │ 块 2 │◄─────┘ │
│ │ 行 12-28 │──────┐ │
│ └────────────────┘ │ │
│ ┌────────────────┐ │ │
│ │ 块 3 │◄─────┘ │
│ │ 行 25-40 │ │
│ └────────────────┘ │
│ │
│ 为什么是 400/80?平衡了语义的连贯性与粒度。 │
│ 重叠确保跨块边界的内容在两个块中都被捕获。 │
│ 这两个值都是可配置的。 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. 嵌入 │
│ 每个块 -> 嵌入提供者 -> 向量 │
│ │
│ "讨论 REST vs GraphQL" -> │
│ OpenAI/Gemini/Local -> │
│ [0.12, -0.34, 0.56, ...] (1536 维) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. 存储 │
│ ~/.clawdbot/memory/<agentId>.sqlite │
│ │
│ 表格: │
│ - 块 (id, path, start_line, end_line, text, hash) │
│ - 块_vec (id, embedding) -> sqlite-vec │
│ - 块_fts (text) -> FTS5 全文 │
│ - embedding_cache (hash, vector) -> 避免重新嵌入 │
└─────────────────────────────────────────────────────────────┘
sqlite-vec 是一个 SQLite 扩展,允许直接在 SQLite 中进行向量相似性搜索,无需外部向量数据库。
FTS5 是 SQLite 的内置全文搜索引擎,支持 BM25 关键字匹配。它们共同允许 Clawdbot 从单个轻量级数据库文件运行混合搜索(语义 + 关键字)。
当您搜索内存时,Clawdbot 并行运行两个搜索策略。向量搜索(语义)查找具有相同含义的内容,而 BM25 搜索(关键字)查找具有确切标记的内容。
结果通过加权评分组合:
finalScore = (0.7 * vectorScore) + (0.3 * textScore)
为什么是 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 的“工作”代理,每个都有不同的记忆和个性。
每个 AI 模型都有一个上下文窗口限制。Claude 有 200K 标记,GPT-5.1 有 1M。长时间的对话最终会达到这个限制。
当这种情况发生时,Clawdbot 使用压缩:总结较旧的对话以获得紧凑的条目,同时保持最近的消息完整。
┌─────────────────────────────────────────────────────────────┐
│ 压缩前 │
│ 上下文:180,000 / 200,000 标记 │
│ │
│ [回合 1] 用户:”让我们构建一个 API“ │
│ [回合 2] 代理:”当然!你需要什么端点?“ │
│ [回合 3] 用户:”用户和身份验证“ │
│ [回合 4] 代理:*创建 500 行架构* │
│ [回合 5] 用户:”添加速率限制“ │
│ [回合 6] 代理:*修改代码* │
│ ... (100 多个回合) ... │
│ [回合 150] 用户:”状态如何?“ │
│ │
│ ⚠️ 接近限制 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 压缩触发 │
│ │
│ 1. 将回合 1-140 总结为紧凑的摘要 │
│ 2. 保留回合 141-150(最近的上下文) │
│ 3. 将摘要保存到 JSONL 转录 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 压缩后 │
│ 上下文:45,000 / 200,000 标记 │
│ │
│ [摘要] “构建了 REST API,具有 /users、/auth 端点。 │
│ 实现了 JWT 身份验证、速率限制(100 req/min)、 │
│ PostgreSQL 数据库。已部署到暂存 v2.4.0。 │
│ 当前关注点:生产部署准备。” │
│ │
│ [保留回合 141-150 原样] │
│ │
└─────────────────────────────────────────────────────────────┘
自动:在接近上下文限制时触发
Auto-compaction complete 在详细模式下手动:使用 /compact 命令
/compact 关注决策和开放问题
与某些优化不同,压缩会持久地写入磁盘。摘要写入会话的 JSONL 转录文件,因此未来的会话以压缩的历史记录开始。
基于 LLM 的压缩是一个有损的过程。重要信息可能会被总结并潜在地丢失。为了应对这一点,Clawdbot 使用了预压缩内存刷新。
┌─────────────────────────────────────────────────────────────┐
│ 上下文接近限制 │
│ │
│ ████████████████████████████░░░░░░░░ 75% 的上下文 │
│ ↑ │
│ 软阈值被越过 │
│ (contextWindow - reserve - softThreshold)│
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 静默内存刷新回合 │
│ │
│ 系统:”预压缩内存刷新。现在存储持久内存 │
│ (使用 memory/YYYY-MM-DD.md)。 │
│ 如果没有内容存储,请用 NO_REPLY 回复。” │
│ │
│ 代理:审查对话以获取重要信息 │
│ 将关键决策/事实写入内存文件 │
│ -> NO_REPLY (用户看不到任何内容) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 压缩安全进行 │
│ │
│ 重要信息现在存储在磁盘上 │
│ 压缩可以在不丢失知识的情况下进行 │
└─────────────────────────────────────────────────────────────┘
内存刷新可以在 clawdbot.yaml 文件或 clawdbot.json 文件中配置。
{
agents: {
defaults: {
compaction: {
reserveTokensFloor: 20000,
memoryFlush: {
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "会话即将压缩。现在存储持久内存。",
prompt: "将持久笔记写入 memory/YYYY-MM-DD.md;如果没有内容存储,请回复 NO_REPLY。"
}
}
}
}
}
工具结果可能非常庞大。单个 exec 命令可能输出 50,000 个字符的日志。修剪会修剪这些旧输出而不重写历史记录。这是一个有损的过程,旧输出无法恢复。
┌─────────────────────────────────────────────────────────────┐
│ 修剪前 (内存中) │
│ │
│ 工具结果 (exec):[50,000 个字符的 npm 安装输出] │
│ 工具结果 (read):[大型配置文件,10,000 个字符] │
│ 工具结果 (exec):[构建日志,30,000 个字符] │
│ 用户:”构建是否成功?“ │
└─────────────────────────────────────────────────────────────┘
│
▼ (软修剪 + 硬清除)
┌─────────────────────────────────────────────────────────────┐
│ 修剪后 (发送到模型) │
│ │
│ 工具结果 (exec):”npm WARN deprecated...[修剪] │
│ ...成功安装。” │
│ 工具结果 (read):”[旧工具结果内容已清除]“ │
│ 工具结果 (exec):[保留 - 太新了,无法修剪] │
│ 用户:”构建是否成功?“ │
└─────────────────────────────────────────────────────────────┘
JSONL 文件在磁盘上:未更改(完整输出仍然存在)
Anthropic 缓存提示前缀长达 5 分钟,以减少重复调用时的延迟和成本。当在 TTL 窗口内发送相同的提示前缀时,缓存标记的成本约低 90%。当 TTL 过期后,下一个请求必须重新缓存整个对话历史,费用全额计费。
问题:如果会话在 TTL 之后变为空闲,下一个请求将失去缓存,并且必须以全额“缓存写入”价格重新缓存整个对话历史。
Cache-TTL 修剪通过检测缓存何时过期并在下一个请求之前修剪旧工具结果来解决这个问题。较小的提示意味着较低的成本:
{
agent: {
contextPruning: {
mode: "cache-ttl", // 仅在缓存过期后修剪
ttl: "600", // 匹配您的 cacheControlTtl
keepLastAssistants: 3, // 保护最近的工具结果
softTrim: {
maxChars: 4000,
headChars: 1500,
tailChars: 1500
},
hardClear: {
enabled: true,
placeholder: "[旧工具结果内容已清除]"
}
}
}
}
会话并非永恒。它们根据可配置的规则重置,创建内存的自然边界。默认行为是每天重置。但是,还有其他模式可用。
| 模式 | 行为 |
|---|---|
daily | 在固定时间(默认:4 AM 本地时间)重置 |
idle | 在 N 分钟不活动后重置 |
daily+idle | 以先到者为准 |
当您运行 /new 以启动新会话时,会话内存钩子可以自动保存上下文:
/new
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 会话-内存钩子触发 │
│ │
│ 1. 从结束会话中提取最后 15 条消息 │
│ 2. 通过 LLM 生成描述性 slug │
│ 3. 保存到 ~/clawd/memory/2026-01-26-api-design.md │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 新会话开始 │
│ │
│ 之前的上下文现在可以通过 memory_search 进行搜索 │
└─────────────────────────────────────────────────────────────┘
Clawdbot 的内存系统之所以成功,是因为它遵循了几个关键原则:
内存是普通的 Markdown。您可以读取它、编辑它、版本控制它。没有不透明的数据库或专有格式。
代理搜索相关内容,而不是将所有内容塞入上下文中。这保持了上下文的关注点和降低了成本。
重要信息存储在磁盘上,而不仅仅是在对话历史中。压缩无法销毁已经保存的内容。
向量搜索单独使用会错过确切的匹配。关键字搜索单独使用会错过语义。混合搜索提供了两者的优势。