# 🚀 Day 1 實作摘要：RAG 系統從零到一

> **給 Claude Code 的提示**：這是一份學習導向的實作指引。請在關鍵步驟詢問使用者的理解，而非直接給答案。

---

## 📋 上午任務（3-4 小時）

### 🎯 學習目標
- 建立可運作的 RAG 最小原型
- 理解「檢索-增強-生成」三階段的實際運作
- 觀察 Embedding 如何將文字轉為向量

---

### ⏰ Hour 1-2：環境建置與核心概念

#### 📦 安裝套件
```bash
mkdir rag-interview-prep && cd rag-interview-prep
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

pip install langchain-google-genai sentence-transformers chromadb python-dotenv langchain
```

#### 🔑 API Key 設定
```bash
# 建立 .env 檔案
echo "GOOGLE_API_KEY=你的Gemini_API_Key" > .env
```

**💡 概念檢查點 1：**
- 為什麼我們需要「兩種」模型？（Embedding + LLM）
- Embedding 模型和 LLM 的工作分別是什麼？

**預期理解：**
- Embedding：把文字變成數字（向量），用來「搜尋相似內容」
- LLM：讀懂文字、生成回答，但「不負責搜尋」

---

#### 🗺️ 畫出資料流程圖

**在實作前，請先手繪或用工具畫出這個流程：**

```
使用者問題 
    ↓
[步驟1] Embedding（問題轉向量）
    ↓
[步驟2] 向量資料庫搜尋（找最相似的文件）
    ↓
[步驟3] 取出 top-k 個文件
    ↓
[步驟4] 組裝 Prompt（問題 + 文件）
    ↓
[步驟5] LLM 生成回答
    ↓
輸出答案
```

**💡 概念檢查點 2：**
- 如果跳過步驟 2-3，直接把問題丟給 LLM 會怎樣？
- 為什麼不能讓 LLM 直接「記住」所有文件？

**預期理解：**
- LLM 有 context window 限制（無法一次讀所有文件）
- LLM 會「幻覺」（編造不存在的資訊）
- RAG 讓 LLM 能「查資料後再回答」

---

### ⏰ Hour 3-4：寫第一個 RAG 程式

#### 📝 準備知識庫
```bash
# 建立 data 資料夾
mkdir data

# 建立 knowledge.txt
# 內容可以是：你的履歷、專案說明、技術文件摘要
```

**建議內容範例：**
```
我是一名軟體工程師，專長是 Python 和 API 開發。
曾經使用 pandas 進行資料處理，並有後端系統開發經驗。
目前正在學習 RAG 和 LLM 技術，準備應徵華碩的 AI 軟體開發工程師。
我對向量資料庫、語意搜尋、和模型微調有基礎理解。
```

---

#### 💻 核心程式碼

**檔案：`simple_rag.py`**

```python
from langchain_google_genai import GoogleGenerativeAI
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from dotenv import load_dotenv

load_dotenv()

# === 第一階段：讀取與切分文件 ===
print("📖 讀取知識庫...")
with open("data/knowledge.txt", "r", encoding="utf-8") as f:
    text = f.read()

print(f"✅ 原始文字長度：{len(text)} 字元")

# === 💡 概念：為什麼要「切分」文件？===
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 每段最多 500 字元
    chunk_overlap=50     # 段落間重疊 50 字元
)
docs = splitter.create_documents([text])

print(f"✅ 切成 {len(docs)} 個文件塊 (chunks)")
print(f"第一個 chunk 內容：\n{docs[0].page_content}\n")

# === 第二階段：建立 Embedding 與向量資料庫 ===
print("🔄 建立 Embedding 模型...")
embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    model_kwargs={'device': 'cpu'}  # VM 環境使用 CPU
)

print("💾 建立向量資料庫...")
vectordb = Chroma.from_documents(
    docs,
    embeddings,
    persist_directory="./db"
)

print("✅ 向量資料庫建立完成！")

# === 第三階段：設定 LLM 與 QA Chain ===
print("🤖 連接 Gemini LLM...")
llm = GoogleGenerativeAI(
    model="gemini-1.5-flash",
    temperature=0  # 讓回答更穩定
)

retriever = vectordb.as_retriever(search_kwargs={"k": 3})
qa = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=retriever,
    return_source_documents=True
)

print("✅ RAG 系統啟動完成！\n")

# === 測試 ===
while True:
    question = input("🙋 請輸入問題（輸入 'quit' 離開）：")
    if question.lower() == 'quit':
        break
    
    print("\n🔍 搜尋相關文件中...")
    result = qa({"query": question})
    
    print(f"\n💬 答案：\n{result['result']}")
    print(f"\n📚 參考了 {len(result['source_documents'])} 個文件片段")
    
    for i, doc in enumerate(result['source_documents']):
        print(f"\n--- 片段 {i+1} ---")
        print(doc.page_content[:200])
```

---

#### 🧪 執行與觀察

```bash
python simple_rag.py
```

**測試問題：**
1. 「這個人有什麼技術專長？」
2. 「他在準備什麼面試？」
3. 「他用過哪些程式語言？」（如果你寫在 knowledge.txt 裡）

**💡 概念檢查點 3（執行後）：**
- 觀察「參考了幾個文件片段」？為什麼是這個數量？
- 如果問「你會做菜嗎？」（knowledge.txt 沒寫的），系統會怎麼回答？
- 如果系統「編造資訊」，該如何改進？

**預期發現：**
- `k=3` 代表檢索 3 個最相關片段
- 問沒有的資訊，LLM 可能會幻覺（需要改 prompt）

---

## 📋 下午任務（3-4 小時）

### 🎯 學習目標
- 深入理解 Embedding 的運作原理
- 實驗並觀察參數如何影響結果
- 建立「實驗→觀察→結論」的工程思維

---

### ⏰ Hour 5-6：深入理解 Embedding

#### 🔬 實驗程式

**檔案：`embedding_experiment.py`**

```python
from sentence_transformers import SentenceTransformer
import numpy as np

# 載入 Embedding 模型
print("載入模型中...")
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')

def cosine_similarity(vec1, vec2):
    """計算兩個向量的餘弦相似度"""
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

# === 實驗 1：觀察向量維度 ===
test_text = "這是一個測試句子"
embedding = model.encode(test_text)

print(f"\n=== 實驗 1：向量基本資訊 ===")
print(f"句子：{test_text}")
print(f"向量維度：{len(embedding)}")
print(f"向量前 10 個數值：{embedding[:10]}")

# === 實驗 2：相似度測試 ===
texts = [
    "我喜歡用 Python 寫程式",
    "我擅長 Python 開發",
    "Python 是一種爬蟲類動物",
    "今天天氣很好"
]

print(f"\n=== 實驗 2：語意相似度 ===")
embeddings = [model.encode(t) for t in texts]

for i in range(len(texts)):
    for j in range(i+1, len(texts)):
        sim = cosine_similarity(embeddings[i], embeddings[j])
        print(f"\n句子 {i+1} vs 句子 {j+1}")
        print(f"  內容：「{texts[i]}」")
        print(f"       vs 「{texts[j]}」")
        print(f"  相似度：{sim:.4f}")
```

**執行並記錄：**
```bash
python embedding_experiment.py
```

**💡 概念檢查點 4（執行後）：**
- 哪兩個句子最相似？為什麼？
- 「Python 程式」和「Python 動物」的相似度如何？
- 相似度分數 0.8 和 0.3 代表什麼意思?

**預期發現：**
- 句子 1 和 2（都在講 Python 開發）相似度最高（>0.7）
- 句子 3（Python 動物）跟句子 1-2 也有點相似（因為都有 Python）
- 句子 4（天氣）跟其他句子相似度很低（<0.3）

---

#### 📊 延伸實驗（自主探索）

**加入你自己的測試句子：**
```python
# 嘗試這些對比
your_tests = [
    "RAG 是檢索增強生成",
    "RAG 包含檢索、增強、生成三步驟",
    "向量資料庫用來儲存 embedding",
    "貓咪很可愛"
]
```

**思考問題：**
- 同義詞會有高相似度嗎？（例如「軟體工程師」vs「程式設計師」）
- 如果用英文句子，結果會不會不同？
- 向量維度（384）為什麼是這個數字？

---

### ⏰ Hour 7-8：參數調整實驗

#### 🧪 實驗目的
理解三個關鍵參數如何影響 RAG 效果：
1. **chunk_size**：每個文件塊的大小
2. **chunk_overlap**：塊之間的重疊量
3. **top_k**：檢索幾個最相關的文件

---

#### 💻 實驗程式

**檔案：`parameter_experiments.py`**

```python
from langchain_google_genai import GoogleGenerativeAI
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from dotenv import load_dotenv
import os
import shutil

load_dotenv()

# 讀取資料
with open("data/knowledge.txt", "r", encoding="utf-8") as f:
    text = f.read()

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    model_kwargs={'device': 'cpu'}  # VM 環境使用 CPU
)

# === 實驗 1：Chunk Size 影響 ===
print("\n=== 實驗 1：Chunk Size 影響 ===")

chunk_sizes = [200, 500, 1000]
test_question = "這個人有什麼專長？"

for size in chunk_sizes:
    print(f"\n--- 測試 chunk_size = {size} ---")
    
    # 清除舊的向量資料庫
    if os.path.exists("./db_temp"):
        shutil.rmtree("./db_temp")
    
    # 建立新的切分與資料庫
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=size,
        chunk_overlap=50
    )
    docs = splitter.create_documents([text])
    print(f"切成 {len(docs)} 個chunks")
    
    vectordb = Chroma.from_documents(
        docs, embeddings, persist_directory="./db_temp"
    )
    
    # 測試
    llm = GoogleGenerativeAI(model="gemini-1.5-flash", temperature=0)
    retriever = vectordb.as_retriever(search_kwargs={"k": 3})
    qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
    
    result = qa.run(test_question)
    print(f"回答：{result[:150]}...")  # 只顯示前 150 字

# === 實驗 2：Top-K 影響 ===
print("\n\n=== 實驗 2：Top-K 影響 ===")

# 用固定的 chunk_size 建立資料庫
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
docs = splitter.create_documents([text])

if os.path.exists("./db_temp"):
    shutil.rmtree("./db_temp")

vectordb = Chroma.from_documents(
    docs, embeddings, persist_directory="./db_temp"
)

top_ks = [1, 3, 5]

for k in top_ks:
    print(f"\n--- 測試 top_k = {k} ---")
    
    retriever = vectordb.as_retriever(search_kwargs={"k": k})
    qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
    
    result = qa.run(test_question)
    print(f"參考 {k} 個文件")
    print(f"回答：{result[:150]}...")

# 清理
shutil.rmtree("./db_temp")
```

---

#### 📝 記錄你的觀察

**建立 `experiments.md` 檔案記錄：**

```markdown
# RAG 參數實驗紀錄

## 實驗日期：[填寫]

### 實驗 1：Chunk Size

| Chunk Size | Chunks 數量 | 回答品質 | 觀察 |
|------------|-------------|----------|------|
| 200        |             |          |      |
| 500        |             |          |      |
| 1000       |             |          |      |

**發現：**
- chunk_size 太小時：
- chunk_size 太大時：
- 最佳平衡點：

### 實驗 2：Top-K

| Top-K | 回答品質 | 觀察 |
|-------|----------|------|
| 1     |          |      |
| 3     |          |      |
| 5     |          |      |

**發現：**
- k 太小時：
- k 太大時：
- 最佳平衡點：

## 問題與思考

1. 為什麼需要 chunk_overlap？
   我的理解：

2. 如果文件很長（10萬字），要如何設定參數？
   我的想法：

3. 如何判斷「回答品質」好不好？
   我的標準：
```

---

#### 💡 概念檢查點 5（實驗後）：

**核心思考問題：**

1. **Chunk Size 的取捨：**
   - 太小：上下文不足，LLM 無法理解完整意思
   - 太大：不精準，可能包含無關資訊
   - 你的結論是什麼？

2. **Top-K 的取捨：**
   - 太少：可能遺漏重要資訊
   - 太多：雜訊增加，LLM 可能混淆
   - 你的結論是什麼？

3. **為什麼需要 Overlap？**
   - 如果文件被「切斷」在關鍵句子中間怎麼辦？
   - Overlap 如何解決這個問題？

**預期理解：**
- 沒有「完美參數」，要根據資料特性調整
- chunk_size 約 500-800 通常是好起點
- top_k=3 對一般問答足夠
- overlap 防止重要資訊被切斷

---

## ✅ Day 1 完成檢查清單

### 技術成果
- [ ] 環境安裝成功，能執行程式
- [ ] RAG 系統能正確回答問題
- [ ] 完成 Embedding 相似度實驗
- [ ] 完成參數調整實驗
- [ ] 記錄實驗結果到 `experiments.md`

### 概念理解
- [ ] 能說明 RAG 三階段流程（檢索-增強-生成）
- [ ] 能解釋 Embedding 與 LLM 的分工
- [ ] 能解釋為什麼需要向量資料庫
- [ ] 能說明 chunk_size 和 top_k 的取捨
- [ ] 能用自己的話說明「為什麼 RAG 比直接問 LLM 好」

### 實作能力
- [ ] 能獨立修改 knowledge.txt 並重新建索引
- [ ] 能調整參數並觀察結果差異
- [ ] 能看懂程式碼每一段在做什麼

---

## 🎯 給 Claude Code 的建議

**在引導使用者時：**

1. **環境建置階段**：確認每個套件都安裝成功，特別是 sentence-transformers（會下載模型，需要時間）

2. **第一次執行**：讓使用者先用簡單的 knowledge.txt（3-5 句話），確保流程跑通

3. **Embedding 實驗**：鼓勵使用者自己寫測試句子，觀察相似度變化

4. **參數實驗**：提醒使用者「記錄觀察」比「跑完實驗」更重要

5. **遇到錯誤**：
   - 先檢查 API Key 是否正確
   - HuggingFace 模型第一次會下載，需等待
   - 如果 Gemini API 超限，加 `time.sleep(2)`

---

## 📚 Day 1 關鍵學習重點總結

### 核心概念
1. **RAG = 外掛知識庫**：讓 LLM 能「查資料後再回答」
2. **Embedding = 語意搜尋的關鍵**：把文字變成向量，用數學計算相似度
3. **參數沒有完美解**：要根據資料特性實驗調整

### 工程思維
1. **模組化設計**：Embedding 和 LLM 分離，可獨立替換
2. **實驗驅動**：改參數 → 觀察結果 → 記錄結論
3. **成本意識**：為什麼用 HuggingFace Embedding？因為免費且品質夠

### 面試準備
- 能畫出 RAG 流程圖
- 能解釋為什麼需要 RAG（vs 直接用 LLM）
- 能說明你做過的實驗與觀察
