戰(zhàn):融合BM25與向量召回提升知識(shí)庫(kù)搜索效果)
上個(gè)月我在處理某知識(shí)庫(kù)系統(tǒng)的搜索需求時(shí)被兩個(gè)完全相反的問(wèn)題夾擊用戶問(wèn)“設(shè)備重啟后沒(méi)有聲音”向量檢索模型輕松召回了音頻故障相關(guān)文檔但同一套系統(tǒng)在面對(duì)“RT-2200 說(shuō)明書(shū)”這種查詢時(shí)把型號(hào)切成了一段語(yǔ)義碎片結(jié)果排在第一頁(yè)的居然是某個(gè)型號(hào)介紹頁(yè)面。反過(guò)來(lái)看那套還在被業(yè)務(wù)方懷念的全文搜索引擎處理口語(yǔ)化問(wèn)題時(shí)完全啞火——用戶說(shuō)“屏幕一直閃”文檔里寫(xiě)的是“顯示屏出現(xiàn)頻閃故障”BM25 根本匹配不到。兩邊都覺(jué)得自己沒(méi)做錯(cuò)可用戶就是找不到東西。這就是混合檢索Hybrid Search真正要解決的問(wèn)題。它不是一個(gè)花哨的模型也不是某種“銀彈算法”而是把兩種互補(bǔ)的召回機(jī)制拼在一起精確匹配靠關(guān)鍵詞語(yǔ)義理解靠向量。這篇我把這節(jié)內(nèi)容完整展開(kāi)——兩種檢索到底各有什么死角、分?jǐn)?shù)怎么融合、項(xiàng)目里怎么落地、調(diào)參的時(shí)候真實(shí)踩過(guò)哪些坑以及為什么混合檢索在某些數(shù)據(jù)集上反而沒(méi)用。1. 單一通道的死角向量檢索會(huì)騙人關(guān)鍵詞檢索會(huì)漏人要理解混合檢索的價(jià)值不能只看它“多了一個(gè)召回通道”這一表象而要回到一個(gè)扎心的事實(shí)每次只用一個(gè)通道做召回必然漏掉另一類本應(yīng)被命中的文檔。而這種漏不是調(diào)調(diào)參數(shù)就能解決的。1.1 向量檢索的“語(yǔ)義幻覺(jué)”先說(shuō)稠密檢索也就是向量檢索。它的基本思路是把文本映射成一個(gè)幾百維的向量查詢時(shí)把用戶 query 也映射成向量然后找距離最近的文檔片段。問(wèn)題在于嵌入模型擅長(zhǎng)的是“語(yǔ)義相似”而不是“信息正確”。舉個(gè)體感很明顯的例子。我在某電商客服知識(shí)庫(kù)里做過(guò)測(cè)試收錄了兩篇文檔一篇標(biāo)題是《筆記本電腦電池鼓包檢測(cè)與處理》另一篇是《移動(dòng)電源鼓包風(fēng)險(xiǎn)警告》用戶輸入“筆記本電池鼓包怎么檢測(cè)”向量檢索給這兩篇文檔的相似度分?jǐn)?shù)幾乎一樣。如果你用向量結(jié)果直接去生成回答大模型完全有可能拿移動(dòng)電源那篇的內(nèi)容拼進(jìn)筆記本的回答里。關(guān)鍵詞檢索不會(huì)有這個(gè)問(wèn)題因?yàn)椤肮P記本”“移動(dòng)電源”這兩個(gè)詞面完全不同BM25 一眼就能區(qū)分開(kāi)。向量檢索另一個(gè)天然弱點(diǎn)是精確匹配。型號(hào)、訂單號(hào)、日期、SKU 編號(hào)這類 token在嵌入模型眼里只是一團(tuán)沒(méi)有語(yǔ)義的字符串經(jīng)常被“平均化”甚至直接忽略。用戶輸入“單號(hào) INV-2024-0917 的發(fā)票要紅沖”向量檢索會(huì)把重心放在“發(fā)票”“紅沖”上而“INV-2024-0917”這個(gè)最關(guān)鍵的約束反倒沒(méi)起作用。你搜“RT-2200 說(shuō)明書(shū)”向量模型可能把“RT-2200”拆成“RT”和“2200”當(dāng)成兩個(gè)互不相關(guān)的詞命中的結(jié)果自然亂七八糟。1.2 關(guān)鍵詞檢索的“字面潔癖”BM25 這類稀疏檢索走的是另一條極端只看詞面不管語(yǔ)義。它的眼神好到能捕捉“INV-2024-0917”這種精確串號(hào)但壞也壞在太死板用戶稍微換個(gè)說(shuō)法它就六親不認(rèn)。真實(shí)客服日志里“屏幕一直閃”和“顯示屏出現(xiàn)頻閃故障”在語(yǔ)義上指向同一個(gè)問(wèn)題可 BM25 對(duì)這組詞的重疊度計(jì)算幾乎為零?!伴W”和“頻閃”確實(shí)有共同字但詞頻統(tǒng)計(jì)和逆文檔頻率并不會(huì)自動(dòng)把它們當(dāng)成同一個(gè)意圖。用戶說(shuō)“重裝系統(tǒng)后電腦卡頓”文檔里寫(xiě)的是“操作系統(tǒng)重新安裝之后出現(xiàn)性能下降”這是語(yǔ)義上的同一件事字面上沒(méi)有任何一個(gè)關(guān)鍵詞對(duì)齊。中文場(chǎng)景下還有分詞這道坎。同一段話用“重新啟動(dòng)”和“重啟”兩種說(shuō)法分詞器切出來(lái)的詞條完全不同BM25 的得分自然天差地別。你當(dāng)然可以花大力氣維護(hù)同義詞表、近義詞庫(kù)但這是一條永遠(yuǎn)追不完的路——新詞每天都有黑話和縮寫(xiě)層出不窮靠人工詞典去補(bǔ)語(yǔ)義鴻溝本質(zhì)上是拿慢動(dòng)作追趕高速列車。1.3 先分清“召回”和“排序”再談混合很多人一聽(tīng)混合檢索就覺(jué)得是“把兩個(gè)打分列表按某種權(quán)重相加”這其實(shí)是誤解。要理解混合檢索的真正價(jià)值必須先分清兩個(gè)概念召回Recall和排序Ranking。召回是從整個(gè)文檔庫(kù)里把“可能相關(guān)”的候選撈出來(lái)排序是對(duì)候選集精排。排序做得再好如果候選池里本身就沒(méi)有正確文檔一切都是零。傳統(tǒng)搜索里這兩個(gè)階段通常在同一個(gè)引擎里完成但在向量檢索興起之后很多人只關(guān)注排序階段的相似度分?jǐn)?shù)卻忽略了向量召回本身可能從一開(kāi)始就漏掉了精確匹配的那一篇?;旌蠙z索的核心價(jià)值不在于把兩個(gè)排序結(jié)果優(yōu)雅地合并而是讓兩個(gè)“召回通道”互補(bǔ)關(guān)鍵詞通道保證精確匹配不會(huì)漏向量通道保證語(yǔ)義改寫(xiě)不會(huì)被丟。把候選先撈全后面的重排才有意義。2. 兩套不同的打分哲學(xué)BM25 與向量相似度在算什么既然要混合就得先搞清楚混的是什么東西。BM25 和向量相似度是兩套完全不同的打分體系各自的分?jǐn)?shù)含義、分布、量綱都毫無(wú)可比性。先拆開(kāi)講清楚再談融合。2.1 BM25 的老派公式詞頻、IDF 與文檔長(zhǎng)度懲罰BM25 是很多年前從概率檢索模型演化出來(lái)的打分函數(shù)到今天依然霸榜全文檢索靠的不是新而是簡(jiǎn)單可靠。它的公式長(zhǎng)這樣score(D, Q) Σ IDF(qi) × [ f(qi, D) × (k1 1) ] / [ f(qi, D) k1 × (1 - b b × |D| / avgdl) ]這里的 f(qi, D) 是詞項(xiàng) qi 在文檔 D 中的出現(xiàn)次數(shù)|D| 是文檔長(zhǎng)度avgdl 是語(yǔ)料平均文檔長(zhǎng)度k1 取 1.2 到 2.0 之間b 通常取 0.75。翻譯成人話就是這個(gè)詞在文檔里出現(xiàn)得越多文檔得分越高但這個(gè)詞如果在整個(gè)語(yǔ)料里遍地都是比如“的”“是”“問(wèn)題”它的逆文檔頻率 IDF 就會(huì)被壓得很低起不到區(qū)分作用文檔越長(zhǎng)詞頻帶來(lái)的收益越要打折免得長(zhǎng)文檔靠堆字?jǐn)?shù)取勝。舉個(gè)例子一個(gè)文檔里“發(fā)票”出現(xiàn)了 5 次另一個(gè)文檔出現(xiàn) 2 次如果“發(fā)票”在整個(gè)知識(shí)庫(kù)里出現(xiàn)在大量文檔中前者的優(yōu)勢(shì)沒(méi)那么大但如果“發(fā)票紅沖”這個(gè)詞組只出現(xiàn)在少數(shù)幾個(gè)文檔里那只要出現(xiàn)一次就基本鎖定目標(biāo)。BM25 的“可解釋性”是它最大的優(yōu)點(diǎn)任何一個(gè)分?jǐn)?shù)你都能說(shuō)出是哪個(gè)詞貢獻(xiàn)的排查問(wèn)題無(wú)比方便。而它的局限也很鮮明所有計(jì)算都建立在詞面精確相等之上換一種說(shuō)法即失效。2.2 向量嵌入把一句話變成一個(gè)坐標(biāo)點(diǎn)向量嵌入的思路完全不一樣。先用一個(gè)預(yù)訓(xùn)練模型把每個(gè)文本片段編碼成一個(gè)固定維度的向量比如常見(jiàn)的 768 維。語(yǔ)義相近的文本在高維空間里的向量距離也相近。查詢時(shí)把用戶 query 也編碼成同維向量然后計(jì)算 query 向量與文檔向量的余弦相似度cosine_similarity(A, B) (A · B) / (|A| × |B|)這個(gè)分?jǐn)?shù)越接近 1表示兩個(gè)文本在語(yǔ)義上越接近。它的優(yōu)勢(shì)在于能繞過(guò)“字面差異”用戶說(shuō)“設(shè)備重啟后沒(méi)有聲音”文檔寫(xiě)的是“開(kāi)機(jī)后揚(yáng)聲器無(wú)聲”它們的用詞完全不同但在語(yǔ)義空間里可能離得很近模型能識(shí)別出這是同一類問(wèn)題。但要注意向量嵌入是在語(yǔ)義層面做壓縮必然會(huì)舍棄一些字面細(xì)節(jié)。一個(gè) 768 維的向量要承載整段話的含義它不可能把每一個(gè)型號(hào)、每一個(gè)編號(hào)都精確保留下來(lái)。這就是為什么純粹做語(yǔ)義檢索的系統(tǒng)在精確匹配類查詢上總有一種“模棱兩可”的難受感——它不是不會(huì)查而是天然不擅長(zhǎng)。2.3 分?jǐn)?shù)融合的三種主流算法當(dāng)兩個(gè)通道各回一份候選列表后問(wèn)題就來(lái)了BM25 的分?jǐn)?shù)可能是 4.8向量相似度可能是 0.72這倆完全不是一個(gè)量綱怎么合并目前業(yè)界常見(jiàn)做法主要有三種特性對(duì)比如下融合方式計(jì)算邏輯優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景RRFReciprocal Rank Fusionscore Σ 1 / (k rank)k 通常取 60對(duì)原始分?jǐn)?shù)尺度完全免疫只依賴排名穩(wěn)定、可解釋忽略原始分?jǐn)?shù)強(qiáng)度前兩名之間的差距被壓平快速上線、兩個(gè)通道分?jǐn)?shù)量綱差異極大的情況歸一化加權(quán)和先對(duì)兩個(gè)通道的分?jǐn)?shù)做 min-max 歸一化再按權(quán)重相加權(quán)重可調(diào)能精細(xì)控制兩個(gè)通道影響力依賴歸一化統(tǒng)計(jì)方式批次波動(dòng)會(huì)帶來(lái)不穩(wěn)定已有評(píng)測(cè)集、追求最佳效果學(xué)習(xí)排序LTR把特征輸入排序模型訓(xùn)練打分器上限最高需要標(biāo)注數(shù)據(jù)、訓(xùn)練成本高數(shù)據(jù)量大、長(zhǎng)期迭代的搜索系統(tǒng)RRF 特別值得多說(shuō)兩句。它的思想很樸素如果一個(gè)文檔在兩個(gè)通道里排名都靠前那它一定很相關(guān)。把排名的倒數(shù)加在一起排名第 1 貢獻(xiàn) 1/(160)≈0.016排名第 10 貢獻(xiàn)約 0.014雖然差距不大但多次取和就體現(xiàn)出排序穩(wěn)定的文檔優(yōu)勢(shì)了。實(shí)踐中我多數(shù)時(shí)候會(huì)先用 RRF 跑通流程效果不好再切換成歸一化加權(quán)和做精細(xì)調(diào)參。3. 落地方案在知識(shí)庫(kù)搜索里完成混合檢索改造理論講完上實(shí)操。我做知識(shí)庫(kù)搜索時(shí)常用兩套方案一是全部跑在同一個(gè)融合引擎里二是向量庫(kù)和關(guān)鍵詞引擎分開(kāi)部署、在應(yīng)用層合并。下面詳細(xì)展開(kāi)。3.1 場(chǎng)景預(yù)設(shè)與數(shù)據(jù)準(zhǔn)備為了講得具體我虛擬一個(gè)項(xiàng)目某公司產(chǎn)品知識(shí)庫(kù)規(guī)模約 2 萬(wàn)篇文檔包含產(chǎn)品說(shuō)明書(shū)、故障排查指南、售后政策、常見(jiàn)問(wèn)題 FAQ。用戶的典型查詢風(fēng)格非常口語(yǔ)化什么“我的設(shè)備老是重啟怎么辦”“屏幕閃得厲害”“RT-2200 說(shuō)明書(shū)有嗎”兩類問(wèn)題夾雜出現(xiàn)。數(shù)據(jù)準(zhǔn)備階段最重要的一步是分塊chunk。我見(jiàn)過(guò)很多失敗案例都是直接用固定長(zhǎng)度把文檔切碎結(jié)果把一個(gè)故障現(xiàn)象和它的解決方案切成兩半搜索時(shí)明明命中了片段片段里卻沒(méi)有答案。我采用的做法是先按段落切再把段落內(nèi)超過(guò) 800 字的繼續(xù)拆分相鄰塊之間保留 80~120 字的重疊確??缍涡畔⒉粊G。同時(shí)把標(biāo)題、文檔類型、更新時(shí)間放進(jìn)元數(shù)據(jù)后續(xù)排序和過(guò)濾都靠它。每一條文檔片段最終需要兩份表達(dá)一份是原始文本喂給 BM25一份是嵌入向量喂給向量檢索。同一個(gè)文本片段兩個(gè)通道各自索引互不干擾。3.2 方案一在統(tǒng)一引擎里跑雙通道 RRF如果現(xiàn)有技術(shù)棧已經(jīng)用了開(kāi)源搜索引擎且版本支持 RRF可以直接在一個(gè)引擎里完成混合檢索。下面這個(gè)示例基于 8.8 以上版本的開(kāi)源搜索服務(wù)的原生 RRF 能力索引中有文本字段和向量字段。先創(chuàng)建索引映射里同時(shí)聲明 text 類型和 dense_vector 類型PUT /knowledge_base { mappings: { properties: { title: { type: text, analyzer: ik_max_word }, content: { type: text, analyzer: ik_max_word }, content_vector: { type: dense_vector, dims: 768, index: true, similarity: cosine }, doc_type: { type: keyword }, updated_at: { type: date } } } }寫(xiě)入文檔時(shí)需要同時(shí)塞入文本內(nèi)容和向量。Python 偽代碼如下import requests def index_document(doc_id, title, content, embedding): payload { title: title, content: content, content_vector: embedding.tolist(), doc_type: doc_type, updated_at: updated_at } requests.put( f{ES_URL}/knowledge_base/_doc/{doc_id}, jsonpayload )查詢時(shí)同時(shí)執(zhí)行兩個(gè)子查詢一個(gè)走 BM25 的 match 查詢一個(gè)走向量的 knn 查詢?cè)儆?RRF 把結(jié)果融合。核心查詢體如下POST /knowledge_base/_search { size: 10, query: { rrf: { rank_window_size: 100, retriever: [ { standard: { query: { match: { content: 屏幕閃得厲害 } } } }, { knn: { field: content_vector, query_vector: [0.12, -0.35, ...], k: 100, num_candidates: 200 } } ] } } }rank_window_size 是 RRF 的候選窗口大小我一般設(shè) 100意思是每個(gè)通道各取前 100 條進(jìn)入融合池。k 是向量通道要返回的近鄰數(shù)num_candidates 是圖搜索的候選數(shù)量。k 設(shè)太小會(huì)把質(zhì)量差一點(diǎn)的候選直接排除在融合池外設(shè)太大又拖慢速度100 是個(gè)兼顧的起點(diǎn)。這套方案的好處是運(yùn)維簡(jiǎn)單一個(gè)引擎搞定全部。缺點(diǎn)是如果你還用著不支持 RRF 的舊版本就得自己在應(yīng)用層實(shí)現(xiàn)融合邏輯。3.3 方案二向量數(shù)據(jù)庫(kù)與關(guān)鍵詞引擎分頭部署、應(yīng)用層合并還有一種常見(jiàn)架構(gòu)向量數(shù)據(jù)已經(jīng)放在某個(gè)開(kāi)源向量數(shù)據(jù)庫(kù)里關(guān)鍵詞搜索則跑在另外一套搜索引擎上。兩個(gè)系統(tǒng)各自召回應(yīng)用層寫(xiě)一個(gè)融合函數(shù)把結(jié)果合并。融合邏輯不復(fù)雜用 Python 實(shí)現(xiàn) RRF 也就幾十行def rrf_fusion(results_a, results_b, k60, size10): score_map {} # results_a: list of (doc_id, rank) # results_b: list of (doc_id, rank) for rank, doc_id in enumerate(results_a): score_map[doc_id] score_map.get(doc_id, 0) 1.0 / (k rank 1) for rank, doc_id in enumerate(results_b): score_map[doc_id] score_map.get(doc_id, 0) 1.0 / (k rank 1) # 按融合分?jǐn)?shù)從高到低取前 size 個(gè) ranked sorted(score_map.items(), keylambda x: x[1], reverseTrue) return [doc_id for doc_id, _ in ranked[:size]]實(shí)際項(xiàng)目中這個(gè)函數(shù)會(huì)被包在一個(gè)服務(wù)接口里調(diào)用方傳入 query服務(wù)端并行請(qǐng)求兩個(gè)引擎等兩個(gè)結(jié)果都回來(lái)后做融合。注意這里兩個(gè)請(qǐng)求可以并發(fā)發(fā)起總耗時(shí)約等于較慢的那個(gè)通道而不是兩者之和。分頭部署的好處是架構(gòu)靈活各系統(tǒng)可以獨(dú)立擴(kuò)展。壞處是要自己維護(hù)兩套索引的同步文檔新增、更新、刪除都要雙寫(xiě)。我踩過(guò)好幾次“文檔剛上線關(guān)鍵詞引擎里已經(jīng)能搜到但向量庫(kù)里還沒(méi)有”的尷尬解決方案后面單開(kāi)一節(jié)講。3.4 嵌入模型選型時(shí)我關(guān)注的三個(gè)點(diǎn)選嵌入模型這件事很多文檔里只是一句“選擇合適的模型”但實(shí)際選型時(shí)我一般盯三個(gè)點(diǎn)如果是中文為主、夾雜一些英文術(shù)語(yǔ)的文檔優(yōu)先選中文場(chǎng)景優(yōu)化過(guò)的開(kāi)源嵌入模型它們?cè)凇鞍l(fā)票紅沖”“頻閃故障”這類中文專業(yè)詞上表現(xiàn)更穩(wěn)如果文檔里經(jīng)常出現(xiàn)代碼、型號(hào)、編號(hào)要注意模型分詞是否會(huì)被這類特殊符號(hào)干擾必要時(shí)可以做字段預(yù)處理給編號(hào)加保護(hù)向量的維度要和向量數(shù)據(jù)庫(kù)及索引聲明一致。常見(jiàn)的是 768 維或 1024 維有的模型輸出 1536 維改動(dòng)成本不低最好在第一步就定下來(lái)。4. 權(quán)重怎么調(diào)、效果怎么驗(yàn)一次從 0.42 到 0.55 的調(diào)整記錄混合檢索不是寫(xiě)完代碼就完事融合權(quán)重怎么定效果到底好不好都得拿數(shù)據(jù)說(shuō)話。這一節(jié)講我一套固定的調(diào)參流程。4.1 先造評(píng)測(cè)集50 條 Query 加上人工標(biāo)注沒(méi)有評(píng)測(cè)集的調(diào)參全是拍腦袋。我的做法是從搜索日志里撈真實(shí)用戶 query去掉重復(fù)和亂碼挑出 50~100 條有代表性的覆蓋三類口語(yǔ)化描述、精確型號(hào)查詢、混合型查詢。然后給每條 query 標(biāo)注相關(guān)文檔。標(biāo)注很簡(jiǎn)單三道層級(jí)相關(guān)、部分相關(guān)、不相關(guān)。如果這個(gè)文檔片段能夠直接回答用戶問(wèn)題標(biāo)相關(guān)只是打擦邊球標(biāo)部分相關(guān)牛頭不對(duì)馬嘴標(biāo)不相關(guān)。標(biāo)注工作量不小但這一步省不掉后續(xù)所有權(quán)重選擇都建立在它的基礎(chǔ)上。評(píng)測(cè)指標(biāo)我用三個(gè)各有側(cè)重RecallK正確文檔出現(xiàn)在前 K 個(gè)結(jié)果里的比例K 取 10。它是召回質(zhì)量的直接度量MRRMean Reciprocal Rank第一個(gè)相關(guān)結(jié)果排在第幾位評(píng)價(jià)“用戶能不能第一眼看到答案”NDCG10綜合排序質(zhì)量相關(guān)文檔越靠前分越高部分相關(guān)也算分。4.2 歸一化細(xì)節(jié)直接把分?jǐn)?shù)相加是新手最容易踩的坑我在早先的一個(gè)項(xiàng)目里犯過(guò)這個(gè)錯(cuò)把 BM25 原始分?jǐn)?shù)和向量余弦相似度直接加權(quán)相加結(jié)果向量通道的貢獻(xiàn)幾乎被完全壓掉。原因很簡(jiǎn)單BM25 的分?jǐn)?shù)范圍通常在 0~10甚至更高而余弦相似度在 0~1 之間。權(quán)重設(shè)成 0.5 和 0.5一加之后 BM25 的分?jǐn)?shù)把向量分?jǐn)?shù)淹沒(méi)得徹徹底底。正確做法是先歸一化再相加。最常用的 min-max 歸一化是把當(dāng)前查詢返回的 topK 分?jǐn)?shù)線性變換到 [0, 1]normalized_score (raw_score - min_score) / (max_score - min_score)但這里有個(gè)隱蔽的坑。如果歸一化的 min 和 max 取自當(dāng)前查詢的返回列表那每次查詢的基準(zhǔn)都不一樣會(huì)出現(xiàn)“同一篇文檔在不同的查詢里歸一化分?jǐn)?shù)不同”的現(xiàn)象融合結(jié)果不穩(wěn)定。更穩(wěn)的做法是在評(píng)測(cè)集上先離線統(tǒng)計(jì)每個(gè)通道分?jǐn)?shù)的分位數(shù)用固定的經(jīng)驗(yàn)上下界做歸一化線上查詢時(shí)直接用同一套參數(shù)。我通常取 0.01 分位數(shù)和 0.99 分位數(shù)作為上下界遇到超出邊界的分?jǐn)?shù)就截?cái)嗵幚怼?.3 網(wǎng)格搜索與一輪真實(shí)對(duì)比歸一化方案定下來(lái)之后我會(huì)上一個(gè)網(wǎng)格搜索遍歷權(quán)重組合。假設(shè)兩個(gè)通道權(quán)重分別為 alpha 和 1-alpha把 alpha 從 0.1 到 0.9 每隔 0.1 跑一遍評(píng)測(cè)集記錄每個(gè)組合的 MRR 和 NDCG10選最優(yōu)的。代碼邏輯大致長(zhǎng)這樣best_score 0 best_alpha 0.5 for alpha in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]: final_rank merge_and_rank( bm25_results, vector_results, alphaalpha, beta1-alpha ) score evaluate(final_rank, ground_truth, metricmrr) if score best_score: best_score score best_alpha alpha在某個(gè)實(shí)際知識(shí)庫(kù)項(xiàng)目里純 BM25 的 MRR 是 0.42純向量是 0.44混合檢索用 RRF 跑到 0.51再用歸一化加權(quán)和加網(wǎng)格搜索調(diào)到 0.55。提升最大的是口語(yǔ)化查詢場(chǎng)景——用戶的描述方式和文檔里的用詞幾乎沒(méi)什么字面重合純 BM25 對(duì)這類查詢基本不可用向量通道把它們撈了回來(lái)而精確型號(hào)類查詢BM25 通道又托住了底讓向量不會(huì)把結(jié)果帶偏。但我也要潑一盆冷水如果你們的業(yè)務(wù)場(chǎng)景非常規(guī)范用戶 query 就是標(biāo)準(zhǔn)術(shù)語(yǔ)比如法律條文檢索、藥品說(shuō)明書(shū)查詢純 BM25 可能已經(jīng)做到 0.75 以上的 MRR混合檢索提升的幅度很有限甚至因?yàn)橄蛄客ǖ酪朐胍舳晕⑾陆怠;旌蠙z索不是萬(wàn)能的它的收益來(lái)自兩種查詢類型的并存。5. 回歸實(shí)戰(zhàn)分塊、同步、延遲三個(gè)容易翻車的細(xì)節(jié)最后這部分是我在真實(shí)項(xiàng)目中反復(fù)踩過(guò)的坑每一個(gè)都讓效果白降一截。希望各位不用再走一遍。5.1 分塊不合理會(huì)讓混合檢索雪上加霜混合檢索里兩個(gè)通道共用同一套分塊結(jié)果分塊切得不好兩個(gè)通道會(huì)同時(shí)受害。最典型的錯(cuò)誤是用固定 512 字硬切沒(méi)有考慮段落邊界把“問(wèn)題描述”和“解決方案”切到兩個(gè) block 里。用戶搜“重啟后沒(méi)聲音”召回了“重啟后沒(méi)聲音”這一段里面只有故障描述沒(méi)有答案而含答案的那一段因?yàn)殛P(guān)鍵詞較少排名被壓到很后面。我的建議是優(yōu)先考慮段落級(jí)分塊一個(gè)標(biāo)題下的完整小節(jié)盡量保持完整。如果段落太長(zhǎng)再切分時(shí)保留重疊區(qū)域。重疊的 text 會(huì)在 BM25 里重復(fù)計(jì)分但不影響大局換來(lái)的是語(yǔ)義完整性這筆賬劃算。5.2 兩套索引的數(shù)據(jù)一致性真不是小事分頭部署方案下向量庫(kù)和關(guān)鍵詞引擎的索引更新是兩個(gè)獨(dú)立的動(dòng)作。新增文檔時(shí)一個(gè)通道已寫(xiě)入、另一個(gè)通道還沒(méi)寫(xiě)完就會(huì)產(chǎn)生“搜得到但點(diǎn)開(kāi)是舊版本”或“新文檔完全搜不到”的問(wèn)題。最穩(wěn)的做法是引入異步事件同步文檔變更先發(fā)消息隊(duì)列兩個(gè)系統(tǒng)各自消費(fèi)更新完成的文檔再標(biāo)記為可見(jiàn)。如果不想引入消息中間件至少要有一個(gè)定期對(duì)賬任務(wù)比對(duì)新舊文檔列表把差異補(bǔ)上。我見(jiàn)過(guò)某團(tuán)隊(duì)上線混合檢索后業(yè)務(wù)反饋“前幾天上傳的新資料一直搜不到”排查發(fā)現(xiàn)是向量庫(kù)的批量導(dǎo)入任務(wù)每天晚上才跑一次新文檔要隔天才能進(jìn)向量索引。這種問(wèn)題搜索引擎不會(huì)報(bào)錯(cuò)用戶只會(huì)在搜索框里一次次嘗試然后失望離開(kāi)。5.3 延遲與成本向量維度、圖索引與并行查詢向量檢索是有成本的。一個(gè) 768 維的 float 向量單條文檔占 768 × 4 字節(jié)約 3KB。10 萬(wàn)條文檔就是 300MB上百萬(wàn)條就是 3GB這還沒(méi)算索引結(jié)構(gòu)的額外開(kāi)銷。生產(chǎn)環(huán)境里給向量字段建圖索引后內(nèi)存占用還會(huì)再翻一些需要提前規(guī)劃。查詢延遲方面暴力掃描全部向量的方式在小規(guī)模數(shù)據(jù)上還能忍數(shù)據(jù)量一大就必須用近似最近鄰索引。開(kāi)源的向量庫(kù)基本都支持 HNSW 這類圖索引關(guān)鍵字參數(shù)是 ef_search值越大召回質(zhì)量越高、延遲越高。我會(huì)在可控延遲內(nèi)把 ef_search 調(diào)到吞吐量允許的上限不要一味追求快否則混合檢索不如純關(guān)鍵詞檢索。另一個(gè)細(xì)節(jié)是并行查詢。兩個(gè)通道互相獨(dú)立完全可以在應(yīng)用層并發(fā)請(qǐng)求分別等兩個(gè)結(jié)果回來(lái)后再融合。線程池或者協(xié)程都能做這個(gè)優(yōu)化很小但收益直接總耗時(shí)從兩通道之和降到了較慢通道的時(shí)間。最后分享一個(gè)我判斷是否該上混合檢索的方法。接到一個(gè)搜索系統(tǒng)我不會(huì)上來(lái)就搭混合檢索而是先拿純 BM25 和純向量各自跑一遍測(cè)試集看各自的失分點(diǎn)在哪。如果向量失分多在型號(hào)、編號(hào)這類精確匹配BM25 失分多在口語(yǔ)化、同義改寫(xiě)那混合檢索的收益會(huì)很可觀如果兩個(gè)通道的出錯(cuò)場(chǎng)景高度重疊或者其中一邊已經(jīng)足夠好那混合的邊際收益就很小。混合檢索本身不是魔法它只是給兩個(gè)互補(bǔ)的信號(hào)一個(gè)共存的機(jī)會(huì)。真正花時(shí)間的永遠(yuǎn)是數(shù)據(jù)準(zhǔn)備、評(píng)測(cè)集標(biāo)注和分塊策略。把這三件事做好剩下的融合權(quán)重都是細(xì)枝末節(jié)。