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 模型的即時價格