【Agent Memory篇】02:OpenClaw的Embedding 引擎与向量存储
文章目录
-
- 📖 引言:Embedding 在记忆系统中的角色
- 🌐 6 大 Embedding 提供商详解
- 🔄 Auto 模式的提供商自动选择逻辑
- 🔁 Embedding 降级回退(Fallback)机制
- 📦 sqlite-vec 向量扩展的加载与使用
- 🗄️ SQLite 存储 Schema 详解
- 🔪 分块策略(Chunking):Token 计数、重叠、边界处理
- 📋 批量 Embedding 处理(Batch API)
- 💾 Embedding 缓存策略
- 🖼️ 多模态 Embedding 支持(Gemini embedding-2-preview 图片)
- 🔍 向量归一化与验证(sanitizeAndNormalizeEmbedding)
- 📊 Embedding 操作性能优化
- 🎯 小结与下篇预告
- 📚 参考文献
📖 引言:Embedding 在记忆系统中的角色
在 OpenClaw 的记忆系统中,Embedding(嵌入) 是连接自然语言理解与高效检索的桥梁。当用户提问或系统需要查询历史上下文时,Embedding 引擎会将文本转换为高维向量表示,而向量存储则利用这些数值向量实现快速、准确的语义搜索。
与传统的关键词搜索不同,基于 Embedding 的检索捕捉的是语义相似度。例如:“我昨天提到的项目” 和 “我之前启动的那个工作” 在语义上相近,Embedding 能识别这种相似性,而关键词搜索则可能完全失效。
这一篇我们将深入探讨 OpenClaw 的 Embedding 引擎架构、多种提供商的集成、向量存储方案、分块策略和缓存机制。
🌐 6 大 Embedding 提供商详解
OpenClaw 支持六种 Embedding 提供商,既有云端 API 服务,也有本地离线方案:
1️⃣ OpenAI(文本嵌入-3 系列)
默认模型:text-embedding-3-small
特点:
- 业界标准,广泛应用
- 两个模型选择:
text-embedding-3-small(维度 1536)和text-embedding-3-large(维度 3072) - Token 限制:8192 tokens 对两个模型均适用
API 调用方式:
POST https://api.openai.com/v1/embeddings
{
"model": "text-embedding-3-small",
"input": "Your text here"
}
Token 限制处理:
- 单个请求最多 8192 tokens
- 超出限制的文本会在分块阶段被自动拆分
特殊处理:
- 支持批处理 API(Batch API),单次可提交最多 50,000 个请求
- 批处理请求通过文件上传到 OpenAI,然后轮询查询结果
- 完成窗口:24 小时
2️⃣ Google Gemini(多代模型 + 多模态支持)
默认模型:gemini-embedding-001
特点:
- 支持多种 Embedding 模型:
gemini-embedding-001、gemini-embedding-2-preview等 gemini-embedding-2-preview最新,支持多模态(文本+图像)- Token 限制:
text-embedding-004限制为 2048 tokens;gemini-embedding-2-preview为 8192 tokens
API 调用方式:
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent
{
"content": {
"parts": [
{ "text": "Your text here" }
]
},
"taskType": "RETRIEVAL_DOCUMENT"
}
Task Types(任务类型):
Gemini Embedding API 要求指定任务上下文,OpenClaw 采用以下策略:
- 查询(Query):
RETRIEVAL_QUERY- 用于查询文本 - 文档(Document):
RETRIEVAL_DOCUMENT- 用于索引文本 - 其他类型:
SEMANTIC_SIMILARITY、CLASSIFICATION、CLUSTERING等,根据需求配置
特殊处理:
gemini-embedding-2-preview支持outputDimensionality参数,可设置为 768、1536 或 3072,允许灵活控制向量维度- 支持批处理 API,但端点为
:asyncBatchEmbedContent - 支持多模态 Embedding(见后续 “多模态 Embedding” 章节)
3️⃣ Voyage AI(轻量级到重型多档)
默认模型:voyage-4-large
特点:
- 不同大小的模型:
voyage-3、voyage-3-lite、voyage-code-3等 - Token 限制各异:
voyage-3/voyage-code-3:32,000 tokens(业界最高)voyage-3-lite:16,000 tokensvoyage-4-large:一般为 8192 tokens
API 调用方式:
POST https://api.voyageai.com/v1/embeddings
{
"model": "voyage-4-large",
"input": ["Your text here"],
"input_type": "document"
}
特殊处理:
- 支持
input_type参数:"query"或"document" - 批处理 API,完成窗口:12 小时
4️⃣ Mistral(轻量级开源友好)
默认模型:mistral-embed
特点:
- 轻量级模型,适合生产环境
- 无单个请求 Token 限制公开信息,OpenClaw 按保守策略处理
API 调用方式:
POST https://api.mistral.ai/v1/embeddings
{
"model": "mistral-embed",
"input": "Your text here"
}
特殊处理:
- 使用 Bearer Token 认证
5️⃣ Ollama(本地运行,自由度高)
默认模型:nomic-embed-text
特点:
- 完全本地运行,无 API 依赖
- 支持任何 Ollama 兼容的 Embedding 模型
- 无速率限制,隐私性最强
- 需要 Ollama 服务运行在本地或指定地址
API 调用方式:
POST http://localhost:11434/api/embeddings
{
"model": "nomic-embed-text",
"prompt": "Your text here"
}
特殊处理:
- 每个请求处理单个文本段落(不支持批量 API)
- 需要通过配置指定 Ollama 服务地址
- 内存占用及速度取决于本地硬件
6️⃣ Local(本地 Llama C++ 集成)
默认模型:hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf
特点:
- 基于
node-llama-cpp,无需外部服务 - 直接在 Node.js 进程中运行 GGUF 格式模型
- Token 限制:保守采用 2048 tokens
- 最适合小规模、隐私敏感的部署
特殊处理:
- 需要安装可选依赖
node-llama-cpp(在 Node 24+ 上推荐) - 支持 Hugging Face 模型 URL(
hf:前缀)或本地文件路径 - 模型会自动下载到
modelCacheDir目录 - 启动时加载模型,首次加载可能较慢
自动选择逻辑:
当用户配置 provider: "auto" 时,OpenClaw 按以下优先级尝试:
- Local - 仅当模型文件本地存在时
- OpenAI - 如果配置了 API Key
- Gemini - 如果配置了 API Key
- Voyage - 如果配置了 API Key
- Mistral - 如果配置了 API Key
- 若全部失败,降级到 FTS-only 模式(仅全文搜索,无向量)
🔄 Auto 模式的提供商自动选择逻辑
在 createEmbeddingProvider() 中实现:
if (requestedProvider === "auto") {
const missingKeyErrors: string[] = [];
let localError: string | null = null;
// 尝试本地模型(如果文件存在)
if (canAutoSelectLocal(options)) {
try {
const local = await createProvider("local");
return { ...local, requestedProvider };
} catch (err) {
localError = formatLocalSetupError(err);
}
}
// 尝试远程提供商(OpenAI、Gemini、Voyage、Mistral)
for (const provider of REMOTE_EMBEDDING_PROVIDER_IDS) {
try {
const result = await createProvider(provider);
return { ...result, requestedProvider };
} catch (err) {
if (isMissingApiKeyError(err)) {
missingKeyErrors.push(message);
continue; // 继续尝试下一个
}
throw err; // 网络错误等致命错误直接抛出
}
}
// 全部失败,返回 null provider(FTS-only)
return {
provider: null,
requestedProvider,
providerUnavailableReason: reason
};
}
关键特性:
- 不尝试 Ollama:Auto 模式假设用户无本地 Ollama 实例,避免隐式依赖
- 仅捕获认证错误:网络/连接错误仍然会被抛出
- 优雅降级:当所有提供商都无法使用时,系统仍可在 FTS-only 模式下工作
🔁 Embedding 降级回退(Fallback)机制
OpenClaw 支持双层 Fallback 机制:
主 Fallback(Primary Fallback)
if (requestedProvider === "auto") {
// auto 模式已处理
}
try {
const primary = await createProvider(requestedProvider);
return { ...primary, requestedProvider };
} catch (primaryErr) {
if (fallback && fallback !== "none" && fallback !== requestedProvider) {
try {
const fallbackResult = await createProvider(fallback);
return {
...fallbackResult,
requestedProvider,
fallbackFrom: requestedProvider,
fallbackReason: reason // 包含主提供商的错误原因
};
} catch (fallbackErr) {
// 两者都失败
if (isMissingApiKeyError(primaryErr) && isMissingApiKeyError(fallbackErr)) {
return { provider: null, ... }; // 降级到 FTS-only
}
throw wrapped; // 其他错误抛出
}
}
}
运行时 Fallback(Batch Fallback)
批处理失败时,自动回退到逐条 Embedding:
try {
return await this.runBatchWithTimeoutRetry({
provider: params.provider,
run: params.run
});
} catch (err) {
// 记录失败,如果失败次数超过阈值则禁用批处理
await this.recordBatchFailure({
provider: params.provider,
message,
forceDisable: /asyncBatchEmbedContent not available/i.test(message)
});
// 回退到逐条处理
return await params.fallback();
}
配置示例(OpenClaw 配置文件):
agents:
defaults:
memorySearch:
provider: "openai"
fallback: "local" # OpenAI 失败时尝试本地
model: "text-embedding-3-small"
remote:
batch:
enabled: true
wait: true
concurrency: 4
📦 sqlite-vec 向量扩展的加载与使用
加载机制
sqlite-vec 是 SQLite 的向量扩展,提供高效的向量搜索能力。加载过程在 loadSqliteVecExtension() 中:
export async function loadSqliteVecExtension(params: {
db: DatabaseSync;
extensionPath?: string;
}): Promise<{ ok: boolean; extensionPath?: string; error?: string }> {
try {
const sqliteVec = await import("sqlite-vec");
const resolvedPath = params.extensionPath?.trim() ? params.extensionPath.trim() : undefined;
const extensionPath = resolvedPath ?? sqliteVec.getLoadablePath();
params.db.enableLoadExtension(true);
if (resolvedPath) {
params.db.loadExtension(extensionPath);
} else {
sqliteVec.load(params.db); // 使用 npm 包提供的默认路径
}
return { ok: true, extensionPath };
} catch (err) {
return { ok: false, error: err.message };
}
}
虚拟表创建
加载成功后,创建向量虚拟表(使用 vec0 引擎):
private ensureVectorTable(dimensions: number): void {
if (this.vector.dims === dimensions) {
return; // 已创建,维度匹配
}
if (this.vector.dims && this.vector.dims !== dimensions) {
this.dropVectorTable(); // 维度不匹配,删除重建
}
this.db.exec(
`CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
id TEXT PRIMARY KEY,
embedding FLOAT[${dimensions}]
)`
);
this.vector.dims = dimensions;
}
查询使用
向量查询使用 sqlite-vec 的 vec_distance_*() 函数:
// 检索最相似的 K 个文档
SELECT chunks.id, chunks.text, chunks.path
FROM chunks_vec
INNER JOIN chunks ON chunks_vec.id = chunks.id
WHERE chunks.source = ?
ORDER BY vec_distance_euclidean(chunks_vec.embedding, ?) ASC
LIMIT 10;
支持的距离度量:
- 欧几里得距离(
vec_distance_euclidean) - 余弦距离(
vec_distance_cosine) - 内积(
vec_inner_product)等
🗄️ SQLite 存储 Schema 详解
1. files 表 - 文件元数据
CREATE TABLE IF NOT EXISTS files (
path TEXT PRIMARY KEY,
source TEXT NOT NULL DEFAULT 'memory',
hash TEXT NOT NULL,
mtime INTEGER NOT NULL,
size INTEGER NOT NULL
);
CREATE INDEX idx_chunks_source ON files(source);
字段说明:
path:文件路径(主键)source:来源标记('memory'、'sessions'等)hash:文件内容哈希,用于检测变更mtime:修改时间戳(毫秒)size:文件大小(字节)
2. chunks 表 - 分块内容
CREATE TABLE IF NOT EXISTS chunks (
id TEXT PRIMARY KEY,
path TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'memory',
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
hash TEXT NOT NULL,
model TEXT NOT NULL,
text TEXT NOT NULL,
embedding TEXT NOT NULL, -- JSON 序列化的向量
updated_at INTEGER NOT NULL
);
CREATE INDEX idx_chunks_path ON chunks(path);
CREATE INDEX idx_chunks_source ON chunks(source);
字段说明:
id:分块唯一标识(由路径、行号、内容哈希生成)start_line / end_line:分块在源文件中的行号范围hash:分块文本的内容哈希model:生成该 Embedding 的模型名称embedding:JSON 格式的浮点数数组(用于文本搜索时的回显)updated_at:索引时间戳
3. embedding_cache 表 - Embedding 缓存
CREATE TABLE IF NOT EXISTS embedding_cache (
provider TEXT NOT NULL,
model TEXT NOT NULL,
provider_key TEXT NOT NULL,
hash TEXT NOT NULL,
embedding TEXT NOT NULL,
dims INTEGER,
updated_at INTEGER NOT NULL,
PRIMARY KEY (provider, model, provider_key, hash)
);
CREATE INDEX idx_embedding_cache_updated_at ON embedding_cache(updated_at);
字段说明:
provider:提供商名称('openai'、'gemini'等)model:模型名称provider_key:区分不同认证身份(如不同 API Key 对应的哈希)hash:文本内容哈希(缓存键)embedding:缓存的向量(JSON 格式)dims:向量维度updated_at:缓存时间戳
自动清理:
配置 cache.maxEntries 后,超过数量的旧条目自动删除(按 updated_at ASC 排序)
4. chunks_fts 虚拟表 - FTS5 全文搜索
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
text,
id UNINDEXED,
path UNINDEXED,
source UNINDEXED,
model UNINDEXED,
start_line UNINDEXED,
end_line UNINDEXED
);
字段说明:
text:被索引的分块文本(支持 MATCH 查询)- 其他字段:标记为
UNINDEXED(仅用于结果返回,不参与搜索)
FTS5 特性:
- 支持布尔查询:
text MATCH 'word1 AND word2 OR word3' - 支持短语搜索:
text MATCH '"exact phrase"' - 支持通配符:
text MATCH 'word*'
5. chunks_vec 虚拟表 - 向量搜索
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
id TEXT PRIMARY KEY,
embedding FLOAT[${dimensions}]
);
字段说明:
id:分块 ID,与chunks表的 id 字段一一对应embedding:Float32Array 格式的向量
🔪 分块策略(Chunking):Token 计数、重叠、边界处理
分块参数配置
在 OpenClaw 配置中:
agents:
defaults:
memorySearch:
chunking:
tokens: 512 # 每个分块的目标 token 数
overlap: 64 # 相邻分块的重叠 token 数
分块算法
chunkMarkdown() 函数的核心逻辑:
export function chunkMarkdown(
content: string,
chunking: { tokens: number; overlap: number }
): MemoryChunk[] {
const lines = content.split("\n");
const maxChars = Math.max(32, chunking.tokens * 4); // 1 token ≈ 4 字符
const overlapChars = Math.max(0, chunking.overlap * 4); // 重叠大小
const chunks: MemoryChunk[] = [];
let current: Array<{ line: string; lineNo: number }> = [];
let currentChars = 0;
const flush = () => {
// 将当前积累的行写入一个分块
const text = current.map(entry => entry.line).join("\n");
const startLine = current[0].lineNo;
const endLine = current[current.length - 1].lineNo;
chunks.push({
startLine,
endLine,
text,
hash: hashText(text),
embeddingInput: buildTextEmbeddingInput(text)
});
};
const carryOverlap = () => {
// 从当前分块的末尾保留重叠部分,作为下一分块的开头
if (overlapChars <= 0 || current.length === 0) {
current = [];
currentChars = 0;
return;
}
let acc = 0;
const kept: Array<{ line: string; lineNo: number }> = [];
for (let i = current.length - 1; i >= 0; i -= 1) {
const entry = current[i];
acc += entry.line.length + 1;
kept.unshift(entry);
if (acc >= overlapChars) break;
}
current = kept;
currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0);
};
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const lineNo = i + 1;
// 长行会被进一步拆分
const segments: string[] = [];
if (line.length === 0) {
segments.push("");
} else {
for (let start = 0; start < line.length; start += maxChars) {
segments.push(line.slice(start, start + maxChars));
}
}
for (const segment of segments) {
const lineSize = segment.length + 1;
// 检查是否超出单分块大小限制
if (currentChars + lineSize > maxChars && current.length > 0) {
flush();
carryOverlap();
}
current.push({ line: segment, lineNo });
currentChars += lineSize;
}
}
flush(); // 处理最后一个分块
return chunks;
}
分块特性
- 行边界对齐:分块尽量在行尾处切割,避免中断句子
- 重叠上下文:分块间保留
overlap数量的 token 重叠,保证语义连贯性 - 长行处理:超过
maxChars的单行会被进一步分段 - Token 估计:使用简单比例 1 token ≈ 4 字符(UTF-8)
Token 限制强制
在索引前,通过 enforceEmbeddingMaxInputTokens() 确保分块不超过提供商限制:
export function enforceEmbeddingMaxInputTokens(
provider: EmbeddingProvider,
chunks: MemoryChunk[],
hardMaxInputTokens?: number
): MemoryChunk[] {
const providerMaxInputTokens = resolveEmbeddingMaxInputTokens(provider);
const maxInputTokens = hardMaxInputTokens
? Math.min(providerMaxInputTokens, hardMaxInputTokens)
: providerMaxInputTokens;
const out: MemoryChunk[] = [];
for (const chunk of chunks) {
if (estimateUtf8Bytes(chunk.text) <= maxInputTokens) {
out.push(chunk);
} else {
// 进一步拆分
for (const text of splitTextToUtf8ByteLimit(chunk.text, maxInputTokens)) {
out.push({
startLine: chunk.startLine,
endLine: chunk.endLine,
text,
hash: hashText(text)
});
}
}
}
return out;
}
📋 批量 Embedding 处理(Batch API)
批处理流程
对于支持批处理的提供商(OpenAI、Gemini、Voyage),OpenClaw 采用异步批处理优化吞吐量:
1. 收集要索引的分块 → 2. 分组(按大小限制)→ 3. 提交批处理任务
↓
4. 轮询查询批处理状态
↓
5. 任务完成后拉取结果
OpenAI 批处理(Batch API)
async function submitOpenAiBatch(params: {
openAi: OpenAiEmbeddingClient;
requests: OpenAiBatchRequest[];
agentId: string;
}): Promise<OpenAiBatchStatus> {
// 1. 构建 JSONL 文件并上传
const inputFileId = await uploadBatchJsonlFile({
client: params.openAi,
requests: params.requests // 格式: { custom_id, method: "POST", url, body }
});
// 2. 创建批处理任务
return await postJsonWithRetry<OpenAiBatchStatus>({
url: `${baseUrl}/batches`,
body: {
input_file_id: inputFileId,
endpoint: "/v1/embeddings",
completion_window: "24h"
}
});
// 3. 轮询状态(由 waitForOpenAiBatch 处理)
// 4. 拉取输出文件并解析
}
请求格式:
{
"custom_id": "chunk-123",
"method": "POST",
"url": "/v1/embeddings",
"body": {
"model": "text-embedding-3-small",
"input": "Your text chunk"
}
}
响应格式:
{
"custom_id": "chunk-123",
"result": {
"status_code": 200,
"body": {
"data": [
{ "index": 0, "embedding": [0.1, 0.2, ...] }
]
}
}
}
Gemini 批处理
async function submitGeminiBatch(params: {
gemini: GeminiEmbeddingClient;
requests: GeminiBatchRequest[];
agentId: string;
}): Promise<GeminiBatchStatus> {
// 1. 构建 JSONL 并上传到 Google Cloud Storage
const fileId = await uploadFileToGCS({
jsonl,
displayName: `memory-embeddings-${Date.now()}`
});
// 2. 提交异步批处理任务
return await postJson({
url: `${baseUrl}/models/${modelPath}:asyncBatchEmbedContent`,
body: {
batch: {
displayName: `memory-embeddings-${agentId}`,
inputConfig: {
file_name: fileId
}
}
}
});
// 3. 轮询 batches/{batchName} 直至完成
// 4. 下载输出文件
}
请求格式:
{
"key": "chunk-123",
"request": {
"content": {
"parts": [{ "text": "Your text chunk" }]
},
"taskType": "RETRIEVAL_DOCUMENT"
}
}
Voyage 批处理
async function submitVoyageBatch(params: {
client: VoyageEmbeddingClient;
requests: VoyageBatchRequest[];
agentId: string;
}): Promise<VoyageBatchStatus> {
// 1. 上传 JSONL 文件
const inputFileId = await uploadBatchJsonlFile({
client: params.client,
requests: params.requests // 格式: { custom_id, body: { input: "text" } }
});
// 2. 创建批处理任务
return await postJsonWithRetry({
url: `${baseUrl}/batches`,
body: {
input_file_id: inputFileId,
endpoint: "/embeddings",
completion_window: "12h",
request_params: {
model: params.client.model,
input_type: "document"
}
}
});
}
批处理配置与容错
agents:
defaults:
memorySearch:
remote:
batch:
enabled: true # 启用批处理
wait: true # 等待批处理完成(vs 异步轮询)
concurrency: 4 # 并发提交的批处理组数
pollIntervalMs: 5000 # 轮询间隔
timeoutMs: 600000 # 总超时时间(10 分钟)
失败处理:
- 批处理失败后自动回退到逐条 Embedding
- 如果批处理连续失败 2 次以上,自动禁用批处理,使用逐条模式
- 某些错误(如
asyncBatchEmbedContent not available)会强制禁用
💾 Embedding 缓存策略
缓存命中机制
在索引前,通过 loadEmbeddingCache() 检查已缓存的向量:
private loadEmbeddingCache(hashes: string[]): Map<string, number[]> {
if (!this.cache.enabled || !this.provider) {
return new Map();
}
const baseParams = [this.provider.id, this.provider.model, this.providerKey];
const batchSize = 400; // 单次查询的最大哈希数
for (let start = 0; start < unique.length; start += batchSize) {
const batch = unique.slice(start, start + batchSize);
const rows = this.db.prepare(
`SELECT hash, embedding FROM embedding_cache
WHERE provider = ? AND model = ? AND provider_key = ? AND hash IN (...)`
).all(...baseParams, ...batch);
for (const row of rows) {
out.set(row.hash, parseEmbedding(row.embedding));
}
}
return out;
}
缓存键(四元组):
provider:提供商 IDmodel:模型名称provider_key:认证身份哈希(防止不同 API Key 的向量混淆)hash:文本内容哈希
缓存写入
新生成的向量自动写入 embedding_cache 表:
private upsertEmbeddingCache(entries: Array<{ hash: string; embedding: number[] }>): void {
const stmt = this.db.prepare(
`INSERT INTO embedding_cache (...) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(...) DO UPDATE SET
embedding=excluded.embedding,
dims=excluded.dims,
updated_at=excluded.updated_at`
);
for (const entry of entries) {
stmt.run(
this.provider.id,
this.provider.model,
this.providerKey,
entry.hash,
JSON.stringify(entry.embedding),
entry.embedding.length,
Date.now()
);
}
}
自动清理
protected pruneEmbeddingCacheIfNeeded(): void {
if (!this.cache.enabled) return;
const max = this.cache.maxEntries;
const count = this.db.prepare(
`SELECT COUNT(*) as c FROM embedding_cache`
).get().c;
if (count > max) {
const excess = count - max;
this.db.prepare(
`DELETE FROM embedding_cache
WHERE rowid IN (
SELECT rowid FROM embedding_cache
ORDER BY updated_at ASC
LIMIT ?
)`
).run(excess); // 删除最旧的条目
}
}
配置示例:
agents:
defaults:
memorySearch:
cache:
enabled: true
maxEntries: 10000 # 最多缓存 10,000 个向量
🖼️ 多模态 Embedding 支持(Gemini embedding-2-preview 图片)
多模态支持检测
OpenClaw 通过 supportsMemoryMultimodalEmbeddings() 检测提供商是否支持多模态:
export function supportsMemoryMultimodalEmbeddings(params: {
provider: string;
model: string;
}): boolean {
if (params.provider !== "gemini") {
return false;
}
const normalized = normalizeGeminiEmbeddingModelForMemory(params.model);
return normalized === "gemini-embedding-2-preview";
}
支持提供商:
- ✅ Gemini embedding-2-preview
- ❌ OpenAI、Voyage、Mistral、Ollama、Local(仅支持文本)
多模态文件格式
支持的文件类型:
const MEMORY_MULTIMODAL_SPECS = {
image: {
extensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".heic", ".heif"]
},
audio: {
extensions: [".mp3", ".wav", ".ogg", ".opus", ".m4a", ".aac", ".flac"]
}
};
多模态分块构建
buildMultimodalChunkForIndexing() 将媒体文件转换为 Embedding Input:
export async function buildMultimodalChunkForIndexing(entry: SessionFileEntry) {
const modality = classifyMemoryMultimodalPath(entry.path, settings);
// 1. 读取文件内容
const content = await fs.readFile(entry.absPath);
if (content.byteLength > settings.maxFileBytes) {
return null; // 文件过大,跳过
}
// 2. Base64 编码
const base64 = content.toString("base64");
// 3. 构建 EmbeddingInput
const embeddingInput: EmbeddingInput = {
text: buildMemoryMultimodalLabel(modality, entry.path),
parts: [
{ type: "text", text: labelText },
{
type: "inline-data",
mimeType: resolveMimeType(modality),
data: base64
}
]
};
return { chunk, structuredInputBytes };
}
Gemini 多模态 API 格式
export function buildGeminiEmbeddingRequest(params: {
input: EmbeddingInput;
taskType: GeminiTaskType;
outputDimensionality?: number;
modelPath?: string;
}): GeminiEmbeddingRequest {
return {
content: {
parts: params.input.parts?.map(part =>
part.type === "text"
? { text: part.text }
: {
inlineData: {
mimeType: part.mimeType,
data: part.data // Base64 编码的媒体数据
}
}
) ?? [{ text: params.input.text }]
},
taskType: params.taskType,
outputDimensionality: params.outputDimensionality
};
}
🔍 向量归一化与验证(sanitizeAndNormalizeEmbedding)
所有 Embedding 都经过 sanitizeAndNormalizeEmbedding() 处理,确保数值稳定性和一致性:
export function sanitizeAndNormalizeEmbedding(vec: number[]): number[] {
// 1. 清理非法值(NaN、Infinity)
const sanitized = vec.map((value) =>
Number.isFinite(value) ? value : 0
);
// 2. 计算 L2 范数(欧几里得模)
const magnitude = Math.sqrt(
sanitized.reduce((sum, value) => sum + value * value, 0)
);
// 3. 若向量非零,则归一化为单位向量
if (magnitude < 1e-10) {
return sanitized; // 零向量保持不变
}
return sanitized.map((value) => value / magnitude);
}
作用:
- 数值稳定:移除 NaN 和 Infinity,防止数据库存储和计算错误
- 向量归一化:所有非零向量的 L2 范数都为 1.0
- 距离度量一致性:归一化后,欧几里得距离与余弦距离等价
好处:
- 便于使用余弦相似度进行搜索
- 提高向量搜索性能(某些加速库要求单位向量)
- 减少浮点数值误差的影响
📊 Embedding 操作性能优化
并发策略
protected getIndexConcurrency(): number {
return this.batch.enabled ? this.batch.concurrency : 4;
}
逻辑:
- 启用批处理时:使用配置的
concurrency值(通常 2-8) - 关闭批处理时:使用固定的 4 个并发线程
超时配置
private resolveEmbeddingTimeout(kind: "query" | "batch"): number {
const isLocal = this.provider?.id === "local";
if (kind === "query") {
// 查询超时
return isLocal ? 5 * 60_000 : 60_000; // 本地 5 分钟,远程 1 分钟
} else {
// 批处理超时
return isLocal ? 10 * 60_000 : 2 * 60_000; // 本地 10 分钟,远程 2 分钟
}
}
重试策略
protected async embedBatchWithRetry(texts: string[]): Promise<number[][]> {
let attempt = 0;
let delayMs = 500; // 初始延迟
while (true) {
try {
return await this.provider.embedBatch(texts);
} catch (err) {
if (!this.isRetryableEmbeddingError(message) || attempt >= 3) {
throw err;
}
// 指数退避 + 随机抖动
delayMs = Math.min(8000, delayMs * 2);
const waitMs = delayMs * (1 + Math.random() * 0.2);
await new Promise(resolve => setTimeout(resolve, waitMs));
attempt += 1;
}
}
}
private isRetryableEmbeddingError(message: string): boolean {
return /(rate.limit|too many requests|429|resource exhausted|5\d\d|tokens per day)/i.test(message);
}
🎯 小结与下篇预告
本文深入讲解了 OpenClaw 记忆系统的 Embedding 引擎与向量存储 架构:
核心要点
- 6 种 Embedding 提供商支持不同的使用场景,从云端高性能到本地离线方案
- Auto 模式优雅地根据可用资源自动选择最合适的提供商
- Fallback 机制确保系统的容错能力
- sqlite-vec 向量虚拟表提供了高效的语义搜索能力
- 分块策略在语义完整性与处理效率间取得平衡
- 批处理 API 大幅提升大规模索引的吞吐量
- 多模态支持使得系统能够理解文本之外的信息
- 缓存与优化显著降低了重复计算的成本
下篇展望
本系列第 3 篇将聚焦 记忆检索与重排序,讨论:
- 混合搜索策略(向量 + FTS 融合)
- 查询时的 Embedding 和排序
- 重排序(Re-ranking)算法提升精度
- 记忆上下文的动态组装
- 与 LLM 的集成模式
📚 参考文献
-
OpenClaw 源代码
src/memory/embeddings.ts- Embedding 提供商工厂src/memory/embeddings-{openai,gemini,voyage,mistral,ollama}.ts- 各提供商实现src/memory/batch-{openai,gemini,voyage}.ts- 批处理 API 实现src/memory/sqlite-vec.ts- sqlite-vec 扩展加载src/memory/memory-schema.ts- 数据库 Schema 定义src/memory/manager-embedding-ops.ts- Embedding 操作管理
-
相关技术文档
- OpenAI Embeddings API: https://platform.openai.com/docs/guides/embeddings
- Google Gemini API: https://ai.google.dev/docs
- Voyage AI Embeddings: https://docs.voyageai.com/docs/embeddings
- Mistral AI Embeddings: https://docs.mistral.ai/capabilities/embeddings/
- sqlite-vec: https://github.com/asg017/sqlite-vec
-
向量检索与相似度
- Cosine Similarity vs Euclidean Distance
- Vector Normalization in Information Retrieval
- Embedding-based Search Systems
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)