- 人工智能
- 大模型
- Agent 记忆
- AI Agent
- RAG
- 知识图谱
- dsh-plugin
【免费下载链接】MemOS
Self-evolving memory OS for LLM & AI Agents: ultra-persistent memory, hybrid-retrieval, and cross-task skill reuse, with 35.24% token savings and DeepSeek Harness support.
导读
apps/memos-local-plugin/core/embedding/是 MemOS 本地插件(memos-local-plugin)中唯一与 Embedding Provider 打交道的模块:它把「local / OpenAI 兼容 / Gemini / Cohere / Voyage / Mistral」六类向量服务统一收敛为一个Embedder门面,屏蔽了角色语义、批量请求、重试退避、维度规整与归一化等全部细节,为上层检索提供「把文本变成可用于余弦相似度的向量」的唯一入口。读完本文,你将掌握该层的完整数据流、六类 Provider 的接入方式与参数差异、缓存与错误处理的设计取舍,以及如何用配置文件把 MemOS 指向任一家向量服务。
1. 模块定位:为什么需要一层 Embedding 门面
core/embedding的设计目标可以用一句话概括(README 原文):
"Give me a vector for this text, in a way that makes cosine similarity reflect semantic similarity."
答案是「取决于 Provider」——不同供应商的接口、鉴权方式、维度、角色语义各不相同。因此该模块做了两条硬性约束:
- 单点出口:
core/中任何需要向量的地方都必须经过Embedder门面,Provider 实现不导出到本目录之外(见 types.ts 顶部注释)。门面的公共入口集中在 index.ts,导出createEmbedder、createEmbedderWithProvider、makeProviderFor及全部类型与工具函数。 - Provider 只做一件事:每个 Provider 实现同一个
EmbeddingProvider接口(name+embed(texts, role, ctx)),只管发起 HTTP / 本地推理调用并返回原始number[][];批处理、缓存、重试、维度校验与 L2 归一化全部由门面统一完成(见 embedder.ts 中EmbeddingProvider接口注释)。
这样上层(retrieval / capture)永远只面对一个稳定的门面,换模型、换厂商都不需要改业务代码。
1.1 最小使用示例
import { createEmbedder, type EmbeddingConfig } from "./core/embedding"; const embedder = createEmbedder(cfg.embedding); const vec = await embedder.embedOne("hello world"); const vecs = await embedder.embedMany([ { text: "user asked X", role: "document" }, { text: "user asked X", role: "query" }, ]);embedOne接收字符串或{ text, role },返回一个Float32Array;embedMany返回按输入顺序排列的Float32Array[],内部自动去重(同一文本重复 N 次只产生一次 Provider 往返);- 返回向量的长度严格等于
config.embedding.dimensions(配置为 0 时按 Provider 原生维度自动推断,见 normalize.ts 的postProcess)。
embedOne在实现上就是对embedMany的薄封装(embedder.ts 第 105-111 行),所以下文以embedMany的数据流为准。
2. 六类 Provider 一览
| Provider 名称 | 底层服务 | 维度 / 模型默认值 | 角色语义 |
|---|---|---|---|
local | @huggingface/transformers运行Xenova/all-MiniLM-L6-v2 | 384 维,首次调用下载约 23 MB 模型,int8 量化 + CPU 推理 | 不区分角色 |
openai_compatible | POST <endpoint>/embeddings,兼容 OpenAI 原生、Azure、智谱、硅基流动、百炼、Groq 等 | 默认模型text-embedding-3-small,默认端点https://api.openai.com/v1/embeddings | 不区分角色 |
gemini | generativelanguage.googleapis.com/v1beta/models/<model>:batchEmbedContents | 默认模型text-embedding-004(768 维) | 区分RETRIEVAL_DOCUMENT/RETRIEVAL_QUERY |
cohere | api.cohere.ai/v1/embed | 默认模型embed-english-v3.0 | 通过input_type区分search_document/search_query |
voyage | api.voyageai.com/v1/embeddings | — | 区分 document / query 角色 |
mistral | api.mistral.ai/v1/embeddings | OpenAI 兼容的请求/响应形状 | 不区分角色 |
openai_compatible的请求体为{ input: string[], model },响应为{ data: [{ embedding: number[] }] };endpoint未指定时默认指向 OpenAI 官方地址,也可通过endpoint指向 Azure 等任何兼容实现(openai.ts)。gemini默认在 URL 上以?key=<API_KEY>携带密钥(gemini.ts),这一点在「注意事项」一节还会强调日志脱敏问题。- 所有云端 Provider 都要求配置
apiKey,缺失时直接抛出MemosError(code=embedding_unavailable),不会静默回退。
3. 核心数据流:从输入到可存库的向量
README 给出了门面内部的完整流水线,结合 embedder.ts 的实现可还原为:
inputs ─▶ normalize(input list → role-tagged {text}) │ ▼ sha256(provider|model|role|text) → cache lookup (LRU) │ ├── hit ──────────────────────────┐ ▼ └── miss → batched by role │ batch k texts ──▶ provider.embed() │ │ │ │ ▼ ▼ ▼ dim-enforce + L2-normalize (Float32Array) ──────▶ interleave in input order │ └── cache.set(key, vec)具体步骤拆解:
- 输入规整:每个入参统一为
{ text, role },纯字符串默认role: "document"(toInput函数,embedder.ts 第 79-82 行)。 - 缓存查找:以
sha256(provider|model|role|text)的 64 位十六进制串为键查 LRU 缓存。 - 批处理:未命中的输入先按
role分组,再以batchSize(默认 32)切块,逐块调用provider.embed()。 - 后处理:
postProcess完成维度规整、Float32Array化、L2 归一化(默认开启)。 - 回填:按原输入下标回填结果,并写入缓存,保证
embedMany的输出顺序与输入顺序严格一致。
3.1 批处理(Batching)
batchSize(默认 32)决定每次 HTTP 调用携带的文本条数。关键点在于先按 role 分组、再切批:一个混合了query与document的列表会产出两组各自角色正确的往返请求,而不是一次角色模糊的请求(README 2.1 节)。对 Cohere / Gemini / Voyage 这类「查询与文档使用不同input_type/taskType」的服务,这一步直接决定了向量质量。
3.2 维度规整与归一化(Normalization)
Provider 返回的是其原生维度的浮点数组,门面会强制对齐配置声明的dimensions(normalize.ts 的enforceDim):
- 维度相等 → 直接通过;
- 维度更大 →截断。这是把 1536 维模型接入 384 维存量库的旋钮,无需重塑 SQLite 表;
- 维度更小 →直接抛错(
EMBEDDING_UNAVAILABLE)。静默零填充会污染下游余弦相似度,因此宁可失败也不伪装。
随后向量被转为Float32Array并做 L2 归一化(除非config.normalize=false)。「归一化一次」的设计收益在于:查询时无需重复归一化,余弦相似度在已存 blob 上退化为点积,配合 vector.ts 中预存的平方 L2 范数(norm2),每次检索能省一次 sqrt 与一次全向量扫描。
补充说明:
localProvider 在推理时已通过{ pooling: "mean", normalize: true }完成均值池化与归一化(local.ts 第 115-116 行),不会再叠加归一化。
3.3 角色语义(Role)
部分服务对「被检索的内容」与「检索词」分别建模。门面用EmbedRole统一暴露这一语义:
| role | local | openai | gemini | cohere | voyage | mistral |
|---|---|---|---|---|---|---|
document | n/a | n/a | RETRIEVAL_DOCUMENT | search_document | document | n/a |
query | n/a | n/a | RETRIEVAL_QUERY | search_query | query | n/a |
调用约定:嵌入用户的搜索文本时传role: "query";嵌入入库内容时传role: "document"(或省略,默认为 document)。
4. 配置项全解
EmbeddingConfig的完整字段定义在 types.ts,以下是全部可配置项及其默认值:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
provider | "local" \| "openai_compatible" \| "gemini" \| "cohere" \| "voyage" \| "mistral" | — | 必填,选择 Provider |
endpoint | string | 各 Provider 官方地址 | 自定义端点(如 Azure、自建反向代理) |
model | string | 各 Provider 默认模型 | 模型标识(如bge-m3) |
dimensions | number | — | 期望输出维度;<= 0表示按 Provider 原生维度自动推断 |
apiKey | string | — | 云端 Provider 必填 |
providerIgnore | string[] | — | OpenRouter 路由:跳过指定 Provider |
providerOrder | string[] | — | OpenRouter 路由:首选顺序 |
openRouter | boolean | false | 是否为 OpenRouter 反向代理 / CNAME 显式启用路由字段 |
cache.enabled | boolean | true | 是否启用内存 LRU 缓存 |
cache.maxItems | number | 20000 | 缓存条目上限 |
timeoutMs | number | 30000 | 单次 HTTP 调用超时 |
maxRetries | number | 2 | 瞬时错误(5xx / 429 / 网络)最大重试次数 |
batchSize | number | 32 | 每次 HTTP 往返的最大文本数 |
headers | Record<string, string> | — | 追加到出站 HTTP 请求的额外头 |
normalize | boolean | true | 是否对所有输出向量做 L2 归一化 |
onError | 函数 | — | 终端失败时的错误回调(用于写入system_error日志行) |
onStatus | 函数 | — | 成功/失败的状态回调(供 Overview 模型卡片消费) |
实际部署时,provider与apiKey两项已在插件模板配置中暴露(config.openclaw.yaml 第 20-22 行):
embedding: provider: local # local | openai_compatible | gemini | cohere | voyage | mistral apiKey: "" # required for cloud providers切换到云端服务只需把provider改为目标厂商并填入apiKey,其余高级字段(model、batchSize、timeoutMs、normalize等)按需在配置中追加。更完整的进阶参数说明见 CONFIG-ADVANCED.md。
4.1 两个容易被忽略的配置回调
onError:仅在 Provider终端失败(重试耗尽)时触发一次,用于把system_error写入api_logs,让 Logs 查看器能展示基础设施故障;回调内部异常会被吞掉,绝不会掩盖原始错误(embedder.ts 第 229-243 行)。onStatus:成功与失败都会调用,是 Overview 模型卡片的机器可读数据源;EmbedStatusDetail携带durationMs、retryDecision、retryReason等诊断字段(types.ts 第 77-87 行)。
5. 缓存设计:为什么只用内存 LRU
默认缓存为进程内 LRU,maxItems = 20000。按 384 维 ×Float32Array估算约 20 MB 内存,代价极低(README 第 3 节)。实现位于 cache.ts:
- 缓存键由
node:crypto的sha256对provider|model|role|text取 64 位十六进制摘要(makeCacheKey); LruEmbedCache用Map(插入有序)+ 命中即「删除再重插」实现 LRU 提升,set时超出maxItems逐出最旧条目并计数evictions;- 关闭缓存(
cache.enabled: false)时自动换入NullEmbedCache,调用方代码零改动——门面内部只依赖EmbedCache接口。
README 明确解释了「不落盘」的三点理由:
- 重启后重新嵌入对
local免费、对云端只是几分钱; - 把文本 blob(无论是否哈希)持久化到磁盘会模糊「机密文本只存在于 SQLite blob」的安全边界;
core/storage/repos/*中的仓库在向量入库后已自行缓存向量——检索期命中由 SQLite 本身服务,比在嵌入层再做第二层缓存更优。
另外两个与缓存相关的实现细节值得一提:
- 请求内去重:同一次
embedMany里重复出现的文本,只有第一次会真正 miss,后续出现都复用同一趟往返的结果(embedder.ts 第 148-158 行)。 - 关闭缓存同时关闭去重:
cache.enabled: false时每个输入会拿到独立 key(追加#i后缀),这是为了保留「关闭缓存做基准测试」的场景(第 122-134 行)。
6. 容错与错误处理
6.1 重试与退避
所有 HTTP 调用统一走 fetcher.ts 的httpPostJson,它负责:
- 超时:单次调用
AbortSignal.timeout(timeoutMs)(默认 30s),并与调用方传入的AbortSignal合并; - 瞬时错误重试:HTTP 5xx / 429 / 网络错误(
timeout、ETIMEDOUT、ECONNRESET、EAI_AGAIN、socket hang up 等)按指数退避重试,最多maxRetries(默认 2)次; - Retry-After 尊重:429 / 503 响应头携带
Retry-After时,会解析并记录冷却(recordRetryCooldown),冷却期内对同一 provider/url/模型的作用域直接跳过调用; - 绝对截止时间:
deadlineAt是跨重试共用的端到端 deadline,退避计划若无法在截止前完成则放弃(retryDecision: "defer"); - 4xx 不重试:非瞬时错误(如 400)直接抛出,不浪费重试预算。
上述行为在 fetcher.test.ts 中有系统覆盖,包括「按 HTTP-date 格式的 Retry-After 等待后重试 429」「冷却期命中」「deadline 不足则 defer」「400 不重试」等用例。
6.2 错误语义:不自动回退
- 所有不可恢复的失败统一冒泡为
MemosError(code=embedding_unavailable),details携带{ provider, url, … }; - 门面不会在云端 Provider 失败时自动回退到
local(embedder.ts 顶部注释明确说明)——这是有意为之:上层(retrieval / capture)可以自行决定用本地嵌入器重试,但本层保持错误语义清晰,便于测试与排障; - 失败后
lastError记录时间戳与消息,且不会被随后的成功清除——Viewer 通过比较lastError.at与lastOkAt决定 Overview 卡片显示绿色还是红色,避免「一次缓存友好的成功」掩盖仍真实存在的 Provider 故障(types.ts 第 156-163 行)。
6.3 失败重试队列
除了同步重试,模块还提供createEmbeddingRetryWorker(retry-worker.ts),基于embedding_retry_queue仓库的异步重试机制:失败任务落库后由 worker 领取(claim)、续租(touchClaimHeld)、重放;单元测试 retry-queue.test.ts 与 retry-worker.test.ts 覆盖了领取、失败重试与系统错误事件上报(systemErrorEvent)等场景。这为「同步退避耗尽但值得稍后重试」的批量嵌入任务提供了兜底通道。
7. 日志通道
门面按通道名细分日志,方便按 provider 维度过滤:
embedding— 门面初始化与统计(init时打印 provider / model / dimensions / cacheEnabled / batchSize);embedding.cache— 缓存写入 / 清除 / 命中;embedding.local— HF pipeline 加载与逐调用 trace;embedding.openai_compatible/embedding.gemini/embedding.cohere/embedding.voyage/embedding.mistral— 各自 HTTP 尝试 / 时长 / 状态。
每行日志都会携带{ traceId, sessionId, ... }等上下文字段(由 core/logger/context.ts 的上下文传播器注入),可与链路追踪对接。
8. 测试体系
单元测试集中在tests/unit/embedding/,与 README 第 6 节一一对应:
- normalize.test.ts — 维度规整、L2、Float32 转换;
- cache.test.ts — LRU 逐出、命中/未命中计数、Null 缓存行为对齐;
- fetcher.test.ts — 5xx/429 重试、超时、错误映射;
- providers.test.ts — 每个 Provider 用 fake
fetch挂载一次往返,覆盖鉴权、角色翻译、响应解析与错误面; - local.test.ts 与 local-abort.test.ts — 惰性加载单例不变量、请求中止时的响应行为(不真实下载模型);
- embedder.test.ts — 门面端到端:缓存命中路径、混合角色、batchSize 分块、重复去重、统计计数、维度规整;
- retry-queue.test.ts / retry-worker.test.ts — 异步重试队列与 worker。
providers.test.ts用「每个测试挂载一个 fakefetch」的方式把各 Provider 的鉴权头、请求体、角色翻译与响应解析都固化成了可回归的行为契约。
9. 注意事项与已知边界
local首次调用会下载模型到 huggingface 缓存目录(约 23 MB)。真正触发local推理的测试必须显式开启(env-gated)且不占用单元测试预算;LocalEmbeddingProvider通过模块级extractorPromise惰性加载、进程内共享单例(local.ts)。gemini的?key=<API_KEY>把密钥放进 URL。fetcher.ts依赖 logger 的脱敏管道在日志中抹掉查询串;不要在别处打印原始 URL。voyage与cohere按 token 计费。调大batchSize虽能摊薄 HTTP 开销,但会快速逼近 TPM 上限,需权衡。- 更改
dimensions会破坏存量余弦比较。向量已写入 SQLite 后再改配置中的dimensions,新老行之间无法做余弦比对。README 的建议是优先整体升级模型并在下一轮通过resetCache() + recomputeOnDemand重新嵌入——该重算路径属于 Phase 9,当前尚未实现,属于已知边界。 - 查询侧预归一化:由于写入侧已做 L2 归一化,检索侧直接以点积代替余弦即可(见 vector.ts 的
dot/cosine/norm2实现),这也是「归一化一次」设计的直接收益点。
10. 小结
core/embedding是一个教科书式的「门面 + 策略」分层:六个 Provider 各自只负责协议翻译,而批处理、缓存、重试、维度规整、归一化、统计与错误语义全部收敛到Embedder统一实现。对使用者而言,切换嵌入服务只需改embedding.provider与embedding.apiKey两个配置项;对二次开发者而言,EmbeddingProvider接口(types.ts)就是扩展新厂商的唯一契约——实现embed()并把它注册进makeProviderFor(embedder.ts 第 349-370 行)即可。想要进一步了解向量如何落库与检索,可直接阅读 vector.ts 与 core/storage 目录。
- 人工智能
- 大模型
- Agent 记忆
- AI Agent
- RAG
- 知识图谱
- dsh-plugin
【免费下载链接】MemOS
Self-evolving memory OS for LLM & AI Agents: ultra-persistent memory, hybrid-retrieval, and cross-task skill reuse, with 35.24% token savings and DeepSeek Harness support.
相关推荐
MemOS 本地插件 LLM 层深度解析:LlmClient 门面、多 Provider 路由与容错设计
MemOS 本地插件 LLM 层深度解析:LlmClient 门面、多 Provider 路由与容错设计 导读 core/llm/ 是 MemOS 本地插件(
人工智能大模型Agent 记忆AI AgentRAG知识图谱dsh-pluginMemOS 本地插件 core/pipeline 深度解析:算法编排器与 MemoryCore 门面层的架构设计
MemOS 本地插件 core/pipeline 深度解析:算法编排器与 MemoryCore 门面层的架构设计 导读 apps/memos local plu
人工智能大模型Agent 记忆AI AgentRAG知识图谱dsh-pluginMemOS 本地插件 JSON-RPC Bridge 深度解析:非 TypeScript 适配器的统一接入层
MemOS 本地插件 JSON RPC Bridge 深度解析:非 TypeScript 适配器的统一接入层 导读 MemOS 本地插件( apps/memos
人工智能大模型Agent 记忆AI AgentRAG知识图谱dsh-plugin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考