本文由 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 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)
評估流程:
- 使用第三模組建立的測試資料集(包含原始答案)
- 使用 LLM,從原始答案生成對應問題(原始答案 A → 生成問題 Q )
- 用上述問題透過 RAG 系統生成新答案(生成問題 Q → RAG 生成答案 A’ )
- 計算原始答案( A )與生成答案( A’ )的餘弦相似度
- 得出 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
》線上評估
只有簡介,沒有實作。
-
時機: 系統部署後,透過真實用戶的互動來進行。
-
方法:
-
A/B 測試:將流量分成兩組,一組使用舊版系統,一組使用新版系統,藉此比較新系統在真實商業指標上的表現 。
-
用戶回饋 (User Feedback):直接收集用戶的意見,例如常見的「讚/倒讚」按鈕 。
-
監控 (Monitoring):觀察系統的整體「健康狀況」,例如 CPU 使用率、回應延遲等效能指標 。
-
-
關注點:商業指標、用戶滿意度、系統健康狀態
》小結
我們可以這樣比喻:
documents-with-ids.json就像是教科書,包含了所有知識和標準答案。ground-truth-data.csv就像是老師出的考卷,上面只有「問題」,以及每個問題對應到教科書的哪個章節。
我們的評估流程就是:
- 看著考卷 (CSV) 上的問題。
- 讓 RAG 系統去翻查教科書 (JSON) 來找出相關資料並生成回答。
- 最後,把系統的回答和教科書上的標準答案 (也是來自 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 |
|---|---|---|---|
| 排名 | |||
| 平均餘弦相似度 | 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 系統的輸出進行質化評估。
》兩種評估框架
- A → Q → A’ 評估:評審判斷生成的答案與原始答案的相關性,適用於離線評估。
- 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 評審更能理解上下文、邏輯和細微的語意差別,使其判斷結果更接近人類。它可以識別出那些「看起來很像,但其實是錯的」的答案。
▌參考資料