Share

外观
风格

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

2026年2月10日 · 专栏

Create a minimalist two-tone comic cover illustration in simple line-art style. Topic: 给 AI Agent 装一个语义记忆:SQLite 向量搜索实战 Summary: 用 sqlite-vector + Gemini Embedding 给本地 AI Agent 加上语义记忆,不需要外部服务,一个 SQLite 文件搞定。 Key visual symbols: 专栏, AI, SQLite, 向量搜索, Embedding Style constraints: - hand-drawn doodle comic style - clean bold outlines, flat colors only - strictly two-color palette: #111111 and #F5EEDC - wide 16:9 composition, one clear focal object - keep negative space on the right side Do not include any text, letters, logos, watermarks, UI widgets, or photorealistic rendering.

封面图

本地跑一个 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 秒,没必要折腾。

参考链接

(完)