Skip to main content
文本向量化 讲了怎么把接口调通。这一页讲怎么把它用对 Embedding 的坑几乎都不在「调不通」——接口返回 200、维度也对,但召回质量悄悄崩掉。 下面每一条建议都对应 API易 2026 年 8 月 25 日 (UTC+8) 的实测数据,不是通用套话。
测试方法:围绕「大模型网关接入」构造 20 篇中文文档 + 一一对应的 20 篇英文文档, 20 条中文 query + 20 条英文 query 人工标注答案,同一批语料在同一时间窗内跑三个模型。 语料规模有限,小于 5 个百分点的差距不构成结论,请按自己的语料复现。

一、先选对模型

选 bge-m3 的场景

  • 语料以中文为主:检索质量与 3-small 同档,单价是它的一半, 同一批中文文本的 token 数又只有它的 42% —— 实际花费约 1/5
  • 想省存储:1024 维比 1536 省 33%、比 3072 省 66%
  • 需要覆盖冷门语种(100+ 语言)
  • 希望本地能跑同一个开源模型,保证线上线下向量一致

选 OpenAI 的场景

  • 语料以英文或代码为主:检索质量高一档。这类内容上 bge-m3 的 token 数反而多 15%–50%, 但单价减半之后总花费仍然更低,所以这里该按质量选,不是按价格选
  • 知识库里中英文混排、又只要返回一条答案
  • 需要 dimensions 降维来压缩存储
  • 已有大量按 OpenAI 分数带标定的阈值,不想重标
中英混合库是 bge-m3 的弱项(Recall@1 65%)。原因不是它跨语言不行,恰恰相反: 它给「同一件事的中文版和英文版」打的分太接近(实测 0.75–0.87,OpenAI 只有 0.56–0.69), 中文提问经常把英文版排到中文版前面。只要一条答案的 RAG,请按语种分库,或在检索时带上语种过滤条件; 要跨语种把资料召齐的场景,这反而是它的优点。

二、长文档一定要切块

bge-m3 的窗口是 8192 token,一篇上万字的手册整篇塞得进去。但不该这么做。 实测:把 20 个小节串成一篇长手册,候选集里另放 20 篇「每篇正好对应其中一节」的强干扰短文, 再用 20 条问题去检索—— 同一个问题,对「整篇」和「正确那一小节」的相似度平均相差 +0.10 一篇长文的单一向量是全文语义的平均值,会被任何一段精准的短文本压过去。
切块建议
  • 按语义段落切 200–500 token,块间重叠 10%–15%
  • 中文约 2.1 字一个 token,所以 200–500 token ≈ 420–1050 个汉字
  • 不要切成几十 token 的碎块:每条输入固定附带 2 个特殊 token, 500 token 的块里占 0.4%,16 token 的碎块里就是 12.5% 的纯浪费
  • 把标题拼进每个块的开头,能明显改善「这段在讲什么」的可辨识度

三、相似度阈值必须按模型重标

这是从 OpenAI 迁到 bge-m3 时最容易翻车的地方。两者的分数带完全不同。 同一批人工标注的语义对,三个模型给出的分:
bge-m3地板在 0.42,OpenAI 在 0.09。 照抄「低于 0.3 就丢掉」这类经验值,在 bge-m3 上等于完全不设防; 照抄「0.8 以上才算相关」,则会把绝大多数正确结果一起扔掉。
实测出的最佳单一阈值(bge-m3): 作为对照,text-embedding-3-small 中文场景的最佳阈值是 0.453-large0.33
落地做法:起步取 0.5,把 0.45–0.60 当成需要人工确认的灰区, 上线前用自己语料的 50–100 条标注样本重标一次。

四、相似度不能用来判断「说得对不对」

这一条对所有 embedding 模型都成立,不是某个模型的缺陷,但必须提前知道: 三个模型全部失守。余弦相似度衡量的是「在不在谈同一件事」,不是「说法是否一致」。 所以否定、价格数字、版本号、实体名这类关键差异,检索阶段一定分不开。正确的兜底是:
1

向量召回 Top 50–100

bge-m3 把候选范围快速缩小,阈值只用来挡掉明显无关的。
2

重排序精排 Top 3–5

bge-reranker-v2-m3 对候选逐条打分。 它是 query 和文档拼在一起过一遍模型的 Cross-Encoder,恰好擅长区分这类细微差异, 而且和 bge-m3 出自同一个模型家族。
3

生成阶段让大模型自己判断

把 Top 3–5 连同原始问题一起交给大模型,在提示词里明确要求「若检索内容与问题不符,直接说没有找到」。

五、批量与并发怎么开

批量:64–128 是拐点

128 条以后每条摊薄成本几乎不再下降(58ms → 53ms),单次耗时却涨了 7 倍。 单次耗时直接决定客户端超时风险,以及一次失败要重做多少工作。

并发:线上 8,灌库 32–48

  • 线上实时检索走并发 8:实测 200 次零失败
  • 离线灌库可以开到 32–48:吞吐最高,但已经开始出现 429,必须带指数退避
  • 不要超过 64:96 并发失败率 11.8%,且出现 59 秒级的挂起请求
客户端必须带重试。 即使在低并发下也观测到约 0.8% 的连接被中断 (Connection aborted / Remote end closed connection)。 这类失败重试一次就能过,但不重试就是灌库中间断一条。离线灌库的客户端超时建议设 60–90 秒,不要设几百秒等着 —— 挂起比失败更难处理。

六、成本怎么算

成本由两件事相乘决定:单价同一段文本被切成多少 token。两者在这里都不一样。 bge-m3$0.01 / 1M tokenstext-embedding-3-small$0.02,单价先差一半。 再叠上分词效率——bge-m3 用 XLM-R 的 SentencePiece,中文约 2.1 字/token, OpenAI 的 cl100k 中文只有约 0.9 字/token 中文语料的实际花费约为 text-embedding-3-small 的 1/5。 英文和代码上 bge-m3 消耗的 token 更多,但单价减半之后总花费仍然更低—— 所以这两类语料该不该用它,看的是检索质量(英文 Recall@1 70% vs 80%),不是价格 存储侧同样有差距。向量已 L2 归一化、返回值本身就是 fp16 精度, 所以用 float16 存 bge-m3 的向量是零精度损失的
向量已经归一化(实测 L2 范数 0.99992–1.00029),所以点积就等于余弦相似度。 向量库里索引选 IP(内积)和 COSINE 结果等价,选 IP 还能省一次归一化开销。

七、几个会静默出错的坑

7.1 LangChain 的默认配置会让召回率从 80% 掉到 15%

langchain_openai.OpenAIEmbeddings 默认 check_embedding_ctx_length=True, 它会先用 tiktoken 把文本编码成 token id,再把整数数组发给 /v1/embeddings这在 OpenAI 自家模型上没问题,因为分词器就是 tiktoken。 但 bge-m3 用的是 XLM-R 分词器,两套 id 空间完全不同。接口照样返回 200、维度照样是 1024、usage 也正常,只有召回质量悄悄崩掉。
实测代价: 修复方式:
同类风险存在于任何「客户端先分词再发 id」的封装。接第三方 embedding 模型时, 先确认 SDK 发出去的是原始文本还是 token id。

7.2 空字符串会被当成有效输入

input: ""bge-m3 上返回 200(OpenAI 官方此处是 400),会得到一条 1024 维向量并计费 2 token。 切块脚本如果没过滤空块,知识库里就会混进一批无意义向量,还会在检索时随机冒出来。 入库前自己过滤空白文本。

7.3 dimensions 参数不能用

返回 400:Model "bge-m3" does not support matryoshka representation, changing output dimensions will lead to poor results. bge-m3 没有做 Matryoshka 训练,截断向量会显著掉点。要压缩存储请用 float16,不要自己截断维度。

7.4 模型名大小写敏感、没有别名

只有 bge-m3 可用。BAAI/bge-m3BGE-M3 都会返回 503「无可用渠道」。

7.5 8192 是「每条输入」的上限

单条超过 8192 token 直接返回 400,不会静默截断(这是好事:不会拿到一个丢了后半段却看着正常的向量)。 但这个上限是逐条判定的,不是整次请求:实测单次请求塞进 1024 条 / 102560 token 仍然正常返回。 批量里只要有一条超限,整个请求就 400,报错里的 token 数是那一条的,不是总和。

八、一份可以直接抄的最小实现

相关文档

文本向量化 API

接口参数、返回格式、快速上手

重排序模型

bge-reranker-v2-m3,精排环节的正确解法

RAG 实战调优

两段式检索架构、召回条数怎么定

模型价格

全部 embedding 模型的实时价格