← 返回资讯

RAG Embedding 选型指南

2026-07-01

上线一个 RAG 系统之后最容易被忽视的一个环节就是 Embedding 选型——很多团队把精力全砸在 Prompt 和重排上,结果检索 Top-5 里压根没有正确答案,回头查半天才发现问题出在最底层的向量化模型选错了。Embedding 模型的好坏直接决定 RAG 系统的检索天花板:模型不好,后续重排再强也难救回来。选型核心是语言匹配度 × 语义质量 × 运营成本三角平衡,而非单纯追求最高维度。

这里先说一个容易踩的坑:Embedding 选型不是一次性决策。你线上跑着跑着发现效果不达标想换模型,这时候不是改个配置那么简单——旧向量库里存的全是旧模型的向量,新模型生成的向量哪怕维度一样,坐标系也完全不同,必须整库重新生成、覆盖写入,中途不能新旧混跑。所以选型这一步宁可多花一天做评测,也别急着上线后再返工。

主流模型横向对比

模型维度最大输入 token语言定价(1M token)部署方式
text-embedding-3-small15368191多语言~$0.02API
text-embedding-3-large30728191多语言~$0.13API
BAAI/bge-m310248192中英双语优秀免费本地/自建
BAAI/bge-large-zh1024512中文专项免费本地/自建
Cohere embed-v31024512多语言~$0.10API
jina-embeddings-v310248192多语言~$0.02API/本地

维度越高不等于越好:text-embedding-3-small 在中文 MTEB 基准上得分与 large 版本差距较小,但成本相差 6 倍,大部分中文业务场景选 small 足够。

补两个表格里体现不出来的细节。第一个是 BAAI/bge-large-zh 那个 512 token 的输入上限——这不是一个宽松的参考值,是硬截断,超出部分模型直接丢弃不报错,如果你的文档切块没控制好长度,很可能一大段内容被悄悄截掉都不知道,检索出来的向量其实只代表了半句话的语义。第二个是 Cohere embed-v3 的一个隐藏能力:它支持 input_type 参数区分 search_documentsearch_query,也就是说索引文档和检索查询用的是两套不同的编码策略,理论上比”一套向量打天下”的模型更贴近真实检索场景,如果你的业务里查询语句和文档语言风格差异很大(比如查询是短问句、文档是长陈述句),这个细节值得纳入对比。

选型决策树

你的文档主要是中文吗?
  ├─ 是 → 有 GPU 自建能力?
  │         ├─ 是 → bge-m3(MTEB 中文 SOTA,离线批量处理快)
  │         └─ 否 → text-embedding-3-small(成本低、API 稳定)
  └─ 否(英文为主或中英混合)
            ├─ 精度优先 → text-embedding-3-large / Cohere embed-v3
            └─ 成本优先 → text-embedding-3-small / jina-v3

关键工程参数

维度裁剪(Matryoshka Embedding):text-embedding-3 系列支持指定输出维度(如 256/512),在向量库存储紧张时可裁剪,精度损失可接受。

from openai import OpenAI
client = OpenAI()

response = client.embeddings.create(
    input="你的文档片段",
    model="text-embedding-3-small",
    dimensions=512  # 裁剪到 512 维,节省存储 67%
)
vector = response.data[0].embedding

这个 dimensions 参数背后的原理叫 Matryoshka Representation Learning(俄罗斯套娃表征学习),训练时就刻意把”更重要的语义信息”排在向量前几百维,后面的维度是对前面维度的补充精修。所以从 1536 裁到 512,本质是”截取信息密度最高的那一段”,而不是简单丢弃——这也是为什么精度损失能控制在可接受范围。

这里有个容易踩的坑必须提醒:这套裁剪机制只对明确标注支持 MRL 训练的模型有效(比如 text-embedding-3 系列、部分开源 bge 新版本)。如果你对一个没有 MRL 训练过的老模型(比如 text-embedding-ada-002)强行做维度截断,效果会断崖式下跌,因为它的每一维权重是均匀分布的,砍掉后半段等于随机丢信息。调用前一定确认模型文档里明确写了支持自定义 dimensions,没写就别赌。

另外,裁剪之后向量已经不是单位长度了,如果你的向量库用余弦相似度检索,大部分库会自动做归一化没问题;但如果你自己手写点积计算相似度,记得裁剪后手动对向量做 L2 归一化,否则相似度分数会失真,排序也会跟着错。

批量处理:索引阶段切勿逐条调用 API,单次最多传 2048 条文本,吞吐量提升 10x 以上:

# 批量 embedding,切块后分批送入
def batch_embed(texts: list[str], batch_size=512) -> list[list[float]]:
    results = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        resp = client.embeddings.create(model="text-embedding-3-small", input=batch)
        results.extend([d.embedding for d in resp.data])
    return results

批量跑索引的时候,你大概率会撞上 RateLimitError: 429,报错信息一般长这样:Rate limit reached for text-embedding-3-small in organization ... on tokens per min。根因不是你调用次数多,而是每分钟 token 消耗量超过了账号等级配的上限——批量传入的文本越长,单次请求消耗的 token 越多,越容易撞线。裸重试没用,得配退避策略,实践中这样写更稳:

import time
import random
from openai import RateLimitError

def batch_embed_with_retry(texts: list[str], batch_size=512, max_retries=5):
    results = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        for attempt in range(max_retries):
            try:
                resp = client.embeddings.create(model="text-embedding-3-small", input=batch)
                results.extend([d.embedding for d in resp.data])
                break
            except RateLimitError:
                if attempt == max_retries - 1:
                    raise
                wait = (2 ** attempt) + random.random()  # 指数退避 + 抖动
                time.sleep(wait)
    return results

指数退避里那个 random.random() 的抖动不是随手加的:如果你有多个并发任务同时撞到限流,不加抖动的话它们会按同样的间隔一起重试,等于变相制造下一波限流峰值,加了随机抖动能把重试请求错开。

批量传文本还有个隐藏坑是单条文本本身超长。OpenAI 的 Embedding 接口对单条输入也有 token 上限(text-embedding-3 系列是 8191 token),如果切块时没控制好长度,报错会是 This model's maximum context length is 8191 tokens, however you requested ... tokens,此时不是重试能解决的,得回头检查切块逻辑——中文场景下经验值是 1 个汉字约等于 1.5~2 个 token,切块时按这个比例预留余量,别卡着上限切。

评测方法:别靠感觉选型

用自己的数据集评测才可靠,推荐用 Hit Rate@5 作为核心指标:

# 准备 50~100 条"问题-正确文档"对
# 对每个问题检索 Top-5,看正确文档是否出现
hit = sum(1 for q, doc in qa_pairs if doc in retrieve(q, top_k=5))
hit_rate = hit / len(qa_pairs)
print(f"Hit Rate@5: {hit_rate:.2%}")

经验值:Hit Rate@5 > 80% 视为合格;低于 60% 说明 Embedding 与语料不匹配,需换模型或优化切块。

Hit Rate@5 好用但有个盲区:它只回答”正确文档有没有出现在 Top-5 里”,不管排第 1 还是排第 5。如果你的下游用法是把 Top-5 整段塞给大模型做生成,排名靠后问题不大;但如果你只取 Top-1 或者要展示”最相关的一条”给用户看,就得换成 MRR(Mean Reciprocal Rank,平均倒数排名)——正确答案排第几名,得分就是 1/名次,再对所有问题取平均。同样的检索结果,Hit Rate@5 可能显示合格,MRR 却很低,说明模型能”找到”但排序能力差,这时候该动的是重排模型而不是 Embedding 本身。

评测时还有个真实踩过的坑:中文语料下,问题和文档如果分词粒度差异很大,Hit Rate 会莫名其妙偏低。比如文档里写的是”大模型推理成本”,用户问题问的是”跑一次大模型要多少钱”,字面重叠几乎为零,纯靠 Embedding 的语义理解能力兜底。如果你评测发现某个模型在这类”问法和文档用词差异大”的问题上得分明显更低,说明它的语义泛化能力弱,这种情况堆参数或者换个”看起来更强”的模型未必有用,反而应该先检查评测集里这类问题占比高不高,再决定要不要加混合检索(关键词+向量)来兜底,具体做法可以参考混合检索:关键词+向量

常见问题

能混用多个 Embedding 模型吗? 不推荐在同一个向量库里混用,因为不同模型的向量空间不可比较,检索时会产生错误的相似度排名。如果需要切换模型,必须对整个知识库重新 embedding 后再覆盖写入。

中文文档用英文 Embedding 模型效果差吗? 取决于模型。text-embedding-3-small 的多语言能力较强,中文效果可接受。bge-large-zh 针对中文专项训练,在纯中文语料上通常高出 5~10 个百分点,但不支持长文本(最大 512 token),切块时需更细。

Embedding 模型需要 fine-tune 吗? 大多数场景不需要。先评测标准模型,Hit Rate 低时优先优化切块策略和混合检索,最后才考虑对 Embedding 做领域微调(成本较高)。

调用返回 401 或者 403 怎么排查? 先别怀疑账号被封,大概率是三个原因之一:一是 API Key 复制的时候带了首尾空格或换行符(尤其从网页复制粘贴到 .env 文件很容易带进去),二是 Key 对应的是别的接口(比如把 Chat 用的 Key 拿去调 Embedding 接口,权限范围不一样),三是账号欠费或者免费额度用完了但请求还在发。排查顺序建议:先打印 Key 的长度和前后 4 位确认没有隐藏字符,再去控制台确认这个 Key 有没有 Embedding 权限,最后看账单页面余额。

429 限流和普通网络超时怎么区分处理? 429 是服务端主动拒绝,重点是控制请求节奏(降低并发数、加大批次间隔、上面那段指数退避代码),加大 timeout 参数没有意义,因为请求根本没被处理。真正的超时(比如 httpx.ReadTimeout 或者 requests.exceptions.Timeout)才是网络链路或者服务端处理慢导致的,这种情况可以适当调大 timeout(比如从默认 10 秒调到 30 秒),同时也要考虑是不是单次批量传的文本量太大导致服务端处理耗时变长,把 batch_size 调小往往比一味加长超时更管用。

归一化(normalize)这一步能省略吗? 如果你直接用向量库自带的余弦相似度检索,大多数向量库内部会自动处理归一化,不用你操心。但如果你打算自己写点积或者欧氏距离来算相似度,就必须先对向量做 L2 归一化(把向量除以自身的模长),否则不同长度的文本生成的向量模长不一样,相似度分数会被这个模长差异污染,排序直接失真。一个简单的自检方法:算完相似度后抽几条你人工判断”明显不相关”的文档,如果它们的分数反而偏高,大概率就是没做归一化。


← 返回 应用模式总览:从 Prompt 到 Agent | 应用模式专题

相关阅读:RAG 重排提升召回 · 混合检索:关键词+向量 · 向量数据库选型

Embedding API 调用量大、成本敏感?力达云聚合 API 支持统一切换 OpenAI / Cohere / 国内 embedding 提供商,无需改代码。