《筆記》RAG 評估策略(下)

本文由 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 評估策略(上) 中,討論的是如何評估流程中的「搜尋引擎模組 」(也稱作檢索, retrieval),當時使用的評估指標是 Hit RateMRR (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 )的情況 :left_arrow: 可能比較接近我想做的佛學QA

》線上評估

只有簡介,沒有實作。

  • 時機: 系統部署後,透過真實用戶的互動來進行。

  • 方法

    • A/B 測試:將流量分成兩組,一組使用舊版系統,一組使用新版系統,藉此比較新系統在真實商業指標上的表現 。

    • 用戶回饋 (User Feedback):直接收集用戶的意見,例如常見的「讚/倒讚」按鈕 。

    • 監控 (Monitoring):觀察系統的整體「健康狀況」,例如 CPU 使用率、回應延遲等效能指標 。

  • 關注點:商業指標、用戶滿意度、系統健康狀態

》小結

我們可以這樣比喻:

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

我們的評估流程就是:

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

▌3.6 離線 RAG 評估:生成評估資料

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

載入資料與設定

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

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)。

# --- 使用 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 生成的答案與原始標準答案的接近程度。

計算與比較餘弦相似度

# --- 定義計算相似度的函數 ---
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
排名 :2nd_place_medal: 2 :3rd_place_medal: 3 :1st_place_medal: 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 筆)來執行評審。

# --- 評審 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 欄位,這直接告訴了你問題出在哪裡。根據影片中的範例:

    • 案例一(主題不匹配):生成的答案討論的是 pip version error,而原始答案關注的是 Docker build error。這立刻讓你知道,可能是 RAG 流程中的檢索 (retrieval) 步驟找錯了文件
    • 案例二(上下文缺失):生成的答案之所以無效,是因為「提供的上下文中不包含啟動 Docker 的具體指令」。這再次明確地指向了檢索步驟的失敗,系統沒有找到包含答案的正確段落。

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

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

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

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

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

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

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

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

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


▌參考資料

1個讚