给 AI Agent 装一个语义记忆:SQLite 向量搜索实战


本地跑一个 AI Agent,时间长了,它会积累大量笔记和文档。搜索这些记忆,最直接的办法是 grep。
问题在于,grep 是关键词匹配。搜"部署配置",找不到"Nginx 反向代理";搜"安全加固",找不到"SSH key 被替换"。你得猜中当时写的那个词才行。
本文介绍一种替代方案:用 SQLite 扩展做向量搜索,让 Agent 理解"部署配置"和"Nginx 反向代理"是同一类东西。整个方案不需要外部服务,不需要 Docker,数据库就是一个本地文件。
一、效果
先看结果。改造前,grep 搜不到任何东西:
$ grep -r "部署配置" notes/
# (沉默)
改造后,语义搜索能找到相关记录:
$ bun search.ts "部署配置"
搜索: "部署配置" (语义)
1. [docs] Nginx 反向代理设置 (0.72)
docs/server-setup.md
2. [docs] Docker Compose 部署 (0.70)
docs/docker.md
3. [log] 迁移服务器到新 VPS (0.68)
logs/2026-02-01.md
搜"部署配置",Nginx、Docker、服务器迁移记录全出来了。
二、方案概览
整个方案只有四样东西。
sqlite-vector,一个 SQLite 扩展(.dylib 文件),给普通表加向量列。Gemini Embedding API,把文本变成 768 维向量,免费额度够用。解析器,把 Markdown 笔记切成小块。搜索脚本,输入自然语言,输出相关记忆。
数据流如下:
写笔记 → 切块 → Embedding → 存入 SQLite
↓
搜索词 → Embedding → cosine 距离 → 返回最相关的 N 条
250 条记录,brute-force 全表扫描,搜一次不到 1 秒。不需要 ANN 索引,不需要定时重建。
三、安装 sqlite-vector
从 GitHub 下载预编译的 macOS arm64 版本。
mkdir -p vendor
curl -L -o /tmp/sv.zip \
"https://github.com/sqliteai/sqlite-vector/releases/download/0.9.80/vector-macos-arm64-0.9.80.zip"
unzip /tmp/sv.zip -d vendor/
mv vendor/vector.dylib vendor/vector0.dylib
整个文件 174KB,比一张截图还小。
注意,macOS 自带的 SQLite 禁用了扩展加载(SQLITE_OMIT_LOAD_EXTENSION),直接调用 db.loadExtension() 会报错。解决办法是让运行时使用 Homebrew 安装的 SQLite。
import { Database } from "bun:sqlite";
Database.setCustomSQLite("/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib");
这行必须在 new Database() 之前调用,全局只需一次。
四、Embedding API
去 Google AI Studio 申请一个免费的 API key。
export GEMINI_API_KEY="AIzaSy..."
调用 gemini-embedding-001 模型,把文本变成 768 维向量。
const res = await fetch(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:batchEmbedContents",
{
method: "POST",
headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
body: JSON.stringify({
requests: texts.map(text => ({
model: "models/gemini-embedding-001",
content: { parts: [{ text }] },
taskType: "RETRIEVAL_DOCUMENT",
outputDimensionality: 768,
})),
}),
}
);
注意两点。第一,这个模型默认输出 3072 维,需要手动指定 outputDimensionality: 768 降维。第二,taskType 分两种:索引文档时用 RETRIEVAL_DOCUMENT,搜索查询时用 RETRIEVAL_QUERY。同一段文本用不同的 taskType 会产生略有不同的向量,这个小区分对搜索质量有肉眼可见的提升。
注意,Google 之前有一个叫 text-embedding-004 的模型,现在已经下线了,调 API 会返回 404。Embedding 模型已经统一到 gemini-embedding-001。
五、内容切块
Markdown 笔记的切块策略取决于内容的组织方式。这里介绍两种常见情况。
带时间戳的日志,比如你的 Agent 每天写一个 logs/2026-02-09.md,里面按时间记录做了什么事。每个时间戳条目(包含它的所有子条目)就是一个 chunk——一个条目就是一件事的完整记录。
- 2026-02-09 09:38:迁移数据库到新服务器
- 备份旧数据 → scp 到新机器
- 修改连接字符串
- 验证查询正常
- 2026-02-09 14:20:修复登录页 CSS 问题
- 原因:flex 布局在 Safari 下的兼容问题
上面这段会切成两个 chunk,分别对应两件独立的事情。
知识文档,比如你维护一份 docs/server-setup.md,按 ## 二级标题组织内容。每个标题通常是一个独立主题——"安装 Nginx"和"配置 SSL 证书"是完全不同的知识点,应该分开索引。
每个 chunk 算一个 SHA256 hash,用于增量索引时判断内容是否变化。
六、向量搜索
sqlite-vector 的搜索 API 跟你想的可能不一样。它没有 vector_distance() 标量函数,不能写 ORDER BY vector_distance(embedding, ?) ASC。
正确的做法是先建量化索引,再通过虚拟表 JOIN 搜索。
-- 1. 初始化向量列
SELECT vector_init('memory_chunks', 'embedding',
'type=FLOAT32,dimension=768,distance=COSINE');
-- 2. 建 1bit 量化表
SELECT vector_quantize('memory_chunks', 'embedding',
'quantization=1bit');
-- 3. 搜索
SELECT m.*, v.distance
FROM memory_chunks m
JOIN vector_quantize_scan('memory_chunks', 'embedding', ?, 5) v
ON m.id = v.rowid;
第二步是必须的,否则第三步会报 unable to retrieve context 错误。这套流程的文档分散在 GitHub README 和 SQLite Cloud 的文档里,需要拼几处才能跑通。
七、增量索引
每次写完笔记,可以自动触发增量索引。比如在写日志的脚本末尾加一行:
GEMINI_API_KEY="..." bun index.ts --file "$TODAY_FILE" 2>/dev/null &
索引时,解析器把文件切成 chunks,算 hash,跟数据库里已有的 hash 对比。只有新增或变更的 chunk 才调 Embedding API,避免重复请求。
全量重建也很快。
GEMINI_API_KEY="..." bun index.ts --rebuild
250 多条记录,大概 30 秒跑完。
八、为什么不用其他方案
为什么不用 Chroma / Pinecone / Qdrant? 这是一个本地 Agent 的记忆系统,250 条记录,不需要外部服务。一个 SQLite 文件搞定,复制到另一台机器也能直接用。
为什么不用 OpenAI Embedding? Gemini 的 Embedding API 免费额度充足,gemini-embedding-001 质量不输 text-embedding-3-small,而且直连 Google,不需要额外网关。
为什么不用 sqlite-vec? sqlite-vector 的 1bit 量化搜索对小数据集足够快,并且它的 API 更贴近 SQLite 原生风格。
九、后续
目前这套系统对 250 条记录够用了。如果以后记忆量超过几千条,可能需要把 brute-force 换成 HNSW 或 IVF 索引,或者在搜索结果上加 rerank(先向量粗筛,再用 LLM 精排)。
但现在 brute-force 不到 1 秒,没必要折腾。
参考链接
(完)