這是檢索優化系列的最後一天,今天要處理一個進階但常見的場景:當答案分散在一份長文件的不同位置,或是需要綜合多份文件才能回答問題時,該怎麼設計檢索策略。
假設一份技術規格文件有 50 頁,切成了上百個 chunk,而使用者的問題需要綜合文件中第 3 章與第 20 章的內容才能完整回答。單純的 Top-K 檢索,很可能只取到其中一個章節的內容。
先建立文件的「摘要層」,檢索時先在摘要層找出最相關的章節,再深入該章節做細部檢索:
文件摘要層:每個章節先產生一段摘要,並轉成 Embedding
│
▼
第一步:用問題去摘要層檢索,找出最相關的 2-3 個章節
│
▼
第二步:只在這幾個章節內的 chunk 做細部向量搜尋
def hierarchical_search(question, top_k_sections=2, top_k_chunks=5):
relevant_sections = search_section_summaries(question, top_k=top_k_sections)
section_ids = [s["id"] for s in relevant_sections]
results = search_chunks_within_sections(question, section_ids, top_k=top_k_chunks)
return results
這個做法的好處是先用較粗的粒度縮小範圍,避免長文件中不相關的章節干擾檢索結果。
即使檢索到了正確的 chunk,有時候該 chunk 本身脈絡不完整(例如只切到一段的中間)。常見的補救方式是 取回鄰近 chunk(Parent-Child Retrieval / Sentence Window):
def retrieve_with_context_window(chunk_id, window=1):
"""
取回目標 chunk 前後各 window 個 chunk,一起組成完整上下文
"""
return get_chunks_by_index_range(chunk_id - window, chunk_id + window)
概念上是:**用小 chunk 做精準檢索(提升相似度計算的準確性),但用大範圍的上下文送給 LLM(保留完整語意)。**這是目前很多 RAG 框架內建支援的做法(例如 LlamaIndex 的 SentenceWindowNodeParser)。
例如:「比較 A 產品和 B 產品的價格與規格差異」,答案分別存在於「A 產品文件」與「B 產品文件」中,單純檢索可能只取回其中一份文件的內容比例過高。
def multi_document_search(question, entities, top_k_per_entity=3):
"""
entities: 從問題中抽取出的多個比較對象,如 ["A產品", "B產品"]
針對每個實體分別檢索,確保每個實體都有對應的內容被取回
"""
all_results = []
for entity in entities:
entity_query = f"{entity}{question}"
results = search_similar_chunks(entity_query, top_k=top_k_per_entity)
all_results.extend(results)
return all_results
這種做法先用 LLM 或規則從問題中抽取出「比較對象」,針對每個對象各自做檢索,確保最終送給 LLM 的內容涵蓋所有需要比較的對象,而不是被單一文件的內容佔滿整個 Top-K。
從 Day 15 到今天,我們陸續討論了:
| Day | 主題 | 解決的問題 |
|---|---|---|
| 15 | Top-K 檢索的侷限 | 建立問題意識 |
| 16 | Hybrid Search | 精確關鍵字匹配不足 |
| 17 | Rerank | 語意相似 ≠ 真正相關 |
| 18 | Query Rewriting / HyDE | 問題表達不清楚 |
| 19 | 多輪對話 | 指代消解、上下文延續 |
| 20 | 長文件 / 多文件 | 資訊分散、需要綜合比較 |
這些技巧不需要每個專案都全部套用,建議根據自己知識庫的特性與使用者提問模式,挑選真正對症下藥的優化手段。
檢索優化沒有一勞永逸的公式,而是要持續觀察系統實際遇到的檢索失敗案例,針對根因逐一補強。明天開始,我們要進入生成與評估階段,看看檢索到內容之後,如何設計 Prompt 讓 LLM 產出高品質的回答,以及如何量化評估整個系統的表現。