本文由 Claude 協助生成,內容有可能不完全正確。
3.1 介紹
3.2 產生 Ground truth 資料
3.3 文字搜尋的 RAG 評估
3.4 向量搜尋的 RAG 評估
提醒:本節課程的 3.4 Ranking evaluation: vector search,講師尚未完成(不曉得是不是因為改用 Qdrant 的原因)。Homework 也尚未完成。
▌課程複習
以下是課程開始到現在的簡介,包含安裝 Python 模組、環境配置、minsearch、ElasticSeaarch 等,設為隱藏狀態,有需要參考的朋友,請自己點擊展開。
前情提要,請點擊展開
1.1 LLM 與 RAG 介紹
LLM Zoomcamp 是一門專注於大型語言模型(Large Language Model)實際應用的課程,特別強調 RAG(檢索增強生成,Retrieval Augmented Generation)技術的實作。
課程目標: 建立一個基於 LLM 和 RAG 的問答系統,解決 DataTalks.Club 中包含 321 頁常見問題集(FAQ)的資訊檢索問題。
1.2 技術架構
RAG 框架的核心組件:
- 知識庫(資料庫):存儲 FAQ 文件
- LLM:生成回答的語言模型
工作流程:
- 用戶提出問題
- 查詢發送到知識庫
- 檢索相關文件
- 創建包含問題和上下文的提示
- LLM 基於上下文生成答案
- 返回答案給用戶
2. 環境配置與技術實作
2.1 開發環境
支援平台:
- GitHub Codespaces
- Google Colab
- 本地機器
必要套件安裝:
# 安裝必要的 Python 套件
# tqdm: 進度條顯示工具
# jupyter: 互動式開發環境
# openai: OpenAI API 客戶端
# elasticsearch: Elasticsearch 搜尋引擎客戶端
# scikit-learn: 機器學習工具包
# pandas: 資料處理工具
pip install tqdm jupyter==7.1.2 openai elasticsearch scikit-learn pandas
2.2 OpenAI API 設定
我的作法是將 OpenAI API 金鑰放到
.env
中,設為環境變數,不會同步到 GitHub。
API 金鑰配置:
# 設定 OpenAI API 金鑰為環境變數
# 注意:絕對不要將真實的 API 金鑰提交到版本控制系統
export OPENAI_API_KEY=你的金鑰
測試代碼:
# 匯入 OpenAI 客戶端
from openai import OpenAI
# 建立 OpenAI 客戶端,會自動讀取環境變數中的 API 金鑰
client = OpenAI()
# 向 GPT-4o 模型發送聊天請求
response = client.chat.completions.create(
model="gpt-4o", # 使用 GPT-4o 模型
messages=[{
"role": "user", # 用戶角色
"content": "is it too late to join the course" # 查詢內容
}]
)
# 輸出模型的回應內容
print(response.choices[0].message.content)
3. 檢索系統實作
3.1 MinSearch 搜尋引擎
索引建立:
# 匯入 minsearch 模組
from minsearch import Index
# 建立搜尋索引
index = Index(
text_fields=["question", "text", "section"], # 用於全文搜尋的欄位
keyword_fields=["course"] # 用於精確過濾的欄位
)
# 將文件資料載入索引
index.fit(documents)
查詢執行:
# 執行搜尋查詢
results = index.search(
query=q, # 搜尋查詢字串
filter_dict={'course': 'data-engineering-zoomcamp'}, # 過濾條件
boost={'question': 3.0}, # 提升 question 欄位的權重(3倍)
num_results=5 # 返回前5個結果
)
3.2 Elasticsearch 實作
Docker 運行:
# 使用 Docker 運行 Elasticsearch
docker run -it \
--rm \ # 容器停止後自動刪除
--name elasticsearch \ # 容器名稱
-p 9200:9200 \ # 對外端口映射
-p 9300:9300 \ # 集群通信端口
-e "discovery.type=single-node" \ # 單節點模式
-e "xpack.security.enabled=false" \ # 禁用安全功能
docker.elastic.co/elasticsearch/elasticsearch:8.4.3
索引設定:
# Elasticsearch 索引配置
index_settings = {
"settings": {
"number_of_shards": 1, # 分片數量
"number_of_replicas": 0 # 副本數量
},
"mappings": {
"properties": {
"text": {"type": "text"}, # 全文搜尋欄位
"section": {"type": "text"}, # 全文搜尋欄位
"question": {"type": "text"}, # 全文搜尋欄位
"course": {"type": "keyword"} # 精確匹配欄位
}
}
}
4. RAG 流程模組化
4.1 核心函數設計
搜尋函數:
def search(query):
"""
執行文件搜尋
Args:
query (str): 搜尋查詢字串
Returns:
list: 搜尋結果列表
"""
results = index.search(
query=query,
filter_dict={'course': 'data-engineering-zoomcamp'}, # 限制課程範圍
boost={'question': 3.0}, # question 欄位權重提升3倍
num_results=5 # 返回最相關的5個結果
)
return results
提示建立函數:
def build_prompt(query, search_results):
"""
根據查詢和搜尋結果建立 LLM 提示
Args:
query (str): 用戶查詢
search_results (list): 搜尋結果列表
Returns:
str: 格式化的提示字串
"""
# 定義提示模板
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}
If the CONTEXT doesn't contain the answer, output "I don't know".
""".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
LLM 呼叫函數:
def llm(prompt):
"""
調用 LLM 生成回答
Args:
prompt (str): 輸入提示
Returns:
str: LLM 生成的回答
"""
response = client.chat.completions.create(
model='gpt-4o', # 使用 GPT-4o 模型
messages=[{"role": "user", "content": prompt}] # 用戶訊息
)
return response.choices[0].message.content
完整 RAG 流程:
def rag(query):
"""
完整的檢索增強生成流程
Args:
query (str): 用戶查詢
Returns:
str: 最終回答
"""
# 步驟1: 搜尋相關文件
search_results = search(query)
# 步驟2: 建立包含上下文的提示
prompt = build_prompt(query, search_results)
# 步驟3: 使用 LLM 生成回答
answer = llm(prompt)
return answer
▌RAG Evolution(評估)
5. 評估系統與指標
5.1 Ground Truth 資料生成
本節主要說明,以目前資料集的 QA 為基礎,使用 LLM 為每個 Q 產生新的五個 Q。
為避免大家生成 Ground Truth 時,需要一些成本,老師也提供了他所生成的新資料集。
文件 ID 生成:
import hashlib
def generate_document_id(doc):
"""
為文件生成唯一識別碼
Args:
doc (dict): 包含課程、問題、文本的文件字典
Returns:
str: 8位十六進制文件ID
"""
# 組合多個欄位確保唯一性
combined = f"{doc['course']}-{doc['question']}-{doc['text'][:10]}"
# 使用MD5雜湊算法
hash_object = hashlib.md5(combined.encode())
# 返回前8位作為文件ID
return hash_object.hexdigest()[:8]
# 原本為以下三行 code,整合為上方一行 code
# hash_hex = hash_object.hexdigest()
# document_id = hash_hex[:8]
# return document_id
問題生成提示模板:
由一個問答,生成另外五個相同語意的問題。
# LLM 問題生成的提示模板
prompt_template = """
You emulate a student who's taking our course.
Formulate 5 questions this student might ask based on a FAQ record.
The record should contain the answer to the questions, and the questions should be complete and not too short.
If possible, use as fewer words as possible from the record.
The record:
section: {section}
question: {question}
answer: {text}
Provide the output in parsable JSON without using code blocks:
["question1", "question2", ..., "question5"]
""".strip()
批次生成所有問題:
from openai import OpenAI
from tqdm.auto import tqdm
# 初始化 OpenAI 客戶端
client = OpenAI()
def generate_questions(doc):
"""
使用 LLM 為單個文件生成5個問題
Args:
doc (dict): 文件字典
Returns:
str: JSON格式的問題列表
"""
# 格式化提示
prompt = prompt_template.format(**doc)
# 調用 GPT-4o 生成問題
response = client.chat.completions.create(
model='gpt-4o',
messages=[{"role": "user", "content": prompt}]
)
# 返回 JSON 回應
json_response = response.choices[0].message.content
return json_response
# 為所有文件生成問題
results = {}
for doc in tqdm(documents):
doc_id = doc['id']
# 避免重複處理
if doc_id in results:
continue
# 生成問題
questions = generate_questions(doc)
results[doc_id] = questions
5.2 評估指標實作
示範十種評估指標中的兩種:Hit Rate(命中率)、MRR(Mean Reciprocal Rank 平均倒數排名)。
在開始看 Hit Rate code 之前,如果你想知道評估原理的話,請點擊展開。
Hit Rate (HR) 或 Recall at k - 命中率範例說明
情境設定:課程FAQ系統
假設我們有一個課程FAQ的RAG系統,收到了5個學生查詢,系統每次返回前3個最相關的文件(k=3)。
查詢和檢索結果
查詢1:「如何註冊課程?」
- 返回文件:[A, B, C]
- 相關文件:A
, B
, C
- 結果:命中 (第1個結果就相關)
查詢2:「課程已經開始還能加入嗎?」
- 返回文件:[D, E, F]
- 相關文件:D
, E
, F
- 結果:命中 (第2個結果相關)
查詢3:「作業截止日期是什麼時候?」
- 返回文件:[G, H, I]
- 相關文件:G
, H
, I
- 結果:命中 (第3個結果相關)
查詢4:「如何安裝Python環境?」
- 返回文件:[J, K, L]
- 相關文件:J
, K
, L
- 結果:未命中 (前3個都不相關)
查詢5:「期末考試的範圍包括哪些?」
- 返回文件:[M, N, O]
- 相關文件:M
, N
, O
- 結果:未命中 (前3個都不相關)
計算過程
步驟1:統計命中的查詢數
- 查詢1:命中
- 查詢2:命中
- 查詢3:命中
- 查詢4:未命中
- 查詢5:未命中
命中查詢數 = 3
步驟2:統計總查詢數 總查詢數 |Q| = 5
步驟3:計算HR@3
HR@3 = (前3個結果中至少有一個相關文件的查詢數) / 總查詢數
HR@3 = 3 / 5 = 0.6 = 60%
結果解釋
HR@3 = 60% 意味著:
- 在60%的查詢中,用戶在前3個結果裡就能找到至少一個有用的答案
- 還有40%的查詢需要用戶繼續往下看,或者根本找不到相關資訊
不同k值的比較
如果我們計算HR@1(只看第一個結果):
HR@1計算:
- 查詢1:第1個結果相關
- 查詢2:第1個結果不相關
- 查詢3:第1個結果不相關
- 查詢4:第1個結果不相關
- 查詢5:第1個結果不相關
HR@1 = 1/5 = 20%
HR@5計算(假設我們看前5個結果): 如果查詢4和查詢5在第4或第5個位置找到相關文件:
HR@5 = 5/5 = 100%
實際應用意義
對於RAG系統開發者:
- HR@3 = 60% 告訴我們系統在前3個結果的覆蓋能力
- 如果業務要求80%的查詢都能在前3個結果找到答案,那麼當前系統還需要改進
對於用戶體驗:
- 60%的用戶能在前3個結果獲得幫助
- 40%的用戶可能會感到挫折,需要繼續搜尋
對於系統優化:
- 可以分析未命中的查詢(查詢4、5),改進檢索策略
- 考慮增加k值,或改善排序算法
與其他指標的關係
HR vs Precision@k:
- HR@3關注"是否命中"(有沒有找到)
- Precision@3關注"命中品質"(找到多少個)
例如查詢2:
- HR@3:命中了
(HR貢獻:1)
- Precision@3:1個相關/3個總數 = 33.3%
這就是為什麼HR特別適合評估"用戶是否能找到幫助"這個關鍵問題,而不太關心找到多少個有用結果。
5.2.1 Hit Rate(命中率)
def hit_rate(relevance_total):
"""
計算命中率:至少包含一個相關文件的查詢比例
Args:
relevance_total (list): 每個查詢的相關性布林列表
Returns:
float: 命中率 (0.0 到 1.0)
"""
cnt = 0
# 檢查每個查詢是否命中
for line in relevance_total:
if True in line: # 如果結果中包含相關文件
cnt = cnt + 1
# 返回命中比例
return cnt / len(relevance_total)
在開始看 MRR code 之前,如果你想知道評估原理的話,請點擊展開。
Mean Reciprocal Rank (MRR) - 平均倒數排名範例說明
情境設定:學生查詢課程問題
假設我們有一個課程FAQ的RAG系統,收到了4個學生查詢,系統返回排序後的搜尋結果。我們要找到第一個相關文件在哪個位置。
查詢和檢索結果
查詢1:「如何提交作業?」 系統返回排序結果:
“作業提交指南”
← 第一個相關文件在第1位
“課程大綱”
“考試安排”
“學習資源”
第一個相關文件排名:rank₁ = 1
查詢2:「期末考試什麼時候?」 系統返回排序結果:
“課程介紹”
“作業要求”
“期末考試時間表”
← 第一個相關文件在第3位
“成績計算方式”
第一個相關文件排名:rank₂ = 3
查詢3:「Python環境如何安裝?」 系統返回排序結果:
“Python安裝教學”
← 第一個相關文件在第1位
“課程工具介紹”
“常見問題”
“聯絡資訊”
第一個相關文件排名:rank₃ = 1
查詢4:「如何加入課程討論群?」 系統返回排序結果:
“課程大綱”
“學習目標”
“參考書籍”
“討論群加入方式”
← 第一個相關文件在第4位
第一個相關文件排名:rank₄ = 4
計算過程
步驟1:計算每個查詢的倒數排名
- 查詢1:1/rank₁ = 1/1 = 1.0
- 查詢2:1/rank₂ = 1/3 = 0.333
- 查詢3:1/rank₃ = 1/1 = 1.0
- 查詢4:1/rank₄ = 1/4 = 0.25
步驟2:計算總和
Σ (1/rank_i) = 1.0 + 0.333 + 1.0 + 0.25 = 2.583
步驟3:計算平均值
MRR = (1/|Q|) × Σ (1/rank_i)
MRR = (1/4) × 2.583 = 0.646
結果解釋
MRR = 0.646 意味著:
- 平均而言,用戶需要查看 1/0.646 ≈ 1.55個結果 才能找到第一個相關答案
- 系統在快速提供相關資訊方面表現中等
分數意義對比
理想情況(所有查詢第一個結果都相關):
MRR = (1/1 + 1/1 + 1/1 + 1/1) / 4 = 4/4 = 1.0
最差情況(假設相關文件都在第10位):
MRR = (1/10 + 1/10 + 1/10 + 1/10) / 4 = 0.4/4 = 0.1
我們的系統得分 0.646 介於中間,表示有改進空間。
實際案例分析
為什麼查詢2表現較差?
- 用戶問「期末考試什麼時候?」
- 系統返回的前兩個結果是「課程介紹」和「作業要求」
- 用戶需要滾動到第3個才找到「期末考試時間表」
- 這可能表示:
- 檢索演算法需要改進時間相關的查詢理解
- 或者需要提升考試相關文件的權重
為什麼查詢1和3表現優秀?
- 用戶分別問「如何提交作業?」和「Python環境如何安裝?」
- 系統第一個結果就命中相關文件
- 表示對於「how-to」類型的查詢,系統表現良好
與其他指標的差異
MRR vs Hit Rate@k:
以查詢2為例:
- MRR關注:第一個相關結果在第3位 → 倒數排名 = 1/3 = 0.333
- HR@3關注:前3個結果中有相關文件嗎?→ 有
→ 對HR@3貢獻1
MRR vs Precision@k:
以查詢4為例:
- MRR關注:第一個相關結果在第4位 → 1/4 = 0.25
- Precision@3關注:前3個結果中有幾個相關?→ 0個 → P@3 = 0%
實際應用價值
對於用戶體驗:
- MRR = 0.646 表示用戶平均需要查看1-2個結果就能找到答案
- 這對於問答系統來說是可以接受的表現
對於系統優化:
- 重點改善那些相關文件排名較後的查詢(如查詢2和4)
- 可以調整檢索算法,提升特定類型查詢的排序準確性
對於業務決策:
- 如果目標是90%的查詢都在前2個結果找到答案,現在的0.646還需要提升到接近0.8-0.9的水平
5.2.2 Mean Reciprocal Rank (MRR)
def mrr(relevance_total):
"""
計算平均倒數排名:評估相關文件在結果中的位置
Args:
relevance_total (list): 每個查詢的相關性布林列表
Returns:
float: MRR 分數 (0.0 到 1.0)
"""
total_score = 0.0
# 遍歷每個查詢的結果
for line in relevance_total:
# 檢查每個位置
for rank in range(len(line)):
if line[rank] == True: # 找到相關文件
# 加入倒數排名分數 (位置越前分數越高)
total_score = total_score + 1 / (rank + 1)
break # 只計算第一個相關文件
# 返回平均分數
return total_score / len(relevance_total)
5.3 評估框架
def evaluate(ground_truth, search_function):
"""
通用評估框架
Args:
ground_truth (list): Ground Truth 資料列表
search_function (callable): 搜尋函數
Returns:
dict: 包含 hit_rate 和 mrr 的評估結果
"""
relevance_total = []
# 遍歷所有測試查詢
for q in tqdm(ground_truth):
doc_id = q['document'] # 期望的正確文件ID
# 執行搜尋
results = search_function(q)
# 檢查每個結果是否為正確文件
relevance = [d['id'] == doc_id for d in results]
relevance_total.append(relevance)
# 計算並返回評估指標
return {
'hit_rate': hit_rate(relevance_total),
'mrr': mrr(relevance_total),
}
6. 性能比較結果
6.1 實驗設定
- 測試資料集: 4,627 個查詢-文件對
- 評估指標: Hit Rate 和 MRR
- 搜尋引擎: MinSearch vs Elasticsearch
6.2 實驗結果
搜尋引擎 | Hit Rate | MRR |
---|---|---|
MinSearch | 0.772 | 0.661 |
Elasticsearch | 0.740 | 0.603 |
6.3 評估執行
Elasticsearch 評估:
# 使用 lambda 函數包裝搜尋調用
evaluate(ground_truth, lambda q: elastic_search(q['question'], q['course']))
# 結果: {'hit_rate': 0.7395720769397017, 'mrr': 0.6032418413658963}
MinSearch 評估:
# 使用 lambda 函數包裝搜尋調用
evaluate(ground_truth, lambda q: minsearch_search(q['question'], q['course']))
# 結果: {'hit_rate': 0.7722066133563864, 'mrr': 0.661454506159499}
6.4 結果分析
MinSearch 優勢:
- Hit Rate 高出 3.2%
- MRR 高出 5.8%
- 實作簡單,適合快速原型開發
Elasticsearch 優勢:
- 提供持久化儲存
- 支援複雜查詢和聚合
- 更好的擴展性和生產環境適用性
7. 模組化設計優勢
7.1 組件可替換性
模組化設計讓系統各組件可以獨立替換:
- 搜尋引擎: MinSearch ↔ Elasticsearch
- LLM: OpenAI ↔ Mistral AI ↔ AWS Bedrock
- 資料庫: 記憶體 ↔ 持久化儲存
7.2 彈性搜尋配置
MinSearch 配置範例:
def minsearch_search(query, course):
"""
MinSearch 搜尋函數
Args:
query (str): 搜尋查詢
course (str): 課程過濾條件
Returns:
list: 搜尋結果
"""
# 定義欄位權重
boost = {
'question': 3.0, # 問題欄位權重最高
'section': 0.5 # 段落欄位權重較低
}
# 執行搜尋
results = index.search(
query=query,
filter_dict={'course': course}, # 課程過濾
boost_dict=boost, # 權重配置
num_results=5 # 結果數量
)
return results
Elasticsearch 查詢範例:
def elastic_search(query, course):
"""
Elasticsearch 搜尋函數
Args:
query (str): 搜尋查詢
course (str): 課程過濾條件
Returns:
list: 搜尋結果
"""
# 建構 Elasticsearch 查詢
search_query = {
"size": 5, # 返回結果數量
"query": {
"bool": { # 布林查詢
"must": { # 必須匹配條件
"multi_match": { # 多欄位匹配
"query": query,
"fields": ["question^3", "text", "section"], # question 欄位3倍權重
"type": "best_fields" # 使用最佳欄位匹配
}
},
"filter": { # 過濾條件
"term": {
"course": course # 精確匹配課程
}
}
}
}
}
# 執行搜尋
response = es_client.search(index=index_name, body=search_query)
# 提取搜尋結果
result_docs = []
for hit in response['hits']['hits']:
result_docs.append(hit['_source'])
return result_docs
8. 結論與建議
8.1 主要成果
- 成功建立 RAG 系統: 整合檢索和生成功能
- 完整評估框架: 提供多種評估指標
- 模組化架構: 支援組件靈活替換
- 性能比較: MinSearch 在測試中表現優於 Elasticsearch
8.2 未來發展方向
- 進階評估指標: 實作 NDCG、MAP 等指標
- 多模型比較: 測試不同 LLM 的性能
- 參數優化: 調整 boost 權重和查詢參數
- 生產部署: 考慮使用 Elasticsearch 的生產環境優勢
8.3 實用建議
- 開發階段: 使用 MinSearch 進行快速原型開發
- 生產環境: 考慮 Elasticsearch 的穩定性和擴展性
- 評估策略: 結合多種指標進行全面評估
- 持續優化: 定期更新 Ground Truth 資料並重新評估