# 《筆記》RAG 評估策略（下）

**URL:** https://vip.studycamp.tw/t/%E3%80%8A%E7%AD%86%E8%A8%98%E3%80%8Brag-%E8%A9%95%E4%BC%B0%E7%AD%96%E7%95%A5%EF%BC%88%E4%B8%8B%EF%BC%89/8282
**Category:** 自然語言處理
**Created:** [2025年七月23日 00:13 UTC](https://vip.studycamp.tw/t/%E3%80%8A%E7%AD%86%E8%A8%98%E3%80%8Brag-%E8%A9%95%E4%BC%B0%E7%AD%96%E7%95%A5%EF%BC%88%E4%B8%8B%EF%BC%89/8282 "2025-07-23T00:13:21Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![sky](https://vip.studycamp.tw/user_avatar/vip.studycamp.tw/sky/32/8098_2.png) [@sky](https://vip.studycamp.tw/u/sky)
#### Post date: [2025年七月23日 00:13 UTC](https://vip.studycamp.tw/t/%E3%80%8A%E7%AD%86%E8%A8%98%E3%80%8Brag-%E8%A9%95%E4%BC%B0%E7%AD%96%E7%95%A5%EF%BC%88%E4%B8%8B%EF%BC%89/8282/1 "2025-07-23T00:13:22Z")

</div>

> 本文由 Gemini 協助生成，內容有可能不完全正確。

從生成評估資料，到使用兩種不同指標（餘弦相似度、LLM 作為評審）進行評估的完整流程。

3.5 離線 vs. 線上 RAG 評估

3.6 離線 RAG 評估：生成評估資料 (Offline RAG Evaluation: Generating Data)

3.7 離線 RAG 評估：餘弦相似度 (Cosine Similarity)

3.8 離線 RAG 評估：LLM 作為評審 (LLM as a Judge)

* * *

## ▌3.5 離線 vs. 線上 RAG 評估

開始前我們複習一下 RAG 三個主要模組：搜尋引擎模組 ║ 提示建構模組 ║ LLM模組

下表是當時說明三個模組在本課程各章節演進示意（正確性待驗證），不過不包含老師在進行 evaluation 前，插入的兩個新主題（`dlt` Data Load Tool 資料載入工具 & Agentic RAG）。

| Module | 搜尋引擎模組 | 提示建構模組 | LLM模組 | 主要改進重點 |
| --- | --- | --- | --- | --- |
| 01-intro | MinSearch／ElasticSearch | 基本提示模板 | OpenAI API | 建立基礎架構 |
| 02-open-source | 沿用Module 1 | 沿用Module 1 | 開源LLM (HuggingFace, Ollama) | LLM模組替換 |
| 03-vector | 向量搜尋 (Qdrant, 語意檢索) | 沿用或微調 | 沿用Module 2 | 搜尋引擎模組升級 |
| 04-evaluation | 沿用Module 3 | 評估導向 提示 | 沿用Module 2/3 | 整個流程監控改進 |
| 05-orchestration | 混合搜尋 + 自動化 | 動態提示 | 沿用 | 生產環境整合 |
| 06-project | 自選最佳組合 | 自選最佳組合 | 自選最佳組合 | 端到端實作 |

在 [**RAG 評估策略（上）**](https://vip.studycamp.tw/t/8186/1) 中，討論的是如何評估流程中的「 **搜尋引擎模組** 」(也稱作檢索, retrieval)，當時使用的評估指標是 **Hit Rate** 和 **MRR (Mean Reciprocal Rank)** 。

本筆記則是 **評估整個 RAG 系統** （3.5~3.8），也就是從輸入問題到輸出最終答案（end to end）的品質 。

有兩種評估方法：**離線評估 (Offline Evaluation)** 與 **線上評估 (Online Evaluation)**。

### 》離線評估

- **時機** ：開發階段，在模型或系統正式部署前進行 。

- **方式** ：使用預先準備好的「標準答案」資料集 (ground truth dataset)，（在沒有真實用戶參與的情況下）用自動化指標：餘弦相似度 (Cosine Similarity)、LLM 作為評審 (LLM as a Judge)…，來評估系統的表現 。比較不同方法的優劣，決定最佳實作方案。

#### 1. 餘弦相似度 (Cosine Similarity)

**評估流程：**

1. 使用第三模組建立的測試資料集（包含原始答案）
2. 使用 LLM，從原始答案生成對應問題（原始答案 A → 生成問題 Q ）
3. 用上述問題透過 RAG 系統生成新答案（生成問題 Q → RAG 生成答案 A’ ）
4. 計算原始答案（ A ）與生成答案（ A’ ）的餘弦相似度
5. 得出 RAG 系統品質指標

**資料結構：**

- **原始答案** (A) → **生成問題** (Q) → **RAG 生成答案** (A’)
- 比較 A 與 A’ 的相似度

#### 2. LLM 作為評判者 (LLM as a Judge)

**兩種評估方式：**

**方式一：A → Q → A’ 評估**

- 提供原始答案和 RAG 生成答案給 LLM
- 詢問 LLM 兩個答案的相似度（取代前述的餘弦相似度）
- LLM 給出評估結果

**方式二：Q → A‘ 評估**

- 只提供問題和 RAG 生成的答案
- 詢問 LLM 答案對問題的回應品質
- 適用於沒有標準答案（沒有一開始的 A ）的情況 ⬅ 可能比較接近我想做的佛學QA

### 》線上評估

> 只有簡介，沒有實作。

- **時機** ： 系統部署後，透過真實用戶的互動來進行。

- **方法** ：

- **關注點** ：商業指標、用戶滿意度、系統健康狀態

### 》小結

我們可以這樣比喻：

- **`documents-with-ids.json`** 就像是 **教科書** ，包含了所有知識和標準答案。
- **`ground-truth-data.csv`** 就像是老師出的 **考卷** ，上面只有「問題」，以及每個問題對應到教科書的哪個章節。

我們的評估流程就是：

1. 看著**考卷 (CSV)** 上的問題。
2. 讓 RAG 系統去**翻查教科書 (JSON)** 來找出相關資料並生成回答。
3. 最後，把系統的回答和**教科書上的標準答案 (也是來自 JSON)** 做比對，看答得好不好。

* * *

## ▌3.6 離線 RAG 評估：生成評估資料

本節的目標是 ~~利用先前建立的「標準答案」(Ground Truth) 資料集，實際運行我們的 RAG 系統，為每一筆資料產生由 LLM 生成的答案。這些生成的結果將被儲存起來，用於後續的量化評估~~ 生成 A’。

### 》 **載入資料與設定**

在開始生成答案前，首先需要載入資料並設定好完整的 RAG 流程。

```python
import requests
import pandas as pd
from sentence_transformers import SentenceTransformer
from tqdm.auto import tqdm
import numpy as np
from minsearch import VectorSearch
from openai import OpenAI

# --- 1. 載入資料 ---
# 定義資料來源的 URL 前綴
url_prefix = 'https://raw.githubusercontent.com/DataTalksClub/llm-zoomcamp/main/03-evaluation/'
# 取得帶有 ID 的文件 JSON 資料
docs_url = url_prefix + 'search_evaluation/documents-with-ids.json'
documents = requests.get(docs_url).json()
# 取得標準答案資料集 CSV
ground_truth_url = url_prefix + 'search_evaluation/ground-truth-data.csv'
df_ground_truth = pd.read_csv(ground_truth_url)
# 篩選出僅與 'machine-learning-zoomcamp' 課程相關的資料
df_ground_truth = df_ground_truth[df_ground_truth.course == 'machine-learning-zoomcamp']
ground_truth = df_ground_truth.to_dict(orient='records')
# 建立文件 ID 索引，方便快速查找
doc_idx = {d['id']: d for d in documents}

# --- 2. 設定 RAG 檢索流程 (Vector Search) ---
# 載入預訓練的句子轉換模型
model_name = 'multi-qa-MiniLM-L6-cos-v1'
model = SentenceTransformer(model_name)
# 將「問題 + 內文」合併後進行編碼，產生向量
vectors = [model.encode(doc['question'] + ' ' + doc['text']) for doc in tqdm(documents)]
vectors = np.array(vectors)
# 建立並訓練 minsearch 向量索引
vindex = VectorSearch(keyword_fields=['course'])
vindex.fit(vectors, documents)

# --- 3. 定義完整的 RAG 函數 ---
client = OpenAI() # 初始化 OpenAI 客戶端

def build_prompt(query, search_results):
    # ... (程式碼同上一版本)
    # 此函數根據搜尋結果建立 Prompt
    prompt_template = """
You're a course teaching assistant. Answer the QUESTION based on the CONTEXT from the FAQ database.
Use only the facts from the CONTEXT when answering the QUESTION.
QUESTION: {question}
CONTEXT:
{context}
""".strip()
    context = ""
    for doc in search_results:
        context = context + f"section: {doc['section']}\nquestion: {doc['question']}\nanswer: {doc['text']}\n\n"
    prompt = prompt_template.format(question=query, context=context).strip()
    return prompt

def llm(prompt, model='gpt-4o'):
    # ... (程式碼同上一版本)
    # 此函數呼叫 OpenAI API
    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

def rag(query: dict, model='gpt-4o') -> str:
    # ... (程式碼同上一版本)
    # 此函數整合了搜尋、建立 Prompt 和呼叫 LLM 的所有步驟
    search_results = vindex.search(vector=model.encode(query['question']), filter_dict={'course': query['course']}, num_results=5)
    prompt = build_prompt(query['question'], search_results)
    answer = llm(prompt, model=model)
    return answer

```

### 》 **執行 RAG 流程生成答案**

這個步驟是遍歷標準答案資料集中的每一筆紀錄，使用 RAG 流程來生成一個新的答案 (`answer_llm`)。

```python
# --- 使用 gpt-4o 循序生成答案 ---
# 這個過程相當耗時，影片中提到執行完成花費了將近 3 個小時，成本約為 10 美元。
answers_gpt4o = {}
for i, rec in enumerate(tqdm(ground_truth)):
    if i in answers_gpt4o: continue
    answer_llm = rag(rec, model='gpt-4o')
    doc_id = rec['document']
    answer_orig = doc_idx[doc_id]['text']
    answers_gpt4o[i] = {'answer_llm': answer_llm, 'answer_orig': answer_orig, **rec}

# --- 使用 gpt-3.5-turbo 平行處理以加速 ---
# 平行處理雖然快，但也更容易達到 API 的速率限制
# 透過平行處理，gpt-3.5-turbo 只花了約 6 分鐘，成本僅 0.79 美元
from concurrent.futures import ThreadPoolExecutor
def process_record(rec):
    answer_llm = rag(rec, model='gpt-3.5-turbo')
    doc_id = rec['document']
    answer_orig = doc_idx[doc_id]['text']
    return {'answer_llm': answer_llm, 'answer_orig': answer_orig, **rec}

pool = ThreadPoolExecutor(max_workers=6)
# ... (map_progress 函數定義)
# results_gpt35 = map_progress(pool, ground_truth, process_record)

# --- 將結果儲存為 CSV ---
df_gpt4o = pd.DataFrame.from_dict(answers_gpt4o, orient='index')
# df_gpt35 = pd.DataFrame(results_gpt35)
df_gpt4o.to_csv('data/results-gpt4o.csv', index=False)
# df_gpt35.to_csv('data/results-gpt35.csv', index=False)

```

## ▌3.7 離線 RAG 評估：餘弦相似度 (Cosine Similarity)

本節的目標是使用 **餘弦相似度** 這個量化指標，來評估由 LLM 生成的答案與原始標準答案的接近程度。

### 》 **計算與比較餘弦相似度**

```python
# --- 定義計算相似度的函數 ---
def compute_similarity(record):
    v_llm = model.encode(record['answer_llm'])
    v_orig = model.encode(record['answer_orig'])
    return v_llm.dot(v_orig) # 對於標準化後的向量，點積等於餘弦相似度

# --- 遍歷 gpt-4o 的結果並計算相似度 ---
similarity_4o = [compute_similarity(rec) for rec in tqdm(df_gpt4o.to_dict(orient='records'))]
df_gpt4o['cosine'] = similarity_4o
print("GPT-4o Cosine Similarity Stats:")
print(df_gpt4o['cosine'].describe())

# --- 對 gpt-3.5-turbo 和 gpt-4o-mini 重複相同步驟 ---
# ...

# --- 繪製分數分佈圖以比較模型 ---
import matplotlib.pyplot as plt
import seaborn as sns

sns.distplot(df_gpt4o['cosine'], label='4o')
sns.distplot(df_gpt35['cosine'], label='3.5-turbo')
sns.distplot(df_gpt4o_mini['cosine'], label='4o-mini')
plt.legend()
plt.title("RAG LLM performance")
plt.xlabel("A->Q->A' Cosine Similarity")
plt.show()

```

從數據中可以得出結論：`gpt-4o-mini` 的表現與 `gpt-4o` 非常接近，但價格比 `gpt-3.5-turbo` 還要便宜（約 0.28 美元），使其成為本次評估中的最佳選擇。但需注意其 API 速率限制較為嚴格。

### 》餘弦相似度評估表格

### » 模型效能比較統計表

| 統計指標 | GPT-4o | GPT-3.5 | GPT-4o-mini |
| --- | --- | --- | --- |
| **樣本數量** | 1,830 | 1,830 | 1,830 |
| **平均餘弦相似度** | 0.679 | 0.658 | **0.680** |
| **餘弦相似度標準差** | 0.218 | 0.226 | 0.216 |
| **餘弦相似度最小值** | -0.153 | -0.169 | -0.142 |
| **餘弦相似度第一四分位數 (25%)** | 0.591 | 0.547 | 0.586 |
| **餘弦相似度中位數 (50%)** | 0.735 | 0.715 | 0.734 |
| **餘弦相似度第三四分位數 (75%)** | 0.835 | 0.817 | 0.837 |
| **餘弦相似度最大值** | 0.995 | 1.000 | 0.983 |

### » 模型排名（依平均餘弦相似度）

| 模型 | GPT-4o | GPT-3.5 | GPT-4o-mini |
| --- | --- | --- | --- |
| **排名** | 🥈 **2** | 🥉 **3** | 🥇 **1** |
| **平均餘弦相似度** | 0.679 | 0.658 | **0.680** |
| **與最佳差距** | -0.001 | -0.022 | - |
| **成本效益評價** | 最昂貴 | 最便宜 | **最佳** |

### » 依穩定性排名（標準差最小）

| 模型 | GPT-4o | GPT-3.5 | GPT-4o-mini |
| --- | --- | --- | --- |
| **排名** | 2 | 3 | **1** |
| **餘弦相似度標準差** | 0.218 | 0.226 | **0.216** |

### » 依中位數排名

| 模型 | GPT-4o | GPT-3.5 | GPT-4o-mini |
| --- | --- | --- | --- |
| **排名** | **1** | 3 | 2 |
| **餘弦相似度中位數** | **0.735** | 0.715 | 0.734 |

**總結：** GPT-4o-mini 在平均值和穩定性兩個關鍵指標上表現最佳，因此推薦為最佳選擇。

* * *

## ▌3.8 離線 RAG 評估：LLM 作為評審 (LLM as a Judge)

除了數值指標，我們還可以使用另一個強大的 LLM 來 **扮演評審的角色** ，對 RAG 系統的輸出進行質化評估。

### 》兩種評估框架

1. **A → Q → A’ 評估** ：評審判斷生成的答案與 **原始答案** 的相關性，適用於 **離線評估** 。
2. **Q → A‘ 評估** ：評審判斷生成的答案是否良好地回答了 **問題** ，適用於 **線上監控** 。

### 》 **評審 Prompt 與執行**

為了節省成本，我們只對 `gpt-4o-mini` 的結果進行抽樣（150 筆）來執行評審。

```python
# --- 評審 Prompt 模板 ---
# 為了讓輸出格式一致，我們要求它以可解析的 JSON 格式回傳
prompt1_template = """
You are an expert evaluator...
Original Answer: {answer_orig}
Generated Question: {question}
Generated Answer: {answer_llm}
...
Provide your evaluation in parsable JSON without using code blocks:
{{
  "Relevance": "NON_RELEVANT" | "PARTLY_RELEVANT" | "RELEVANT",
  "Explanation": "[Provide a brief explanation for your evaluation]"
}}
""".strip()
# prompt2_template (Q->A) 結構類似，但不包含 {answer_orig}

# --- 抽樣並執行評估 ---
df_sample = df_gpt4o_mini.sample(n=150, random_state=1)
samples = df_sample.to_dict(orient='records')

evaluations_aqa = []
for record in tqdm(samples):
    prompt = prompt1_template.format(**record)
    evaluation_str = llm(prompt, model='gpt-4o-mini')
    evaluations_aqa.append(json.loads(evaluation_str)) # 解析 JSON

df_evaluations_aqa = pd.DataFrame(evaluations_aqa)
print("A->Q->A' 評估結果統計:")
print(df_evaluations_aqa.Relevance.value_counts())

# --- 分析被評為不相關的案例 ---
# 檢視被評為 'NON_RELEVANT' 的解釋，有助於診斷問題
non_relevant_cases = df_evaluations_aqa[df_evaluations_aqa.Relevance == 'NON_RELEVANT']
print(non_relevant_cases.iloc[0]['Explanation'])
# 範例解釋：生成的答案討論的是 pip 版本錯誤，而原始答案關注的是 Docker build 錯誤。

```

分析結果顯示，LLM 評審能夠有效地找出回答不相關的案例。例如，它能指出 RAG 流程中的**檢索步驟 (retrieval)** 可能失敗了，沒有找到正確的文件。

| | Relevance | Explanation |
| --- | --- | --- |
| 45 | NON\_RELEVANT | The generated answer does not address the ques… |
| 49 | NON\_RELEVANT | The generated answer explicitly states that th… |
| 139 | NON\_RELEVANT | The generated answer provides information abou… |

* * *

## ▌觀察

單看「RELEVANT」或「NON\_RELEVANT」的統計數字，很難立刻看出它比單純的數字分數（如餘弦相似度）好在哪裡。

LLM 作為評審（LLM as a Judge）最大的好處在於：它不僅告訴我們結果的 **「好壞」（What）** ，更重要的是提供了 **「為什麼」（Why）** 。

這使得它成為一個強大的 **診斷工具** ，而不僅僅是一個評分工具。

想像大學入學考試，不管是以前的聯考，或現在的指考。雖然這個比喻不是很恰當，但很容易理解：

- **餘弦相似度** ：就像一台 **讀卡閱卷機** 。它能快速告訴你選擇題考了幾分，但無法告訴您申論題錯在哪裡。

- **LLM 作為評審** ：就像一位 **真人老師** 。他不僅會給您的作文打分數，還會在旁邊寫下評語：「這一段論點很好，但那一段偏題了，而且這個例子不夠有說服力。」

### 》LLM 作為評審的優勢：

### » 1. 提供可操作的診斷資訊 (Provides Actionable Diagnostic Information)

> 這是它與餘弦相似度最根本的區別。

- **餘弦相似度** ：如果一個答案的餘弦相似度分數很低（例如 0.3），你只知道「生成的答案和原始答案在語意上不相似」。但你不知道 **為什麼不相似** 。是因為主題完全錯誤？還是遺漏了關鍵細節？你需要手動去檢查才能發現問題。

- **LLM 作為評審** ：評審的回覆中包含了 **`Explanation`** 欄位，這直接告訴了你問題出在哪裡。根據影片中的範例：

有了這些具體的「診斷報告」，開發者就可以針對性地去優化系統，例如調整檢索策略或改進 embedding 模型，而不是盲目地調整。

### » 2. 評估的維度更豐富 (More Rich/Dimensional Evaluation)

餘弦相似度主要衡量「語意相似性」這一個維度。但一個好的答案不僅僅是語意相似。

透過設計不同的 Prompt，我們可以讓 LLM 評審從多個維度進行評估，例如：

- **相關性 (Relevance)**：答案是否切題？
- **真實性 (Factuality)**：答案是否基於提供的上下文，沒有產生幻覺 (Hallucination)？
- **簡潔性 (Conciseness)**：答案是否言簡意賅，沒有廢話？
- **安全性 (Safety)**：答案是否包含有害內容？

這種多維度的評估遠比單一的數字分數更能全面地衡量答案的品質。

### » 3. 更能模擬人類判斷，找出語意陷阱

餘弦相似度有時會被「欺騙」。例如，兩個句子可能使用了很多相同的詞彙，但意思卻完全相反，此時它們的餘弦相似度分數可能依然很高。

而 LLM 評審更能理解 **上下文、邏輯和細微的語意差別** ，使其判斷結果更接近人類。它可以識別出那些「看起來很像，但其實是錯的」的答案。

* * *

## ▌參考資料

> <https://github.com/DataTalksClub/llm-zoomcamp/blob/main/03-evaluation/README.md>

![](https://img.youtube.com/vi/yTKGSqkhgI4/maxresdefault.jpg)https://www.youtube.com/embed/yTKGSqkhgI4?feature=oembed&wmode=opaque&list=PL3MmuxUbc_hIB4fSqLy_0AfTjVLpgjV3R
