前兩天我們都在優化「怎麼從候選中找出更相關的內容」,今天要換一個角度:問題本身的表達方式,會不會就是檢索效果不好的原因?
真實使用者的提問往往:
這些情況下,直接拿原始問題去做向量搜尋,檢索效果往往不理想。
用 LLM 先把使用者的問題「改寫」成更清楚、更適合檢索的形式,再拿改寫後的問題去做向量搜尋。
def rewrite_query(original_question):
prompt = f"""請將以下使用者問題改寫成更清楚、更適合用於文件檢索的形式,
補充可能省略的主詞或情境,但不要改變原意:
原始問題:{original_question}
改寫後的問題:"""
response = call_llm(prompt)
return response.strip()
範例:
原始問題:「這個怎麼用?」(延續上一輪對話關於「向量資料庫」的討論)
改寫後:「向量資料庫要如何安裝與使用?」
與其只用一種問法去檢索,讓 LLM 針對同一個問題,產生多個不同角度的查詢版本,分別檢索後再合併結果(概念上跟 Day 16 的 RRF 融合類似)。
def expand_query(question, n=3):
prompt = f"""請針對以下問題,產生{n} 個不同角度或用詞的改寫版本,
用於增加文件檢索的涵蓋範圍:
問題:{question}
請以清單格式輸出,每行一個改寫版本。"""
response = call_llm(prompt)
return response.strip().split("\n")
def multi_query_search(question, top_k=5):
queries = [question] + expand_query(question)
all_results = []
for q in queries:
all_results.extend(search_similar_chunks(q, top_k=10))
# 去重並融合排序(可套用 Day 16 的 RRF)
return fuse_and_dedupe(all_results, top_k)
這是一個比較特別的技巧:先讓 LLM 針對問題「生成一份假設性的答案」,再拿這份假設答案去做向量搜尋,而不是直接拿原始問題去搜尋。
原理:問題的向量表示,跟「答案」的向量表示,語意空間其實不完全對齊(問題通常較短、答案通常較長且包含更多具體資訊)。透過先生成一份假設答案,再用這份答案去檢索,反而更容易匹配到知識庫中「真正的答案段落」,因為兩者的文體與資訊密度更接近。
def hyde_search(question, top_k=5):
prompt = f"請針對以下問題,撰寫一段假設性的回答(即使你不確定答案是否正確):\n{question}"
hypothetical_answer = call_llm(prompt)
# 用假設答案的 embedding 去檢索,而非原始問題
query_embedding = get_query_embedding(hypothetical_answer)
return search_by_embedding(query_embedding, top_k)
| 技巧 | 額外成本 | 適合情境 |
|---|---|---|
| Query Rewriting | 1 次額外 LLM 呼叫 | 多輪對話、問題常常過於簡短模糊 |
| Query Expansion | N 次額外檢索 | 使用者用詞習慣多樣、擔心漏掉相關內容 |
| HyDE | 1 次額外 LLM 呼叫 | 問題與答案文體差異大的領域(如專業知識問答) |
這些技巧都會增加額外的 LLM 呼叫或檢索次數,帶來延遲與成本的增加,建議先確認「檢索效果不佳確實是問題本身的表達方式造成的」,再決定是否導入,而不是一開始就疊加所有技巧。
有時候檢索效果不好,問題不在資料庫或演算法,而在於「問題本身沒有問到重點」。透過 Query Rewriting、Expansion、HyDE 等技巧,可以在不改動知識庫的前提下,提升檢索的命中率。明天我們要來看多輪對話情境下,RAG 系統會遇到的額外挑戰。