20.2 向量记忆搜索
生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~320k tokens,输出 ~8k tokens(本节)
上一节我们了解了记忆系统的文件布局和分块机制——Markdown 文件被拆分成若干文本块(Chunk),等待被索引。但"索引"具体意味着什么?如何让 Agent 能够根据用户的自然语言问题找到相关记忆?
答案是向量搜索(Vector Search):将文本块转换为高维向量(即嵌入向量),然后通过余弦相似度找到语义上最接近的结果。本节深入分析 OpenClaw 的嵌入向量引擎、多提供者架构、SQLite 存储方案和 sqlite-vec 向量加速。
20.2.1 嵌入向量(Embeddings)引擎
什么是嵌入向量?
衍生解释:嵌入向量(Embedding)是一种将文本映射到高维浮点数数组的技术。例如,句子 "我喜欢 TypeScript" 可能被映射为一个 1536 维的向量
[0.023, -0.017, 0.041, ...]。语义相近的文本在向量空间中距离更近——"我喜欢 TypeScript" 和 "TypeScript 是我偏好的语言" 的向量夹角很小,而与 "今天天气不错" 的向量夹角很大。通过计算向量间的余弦相似度(Cosine Similarity),可以量化两段文本的语义相关程度。
OpenClaw 的嵌入引擎定义在 src/memory/embeddings.ts 中,核心接口是 EmbeddingProvider:
// src/memory/embeddings.ts
export type EmbeddingProvider = {
id: string; // 提供者标识:"openai" | "gemini" | "voyage" | "local"
model: string; // 模型名称,如 "text-embedding-3-small"
embedQuery: (text: string) => Promise<number[]>; // 将单条文本转换为向量
embedBatch: (texts: string[]) => Promise<number[][]>; // 批量转换
};这是一个经典的策略模式(Strategy Pattern)——所有嵌入提供者实现相同接口,系统在运行时决定使用哪个。embedQuery 用于搜索时将用户查询转换为向量,embedBatch 用于索引时批量处理文本块。
提供者选择流程
createEmbeddingProvider() 是嵌入引擎的入口。它根据配置决定使用哪个提供者,并实现了自动检测(auto) 和回退(fallback) 两种容错机制:
用流程图表示:
canAutoSelectLocal 的判定逻辑值得注意——它只在本地模型文件确实存在于磁盘时才返回 true,远程 URL 或 Hugging Face Hub 路径(hf: 前缀)会被排除:
这样避免了 auto 模式下因为下载大模型而阻塞启动。
向量归一化
所有本地生成的嵌入向量在返回前都会经过**归一化(Normalization)**处理:
衍生解释:L2 归一化将向量缩放到单位长度(模为 1)。归一化后,余弦相似度等价于向量点积,计算更高效。此外,归一化确保不同文本块的向量在同一"尺度"上比较,避免因为向量模长差异导致的偏差。
20.2.2 多嵌入提供者支持
OpenClaw 支持四种嵌入提供者,覆盖从云端到本地的完整光谱。
提供者对比
OpenAI
text-embedding-3-small
1536
云端
OPENAI_API_KEY
最广泛使用,质量稳定
Gemini
gemini-embedding-001
768
云端
GOOGLE_API_KEY
支持 taskType 区分查询/文档
Voyage
voyage-4-large
1024
云端
VOYAGE_API_KEY
专注检索场景,支持 input_type
Local
embeddinggemma-300M
取决于模型
本地
无需
完全离线,隐私优先
OpenAI 提供者
OpenAI 的实现最为简洁——标准的 REST API 调用:
embedQuery 和 embedBatch 共用同一个 embed 函数——OpenAI 的 /embeddings 端点天然支持多文本输入,单条查询只是 input 数组长度为 1 的特例。
Gemini 提供者
Gemini 的实现有两个显著不同:
独立的查询/文档端点:Gemini 提供
embedContent(单条)和batchEmbedContents(批量)两个 API,且通过taskType字段区分用途——查询用RETRIEVAL_QUERY,文档用RETRIEVAL_DOCUMENT。模型路径格式:Gemini API 要求
models/前缀(如models/gemini-embedding-001),代码中有专门的buildGeminiModelPath处理。
衍生解释:
RETRIEVAL_QUERY与RETRIEVAL_DOCUMENT的区别在于嵌入模型内部对文本的处理方式不同。查询通常较短且表达的是"信息需求",文档较长且包含"信息内容"。这种非对称嵌入(Asymmetric Embedding)能提升检索精度——模型针对查询和文档分别优化向量表示。
Voyage 提供者
Voyage AI 专注于检索场景,其 API 与 OpenAI 类似但通过 input_type 字段区分查询和文档:
本地提供者(node-llama-cpp)
本地提供者是最独特的——它在用户机器上运行一个小型嵌入模型,完全不需要网络。默认使用 Google 的 embeddinggemma-300M(GGUF 量化格式,约 300MB):
几个关键设计决策:
懒加载(Lazy Loading):
node-llama-cpp是可选依赖,通过动态import()延迟加载,避免未安装时导致启动失败。模型文件解析:
resolveModelFile支持本地路径和hf:Hub 链接,首次使用会自动下载到缓存目录。串行 Batch:本地模型的
embedBatch实际上是逐条调用getEmbeddingFor——因为本地推理受限于 CPU/GPU 资源,并行度无法像云端 API 那样扩展。
API Key 解析
所有云端提供者共享统一的 API Key 解析流程。以 OpenAI 为例:
三层优先级:远程传入 > 配置文件 > 环境变量(OPENAI_API_KEY)。如果都找不到,requireApiKey 抛出 "No API key found for provider openai" 异常——在 auto 模式下这个异常会被捕获并跳过该提供者。
20.2.3 SQLite 存储
为什么是 SQLite?
OpenClaw 的记忆索引存储在 SQLite 数据库中。对于一个 CLI 工具来说,SQLite 是理想选择:
零服务:无需启动数据库服务器,单文件即整个数据库
进程内:直接嵌入 Node.js 进程,使用
node:sqlite模块访问(Node.js 22+ 原生支持)事务安全:ACID 保证,即使进程崩溃也不会损坏索引
扩展性:可以加载
sqlite-vec等扩展实现向量搜索
衍生解释:Node.js 从 22 版本开始内置了
node:sqlite模块(实验性),提供同步 API(DatabaseSync)直接操作 SQLite 数据库。这意味着不需要像better-sqlite3这样的第三方 native 模块,减少了安装和兼容性问题。OpenClaw 通过requireNodeSqlite()包装函数来引入这个内置模块。
Schema 设计
ensureMemoryIndexSchema() 函数定义了完整的数据库结构:
四张表各司其职:
meta
key
存储索引元数据(如向量维度 vectorDims)
files
path
追踪已索引的文件及其 hash,用于增量同步
chunks
id
存储文本块的完整信息(文本 + 嵌入向量 + 位置)
embedding_cache
复合键
缓存已计算的嵌入,避免重复调用 API
chunks 表是核心——每个文本块对应一行,embedding 字段以 JSON 字符串形式存储完整的浮点数组。model 字段记录生成嵌入时使用的模型,确保不同模型的向量不会混搭比较。
files 表中的 hash 字段实现了增量索引:同步时比较文件当前 hash 与存储的 hash,只有发生变化的文件才需要重新分块和嵌入。source 字段区分记忆来源("memory" 表示记忆文件,"session" 表示会话记录)。
FTS5 全文搜索表
Schema 创建函数还会尝试建立 FTS5 虚拟表:
衍生解释:FTS5(Full-Text Search 5)是 SQLite 内置的全文搜索引擎。它为文本建立倒排索引(Inverted Index),支持
MATCH查询和 BM25 排名。UNINDEXED关键字表示该列只存储数据但不建立全文索引——这里只需要对text列做全文搜索,其他列用于关联查询。
注意 FTS5 的创建被 try/catch 包裹——某些 SQLite 编译版本可能不包含 FTS5 支持,此时系统会优雅降级为纯向量搜索。
20.2.4 sqlite-vec 向量加速
为什么需要向量加速?
chunks 表已经存储了嵌入向量(作为 JSON 字符串)。最简单的搜索方式是:读取所有 chunks,解析每个向量,逐一计算余弦相似度,然后排序——这就是 listChunks + cosineSimilarity 的暴力搜索路径。但当记忆文件很多时,这个 O(n) 的逐一计算会变慢。
sqlite-vec 是一个 SQLite 扩展,通过向量虚拟表提供原生的向量距离计算,直接在 SQL 查询中执行 vec_distance_cosine(),性能远超逐行 JavaScript 计算。
扩展加载
sqlite-vec 通过 npm 包分发,getLoadablePath() 返回编译好的 .dylib/.so/.dll 文件路径。加载失败只记录错误,不会阻止系统运行——系统会回退到暴力搜索。
向量表管理
加载扩展后,MemoryIndexManager 会创建一个 vec0 虚拟表:
vec0 是 sqlite-vec 提供的虚拟表类型。FLOAT[${dimensions}] 声明了固定维度的浮点向量列。如果用户切换了嵌入模型(维度改变),向量表会被完全重建——因为不同维度的向量无法比较。
向量搜索实现
有了 chunks_vec 表,搜索就可以利用 SQL 来完成:
两条路径的性能差异:
向量计算
C 实现,在 SQLite 引擎内
JavaScript cosineSimilarity()
数据传输
只传输 Top-K 结果
所有 chunks 的嵌入向量都要从 DB 读入 JS
排序
SQL ORDER BY
JavaScript .toSorted()
内存占用
低(流式处理)
高(所有向量加载到内存)
注意 vectorToBlob 函数——sqlite-vec 要求向量以 Float32Array 的二进制 Buffer 形式传入,而非 JSON 数组。这避免了序列化/反序列化开销。
双路径降级策略
整个向量搜索的降级策略形成了一个清晰的层次:
这种设计确保了功能的一致性——无论 sqlite-vec 是否可用,用户都能搜索记忆,只是速度不同。对于记忆文件不多的个人用户(几十到几百个 chunks),暴力搜索的延迟通常也可以接受。
本节小结
嵌入向量引擎基于策略模式,
EmbeddingProvider接口统一了embedQuery和embedBatch两个操作,所有提供者实现相同契约。四种提供者覆盖不同场景:OpenAI(通用)、Gemini(非对称嵌入)、Voyage(检索优化)、Local(离线隐私)。
auto模式按 local → openai → gemini → voyage 顺序自动选择。SQLite 存储采用四张表:
meta(元数据)、files(文件追踪)、chunks(文本块 + 嵌入)、embedding_cache(嵌入缓存),通过hash字段实现增量索引。FTS5 虚拟表为全文搜索提供 BM25 排名能力,不可用时优雅降级。
sqlite-vec 扩展提供原生向量距离计算,通过
vec0虚拟表和vec_distance_cosine()函数实现 SQL 内向量搜索。不可用时自动回退到 JavaScript 暴力搜索。双路径降级贯穿整个设计——从嵌入提供者的 auto/fallback,到向量搜索的 sqlite-vec/暴力计算,每个环节都有降级方案,确保功能永远可用。
Last updated