在 Day 11 中,我們認識了 RAG Evaluation,開始從不同面向評估 AI 助理的品質
不過,如果每次測試都需要手動輸入問題、複製回答,再自行判斷結果,將會遇到幾個問題:
因此,今天要將前一天的評估概念,進一步實作成一套 自動化 RAG Evaluation Pipeline
今天的核心目標是:
讓每次修改 RAG 系統後,都能透過固定測試與數據,確認系統是否真的改善
今天將建立以下功能:
[
{
"id": "case_001",
"question": "產品支援哪些作業系統?",
"expected_keywords": [
"Windows 10",
"Windows 11",
"Ubuntu 22.04",
"Ubuntu 24.04"
],
"reference_answer": "本產品支援 Windows 10、Windows 11、Ubuntu 22.04 與 Ubuntu 24.04。",
"should_answer": true
},
{
"id": "case_002",
"question": "安裝完成後需要做什麼?",
"expected_keywords": [
"重新啟動"
],
"reference_answer": "安裝完成後,需要重新啟動系統。",
"should_answer": true
},
{
"id": "case_003",
"question": "是否支援 macOS?",
"expected_keywords": [],
"reference_answer": "目前文件沒有足夠資訊確認是否支援 macOS。",
"should_answer": false
}
]
should_answerRAG 系統不只需要處理「有答案」的問題,也需處理「文件沒有提供答案」的情境
先使用簡單的關鍵字覆蓋率,作為自動化評估的起點
def keyword_coverage(answer, expected_keywords):
if not expected_keywords:
return None
answer_lower = answer.lower()
matched_count = sum(
1
for keyword in expected_keywords
if keyword.lower() in answer_lower
)
return matched_count / len(expected_keywords)
這個方法可以快速檢查回答是否包含必要資訊,但不能代表回答一定正確
def context_precision(relevance_labels):
if not relevance_labels:
return 0.0
relevant_count = sum(
1
for label in relevance_labels
if label > 0
)
return relevant_count / len(relevance_labels)
這裡使用:
0:不相關1:部份相關2:高度相關只要分數大於 0,就視為具備某種程度的相關性
除了品質指標,也需要記錄 RAG Pipeline 的執行時間
import time
def measure_latency(function, *args, **kwargs):
start_time = time.perf_counter()
result = function(*args, **kwargs)
end_time = time.perf_counter()
latency_ms = (end_time - start_time) * 1000
return result, latency_ms
當我們加入 Reranking 或更複雜的評估模型後,品質可能提升,但執行時間也可能增加
因此,不能只觀察準確度,也要確認系統是否符合實際應用需求
為了讓評估程式能夠處理不同測試案例,RAG Pipeline 應該回傳一致的資料格式
建議格式:
{
"question": "安裝完成後需要做什麼?",
"answer": "安裝完成後,需要重新啟動系統。",
"contexts": [
{
"document": "安裝完成後,需要重新啟動系統。",
"metadata": {
"source": "sample.txt",
"chunk_id": "chunk_002"
}
}
],
"latency_ms": 1240
}
至少需要包含:
from src.evaluation.metrics import (
keyword_coverage,
measure_latency
)
def evaluate_case(case, rag_pipeline):
question = case["question"]
expected_keywords = case["expected_keywords"]
result, latency_ms = measure_latency(
rag_pipeline.run,
question
)
answer = result.get("answer", "")
keyword_score = keyword_coverage(
answer,
expected_keywords
)
return {
"id": case["id"],
"question": question,
"answer": answer,
"keyword_coverage": keyword_score,
"latency_ms": round(latency_ms, 2),
"contexts": result.get("contexts", []),
"reference_answer": case.get(
"reference_answer",
""
)
}
將「產生回答」與「評估回答」分離,方便未來替換評估方法
def evaluate_dataset(dataset, rag_pipeline):
results = []
for case in dataset:
result = evaluate_case(
case,
rag_pipeline
)
results.append(result)
return results
此時,我們已經可以一次執行多筆測試案例,而不需要逐筆手動操作
import json
from pathlib import Path
def save_json(results, output_path):
path = Path(output_path)
path.parent.mkdir(
parents=True,
exist_ok=True
)
with open(
path,
"w",
encoding="utf-8"
) as file:
json.dump(
results,
file,
ensure_ascii=False,
indent=2
)
JSON 可以保留完整資料,適合:
CSV 適合用來查看表格與進行統計分析
import csv
from pathlib import Path
def save_csv(results, output_path):
path = Path(output_path)
path.parent.mkdir(
parents=True,
exist_ok=True
)
fieldnames = [
"id",
"question",
"answer",
"keyword_coverage",
"latency_ms"
]
with open(
path,
"w",
encoding="utf-8-sig",
newline=""
) as file:
writer = csv.DictWriter(
file,
fieldnames=fieldnames
)
writer.writeheader()
for result in results:
row = {
field: result.get(field)
for field in fieldnames
}
writer.writerow(row)
適合用途
JSON: 保存完整結構與 Context
CSV: 表格檢視與統計分析
實務上可以兩種格式都可以保存
完整結果之外,也需要產生摘要資訊
def summarize_results(results):
valid_keyword_scores = [
result["keyword_coverage"]
for result in results
if result["keyword_coverage"] is not None
]
latencies = [
result["latency_ms"]
for result in results
]
summary = {
"total_cases": len(results),
"average_latency_ms": 0.0,
"average_keyword_coverage": None
}
if latencies:
summary["average_latency_ms"] = (
sum(latencies) / len(latencies)
)
if valid_keyword_scores:
summary["average_keyword_coverage"] = (
sum(valid_keyword_scores)
/ len(valid_keyword_scores)
)
return summary
這是分析 RAG 問題時非常重要的一步
檢索階段沒有找到必要文件
這可能是:
檢索結果已經包含正確文件,但 LLM 沒有正確使用
回歸測試是指:
在修改系統後,重新執行既有測試,確認原本正常的功能沒有被破壞
雖然系統仍然能產生文字,但原本的功能已經退化
def regression_check(
results,
minimum_score=0.8
):
failures = []
for result in results:
score = result["keyword_coverage"]
if score is None:
continue
if score < minimum_score:
failures.append({
"id": result["id"],
"score": score
})
return failures
這只是簡化版回歸測試
環境應該考慮:
不能只依賴單一關鍵字分數決定系統是否通過
當評估程式可以透過指令執行後,就能進一步整合至 CI/CD
這些數值只是示範,實際門檻應根據:
原因可能是:
採用多層次評估:
第一層:關鍵字與格式檢查
第二層:語意與相關性評估
第三層:Faithfulness 檢查
第四層:人工抽查
自動化評估可以提高效率,但不應完全取代人工審查
平均分數仍可能偏高,但案例 D 可能是使用者最常詢問的問題
除了平均分數,也應記錄:
評估不應只追求整體平均值
如果使用 LLM Judge 評估回答,評估模型可能:
今天將 RAG Evaluation 從概念轉換成可執行的自動化流程
固定的資料集、評估方法與輸出格式,才能讓不同實驗具備可比較性
透過分析個別錯誤,可以確認問題來自:
每次修改模型、Prompt 或檢索參數後,都應重新執行固定測試,確認原本的功能仍然正常
RAG 系統不是完成一次就不需要調整,而是需要持續:
目前系統這代表我們開始從「建立 AI 助理功能」,進一步進入「驗證與維護 AI 助理品質」的階段
今天完成了自動化 RAG Evaluation Pipeline,但目前仍以固定測試資料集與基本評估指標為主
接下來可以進一步研究:
Day 13:建立高品質評估資料集——讓測試案例更接近真實使用情境
從今天的「讓評估流程自動化」,進一步走向「建立具有代表性的測試資料,讓 AI 助理的品質改善更有方向」