RAG 完全指南(四):查詢轉換、Self-RAG 與 Context 壓縮

前言

前兩篇解決了「搜尋」的問題。這篇要解決兩個更深層的問題:

  1. 問題本身就難以直接搜尋:複雜問題需要先轉換或拆解
  2. Context 品質不夠純粹:塞給 LLM 的資訊裡有太多噪音

這裡介紹三個技術:Step-Back Prompting(退一步提問)Self-RAG(自我反思 RAG)Context Compression(上下文壓縮)


技術 1:查詢轉換(Query Transformation)

問題:複雜問題無法直接搜尋

有些問題不適合直接拿來做向量搜尋:

❌ 直接搜尋困難的問題類型:

「為什麼我們公司的 API 最近變慢了?」
→ 問題太具體,知識庫不可能有這個答案

「除了 Redis,還有哪些快取方案?」
→ 包含否定條件,搜尋容易找到 Redis 的文章

「比較 PostgreSQL 和 MySQL 的優缺點,然後說明哪個適合我們的場景」
→ 多個子問題,一次搜尋無法全部覆蓋

Step-Back Prompting(退一步提問)

核心思想:先把具體問題「退一步」抽象化,搜尋更通用的背景知識,再回答具體問題。

具體問題:「Estrogen 會影響 BRCA1 的 transcription 嗎?」
退一步:「BRCA1 基因的調控機制是什麼?」
→ 先搜尋通用知識,再回答具體問題
 1import openai
 2
 3client = openai.OpenAI(api_key="your-api-key")
 4
 5
 6def step_back_query(original_query: str) -> str:
 7    """生成一個比原始問題更抽象的退一步問題"""
 8    prompt = f"""你是一個搜尋查詢優化助手。
 9將以下具體問題「退一步」,改寫成一個更通用、更基本的問題,
10這個更通用的問題可以幫助找到回答原始問題所需的背景知識。
11
12具體問題:{original_query}
13
14更通用的退一步問題(只輸出問題,不要解釋):"""
15
16    response = client.chat.completions.create(
17        model="gpt-4o-mini",
18        messages=[{"role": "user", "content": prompt}],
19        temperature=0,
20    )
21    return response.choices[0].message.content.strip()
22
23
24def rag_with_step_back(query: str, retriever) -> str:
25    """Step-Back RAG pipeline"""
26    # 退一步問題
27    abstract_query = step_back_query(query)
28    print(f"🔍 原始問題:{query}")
29    print(f"🔙 退一步問題:{abstract_query}")
30
31    # 用兩個問題分別搜尋
32    specific_chunks  = retriever.retrieve(query,          top_k=3)
33    abstract_chunks  = retriever.retrieve(abstract_query, top_k=2)
34
35    # 合併 context
36    context = "\n\n---\n\n".join(specific_chunks + abstract_chunks)
37
38    prompt = f"""請根據以下背景知識回答問題。
39
40【背景知識】
41{context}
42
43【問題】{query}
44
45請先概述相關背景,再回答具體問題:"""
46
47    response = client.chat.completions.create(
48        model="gpt-4o",
49        messages=[{"role": "user", "content": prompt}],
50        temperature=0,
51    )
52    return response.choices[0].message.content

Query Decomposition(問題分解)

對複雜的多步驟問題,先分解成子問題再各自搜尋:

 1import json
 2
 3def decompose_query(complex_query: str) -> list[str]:
 4    """將複雜問題分解成可獨立搜尋的子問題"""
 5    prompt = f"""將以下複雜問題分解成 2-4 個簡單的子問題,
 6每個子問題都可以獨立在知識庫中搜尋。
 7以 JSON 陣列格式輸出,例如:["子問題1", "子問題2"]
 8
 9複雜問題:{complex_query}
10
11子問題 JSON:"""
12
13    response = client.chat.completions.create(
14        model="gpt-4o-mini",
15        messages=[{"role": "user", "content": prompt}],
16        temperature=0,
17    )
18    try:
19        return json.loads(response.choices[0].message.content)
20    except json.JSONDecodeError:
21        return [complex_query]  # fallback:退回原始問題
22
23
24def rag_with_decomposition(complex_query: str, retriever) -> str:
25    """問題分解 RAG pipeline"""
26    sub_questions = decompose_query(complex_query)
27    print(f"📋 子問題:")
28    for q in sub_questions:
29        print(f"  - {q}")
30
31    # 每個子問題分別搜尋,收集所有 context
32    all_chunks = []
33    for sub_q in sub_questions:
34        chunks = retriever.retrieve(sub_q, top_k=2)
35        all_chunks.extend(chunks)
36
37    # 去重
38    unique_chunks = list(dict.fromkeys(all_chunks))
39    context = "\n\n---\n\n".join(unique_chunks)
40
41    prompt = f"""請根據以下參考資料,完整回答這個複雜問題。
42
43【參考資料】
44{context}
45
46【問題】{complex_query}"""
47
48    response = client.chat.completions.create(
49        model="gpt-4o",
50        messages=[{"role": "user", "content": prompt}],
51        temperature=0,
52    )
53    return response.choices[0].message.content
54
55
56# 示範
57# complex_q = "比較 Redis 和 Memcached 的架構差異,並說明各自適合哪些使用場景?"
58# answer = rag_with_decomposition(complex_q, retriever)

優點:對複雜、多跳(multi-hop)問題效果顯著
缺點:多次 LLM 呼叫,延遲和成本增加
最佳使用場景:分析類問題、比較類問題、需要推理的多步驟問題


技術 2:Self-RAG(自我反思 RAG)

核心思想

Naive RAG 盲目地把所有檢索到的 chunk 塞給 LLM,不管它們是否真的相關。
Self-RAG 讓 LLM 在整個流程中「自我反思」:

  1. 是否需要檢索?(有些問題 LLM 直接回答就好)
  2. 檢索到的文件是否相關?(過濾不相關的 chunk)
  3. 生成的回答是否有充分根據?(檢查答案是否基於 context)
  4. 回答是否有用?(自我評分)
問題
 ↓
[ISREL?] 這個問題需要外部知識嗎?
 ├── No → 直接生成答案
 └── Yes → 檢索文件
              ↓
           [ISREL] 每個文件相關嗎?過濾不相關的
              ↓
           生成多個草稿答案
              ↓
           [ISSUP] 答案有文件支持嗎?
              ↓
           [ISUSE] 哪個答案最有用?
              ↓
           輸出最佳答案

簡化版 Self-RAG 實作

  1import openai
  2
  3client = openai.OpenAI(api_key="your-api-key")
  4
  5
  6def needs_retrieval(question: str) -> bool:
  7    """判斷問題是否需要外部知識"""
  8    prompt = f"""判斷以下問題是否需要查詢外部知識庫才能回答(Yes/No)。
  9
 10需要外部知識的例子:詢問具體事實、最新資訊、特定領域知識
 11不需要外部知識的例子:數學計算、常識性問題、語言翻譯
 12
 13問題:{question}
 14回答(只輸出 Yes 或 No):"""
 15
 16    response = client.chat.completions.create(
 17        model="gpt-4o-mini",
 18        messages=[{"role": "user", "content": prompt}],
 19        temperature=0,
 20        max_tokens=5,
 21    )
 22    return response.choices[0].message.content.strip().lower() == "yes"
 23
 24
 25def is_relevant(question: str, chunk: str) -> bool:
 26    """判斷文件片段是否與問題相關"""
 27    prompt = f"""判斷以下【文件片段】是否包含回答【問題】的有用資訊(Yes/No)。
 28
 29【問題】:{question}
 30【文件片段】:{chunk[:300]}
 31
 32是否相關(只輸出 Yes 或 No):"""
 33
 34    response = client.chat.completions.create(
 35        model="gpt-4o-mini",
 36        messages=[{"role": "user", "content": prompt}],
 37        temperature=0,
 38        max_tokens=5,
 39    )
 40    return response.choices[0].message.content.strip().lower() == "yes"
 41
 42
 43def is_grounded(answer: str, context: str) -> bool:
 44    """判斷答案是否有 context 的支持,而非憑空捏造"""
 45    prompt = f"""判斷以下【回答】是否完全基於【參考資料】中的內容,
 46沒有加入參考資料以外的資訊(Yes/No)。
 47
 48【參考資料】:{context[:500]}
 49【回答】:{answer[:300]}
 50
 51是否有根據(只輸出 Yes 或 No):"""
 52
 53    response = client.chat.completions.create(
 54        model="gpt-4o-mini",
 55        messages=[{"role": "user", "content": prompt}],
 56        temperature=0,
 57        max_tokens=5,
 58    )
 59    return response.choices[0].message.content.strip().lower() == "yes"
 60
 61
 62def self_rag(question: str, retriever, max_retries: int = 2) -> str:
 63    """Self-RAG pipeline"""
 64
 65    # Step 1: 是否需要檢索?
 66    if not needs_retrieval(question):
 67        print("💭 直接回答(不需要外部知識)")
 68        response = client.chat.completions.create(
 69            model="gpt-4o",
 70            messages=[{"role": "user", "content": question}],
 71            temperature=0,
 72        )
 73        return response.choices[0].message.content
 74
 75    print("🔍 需要外部知識,開始檢索...")
 76
 77    for attempt in range(max_retries):
 78        # Step 2: 檢索文件
 79        raw_chunks = retriever.retrieve(question, top_k=5)
 80
 81        # Step 3: 過濾不相關的文件
 82        relevant_chunks = [c for c in raw_chunks if is_relevant(question, c)]
 83        print(f"  📄 過濾後相關 chunk 數量:{len(relevant_chunks)}/{len(raw_chunks)}")
 84
 85        if not relevant_chunks:
 86            if attempt < max_retries - 1:
 87                print("  ⚠️ 找不到相關文件,嘗試重新查詢...")
 88                continue
 89            return "根據現有資料,無法回答此問題。"
 90
 91        context = "\n\n---\n\n".join(relevant_chunks)
 92
 93        # Step 4: 生成回答
 94        prompt = f"""請根據以下參考資料回答問題。
 95只使用參考資料中的資訊,不要加入其他知識。
 96
 97【參考資料】
 98{context}
 99
100【問題】{question}"""
101
102        answer = client.chat.completions.create(
103            model="gpt-4o",
104            messages=[{"role": "user", "content": prompt}],
105            temperature=0,
106        ).choices[0].message.content
107
108        # Step 5: 檢查答案是否有根據
109        if is_grounded(answer, context):
110            print("  ✅ 答案有文件支持")
111            return answer
112        else:
113            print(f"  ⚠️ 答案可能包含幻覺,嘗試 {attempt + 1}/{max_retries}")
114
115    return answer  # 即使可能有問題,還是返回最後一次的答案
116
117
118# 使用範例
119# answer = self_rag("Python 的 GIL 是什麼?", retriever)
120# answer = self_rag("2 + 2 等於多少?", retriever)  # 不需要外部知識的問題

優點:大幅降低幻覺;對不需要 RAG 的問題不浪費搜尋資源;有自我品質把關
缺點:多次 LLM 呼叫,延遲顯著增加;每步判斷的準確度依賴模型能力
最佳使用場景:高可信度要求的問答(醫療、法律、財務);需要精確溯源的系統


技術 3:Context Compression(上下文壓縮)

核心問題

向量搜尋取回的 chunk 通常包含大量與當前問題無關的「噪音」:

問題:「asyncio 的事件迴圈如何工作?」

取回的 chunk:
「Python 在 3.4 版本引入了 asyncio 模組,
提供非同步 I/O 的支援。asyncio 的核心是事件迴圈(Event Loop),
它透過單執行緒的方式調度協程(coroutine)。
Python 的 asyncio 可以搭配 aiohttp 做非同步 HTTP 請求,
也可以搭配 asyncpg 做非同步資料庫操作。此外,
Python 3.11 對 asyncio 的效能做了大幅改進...」

↑ 很多內容(aiohttp、asyncpg、效能改進)跟問題無關,但都會佔用 context window

Context Compression 的目標:只保留 chunk 中與問題直接相關的部分

方法一:LLM 提取壓縮

 1def compress_with_llm(question: str, chunk: str) -> str | None:
 2    """用 LLM 從 chunk 中提取與問題相關的句子"""
 3    prompt = f"""從以下【文件片段】中,只提取出與【問題】直接相關的句子。
 4如果整個片段都不相關,輸出「不相關」。
 5只輸出相關句子,不要加任何解釋或格式。
 6
 7【問題】:{question}
 8【文件片段】:{chunk}
 9
10相關句子:"""
11
12    response = client.chat.completions.create(
13        model="gpt-4o-mini",
14        messages=[{"role": "user", "content": prompt}],
15        temperature=0,
16        max_tokens=300,
17    )
18    result = response.choices[0].message.content.strip()
19    return None if "不相關" in result else result
20
21
22def rag_with_compression(question: str, retriever, top_k: int = 6) -> str:
23    """帶 Context Compression 的 RAG pipeline"""
24    # 故意取多一點 chunk(因為壓縮後會過濾掉部分)
25    raw_chunks = retriever.retrieve(question, top_k=top_k)
26
27    # 壓縮每個 chunk
28    compressed = []
29    for chunk in raw_chunks:
30        result = compress_with_llm(question, chunk)
31        if result:
32            compressed.append(result)
33
34    print(f"📉 壓縮:{len(raw_chunks)} chunks → {len(compressed)} 個相關片段")
35
36    if not compressed:
37        return "找不到相關資料。"
38
39    context = "\n\n".join(compressed)
40
41    prompt = f"""根據以下資料回答問題:
42
43{context}
44
45問題:{question}"""
46
47    return client.chat.completions.create(
48        model="gpt-4o",
49        messages=[{"role": "user", "content": prompt}],
50        temperature=0,
51    ).choices[0].message.content

方法二:嵌入相似度過濾(無需 LLM 呼叫)

 1import numpy as np
 2
 3def compress_by_sentence_similarity(
 4    question: str, chunk: str, threshold: float = 0.75
 5) -> str:
 6    """把 chunk 切成句子,過濾掉跟問題語意距離遠的句子"""
 7    import re
 8
 9    # 切成句子(簡單版:按句號切)
10    sentences = re.split(r'[。!?\.\!\?]', chunk)
11    sentences = [s.strip() for s in sentences if len(s.strip()) > 10]
12
13    if not sentences:
14        return chunk
15
16    # 取得問題和所有句子的 embedding
17    all_texts  = [question] + sentences
18    embeddings = np.array([get_embedding(t) for t in all_texts])
19
20    q_emb   = embeddings[0]
21    s_embs  = embeddings[1:]
22
23    # 計算每個句子與問題的相似度
24    sims = s_embs @ q_emb / (
25        np.linalg.norm(s_embs, axis=1) * np.linalg.norm(q_emb) + 1e-9
26    )
27
28    # 只保留相似度超過閾值的句子
29    relevant_sentences = [s for s, sim in zip(sentences, sims) if sim >= threshold]
30
31    return "。".join(relevant_sentences) if relevant_sentences else ""

方法三:LangChain ContextualCompressionRetriever

 1from langchain.retrievers import ContextualCompressionRetriever
 2from langchain.retrievers.document_compressors import LLMChainExtractor
 3from langchain_openai import ChatOpenAI, OpenAIEmbeddings
 4from langchain_community.vectorstores import Chroma
 5
 6llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
 7base_retriever = Chroma(
 8    embedding_function=OpenAIEmbeddings()
 9).as_retriever(search_kwargs={"k": 6})
10
11# 建立壓縮器
12compressor = LLMChainExtractor.from_llm(llm)
13
14# 包裝成壓縮式 Retriever
15compression_retriever = ContextualCompressionRetriever(
16    base_compressor=compressor,
17    base_retriever=base_retriever,
18)
19
20# 使用方式跟一般 retriever 相同
21docs = compression_retriever.invoke("asyncio 的事件迴圈如何工作?")
22for doc in docs:
23    print(doc.page_content)

優點:減少 LLM 的 context 長度 → 降低 token 成本、提升回答焦點;有效過濾噪音
缺點:LLM 壓縮方法增加延遲;Embedding 方法可能過度過濾
最佳使用場景:文件較長且包含混雜主題;context window 有限;需要降低 LLM token 成本


組合使用:完整的進階 RAG Pipeline

把這篇和上一篇的技術組合,可以建立一個健壯的生產級 RAG:

 1class AdvancedRAGPipeline:
 2    """結合 Query Decomposition + Hybrid Search + Reranker + Context Compression"""
 3
 4    def __init__(self, chunks: list[str]):
 5        # 初始化各元件(略,見前幾篇)
 6        self.hybrid_retriever   = HybridRetriever(chunks)
 7        self.reranker           = CrossEncoder("BAAI/bge-reranker-base")
 8
 9    def run(self, question: str) -> dict:
10        # 1. 判斷是否需要問題分解
11        sub_questions = decompose_query(question)
12        is_complex = len(sub_questions) > 1
13
14        if is_complex:
15            print(f"🧩 複雜問題,分解為 {len(sub_questions)} 個子問題")
16            all_candidates = []
17            for sub_q in sub_questions:
18                chunks = self.hybrid_retriever.hybrid_search(sub_q, top_k=10)
19                all_candidates.extend(chunks)
20            candidates = list(dict.fromkeys(all_candidates))[:20]
21        else:
22            candidates = self.hybrid_retriever.hybrid_search(question, top_k=20)
23
24        # 2. Rerank
25        pairs  = [[question, c] for c in candidates]
26        scores = self.reranker.predict(pairs)
27        ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
28        top_chunks = [c for c, _ in ranked[:6]]
29
30        # 3. Context Compression
31        compressed = []
32        for chunk in top_chunks:
33            result = compress_with_llm(question, chunk)
34            if result:
35                compressed.append(result)
36
37        if not compressed:
38            return {"answer": "找不到足夠的相關資料。", "sources": []}
39
40        context = "\n\n---\n\n".join(compressed)
41
42        # 4. 生成最終答案
43        prompt = f"""根據以下參考資料詳細回答問題。
44
45【參考資料】
46{context}
47
48【問題】{question}"""
49
50        answer = client.chat.completions.create(
51            model="gpt-4o",
52            messages=[{"role": "user", "content": prompt}],
53            temperature=0,
54        ).choices[0].message.content
55
56        return {"answer": answer, "context_used": compressed}

小結

技術核心思想解決的問題
Step-Back Prompting先抽象再具體具體問題難以直接搜尋
Query Decomposition分而治之複雜多跳問題
Self-RAG讓 LLM 自我把關幻覺、不相關資料混入
Context Compression只保留相關句子噪音過多、token 浪費

下一篇(最終篇),我們進入生產級 RAG 評估:如何量化你的 RAG 品質?RAGAS 指標是什麼?以及 Agentic RAG——當 RAG 和 AI Agent 結合,能做到什麼?


系列導覽

  • 第一篇:基礎概念與第一個 RAG 系統
  • 第二篇:Chunking 策略與向量資料庫選型
  • 第三篇:進階檢索技術(混合搜尋、HyDE、Multi-Query、Reranker)
  • 第四篇(本篇):查詢優化與 Context 壓縮
  • 第五篇:生產級 RAG 評估與 Agentic RAG