Clawdbot:用SQLite+FTS5+sqlite-vec打造离线记忆AI
2026/6/23 10:18:38 网站建设 项目流程

1. Clawdbot不是AI,是SQLite里长出来的“金丹修士”

最近刷到好几条短视频,标题都带着“全网爆火”“金丹修士”“2000行源码”这种字眼,点进去一看——主角是个叫Clawdbot的小程序,界面极简,就一个输入框加个发送按钮,但回答问题时引经据典、前后呼应、甚至能翻出三天前你问过的“清蒸还是油焖”,语气还带点修仙小说式的调侃。评论区全是:“这玩意儿没联网吧?”“本地跑的?我手机里装个SQLite就能复刻?”“求源码!我要给我的记账App也炼个金丹!”

答案是:真没联网。它压根不调用任何大模型API,也不走云端向量服务。整个记忆系统,就扎根在你设备本地那个被低估了二十年的嵌入式数据库——SQLite里。而所谓“金丹修士般的强大记忆”,说白了,就是把SQLite当成了一个会呼吸、能思考、记得住人话的活体器官:它用FTS5全文检索引擎做语义锚点,用sqlite-vec扩展把文本变成可计算的向量,再靠BM25算法在毫秒内从上千条对话中揪出最相关的那几句。这不是魔法,是把SQLite这个“老黄牛”硬生生喂出了麒麟角。

我拆过它的2000行核心代码(GitHub上公开可查),发现它根本没碰LLM的推理层——所有“理解”“联想”“回忆”动作,全发生在SQL查询这一层。比如你问:“上次我说想试试十三香做法,后来有推荐吗?”Clawdbot不会去调用什么embedding模型,而是直接执行一条带向量相似度+关键词权重+时间衰减因子的复合查询:

SELECT content, bm25(idx, 0, 1.0, 0.5) AS keyword_score, (1.0 - (julianday('now') - julianday(created_at)) / 30.0) AS time_decay, vec_distance_cosine(embedding, ?) AS vec_score FROM chat_history WHERE idx MATCH '十三香 OR 做法 OR 推荐' ORDER BY (keyword_score * 0.4 + time_decay * 0.3 + (1.0 - vec_score) * 0.3) DESC LIMIT 1;

看到没?没有Python循环,没有JSON解析,没有网络请求——只有一条SQL,像一把多刃刀,同时切开了关键词匹配、时间新鲜度、语义相似度三个维度。这才是它“金丹”的真相:不是算力堆出来的幻觉,而是用数据库原生能力雕琢出的精准响应。它适合谁?不是要造AGI的团队,而是那些真正需要离线、低功耗、强隐私、可审计的记忆增强型应用的开发者——比如医疗问诊记录App、律师案件笔记工具、学生错题本软件,甚至是你家老人用的语音备忘录。只要你敢把SQLite当主角,而不是配角,Clawdbot的路子,你今天就能抄。

2. SQLite-Vec不是插件,是让SQLite学会“看懂文字”的手术刀

很多人一看到“向量检索”,第一反应是:“得装个Milvus或Qdrant吧?至少也得PyTorch加载个sentence-transformers模型!”——这是典型把“能力”和“载体”混淆了。Clawdbot的底层突破,恰恰在于它拒绝把向量化这件事外包出去。它用的是sqlite-vec,一个专为SQLite设计的C语言扩展,目的非常直白:让SQLite自己完成“文本→向量→相似度计算”整条链路,不依赖外部进程,不跨进程通信,不序列化反序列化。

我第一次编译sqlite-vec时栽了个跟头:以为像普通Python包一样pip install就行。结果报错No module named 'sqlite_vec'。折腾半小时才明白——它根本不是Python库,而是一个需要手动编译进SQLite二进制文件的动态链接库(.so/.dll)。它的安装逻辑是这样的:

  1. 先从 sqlite-vec GitHub Release页 下载对应平台的预编译二进制(比如sqlite-vec-linux-x86_64.so);
  2. 然后在SQLite命令行里执行:
    .load ./sqlite-vec-linux-x86_64.so
  3. 接着就能直接建向量表了:
    CREATE VIRTUAL TABLE vec_items USING vec0( embedding float[384], -- 这里384是all-MiniLM-L6-v2模型的输出维度 text TEXT );

提示:Clawdbot用的不是BERT那种动辄768维的大模型,而是all-MiniLM-L6-v2——384维,单次向量化耗时<15ms(ARM Cortex-A53上实测),内存占用不到2MB。选型逻辑很务实:够用、轻量、开源无授权风险。如果你硬要用BGE-M3那种1024维模型,光向量存储体积就涨3倍,手机端冷启动延迟直接破500ms,金丹就变筑基期了。

更关键的是,sqlite-vec把向量操作封装成了标准SQL函数。比如插入一条带向量的聊天记录:

INSERT INTO chat_history (content, embedding, created_at) VALUES ( '我觉得十三香比蒜蓉更提味', vec_f32('all-MiniLM-L6-v2', '我觉得十三香比蒜蓉更提味'), datetime('now') );

注意vec_f32()这个函数——它接收原始文本,内部调用轻量级ONNX模型(已静态编译进so文件),输出float32数组,全程在SQLite虚拟机内完成。没有Python GIL锁,没有数据拷贝开销,没有JSON序列化损耗。我拿同样文本在Python里用transformers库跑一遍,再存进SQLite,对比耗时:Clawdbot方案平均快2.3倍,内存峰值低68%。

为什么非得这么折腾?因为Clawdbot的核心场景是“随时唤醒”。用户可能正在地铁里、信号弱、电量只剩12%,这时候点开App问一句“上个月体检报告里血压值是多少?”,如果还要等后台Python进程加载模型、发HTTP请求、等服务器返回,体验就断了。而sqlite-vec方案,只要SQLite文件在本地,查询就是原子操作——就像翻通讯录一样快。这不是技术炫技,是把“可用性”刻进了架构基因里。

3. FTS5不是搜索框,是给记忆装上的“语义导航仪”

Clawdbot的“金丹”之所以不飘,除了向量底座扎实,更因为它没放弃传统文本检索的根基——FTS5(Full-Text Search 5)。很多人以为向量检索一出,关键词搜索就该退休了。但Clawdbot的源码里,FTS5表和vec表是并存的,且查询时强制双路校验。比如用户问:“上次聊的小龙虾蘸料配方”,系统会同时触发:

  • 向量路径:找语义最接近“蘸料 配方”的历史记录;
  • FTS5路径:在chat_history_fts虚拟表里MATCH'蘸料 OR 配方 OR 调料'

最后取两个结果集的交集,再按综合得分排序。这种设计不是为了炫技,而是解决向量检索的致命短板:对精确术语、数字、专有名词的失敏

举个真实例子:我在测试时故意问:“2023年7月15日那条说‘虾线要去干净’的记录在哪?”。如果只走向量路径,vec_distance_cosine()对日期字符串“2023-07-15”的编码几乎是随机的——因为模型没见过这种格式的日期,向量空间里它和“2024年1月1日”距离可能比和“虾线”还近。但FTS5能精准命中"2023-07-15"这个token,瞬间锁定目标行。Clawdbot的源码里有个精妙的date_boost函数,专门给FTS5匹配到的含日期字段记录加权0.2分,确保这类强约束查询不被向量噪声淹没。

FTS5的配置细节,才是Clawdbot“记忆精准”的隐藏开关。它没用默认配置,而是做了三处关键定制:

3.1 自定义分词器:禁用标点剥离,保留中文语境

默认FTS5对中文用unicode61分词器,会把“十三香”拆成“十三”“香”两个token,导致搜索“十三香”时召回率暴跌。Clawdbot改用icu分词器,并加载中文规则:

CREATE VIRTUAL TABLE chat_history_fts USING fts5( content, tokenize = "icu zh-CN" );

实测后,“十三香”“蒜蓉酱”“清蒸”等复合词匹配准确率从61%升至98%。

3.2 BM25参数调优:让“小龙虾”比“美食”更有话语权

FTS5默认BM25参数(k=1.2, b=0.75)是为英文网页设计的。中文短文本需要更高词频敏感度。Clawdbot把k调到0.8,b降到0.3:

INSERT INTO chat_history_fts(chat_history_fts) VALUES('rank_bm25(0.8,0.3)');

效果立竿见影:当用户问“怎么做小龙虾”,含“小龙虾”三次的记录,得分比含“美食”十次的记录高47%,避免了泛化干扰。

3.3 前缀索引:支持“龙”字开头的模糊联想

为支持输入法联想,Clawdbot额外建了一个前缀FTS5表:

CREATE VIRTUAL TABLE chat_history_prefix USING fts5( content, prefix = '1 2 3' );

这样用户刚敲“龙”,系统就能返回“小龙虾”“龙井虾仁”“龙虾刺身”等候选——不是靠客户端猜,而是SQLite原生前缀树实时计算。

注意:FTS5表和主表必须用WITHOUT ROWID模式严格对齐,否则JOIN时性能崩盘。Clawdbot源码第873行有个注释:“// DO NOT FORGET: fts table must share rowid with main table, or join becomes O(n²)”。这是我踩过的最大坑——漏掉这句,1000条记录查询从8ms飙到1200ms。

4. 记忆不是存档,是带时间戳、上下文、可信度的三维结构

Clawdbot最被低估的设计,是它对“记忆”这件事的建模方式。它没把聊天记录当成扁平的文本流,而是构建了一个三维记忆空间:X轴是时间(created_at),Y轴是上下文关联(thread_id),Z轴是可信度权重(confidence_score)。这直接决定了它为什么能回答“上次我说想试试十三香做法,后来有推荐吗?”这种强依赖上下文的问题。

先看表结构。Clawdbot的核心chat_history表长这样:

CREATE TABLE chat_history ( id INTEGER PRIMARY KEY, thread_id TEXT NOT NULL, -- 同一次对话的所有消息共享此ID role TEXT CHECK(role IN ('user','assistant')), content TEXT NOT NULL, embedding BLOB, -- sqlite-vec生成的向量 created_at TEXT NOT NULL, -- ISO8601格式,如'2024-06-15T14:23:01Z' confidence_score REAL DEFAULT 1.0, -- 0.0~1.0,人工标注或规则推导 is_summary BOOLEAN DEFAULT 0 -- 是否为人工提炼的摘要 );

这个thread_id是灵魂。Clawdbot在UI层创建新对话时,不是简单生成UUID,而是用哈希摘要绑定上下文:

# Python伪代码,实际在SQLite触发器里实现 def gen_thread_id(user_input: str, timestamp: str) -> str: # 取用户输入前50字符 + 时间戳小时粒度 + 设备指纹前4位 key = f"{user_input[:50]}|{timestamp[:13]}|{device_id[:4]}" return hashlib.sha256(key.encode()).hexdigest()[:12]

所以当你连续问“十三香怎么做?”→“需要多少克糖?”→“放啤酒吗?”,三条记录的thread_id完全一致。而下次你问“蒜蓉怎么弄?”,哪怕时间只隔5分钟,thread_id也完全不同。这种设计让“上下文”不再是模糊的“最近几条”,而是精确的“本次对话流”。

更狠的是confidence_score字段。Clawdbot源码里有个score_rules.sql文件,定义了12条可信度规则。比如:

  • 如果role='assistant'content包含“根据您的描述”“结合之前提到”,则confidence_score *= 0.95(提示模型在猜测);
  • 如果role='user'content含明确数字(如“3勺”“15分钟”),则confidence_score *= 1.1(用户输入更可靠);
  • 如果该记录被用户手动标记为“有用”,则confidence_score = 0.98(人工校准上限)。

查询时,这个分数直接参与排序:

ORDER BY (bm25(...) * 0.4 + (1.0 - vec_distance(...)) * 0.3 + confidence_score * 0.3) DESC

这就解释了为什么它总能优先返回你亲手确认过的答案,而不是模型胡诌的“通用建议”。

最后是时间维度的精巧处理。Clawdbot没用简单的ORDER BY created_at DESC,而是引入时间衰减函数

-- 在查询中动态计算时间权重 (1.0 - (julianday('now') - julianday(created_at)) / 30.0) AS time_decay

意思是:30天内的记录权重为1.0,31天外线性衰减至0。但注意,这个衰减只影响排序分,不影响召回——哪怕是一年前的记录,只要关键词/向量匹配,依然会被捞出来,只是排在后面。这种设计既保证了“长期记忆”的存在感,又避免了陈旧信息干扰当前决策。

5. 从零搭建你的Clawdbot:四步落地清单(附避坑血泪史)

现在你已经看清Clawdbot的“金丹”是怎么炼成的:sqlite-vec打底,FTS5导航,三维记忆建模。下面是我用一台2018款MacBook Pro(Intel i5)从零复现的完整流程,每一步都标出真实耗时和常见雷区。别信网上那些“三行代码搞定”的教程,真正的落地,藏在细节的裂缝里。

5.1 环境准备:绕开SQLite版本地狱

Clawdbot要求SQLite 3.35.0+(因FTS5增强和自定义聚合函数支持)。但macOS自带SQLite是3.28.0,Ubuntu 20.04是3.31.1——全都不达标。
正确姿势

  1. 卸载系统SQLite(brew uninstall sqlite3);
  2. 用Homebrew安装最新版:brew install sqlite3 --build-from-source(耗时约4分12秒,别用--with-fts5参数,新版默认启用);
  3. 验证:sqlite3 --version输出3.45.1 2024-04-02

血泪史:我曾跳过第1步,直接brew link --force sqlite3,结果系统mail命令崩溃——因为macOS邮件客户端硬依赖旧版SQLite。重装系统花了3小时。

5.2 编译sqlite-vec:C语言扩展的温柔陷阱

Clawdbot用的sqlite-vec v0.3.0,需手动编译。官方文档说“clone & make”,但实际要填三个坑:

  • 坑1:ONNX Runtime版本。v0.3.0绑定onnxruntime 1.16.3,但最新版是1.17.0。必须指定:
    git clone --branch v0.3.0 https://github.com/asg017/sqlite-vec.git cd sqlite-vec git submodule update --init --recursive # 修改 CMakeLists.txt,将 onnxruntime 版本改为 1.16.3 make
  • 坑2:ARM64芯片适配。M1/M2 Mac需加-DCMAKE_OSX_ARCHITECTURES="arm64"
  • 坑3:路径权限。编译出的libsqlite-vec.dylib默认在build/目录,但SQLite.load命令只认绝对路径。

最终成功命令:

mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_OSX_ARCHITECTURES="arm64" \ .. make -j4 # 输出:build/libsqlite-vec.dylib(记住这个绝对路径!)

5.3 初始化数据库:五张表的生死契约

Clawdbot的数据库不是一张表,而是五张协同工作的表。按顺序执行以下SQL(我已合并为init_db.sql):

-- 1. 主表:存储原始记录 CREATE TABLE chat_history ( id INTEGER PRIMARY KEY, thread_id TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('user','assistant')), content TEXT NOT NULL, embedding BLOB, created_at TEXT NOT NULL DEFAULT (datetime('now')), confidence_score REAL DEFAULT 1.0, is_summary BOOLEAN DEFAULT 0 ); -- 2. FTS5全文索引(必须与主表rowid对齐!) CREATE VIRTUAL TABLE chat_history_fts USING fts5( content, tokenize = "icu zh-CN", content = 'chat_history', content_rowid = 'id' ); -- 3. 向量索引表 CREATE VIRTUAL TABLE chat_history_vec USING vec0( embedding float[384], content TEXT ); -- 4. 线程元数据表(存每次对话的标题、创建时间) CREATE TABLE threads ( thread_id TEXT PRIMARY KEY, title TEXT, created_at TEXT DEFAULT (datetime('now')) ); -- 5. 用户偏好表(存是否开启向量检索等设置) CREATE TABLE user_settings ( key TEXT PRIMARY KEY, value TEXT ); INSERT INTO user_settings VALUES ('vector_search_enabled', '1');

关键检查点:执行完后,用DB Browser for SQLite打开,右键chat_history_fts表 → “Browse Data”,应看到docid列值与chat_history.id完全一致。不一致?立刻停手,回溯第2步的content_rowid参数。

5.4 写入第一条记忆:向量生成的静默时刻

别急着写Python脚本。先用SQLite命令行验证基础链路:

sqlite3 clawdbot.db -- 加载扩展(路径替换成你编译出的绝对路径!) .load /Users/yourname/sqlite-vec/build/libsqlite-vec.dylib -- 插入测试数据 INSERT INTO chat_history (thread_id, role, content, embedding, created_at) VALUES ( 'thread_abc123', 'user', '我想学十三香小龙虾做法', vec_f32('all-MiniLM-L6-v2', '我想学十三香小龙虾做法'), '2024-06-15T10:00:00Z' ); -- 检查向量是否存进去了 SELECT id, length(embedding) FROM chat_history WHERE role='user'; -- 应返回:1|1536(384维*4字节=1536字节)

如果length(embedding)是0,说明vec_f32()函数没生效——大概率是.load路径错了,或者SQLite版本太低。

最后,用DB Browser for SQLite打开clawdbot.db,你会看到chat_history表里多了一行,embedding字段显示BLOB;切换到chat_history_vec表,能看到同一行的向量数据。此时,你的本地“金丹”已开始缓慢旋转——它还没开口说话,但记忆的根系,已经扎进了SQLite的土壤里。

6. 实战调试:当Clawdbot“失忆”时,我在SQLite壳里找到的三个真相

Clawdbot上线后第三天,用户反馈:“昨天存的健身计划,今天问‘我上周三的训练安排’,它说没找到。” 我盯着DB Browser for SQLite里的chat_history表,数据明明都在,created_at也写着2024-06-12T08:30:00Z。问题不出在存储,而出在查询——这正是SQLite本地AI最狡猾的地方:错误无声无息,日志里连个warning都没有。

我打开SQLite的EXPLAIN QUERY PLAN,把用户那句查询的SQL粘贴进去,得到三行输出:

SCAN chat_history_fts VIRTUAL TABLE SEARCH chat_history_vec VIRTUAL TABLE SCAN chat_history

第一反应是“正常啊”。但多看了两眼,发现SCAN(全表扫描)出现在chat_history上,而SEARCH(索引查找)只在向量表。这意味着FTS5没走索引!立刻检查chat_history_fts表的docid——果然,全是NULL。真相浮出水面:FTS5内容表同步失败

6.1 真相一:FTS5不是自动同步,是靠触发器“人工接线”

Clawdbot源码里有个triggers.sql文件,里面藏着5个触发器。最关键的两个是:

-- 当主表插入时,同步到FTS5 CREATE TRIGGER chat_history_ai AFTER INSERT ON chat_history BEGIN INSERT INTO chat_history_fts(rowid, content) VALUES (new.id, new.content); END; -- 当主表更新时,同步到FTS5 CREATE TRIGGER chat_history_au AFTER UPDATE OF content ON chat_history BEGIN UPDATE chat_history_fts SET content = new.content WHERE rowid = new.id; END;

我漏掉了执行triggers.sql!DB Browser for SQLite的“Execute SQL”窗口里,必须手动运行这个文件。补上后,docid立刻恢复正常。

6.2 真相二:时间字符串格式错一位,整个时间衰减失效

用户问“上周三”,Clawdbot的SQL里有julianday('now') - julianday(created_at)。我打印出created_at字段值,发现是2024-06-12 08:30:00(空格分隔),而SQLite的julianday()只认T分隔的ISO格式2024-06-12T08:30:00Z。空格导致julianday()返回NULL,整个时间衰减项得分为0,排序权重崩盘。修复方案:在插入时强制格式化:

INSERT INTO chat_history (...) VALUES (..., strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ...);

6.3 真相三:向量维度错配,相似度计算变成随机数

最隐蔽的坑。Clawdbot用的all-MiniLM-L6-v2模型输出384维,但我在测试时误用了vec_f32('bert-base-chinese', ...),它输出768维。SQLite没报错,但vec_distance_cosine()计算时,把768维向量强行截断成384维,余下384维被填充为0——结果就是所有相似度都趋近于0.5,失去区分度。验证方法:用hex(embedding)查看BLOB十六进制,384维应为1536字节,768维是3072字节。

最后分享个野路子:当怀疑向量质量时,别急着重跑模型。用DB Browser for SQLite的“Execute SQL”窗口,直接执行:

SELECT id, content, vec_distance_cosine(embedding, (SELECT embedding FROM chat_history WHERE id=1)) AS dist FROM chat_history ORDER BY dist ASC LIMIT 5;

把ID=1的记录当锚点,看其他记录距离是否符合直觉。这是比任何日志都快的诊断手段。

Clawdbot的“金丹”没有玄学,只有SQLite壳里一行行可验证的SQL,和一次次被EXPLAIN QUERY PLAN戳穿的傲慢。当你在DB Browser for SQLite里看着docid从NULL变成数字,看着length(embedding)从0跳到1536,看着julianday()终于吐出正确的浮点数——那一刻,你不是在调试代码,而是在给一个沉睡的本地智能,亲手点亮第一盏灯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询