EmbeddingGemma 2 开源:把 RAG 索引迁到多模态向量前,先过这六道工程闸门
✦
← AI 与大模型

EmbeddingGemma 2 开源:把 RAG 索引迁到多模态向量前,先过这六道工程闸门

楼主:小杉 · 2026-10-08 01:11 · 浏览 264

EmbeddingGemma RAG 向量检索 多模态
小杉 楼主

2026 年 10 月 6 日,Google DeepMind 发布了 EmbeddingGemma 2:一个 740M 参数、Apache 2.0 许可的开源嵌入模型,把文本(含代码)、图片、视频、音频映射到同一个 768 维向量空间,主打端侧运行。

对做 RAG、本地检索和 Agent 记忆的开发者来说,这条消息最容易被理解成“换个更强的嵌入模型”。实际上,多模态嵌入接进生产系统后,要改的不是一行模型名,而是索引版本、编码约定、数值精度、维度策略、切片预算和检索侧的权限过滤。本文按上线顺序,把这些改动整理成六道闸门,并给出可直接改造的代码骨架。

1. 先把公开事实和工程推论分开

类别 内容 出处
公开事实 总参数 740M:文本主干 270M(130M transformer + 140M embedder),视觉编码器 170M,音频编码器 300M,可按需加载 官方博客、模型卡
公开事实 原生输出 768 维,MRL 支持截断到 512 / 256 / 128 维 模型卡
公开事实 8,192 token 共享上下文;图片默认 280 token/张,视频 140 token/帧(默认 1 fps),音频 25 token/秒 模型卡
公开事实 文本任务要加任务前缀;query 与 document 前缀不对称;图片、视频、音频不加前缀 模型卡
公开事实 推理用 bfloat16 或 float32,不要用 float16(会出 NaN 或静默劣化) 模型卡
公开事实 MTEB Code 从 68.76 提升到 78.68;多语言文本 MTEB 61.36(一代 61.15) 官方博客、模型卡
工程推论 换模型等于换向量空间,旧索引必须重建,不能混查 作者建议
工程推论 需要一个“编码器指纹”把模型版本、前缀、维度、精度绑进索引元数据 作者建议

下文所有阈值、分片时长、配置项名称,凡未注明出处的,都是示例值,请按自己的数据和评测结果调整。

2. 为什么不能只改模型名

嵌入模型换代,最危险的错误不是报错,而是不报错:

  • 新旧模型的向量都是 768 维,形状完全兼容,向量库照样接受写入;
  • 但两个模型的向量空间没有对齐关系,旧文档向量和新查询向量算出来的余弦相似度没有意义;
  • 结果是召回率悄悄下降,排序看起来“还行”,线上很难第一时间发现。

所以第一原则是:索引属于某一个具体的编码器配置。模型、前缀、维度、精度、启用的模态,任何一项变了,都视为新索引。

索引迁移:影子双读、评测闸门与切换回滚

3. 闸门一:编码约定(任务前缀)要做成代码,不要靠记忆

EmbeddingGemma 2 和一代一样,文本输入靠短前缀区分任务。检索类任务是不对称的:

  • 查询:task: search result | query: {query}
  • 文档:title: {title} | text: {content},没有标题时写 title: none
  • 代码检索:查询用 task: code retrieval | query: {query},文档的 title 位可以放文件名

模型卡特别提醒了一点:prompt_name="Document" 只会套上 title: none,有真实标题的文档需要自己拼接。而官方文档里不同页面给出的 prompt_name 写法并不完全一致(模型卡用 SearchQuery / Document,其他教程页出现过别的命名),所以别把名字硬编码在业务代码里,启动时读一次 model.prompts 做断言更稳。

# embedder.py —— 示例骨架:把前缀约定收拢到一处
from dataclasses import dataclass

QUERY_PREFIX = {
    "search": "task: search result | query: ",
    "qa": "task: question answering | query: ",
    "code": "task: code retrieval | query: ",
}

def format_doc(text: str, title: str | None = None) -> str:
    # 有标题时必须手工拼接;Document 预设只会写 title: none
    return f"title: {title or 'none'} | text: {text}"

def format_query(text: str, task: str = "search") -> str:
    return QUERY_PREFIX[task] + text

实践要点:

  1. 入库流水线和在线查询共用同一个模块,避免两边各写一份前缀。
  2. 把前缀字符串本身算进编码器指纹(见第 7 节),前缀改了就触发重建。
  3. 图片、视频、音频不加前缀;图文混排输入用 <|image|>、<|video|>、<|audio|> 占位符标出位置。

4. 闸门二:数值精度——把 fp16 挡在门外

模型卡写得很直接:EmbeddingGemma 2 的激活范围超过 float16 的动态范围,用 fp16 推理会返回 NaN,或者返回看起来正常但已经劣化的向量。推荐在支持的硬件上用 bfloat16,其余情况(包括大多数 CPU)用 float32。

很多推理框架和量化脚本默认就是 fp16,所以这一条要写成启动检查,而不是写在文档里:

import torch, numpy as np
from sentence_transformers import SentenceTransformer

def load_embedder(model_id: str, modalities: set[str]):
    dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float32
    cfg = {}
    if "image" not in modalities and "video" not in modalities:
        cfg["vision_config"] = None
    if "audio" not in modalities:
        cfg["audio_config"] = None
    m = SentenceTransformer(model_id, model_kwargs={"torch_dtype": dtype}, config_kwargs=cfg)
    return m, str(dtype)

def health_check(m, probe_texts: list[str]):
    v = m.encode(probe_texts, normalize_embeddings=True)
    assert not np.isnan(v).any(), "embedding contains NaN: check dtype"
    norms = np.linalg.norm(v, axis=1)
    assert np.allclose(norms, 1.0, atol=1e-3), f"unexpected norms: {norms}"

config_kwargs 里置空某个编码器的写法出自模型卡的 Sentence Transformers 示例;模型卡也说明,不同推理库关闭编码器的方式不一样,换框架时要查对应文档。

5. 闸门三:维度策略——截断可以,但要重归一化、要同维

MRL 让 768 维向量可以只保留前 512 / 256 / 128 维。存储收益很直观(以下为按 float32 计算的示例,100 万条向量,不含索引结构开销):

维度 单条字节 100 万条约
768 3,072 B 3.07 GB
512 2,048 B 2.05 GB
256 1,024 B 1.02 GB
128 512 B 0.51 GB

但质量损失并不是线性的,而且多模态比纯文本掉得更厉害。下图数据取自模型卡的截断评测表:

MRL 截断:各维度在文本、代码与多模态基准上的得分

可以看到,256 维时文本和代码基本持平;降到 128 维,MMEB v2 总分从 59.01 掉到 45.65,MSEB 检索从 69.54 掉到 56.71。模型卡给的结论也是:128 维更适合纯文本,多模态场景必须用自己的数据先验证。

三条硬规则:

  1. 截断后必须重新 L2 归一化。直接切片的向量不是单位长度,余弦分数会“看起来合理但排序错了”。用 encode(..., truncate_dim=256, normalize_embeddings=True) 让库来做。
  2. 查询和库必须同一维度。768 维查询不能和 128 维库比较;维度写进索引元数据,查询端从元数据读,而不是从配置文件读。
  3. 按模态分别选维度。纯文本知识库可以评估 256 甚至 128;含图片、视频、音频的库建议从 512 或 768 起步。

6. 闸门四:切片预算——8K 是所有模态共享的

所有模态共用 8,192 token 上下文,按模型卡给出的消耗速率,单一模态的上限大约是:29 张图、58 帧视频(默认 1 fps 时约 58 秒)、约 327 秒音频。图文混排时互相挤占。

8K 共享上下文:不同模态的 token 消耗与切片建议

这直接决定了入库切片方式:

  • 音频:按 25 token/秒算,一段 60 秒的切片约 1,500 token,留出文本描述的余量。示例:60 秒一片、前后重叠 5 秒,元数据记录 start_s / end_s,命中后可以直接跳到对应时间点。
  • 视频:默认 1 fps、140 token/帧。长视频按镜头或固定时长切段,每段控制在几十帧以内;需要更细的画面理解时,模型卡说明视觉 token 预算可以在 70 到 1,120 之间调整,代价是延迟和 token 数。
  • 图文页面(产品页、PDF 截图、带图的工单):一页一条,用占位符保留图片在文字中的位置,比“文字一条、图片一条”更利于跨模态召回。
  • 超长文本:沿用原有分块策略即可,但要重新评估块大小,因为上下文从一代的 2K 扩到了 8K。

切片前先算预算、超了就拆,不要依赖库的静默截断:

TOKENS = {"image": 280, "video_frame": 140, "audio_s": 25}  # 模型卡默认值
CTX = 8192

def fits(n_text_tokens=0, images=0, frames=0, audio_s=0.0, reserve=256):
    used = n_text_tokens + images*TOKENS["image"] + frames*TOKENS["video_frame"] + audio_s*TOKENS["audio_s"]
    return used + reserve <= CTX, used

7. 闸门五:索引迁移——编码器指纹 + 影子双读 + 评测闸门

迁移流程建议拆成五步:

  1. 生成编码器指纹:把影响向量空间的所有因素做成一个哈希,写进每个索引(或 collection)的元数据。
  2. 新建索引,后台重建:旧索引继续服务,新索引全量回填;增量写入期间双写。
  3. 影子双读:线上查询同时打到新旧索引,只返回旧结果,记录两边 top-k 与点击、引用情况。
  4. 离线评测闸门:用固定的标注集(查询 → 应命中的文档/片段)比较 Recall@k、nDCG@k,按模态分开统计;达标才切流。
  5. 切换与回滚:切换只改“当前生效索引”指针,旧索引保留一个观察窗口。
# index_manifest.yaml —— 示例,字段名为作者建议
index: kb_multimodal_v2
embedder:
  model_id: google/embeddinggemma-2
  revision: "<pin a commit hash>"
  dtype: bfloat16
  dim: 512
  modalities: [text, image]
  query_prefix: "task: search result | query: "
  doc_template: "title: {title} | text: {content}"
fingerprint: sha256(model_id|revision|dtype|dim|modalities|query_prefix|doc_template)
cutover_gate:            # 示例阈值
  recall_at_10_min_delta: 0.0     # 新索引不得低于旧索引
  per_modality_required: true
  shadow_days: 7
rollback:
  keep_old_index_days: 14

查询端在发请求前比对指纹:

def assert_compatible(query_fp: str, index_meta: dict):
    if query_fp != index_meta["fingerprint"]:
        raise RuntimeError(
            f"embedder mismatch: query={query_fp[:12]} index={index_meta['fingerprint'][:12]}"
        )

这一步能挡住最常见的事故:某个服务升级了模型或改了维度,另一个服务还在用旧配置查询。

8. 闸门六:检索侧的权限与安全过滤

模型卡明确说明:EmbeddingGemma 2 是预训练嵌入模型,没有做后训练对齐、安全微调或输出审核,安全风险体现在下游怎么使用这些向量。对 RAG 和 Agent 来说,落到工程上是三件事:

  • 权限过滤在检索阶段做:向量库查询带上租户、项目、密级等过滤条件,不要先召回再让大模型“自己别说”。
  • 多模态内容同样要过审:图片里的文字、音频里的语音都会被编码进向量,被召回后可能进入生成模型的上下文。来源不可信的媒体,入库前走和文本一样的内容审查与来源标记。
  • 把检索结果当作不可信输入:Agent 读到召回内容后再调用工具时,召回片段里的“指令”不应直接驱动工具调用。

9. 对 Agent 开发者的额外价值:本地代码检索和多模态记忆

两处值得单独评估:

  1. 代码检索:MTEB Code 提升近 10 分,加上代码检索专用前缀,适合给 Coding Agent 做本地仓库语义检索。建议用文件路径做 title,按函数或类切块,评测集直接取历史 issue 与对应修改文件。
  2. 端侧多模态记忆:截图、语音备忘、录屏可以和文本笔记进同一个向量空间,在本地完成“用一句话找到某段录音”。数据不出设备,但同样要做第 8 节的权限隔离,尤其是多用户共用设备的场景。

10. 上线前检查清单

  • 入库与查询共用一个编码模块,前缀集中定义,有标题的文档手工拼接
  • 启动时检查 dtype:只允许 bfloat16 / float32,健康检查覆盖 NaN 与向量范数
  • 只加载需要的模态编码器,并确认所用推理框架的关闭方式
  • 维度写入索引元数据;截断后重归一化;查询与库同维
  • 多模态库不贸然用 128 维,先用自有标注集评测
  • 入库切片前计算 8K 预算,音视频切片记录时间戳
  • 编码器指纹覆盖模型版本、修订号、精度、维度、模态、前缀
  • 新旧索引并行,影子双读 + 按模态的离线评测闸门,达标再切流
  • 旧索引保留观察窗口,切换只改指针,可一键回滚
  • 检索阶段做权限过滤,召回内容按不可信输入处理

参考资料

  1. Google Blog:EmbeddingGemma 2: an open, lightweight multimodal embedding model(2026-10-06)
  2. Hugging Face:google/embeddinggemma-2 模型卡
  3. Google AI for Developers:EmbeddingGemma 2 model card
  4. Google AI for Developers:Generating Embeddings with EmbeddingGemma 2 and Sentence Transformers
  5. Google AI for Developers:Fine-tune EmbeddingGemma 2 with Sentence Transformers

本文同步发布于 CSDN:https://blog.csdn.net/qq_29737119/article/details/167258841

金知远

六道闸门里索引版本最容易被忽略。多模态嵌入换上去,老索引的向量空间和新模型根本不兼容,混着检索等于把噪声喂给重排。稳妥做法是双写一段时间,等新索引覆盖全量再切流,同时保留按记录回退的能力,别指望一次切换不出问题。

何颖

索引版本这块最容易被低估。我们踩过一次:换了嵌入模型却没重建索引,相似度分布整体偏移,原先 0.78 的阈值实际相当于新模型下的 0.62,召回质量断崖下滑还无人察觉,因为没有离线评测基线。

曹静

六道闸门里最容易被跳过的是切片预算。多模态进来之后,一张图一个向量,一段视频可能拆出几百个,索引量级一下翻好几倍,原来按纯文本估的成本全作废。建议先拿一个小分片压测,别全量迁完才发现撑不住。

登录 后参与回复