iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Backend 工程師的 Azure GenAI 實戰系列 第 34

Day 34:hybrid、vector、semantic ranker 怎麼選——先問答案被哪條腿弄丟,再問哪個模式排得準

  • 分享至 

  • xImage
  •  

檢索模式選型的討論通常在比「哪個模式排名最準」,而你在 demo 規模的語料上得到的答案永遠是「都差不多」。本次量測的最小語料裡,四個模式幾乎每題都排第 1。把同一份語料沿一條事先凍結的路徑放大到 155、再到 773 個 chunk 之後,同一題從三個模式排第 1,變成四個模式全部找不到。這篇把過程量給你看:選型要先問「答案被哪條腿弄丟」,才輪得到「哪個模式排得準」——讀完你能用兩個判準去預測自己的系統哪條腿先失效。

上一篇(Day 33)把排序品質的判定推給「需要相關性標註」,欠了一個問題沒答:檢索模式本身怎麼選。這篇還這筆債,用的是 2026-08-30 一場即用即刪的量測:Basic tier 的 Azure AI Search 服務、三個 index、四個模式、17 題凍結查詢,跑完就刪。

一、選型的問題先換個問法

四個模式的機制 Day 13 講過,這裡只擺定義:keyword 是 BM25 全文檢索,vector 是查詢向量的最近鄰,hybrid 用 RRF 把前兩者的結果融合,hybrid_semantic 再加一層 semantic ranker 重排。

候選產生只有一個階段、兩條腿,而兩條腿丟東西的理由不同

keyword 腿用詞彙重疊排除——查詢的詞和文件的詞對不上就出局。它沒有本 repo 設定的固定名額,語料變大時新文件隨時可能擠進候選集。vector 腿用名次排除——它只交出 vector_k 個最近鄰,第 k+1 名連出場的機會都沒有,跟詞彙無關。

semantic ranker 則根本不在候選產生這一層。官方文件明說它不能「rerun the query over the entire corpus」,它重排的是既有結果集的前 50 名semantic ranking overviewms.date 2026-08-05,查核 2026-08)。候選集裡沒有的東西,它看都看不到。

所以「哪個模式好」在答案不在候選集裡的時候是個假問題。真正可回答的問題是:你的負載會讓哪條腿先把答案丟掉

兩條腿、兩種天花板。上方是凍結的 12 題中英查詢與 acme 可見語料(G1=13、G2=155、G3=773 chunk),語料同時流進兩個深藍節點:keyword 腿(BM25)用詞彙重疊排除,量到最深名次 111;vector 腿只給 vector_k=50 個最近鄰,量到最深名次 45、從未超過 50。KEYWORD 與 VECTOR 模式各自在腿的出口直接回傳。兩條腿匯入 RRF 融合(量到最深名次 138),HYBRID 到此回傳;再往下是淺藍的 semantic ranker 節點,標注「只重排合併後前 50 名:38→2、49→8 得救;59→59、65→65 一動不動」,HYBRID_SEMANTIC 由此回傳。兩條腿各有一條虛線通往右側粉底虛線框的 absent 終點:keyword 腿是詞彙不重疊就丟,vector 腿是被擠出前 50 名就丟;框內寫著「兩條腿都丟了就沒有下游救得回來(Q4 zh 在 G3:四模式全滅)」。

二、量測的形狀:一條凍結的擴張路徑

實驗設計只講骨架,工具都在 day-34 tag 上:

三個 generation 共用同一份 base corpus(7 個文件、3 個租戶),只有 acme 租戶的干擾文件在長大——干擾語料是本系列已發布的文章與 lab 的公開文件:

generation acme 可見 chunk vector_k = 50
G1 13 低於——vector 腿的截斷在結構上不可能
G2 155 高於(3.1×)——截斷變得可能
G3 773 高於(15.5×)

G1 與 G2 之間跨過 50,是整個實驗唯一刻意製造的條件。「變得可能」不等於「發生了」——後者要逐題看排名。

查詢集是 12 題 acme(6 個分層 × 中英各一)加 5 題 globex 對照組,在任何一次查詢送出之前連同相關性標註一起凍結、先 commit 才開跑。看過排名再挑標註,這個比較就不可證偽——沒有任何東西能事後分辨「先凍結才量」與「先量才挑好看的」。

凍結還包括一條事先寫下的裁決。開跑前語料的形狀看起來對 null 有利——gold 集中在兩篇客服文件、干擾全是工程散文,向量距離本來就遠——所以我先押了字據:

如果 vector 腿的截斷根本沒有發生——三個 generation 的 gold chunk 都待在 vector_k = 50 以內,四個模式從頭到尾看起來都差不多——那就把這個 null result 當成結論寫出去。不加題目、不弱化語料、不重切 G2。

結果不是 null。但這段話得先存在,量出來的東西才可信——一個看過結果才做的決定,跟一個為了容納結果而做的決定,事後沒有人分得出來。

對照組 globex 與 opsdemo 的語料三個 generation 逐位元組相同diff -r 全數為空),principal 也不變。它的用途本來只有一個:acme 語料長大時,替「什麼都沒變的租戶」留一組基準。後面會看到它多量到了一件事。

最後是兩句不假裝:干擾語料的排除規則(防止答案洩漏進干擾)系統性地把最接近 gold 的鄰居先剔掉了,所以本次所有數字都是干擾難度的下界;而且擴張路徑上語言比例也在動(英文 chunk 佔比從 G2 的 46.5% 走到 G3 的 64.6%),所以歸因一律寫「沿這條路徑擴張」,不寫「只有規模變了」。

三、翻轉點:同一題,從三個第 1 到四模式全滅

先看最劇烈的一題。Q4 中文版問「如果是客戶自己把系統設定弄錯了會怎樣?」,gold 是 SLA 的排除條款,題面刻意不含 gold 的用詞(lexical decoy 層):

Q4 zh 的名次 G1(13 chunk) G2(155 chunk) G3(773 chunk)
keyword absent absent absent
vector 1 45 absent
hybrid 1 59 absent
hybrid_semantic 1 59 absent

G1 三個模式都排第 1。G3 四個模式全滅——keyword 腿從頭到尾詞彙對不上,vector 腿在 G3 被擠出前 50,兩條腿都空手,RRF 融合空集合還是空集合,semantic ranker 連重排的對象都沒有。可見語料沿這條路徑從 13 個 chunk 走到 773 個,這一題就從「隨便選都對」變成「怎麼選都錯」。

vector 腿的退場是階梯狀的,三題跨語言查詢排在一起看:

題目(vector 模式) G1 G2 G3
Q5 zh(Sev 1 升級) 1 absent absent
Q4 zh(客戶設定錯誤) 1 45 absent
Q3 zh(何時拿到 credit) 8, 2 40, 2 absent, 2

而整批 acme 觀測值裡,vector 模式出現過的最深名次是 45,從未超過 50——DEFAULT_VECTOR_K 就是 50。同一批觀測裡 keyword 走到 111、hybrid 走到 138。這是可證偽的主張:vector 只要出現任何一個大於 50 的名次,「候選集被 k 截斷」的機制說法就死了。它沒有出現。

https://ithelp.ithome.com.tw/upload/images/20260903/20168288nmPur9dqIn.png
忍喵:圈「45」這個數字。keyword 走得到 111、hybrid 走得到 138,只有 vector 停在 45——上限是 50 的時候,最深觀測值卡在 50 之內不是巧合,是天花板的形狀。你 repo 裡那個 k 上次被當成「容量參數」而不是「效能參數」檢討,是什麼時候?

G1 的四個 absent 全部落在 keyword 模式的中文題——13 個 chunk 遠低於 50,vector 腿在結構上不可能截斷,所以 G1 的失敗全是詞彙的失敗,跟 k 無關。兩條腿的死法從第一個 generation 就分得開。

這裡對主線 Day 14 有一個措辭上的修正。lab 的 /rag 在零命中時結構性短路成 no_answer,那個判斷本身不動——零 hits 唯一誠實的讀法仍然是不呼叫 LLM。但 no_answer 的成因不得再被描述成「語料裡沒有答案」:Q4 zh 的答案三個 generation 都在語料裡,換個 generation、換條腿就找得到。講 no_answer 只能說「這次檢索沒有取回任何 hit」。

四、semantic ranker 是修理工,修理範圍是合併後前 50 名

翻轉點講完召回,這節講排名——semantic ranker 到底買到什麼。量測給出一條乾淨的分界線:

題目 hybrid 名次 hybrid_semantic 名次 ranker 有沒有動它
Q2 zh @ G2 38 2 修回來了
Q3 zh @ G2 49, 15 8, 2 修回來了
Q2 zh @ G3 29 2 修回來了
Q4 zh @ G2 59 59 一動不動
Q5 zh @ G2 65 65 一動不動
Q5 zh @ G3 138 138 一動不動

名次在 50 以內的(38、49、29),ranker 一把拉回前 10;名次在 50 以外的(59、65、138),一格都沒動。這與官方文件寫的機制一致:semantic ranker 重排的是既有結果集的前 50 名(來源同 §一)。它是修理工,但修理範圍有牆——牆外的候選,它視同不存在。

所以「要不要開 semantic ranker」這個選型問題,答案取決於你的失敗長什麼樣。答案常在候選集前 50 名內、只是排序不穩:這正是 ranker 的作用範圍,值得開起來量——本次量到的三個案例都被拉回前 10,38→2 這個量級。答案掉在 50 名外、或根本不在候選集:ranker 一點忙都幫不上,該調的是 vector_k 或查詢本身。

順帶一提 hybrid 的「容錯」在這批資料裡的真實長相:Q5 zh 在 G2,vector 腿 absent、keyword 名次 42,hybrid 還找得到——候選只可能來自 keyword 腿——但融合後的名次是 65。一條腿撐住了「找得到」,撐不住「排得前」。兩條腿機制不同是事實;「所以 hybrid 比較不會失敗」是本篇樣本支持不了的結論,這裡不下。

五、控制臂多量到的事:可見性隔離不等於排序隔離

這一條不在設計裡,是量出來才看到的。

globex 的語料三個 generation 逐位元組相同,principal 相同,查詢相同。照直覺它的名次應該三代不動——但它動了,而且每一次變動都落在 keyword 腿(以及含 keyword 腿的 hybrid):Q1 2→2→1、Q2 2→1→1、Q3 3→2→2、Q5 3→3→2。vector 腿四題三代全部釘在 1,一次都沒動。

分數把成因指得更明白:同一題、同一個 chunk、globex 自己的語料一個位元組都沒變,BM25 分數卻從 G1 的 4.298867 走到 G3 的 12.173283——因為 acme 的干擾文件進了同一個 index

機制上這不是 bug,是 BM25 的定義:ACL filter 決定哪些文件可以回傳,不決定分數怎麼算。BM25 的統計量取自被索引的語料,不是取自 filter 之後的結果集。

官方文件把範圍講得更細:分數預設在 shard 層級計算,加 scoringStatistics=global 才是跨 shard 全域(BM25 Relevance Scoringms.date 2025-08-27,查核 2026-08)。

同一頁的 Data volatility 一列明說:「Term frequencies will change as index updates are processed over time, affecting the search scores of matching documents.」本次量到的就是這句話的後果。

本次沒有分辨是 shard 級還是全域造成的,也不需要——兩者的範圍都不是「這個 principal 看得到的那一份」,結論只靠這一點。

對 Day 12 那個「單一 index + ACL filter」的多租戶模型,這是一條要老實寫上的限制:可見性隔離不等於排序隔離。租戶看不到彼此的文件,但本次量到:一個租戶灌語料,另一個租戶的 keyword 名次跟著動了。

這不是「每次寫入必動」的定律——預設統計量在 shard 範圍內算,分數變了名次也不一定跟著變。但 ACL 不在計分路徑上,「鄰居永遠不影響我的排序」同樣沒有任何東西保證。能寫的只有:排序獨立性無從承諾。本次量到的變動每次都只有 1 名、方向甚至是變好;幅度與方向是這份語料的性質,不是機制的保證。

https://ithelp.ithome.com.tw/upload/images/20260903/20168288bgbfu9FjNs.png
忍喵:「名次只動了 1,還變好了」是這份語料的運氣,不是你的 production 會拿到的合約。隔壁租戶明天把一萬份「退款」相關文件灌進共用 index,你的退款 FAQ 的 BM25 統計量就可能跟著改——會不會、改多少,沒有合約替你保證,而你的監控大概只盯著自己租戶的寫入量。共用 index 的租戶隔離,你驗過的是哪一半?

它同時也限縮了本實驗自己:globex 能證明「租戶自己的語料沒變」,不能證明「租戶不受鄰居影響」。控制臂只控住了一半。

六、所以怎麼選

把量到的東西收成判準。第一判準:你的(單租戶可見)語料規模對 vector_k。語料在 k 以下,rank cutoff 這種失敗在結構上不可能發生——本次 G1 的 vector 腿也確實一題都沒丟;語料跨過 k,它從不可能變成可能,而且本次量到它真的發生。這把 vector_k 從「效能參數」變成「召回容量參數」——語料要長大,k 得跟著檢討,而不是抄一個預設值用到天荒地老。

第二判準:查詢與語料的詞彙重疊。英文題(詞彙貼近語料)keyword 腿在小語料夠用,但沿路徑擴張時名次一路滑:Q2 en 1→1→10、Q4 en 2→21→52;同題的 vector 與 hybrid_semantic 全程 1。

跨語言題更直接。中文題面在 en.microsoft 分析器之下,keyword 腿在 G1 就 absent 的是 Q2、Q3、Q4——三題題面沒有任何拉丁字母或數字;還排得進來的 Q1 與 Q5,題面各帶著 99.9%Sev 1。這個對比與「中文詞彙匹配只剩英數 token 可用」一致——寫成一致,不寫成證明,本次沒有對分析器做隔離測試。

兩個判準疊起來,就是 Q4 zh 的死亡路線:詞彙判準說 keyword 腿從頭靠不住,規模判準說 vector 腿在 G3 到頂——兩條腿都失效,四個模式一起滅。選型矩陣長這樣:

你的負載 先看什麼 本次的依據
語料 < k、單語言、詞彙貼近 這個規模量不出模式差異——選型結論留到語料接近目標規模再下 G1 幾乎每題全模式第 1(本語料)
語料會長大 vector_k 是召回天花板,隨語料檢討 最深 45/111/138;Q4 zh 的階梯
跨語言、paraphrase 重 先量 keyword 腿在你的分析器+查詢語言組合下還剩多少召回;它失守的部分全落在 vector 腿,k 的檢討跟著變急 本次中文題在 en.microsoft 下 keyword 近乎全滅
排名不穩、答案還在前 50 答案先進合併後前 50 才輪得到 ranker——確認這點,再開起來量修復幅度 本次 38→2、49→8
答案掉出前 50 或候選集 ranker 無效,調 k 或查詢 59→59、65→65、138→138
多租戶共用 index 可見性隔離 ≠ 排序隔離 globex 的 keyword 名次三代在動

keyword 腿順帶一句:它在本 repo 的參數下沒有固定 k,但不是沒有上限。Azure 文件記載 hybrid 查詢的 BM25 腿有 maxTextRecallSize——預設 1,000、上限 10,000(create a hybrid query,頁面更新 2026-08-06,查核 2026-08)。

三個界線讓這句話精確:它是 preview 參數,本 repo 釘的 stable 2026-04-01 設不了它;stable 路徑是否套用那個預設視窗,官方沒有明講,記為未確立;無論套不套用,1,000 都遠高於本次最大的 773 chunk,在這裡不生效。

最後一條判準凌駕前面所有:這張表的數字綁這份語料與這組題目。它教你的是量法,不是答案——你的選型要用你的語料量。所以,最後一個問題:量一次多少錢。

七、量一次要多少錢

整套量測的資源形狀是「即用即刪」:create-search.sh 建 Basic tier 服務 → 建三個 index → 六趟查詢 → 刪服務。兩個服務(其中一個是失誤多開的,等下講)合計存活 17 分 08 秒

帳單對帳(2026-08-31,Azure Cost Management usageDetails,計價幣別 TWD):

項目 cost(TWD)
Azure AI Search - Basic Unit × 2 個服務(各記 1 unit) 4.276527 × 2
embedding(text-embedding-3-small) 0.527068386
合計 9.080122386

一杯手搖飲的零頭。系列的成本天花板是 US$20/月(Day 4 設的 budget alert),這次連邊都沒碰到——但這篇不換算百分比,因為帳單裡沒有匯率,硬換就是把估算混進量測。

這筆帳還順便回答了一個之前查不到的問題:不滿一小時的 Basic 服務怎麼計費。答案是每個服務各記一個 unit,quantity 各為 1——活 8 分 51 秒的和活 8 分 17 秒的,各拿整整一個 unit。所以「合計存活 17 分鐘」對帳單沒有意義:帳按你開了幾個服務算,不按分鐘數。ephemeral 策略省的是小時數,不是服務數。但只能推到這裡——兩次 run 跨過 UTC 15:00 整點,「只開一個服務會不會只記一個 unit」本次沒有觀測,不寫。

至於那個多開的服務:第一次 run 漏帶了 globex 對照組需要的 --group-id oncall,該臂的量測作廢,整場重跑。失誤的價目是 4.276527 TWD,佔本次總花費 47%。它的副產品剛好有用——acme 臂的 84 個預先登錄觀測值在兩個獨立建立的服務上逐格相同,是現成的可重現性證據——但它是失誤,不是事先規劃的驗證,這裡照實記。

https://ithelp.ithome.com.tw/upload/images/20260903/20168288umR7Tx8zdG.png
忍喵:9 塊台幣量出一張選型矩陣——但先看清楚小字:這個價格是「跑完就刪」的價格。同一個 Basic 服務開著過夜,帳單就換一種算法陪你聊天了。

這天的誠實邊界

  • 不導出 recall rate。 一份語料、一組手挑問題、逐題矩陣。
  • 翻轉點的數字綁這份語料與這組問題,不可外推。你的語料的翻轉點要自己量。
  • 規模不是唯一變數。 沿擴張路徑英文 chunk 佔比從 46.5% 走到 64.6%——語言組成跟規模一起動,歸因只能寫「沿這條路徑擴張」。
  • 量到的是干擾難度的下界。 排除規則系統性剔掉了最接近 gold 的干擾鄰居;且干擾語料是工程文件、gold 是客服條款,領域不同本身就降低了難度。
  • 相關性標註是作者判斷,不是量測。 單一標註者、無一致性係數,判準已預先寫下並凍結。
  • 「最深 45」是 84 個觀測值的事實,不是服務保證。 只能寫「量到的行為與 k 截斷一致」,不能寫「Azure 保證不超過 k」。
  • 「語料超過 k」不等於「答案被截掉」。 必要條件,不是充分條件——所以才要逐題量。
  • 中文觀測全部綁定 en.microsoft 分析器,不是中文檢索的一般結論。
  • 不作 latency 宣稱不宣告 hybrid 互補性
  • Day 13「RRF 分數無鑑別力、不設分數閾值」的結論本次未重新驗證,也未被推翻——本次量名次,不量分數鑑別力。
  • 量測範圍的硬上限:工具要求 top 嚴格大於語料數、MAX_TOP = 1,000,所以這套方法量得到的單租戶語料上限是 999 chunk——production 規模的行為本篇量不到。

小結

回到題目:hybrid、vector、semantic ranker 怎麼選。這篇給的不是一個模式名,是一個順序——先用「語料對 k」和「詞彙重疊」判斷哪條腿會把答案弄丟,再決定 ranker 值不值得開,最後拿你自己的語料驗證——價目在上一節。

這是 Bonus 通道的第四篇。主線 30 天加上 Day 31–34,檢索這條線該交的卷都交了——剩下的問題在你的語料裡,量測工具在 day-34 tag 等你。

下一篇換一條完全不同的線。Day 4 建的那顆 Azure OpenAI resource(kind: OpenAI)已被官方標為新 Foundry portal 不支援(general availability overviewms.date 2026-08-14,查核 2026-09),升級則是同一顆資源上一個屬性的 patch。

下一篇用一顆即建即刪的資源實測那個 patch 動到什麼——包括一條從頭到尾沒被改過的 role assignment,在那次單次觀測裡跟著變寬。

用到的 Azure 服務

  • Azure AI Search(Basic,ephemeral——兩個服務合計存活 17 分 08 秒,皆已刪除;semantic ranker 走 free 方案)
  • Azure OpenAI in Microsoft Foundry(常駐 embed-small;本次零 LLM 呼叫,帳單印證)

本篇的雲端花費已逐筆對帳(2026-08-31,Cost Management):9.08 TWD。Search 服務已全數刪除,subscription 內無殘留 Search 資源。


本文由作者規劃與撰寫,AI(Claude)協助草稿整理與程式碼驗證;技術內容與觀點由作者確認並負責。


上一篇
Day 33:找不到、找錯、答太滿、引用錯——決定處置落點的不是嚴重度,是可判定性
下一篇
Day 35:Azure OpenAI 升級 Foundry:kind 改一個字,沒動過的 role assignment 跟著變寬
系列文
Backend 工程師的 Azure GenAI 實戰35
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言