iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
AI Engineering

從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 19

Day 19:RAG(二)檢索端 — 混合檢索、重排序與查詢改寫

  • 分享至 

  • xImage
  •  

此系列因故中斷,請移駕 RE:從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰 繼續支持。

昨天說明文件的索引,今天處理:怎麼把對的東西找出來。

今天說明的混合檢索、重排序、查詢改寫等3個技術是實務上 CP 值最高的三個優化,而且都不需要換模型

今天要解決的問題

在您常識建立 RAG 系統之時,很有可能也有以下經驗:

失敗一:精確關鍵字漏掉。 使用者問「A-2350 這個型號的保固多久」,向量檢索取回了一堆「保固政策」的通則段落,就是沒有 A-2350 那一頁。因為在語意空間裡,「A-2350」和「B-1180」長得幾乎一樣。

失敗二:對的文件排在第 8 名。 你設 top_k=5,正確答案排第 8,就這樣被切掉了。

失敗三:代名詞查詢。 對話進行到第三輪,使用者問「那它的保固呢?」 檢索系統拿到的字面就是「那它的保固呢」,它完全不知道「它」是什麼。

📊 職缺訊號

這是 RAG(48.1%)任務中最需要工程判斷的部分。面試裡常見的問法是:「您的 RAG 答不出某個問題,該怎麼查?」——能講出「先確認是檢索問題還是生成問題」,再往下分層排查的人,就贏過八成的候選人(明天會給完整的排查流程)。


一、現象:三種檢索方式各有盲區

先把今天要組裝的檢索管線畫出來,四個步驟各自解決一種失敗:

image

圖 19-1:檢索管線的四個步驟與各自解決的失敗(示意流程)。注意最後那個分支——能回傳空結果,是拒答功能的前提

方式 原理 強項 盲區
向量檢索(dense) 語意相似度 同義詞、換句話說、跨語言 精確詞(型號、法條編號、人名)
關鍵字檢索(BM25/sparse) 詞頻與逆文件頻率 精確詞、罕見詞 同義詞、語意相近但用詞不同
混合檢索(hybrid) 兩者結果融合 兼顧兩者 需要多維護一套索引、多一次查詢

兩者的盲區恰好互補,這就是混合檢索的理由。

二、原理:每加一層,值多少

這張圖是今天的核心,它回答「該做到哪一層」:

RAG 檢索品質與延遲的取捨

圖 19-2:RAG 檢索品質與延遲的取捨(示範性評測數據,用於說明趨勢)。左圖是四種檢索策略的 Recall@k,右圖是逐層疊加優化的品質與延遲變化。

左圖的關鍵讀法:混合檢索在 top-5 就達到純向量 top-10 的水準;加上重排序後,top-1 的命中率從 0.52 跳到 0.72

top-1 命中率為什麼重要?因為 Day 16 的 Lost in the Middle 告訴我們,放在最前面的內容被用得最好。讓最相關的那一筆真的排第一,比多塞五筆進去有效得多。

右圖的關鍵讀法:忠實度從 0.71 推到 0.91,但延遲也從 180 ms 漲到 640 ms。這不是「全部都要加」的清單,是一份取捨表,多數企業 RAG 的最佳落點是「混合檢索 + 重排序」(品質 0.88、延遲 620 ms)。

三、動手:三個優化的實作

3.1 混合檢索:RRF 融合

"""hybrid.py —— 用 Reciprocal Rank Fusion 融合兩種檢索結果。"""
from rank_bm25 import BM25Okapi
import jieba          # 中文斷詞:BM25 需要先斷詞才有意義

# 建 BM25 索引(與向量索引並存)
tokenized = [list(jieba.cut(c["text"])) for c in chunks]
bm25 = BM25Okapi(tokenized)

def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60):
    """RRF:只看名次不看分數,因此不需要處理兩套分數的尺度差異。

    這是它比「加權平均分數」更受歡迎的原因——不用調權重。
    """
    scores = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking):
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    return sorted(scores, key=scores.get, reverse=True)

def hybrid_search(query: str, top_k: int = 20):
    # 1) 向量檢索
    vec_hits = collection.query(
        query_embeddings=embed([query], is_query=True).tolist(),
        n_results=top_k)["ids"][0]

    # 2) 關鍵字檢索
    bm25_scores = bm25.get_scores(list(jieba.cut(query)))
    bm25_hits = [chunk_ids[i] for i in bm25_scores.argsort()[::-1][:top_k]]

    # 3) 融合
    return reciprocal_rank_fusion([vec_hits, bm25_hits])

💡 中文的特別提醒

BM25 依賴斷詞,而中文斷詞品質直接影響效果。專有名詞、產品型號、內部術語要加進自訂詞典jieba.load_userdict()),否則「A-2350」可能被切成「A」「2350」,效果大打折扣。這是中文 RAG 特有的工作,英文教學不會提。

3.2 重排序:用貴的模型排少量候選

"""rerank.py —— 交叉編碼器重排序。粗召回多、精排少。"""
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")   # 支援中文

def retrieve_and_rerank(query: str, candidates_k: int = 20, final_k: int = 4):
    candidate_ids = hybrid_search(query, top_k=candidates_k)
    docs = fetch_documents(candidate_ids)

    # 交叉編碼器同時看 query 與 doc,比向量相似度準得多,但也慢得多
    scores = reranker.predict([(query, d["text"]) for d in docs])
    ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)

    # 除了取前 k 筆,也用絕對分數過濾——沒有夠相關的就該回報「找不到」
    return [d for d, s in ranked[:final_k] if s > 0.3]

最後那個 if s > 0.3 很關鍵。 沒有它,即使知識庫裡完全沒有相關內容,檢索仍會回傳「最不差的四筆」,然後模型就會基於不相關的脈絡開始編。能夠回傳空結果的檢索器,是拒答功能的前提(明天詳談)。

3.3 查詢改寫:解決代名詞與模糊查詢

"""rewrite.py —— 把對話脈絡補進查詢,讓它能獨立被檢索。"""
REWRITE_PROMPT = """根據對話歷史,把使用者最新的問題改寫成一個「不需要看歷史也能理解」的完整問句。
只輸出改寫後的問句,不要有其他文字。若問題本身已經完整,原樣輸出。

對話歷史:
{history}

使用者最新問題:{question}
改寫後:"""

async def rewrite_query(history, question):
    # 用便宜的小模型即可,這是高頻但簡單的任務
    resp = await client.chat.completions.create(
        model=SMALL_MODEL,
        messages=[{"role": "user", "content": REWRITE_PROMPT.format(
            history=format_history(history[-4:]), question=question)}],
        max_tokens=100, temperature=0,
    )
    return resp.choices[0].message.content.strip()

# 效果:
#   原始查詢:「那它的保固呢?」        → 檢索必然失敗
#   改寫後:「A-2350 智慧門鎖的保固期是多久?」 → 檢索成功

另外兩個值得知道的改寫技巧:

  • Multi-query:讓模型把一個問題拆成 2–3 個不同角度的子查詢,分別檢索後合併。適合複合問題(「A 和 B 的差別,以及哪個比較適合小型企業」)。
  • HyDE(Hypothetical Document Embeddings):先讓模型「假裝回答」,再用這個假答案去檢索。原理是假答案在語意空間裡比問題更接近真答案。適合問題與文件用語落差大的場景。

四、取捨:加到哪一層就好

你的情況 建議配置 預期延遲
原型驗證階段 純向量 top-5 ~180 ms
文件含大量專有名詞/型號/法條 +混合檢索 ~360 ms
使用者抱怨「答非所問」 +重排序 ~620 ms
多輪對話應用 +查詢改寫(幾乎必加) +200–300 ms
延遲要求極嚴(< 300 ms) 混合檢索,捨棄重排序,改用更好的切塊 ~360 ms

三個常見的誤區:

  1. 無腦調大 top_k。 從 5 調到 20,看似取回更多,實際上是把更多雜訊塞進脈絡——Lost in the Middle 會讓效果不升反降,成本還上升。先做重排序,再考慮調 k。
  2. 以為換更強的生成模型能救檢索。 檢索取回錯的東西,再強的模型也只能基於錯的東西回答。這是本系列反覆強調的分層思維
  3. 忘記量測。 上面每一個優化都該有前後對照的數字。沒有評測集,你只是在憑感覺調參——明天就處理這件事。

4.1 一個真實的優化順序範例

某企業知識助理上線後,答對率只有六成。團隊的優化順序與各自的效果:

順序 動作 答對率變化 花費時間
1 建立 30 題評測集(先量測) 0.61(基線) 半天
2 加入查詢改寫 0.61 → 0.68 2 小時
3 改用結構感知切塊、重建索引 0.68 → 0.79 1 天
4 加入混合檢索(型號查詢改善明顯) 0.79 → 0.85 半天
5 加入重排序 0.85 → 0.88 2 小時

注意第 1 步與第 3 步。 第 1 步不改善任何東西,卻是後面每一步能被驗證的前提;第 3 步是效果最大的一項,而它屬於昨天的索引端——這再次說明「八成的 RAG 失敗在提問之前就決定了」

也請注意這裡沒有出現「換更強的生成模型」。因為在檢索還沒做好之前,換模型的邊際效益極低。

📌 一個排查的順序

RAG 答不好時,永遠先看檢索結果,不要先改提示。具體做法:把該問題的檢索結果原文印出來,問自己一句話——「正確答案在不在這幾段裡?」

  • 不在 → 檢索問題(今天的內容)
  • 在,但模型沒用好 → 生成問題(明天的內容)

這個二分法能省下你大量的瞎猜時間。


今日小結

  • 三種檢索方式盲區互補:向量檢索漏精確詞、關鍵字檢索漏同義詞,混合檢索(RRF 融合)兩者兼顧。
  • 重排序讓 top-1 命中率從 0.52 跳到 0.72——因為 Lost in the Middle,讓最相關的排第一,比多塞五筆有效。
  • 重排序要設絕對分數門檻,能回傳空結果的檢索器,才有辦法做拒答。
  • 多輪對話幾乎必做查詢改寫,否則代名詞查詢必然失敗。
  • 中文 RAG 的特有工作:BM25 的斷詞與自訂詞典(型號、內部術語要加進去)。
  • 三個誤區:無腦調大 top_k、以為換模型能救檢索、忘記量測。
  • 排查順序:先看檢索結果,問「正確答案在不在這幾段裡」——這個二分法能省下大量瞎猜。

明天預告

RAG 三部曲的最後一天:文件找到了,怎麼讓模型只根據它們回答在沒有依據時老實說不知道、以及最重要的是如何量化你的 RAG 到底有多好

明天也會提供 RAG 排查決策樹,可以直接存起來當工作手冊參考。

延伸閱讀


上一篇
Day 18:RAG(一)索引端 — 八成的失敗在提問之前就決定了
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言