全网爆火的小龙虾 Clawdbot,如何实现金丹修士般的强大记忆?深扒 2000 行源码揭秘

230 阅读10分钟

一、痛点引入:为什么 AI 助手总是"失忆"?

最近有个开源项目在开发者圈子里彻底火了——Clawdbot,江湖人称"小龙虾"。

上线不到一个月,GitHub star 数飙升,X 上到处都是"这玩意儿太香了"的安利帖。

它火的原因很多:能连 WhatsApp、Telegram、Discord,能在树莓派上 24 小时运行,完全开源可自托管……但最让我惊艳的,是它的记忆能力

你跟它聊过的事情,它真的能记住。不是那种"假装记住"的敷衍,而是像真人一样,能在合适的时候主动想起来。

这让我想起了 AI 助手的老大难问题

你有没有遇到过这种情况——上周刚跟 AI 聊完项目架构,这周再问它,它一脸懵逼地说"我不知道你之前说过什么"。

或者更扎心的:你花了半小时教 AI 你的代码风格偏好,结果下次对话,它又开始给你写那种你最讨厌的 var 声明。

说白了,大多数 AI 助手的记忆都有两个致命问题:

  1. 记忆是云端的,不是你的 —— 数据存在别人的服务器上,你根本不知道它记了什么、怎么用的
  2. 记忆是黑盒的,不可控 —— 你没法主动管理这些记忆,想删除某条?想导出备份?想迁移到其他工具?对不起,做不到

二、方案对比:Clawdbot vs 主流 AI 记忆方案

在深入源码之前,先看看市面上的 AI 记忆方案:

特性云端 AI 助手本地 RAG 方案Clawdbot
数据存储位置云端服务器本地向量库本地 SQLite
数据所有权平台所有用户所有用户完全控制
记忆格式黑盒通常是 JSONMarkdown 明文
可导出/迁移有限需要导出工具复制文件夹即可
搜索方式未知通常纯向量向量+关键词混合
部署复杂度无需部署需要向量数据库零依赖
隐私性数据上云本地但复杂简单且本地

划重点:Clawdbot 的记忆不是存在某个云端数据库里,也不需要你搭建复杂的向量数据库,而是直接存成 Markdown 文件,放在你自己的电脑上。

这意味着:

  • 你可以用任何编辑器打开、修改这些记忆
  • 你可以用 Git 管理版本历史
  • 你可以随时备份、迁移
  • 你的隐私数据永远不会上传到任何服务器

这不就是我们一直想要的吗?


三、核心技术实现:2000 行代码的精华

好,进入正题。Clawdbot 的记忆模块主要在 src/memory/ 目录下,核心文件包括:

src/memory/
├── manager.ts          # 主记忆管理器(核心,2000+ 行)
├── internal.ts         # 内部工具函数
├── hybrid.ts           # 混合搜索算法
├── embeddings.ts       # Embedding 提供者
├── memory-schema.ts    # SQLite 数据库 Schema
└── ...

3.1 架构总览

先上一张架构图,让你有个全局认识:

┌─────────────────────────────────────────────────────────────┐
│                      Clawdbot Agent                         │
│  ┌─────────────────┐  ┌─────────────────┐                   │
│  │  memory_search  │  │   memory_get    │   Agent Tools     │
│  └────────┬────────┘  └────────┬────────┘                   │
└───────────┼─────────────────────┼───────────────────────────┘
            │                     │
            ▼                     ▼
┌─────────────────────────────────────────────────────────────┐
│                  MemoryIndexManager                         │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                   Hybrid Search                       │   │
│  │  ┌─────────────────┐    ┌─────────────────┐          │   │
│  │  │  Vector Search+  │  Keyword Search │          │   │
│  │  │  (sqlite-vec)   │    │    (FTS5/BM25)  │          │   │
│  │  └─────────────────┘    └─────────────────┘          │   │
│  └──────────────────────────────────────────────────────┘   │
│                            │                                 │
│  ┌─────────────────────────┼─────────────────────────────┐  │
│  │              Embedding Provider                        │  │
│  │   ┌─────────┐  ┌─────────┐  ┌─────────────────────┐   │  │
│  │   │ OpenAI  │  │ Gemini  │  │ Local (llama.cpp)   │   │  │
│  │   └─────────┘  └─────────┘  └─────────────────────┘   │  │
│  └────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
            │                     │
            ▼                     ▼
┌─────────────────────────────────────────────────────────────┐
│                     SQLite Database                          │
│  ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐  │
│  │  files   │ │  chunks  │ │ chunks_vec │ │ chunks_fts   │  │
│  └──────────┘ └──────────┘ └────────────┘ └──────────────┘  │
└─────────────────────────────────────────────────────────────┘
            ▲                     ▲
            │                     │
┌───────────┴─────────────────────┴───────────────────────────┐
│                     Memory Sources                           │
│  ┌─────────────────┐    ┌─────────────────────────────────┐ │
│  │   MEMORY.md     │    │         memory/*.md             │ │
│  │   memory.md     │    │      sessions/*.jsonl           │ │
│  └─────────────────┘    └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

核心设计思路

  1. 记忆以 Markdown 文件形式存储(人类可读)
  2. 文件被分块(chunk)后生成 Embedding 向量
  3. 向量存入 SQLite + sqlite-vec 支持向量搜索
  4. 同时维护 FTS5 全文索引支持关键词搜索
  5. 搜索时混合两种结果,取长补短

3.2 混合搜索:向量 + BM25 的完美结合

这是 Clawdbot 记忆模块最精妙的设计。来看 hybrid.ts 的核心代码:

// src/memory/hybrid.ts
export function mergeHybridResults(params: {
  vector: HybridVectorResult[];
  keyword: HybridKeywordResult[];
  vectorWeight: number;
  textWeight: number;
}): Array<{ pathstringscorenumbersnippetstring; ... }> {

  const byId = new Map<string, { vectorScorenumbertextScorenumber; ... }>();

  // 1. 先收集向量搜索结果
  for (const r of params.vector) {
    byId.set(r.id, {
      ...r,
      vectorScore: r.vectorScore,
      textScore0,  // 关键词得分先设为 0
    });
  }

  // 2. 再合并关键词搜索结果
  for (const r of params.keyword) {
    const existing = byId.get(r.id);
    if (existing) {
      existing.textScore = r.textScore;  // 补充关键词得分
    } else {
      byId.set(r.id, { ...r, vectorScore0textScore: r.textScore });
    }
  }

  // 3. 计算混合得分并排序
  const merged = Array.from(byId.values()).map((entry) => {
    const score = params.vectorWeight * entry.vectorScore
                + params.textWeight * entry.textScore;
    return { ...entry, score };
  });

  return merged.sort((a, b) => b.score - a.score);
}

为什么要混合搜索?

单纯的向量搜索有个问题:它擅长语义相似,但对精确匹配不敏感。

比如你搜"张三的手机号",向量搜索可能返回"李四的联系方式"(语义相似),但你真正想要的是包含"张三"这个精确关键词的内容。

BM25 关键词搜索正好弥补这个缺陷。两者结合:

最终得分 = vectorWeight × 向量相似度 + textWeight × BM25得分

这个坑我踩过:之前做 RAG 项目只用向量搜索,用户搜人名、搜专有名词时经常找不到。后来加上关键词搜索,召回率直接提升 30%。

3.3 Embedding 提供者:三种方案自动降级

Clawdbot 支持三种 Embedding 提供者,而且有优雅的降级机制:

// src/memory/embeddings.ts
export async function createEmbeddingProvider(
  options: EmbeddingProviderOptions,
): Promise<EmbeddingProviderResult> {

  // 自动模式:依次尝试 local -> openai -> gemini
  if (requestedProvider === "auto") {
    // 1. 优先尝试本地模型(如果配置了)
    if (canAutoSelectLocal(options)) {
      try {
        const local = await createProvider("local");
        return { ...local, requestedProvider };
      } catch (err) {
        localError = formatLocalSetupError(err);
      }
    }

    // 2. 依次尝试远程 API
    for (const provider of ["openai""gemini"as const) {
      try {
        const result = await createProvider(provider);
        return { ...result, requestedProvider };
      } catch (err) {
        if (isMissingApiKeyError(err)) {
          missingKeyErrors.push(message);
          continue;  // API Key 缺失,尝试下一个
        }
        throw new Error(message);
      }
    }
  }

  // 指定提供者失败时,尝试 fallback
  try {
    const primary = await createProvider(requestedProvider);
    return { ...primary, requestedProvider };
  } catch (primaryErr) {
    if (fallback && fallback !== "none") {
      const fallbackResult = await createProvider(fallback);
      return {
        ...fallbackResult,
        fallbackFrom: requestedProvider,
        fallbackReason: reason,  // 记录降级原因
      };
    }
    throw new Error(reason);
  }
}

三种提供者对比

提供者模型优点缺点
Localembeddinggemma-300M完全离线、隐私优先、免费需要安装 node-llama-cpp
OpenAItext-embedding-3-small质量高、稳定需要 API Key、有成本
Geminitext-embedding-004Google 生态、免费额度高需要 API Key

敲黑板:本地模型用的是 embeddinggemma-300M,只有 300M 参数,但效果出奇的好。如果你对隐私敏感,强烈推荐这个方案。

3.4 存储层设计:SQLite 一把梭

Clawdbot 没有用 Pinecone、Milvus 这些专业向量数据库,而是用 SQLite + sqlite-vec 扩展。

为什么这么选?

  1. 零依赖:不需要额外部署数据库服务
  2. 便携性:一个 .db 文件搞定,随时备份迁移
  3. 性能够用:个人助手场景,几千条记忆绰绰有余

来看数据库 Schema 设计:

-- 文件索引表:追踪哪些文件被索引了
CREATE TABLE files (
  path TEXT PRIMARY KEY,
  source TEXT NOT NULL,  -- 'memory' | 'sessions'
  hash TEXT NOT NULL,    -- 文件内容 hash,用于增量更新
  mtime INTEGER NOT NULL,
  size INTEGER NOT NULL
);

-- 文本块表:存储分块后的文本和向量
CREATE TABLE chunks (
  id TEXT PRIMARY KEY,
  path TEXT NOT NULL,
  source TEXT NOT NULL,
  start_line INTEGER NOT NULL,  -- 原文件中的起始行
  end_line INTEGER NOT NULL,    -- 原文件中的结束行
  hash TEXT NOT NULL,
  model TEXT NOT NULL,          -- 使用的 embedding 模型
  text TEXT NOT NULL,           -- 原文本
  embedding TEXT NOT NULL,      -- 向量(JSON 格式)
  updated_at INTEGER NOT NULL
);

-- 向量搜索虚拟表(sqlite-vec)
CREATE VIRTUAL TABLE chunks_vec USING vec0(
  id TEXT PRIMARY KEY,
  embedding FLOAT[768]  -- 向量维度
);

-- 全文搜索虚拟表(FTS5)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
  text, id, path, source, model, start_line, end_line
);

设计亮点

  • start_line / end_line 记录了每个 chunk 在原文件中的位置,搜索结果可以精确定位
  • hash 字段用于判断文件是否变更,实现增量更新
  • model 字段记录使用的 embedding 模型,切换模型时自动重建索引

3.5 自动同步机制:改了文件自动更新索引

这是让 Clawdbot 记忆"活"起来的关键。来看 manager.ts 中的同步逻辑:

// src/memory/manager.ts
private ensureWatcher() {
  if (!this.sources.has("memory") || !this.settings.sync.watch) return;

  // 监听 MEMORY.md 和 memory/ 目录
  const watchPaths = [
    path.join(this.workspaceDir, "MEMORY.md"),
    path.join(this.workspaceDir, "memory"),
  ];

  this.watcher = chokidar.watch(watchPaths, {
    ignoreInitial: true,
    awaitWriteFinish: {
      stabilityThreshold: this.settings.sync.watchDebounceMs,
      pollInterval: 100,
    },
  });

  const markDirty = () => {
    this.dirty = true;
    this.scheduleWatchSync();  // 标记脏数据,安排同步
  };

  this.watcher.on("add", markDirty);    // 新增文件
  this.watcher.on("change", markDirty); // 文件修改
  this.watcher.on("unlink", markDirty); // 文件删除
}

同步触发时机

触发条件说明
文件变更chokidar 监听,debounce 后触发
搜索前如果有脏数据,先同步再搜索
会话开始新会话开始时预热索引
定时任务可配置间隔定时同步

增量更新的秘密

// 通过 hash 判断文件是否需要重新索引
const record = this.db
  .prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
  .get(entry.path, "memory");

if (!needsFullReindex && record?.hash === entry.hash) {
  return;  // hash 相同,跳过
}

// hash 不同,重新索引这个文件
await this.indexFile(entry, { source: "memory" });

这个设计太聪明了:只有文件内容真正变化时才重新计算 embedding,大大节省了 API 调用成本。

3.6 会话记忆钩子:自动保存对话历史

Clawdbot 还有个骚操作:当你执行 /new 命令开始新会话时,它会自动把上一个会话的内容保存到记忆文件。

// src/hooks/bundled/session-memory/handler.ts
const saveSessionToMemoryHookHandler = async (event) => {
  // 只在 /new 命令时触发
  if (event.type !== "command" || event.action !== "new"return;

  // 1. 读取前一个会话的最后 15 行
  const sessionContent = await getRecentSessionContent(sessionFile);

  // 2. 用 LLM 生成描述性文件名
  const slug = await generateSlugViaLLM({ sessionContent, cfg });

  // 3. 创建记忆文件:memory/2026-01-27-fix-auth-bug.md
  const filename = `${dateStr}-${slug}.md`;
  const memoryFilePath = path.join(memoryDir, filename);

  // 4. 写入会话元数据和对话摘要
  const entry = [
    `# Session: ${dateStr} ${timeStr} UTC`,
    `- **Session Key**: ${event.sessionKey}`,
    `- **Session ID**: ${sessionId}`,
    `## Conversation Summary`,
    sessionContent,
  ].join("\n");

  await fs.writeFile(memoryFilePath, entry, "utf-8");
};

效果:每次开新会话,之前的对话自动变成可搜索的记忆。你再也不用担心"上次聊的那个方案是什么来着"了。


四、值得借鉴的 3 个设计亮点

读完这 2000 行源码,有几个设计让我印象深刻,值得在自己的项目里借鉴。

亮点 1:混合搜索解决召回率问题

问题场景:用户搜"张三的联系方式",纯向量搜索可能返回"王五的电话号码"——因为向量搜索理解的是"查找某人的联系方式"这个语义意图,而不是精确匹配"张三"这个关键词。

Clawdbot 的解法:向量搜索 + BM25 关键词搜索混合。

const score = vectorWeight * entry.vectorScore + textWeight * entry.textScore;

向量搜索负责语义理解("找联系方式相关的内容"),BM25 负责精确匹配("必须包含张三这个词")。两者加权合并,召回率能提升约 30%。

可借鉴点:如果你在做 RAG 项目,别只用向量搜索,加上关键词搜索效果会好很多。

亮点 2:基于 Hash 的 Embedding 缓存

问题场景:每次同步都重新计算所有文件的 embedding,API 成本爆炸。

Clawdbot 的解法:用内容 hash 作为缓存 key,只计算变化的部分。

// 先查缓存
const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash));

// 只计算缓存里没有的
const missing = chunks.filter((chunk) => !cached.has(chunk.hash));
const newEmbeddings = await this.embedBatchWithRetry(missing.map(c => c.text));

// 写入缓存
this.upsertEmbeddingCache(newEmbeddings);

可借鉴点:任何涉及 Embedding API 调用的项目都应该加缓存。按 Clawdbot 的设计,API 成本能节省 80% 以上。

亮点 3:优雅降级保证可用性

问题场景:sqlite-vec 是 C 扩展,不同平台需要不同的编译版本,Windows 上尤其容易出问题。

Clawdbot 的解法:向量搜索不可用时,自动降级到纯关键词搜索,而不是直接报错。

private async loadVectorExtension(): Promise<boolean> {
  try {
    const loaded = await loadSqliteVecExtension({ dbthis.db, extensionPath });
    if (!loaded.okthrow new Error(loaded.error);
    this.vector.available = true;
    return true;
  } catch (err) {
    // 向量搜索不可用,但不影响关键词搜索
    this.vector.available = false;
    this.vector.loadError = message;
    log.warn(`sqlite-vec unavailable: ${message}`);
    return false;  // 返回 false 而不是抛异常
  }
}

可借鉴点:依赖外部组件时,永远要有 Plan B。Clawdbot 的降级机制保证了即使向量搜索挂了,用户依然能用关键词搜索找到记忆。


五、总结

  1. 本地优先:记忆存成 Markdown 文件,数据完全在你手里
  2. 混合搜索:向量 + BM25,兼顾语义理解和精确匹配
  3. 多提供者:OpenAI/Gemini/本地模型,自动降级
  4. 增量同步:基于 hash 的缓存,省钱又高效
  5. 会话持久化:自动保存对话历史到记忆文件

关于我们

我们是一家面向 AEC 行业的 AI 创业公司,专注企业级AI应用系统的设计与落地。如果你在做类似的AI应用工程化,或者对垂直行业AI落地感兴趣,欢迎评论区讨论或添加联系方式沟通。

微信:Damondut
GitHub: github.com/zhuzhaoyun

本文使用 markdown.com.cn 排版