動手實作 RAG 應用 - 使用 Python 從硬幹到框架

2025-11-30

紀錄使用 Python 實作 RAG 應用,從不使用任何框架到使用 llamaindex 的學習進程筆記。

logo

LLM 與 Embedding 模型準備

首先安裝 ollama 完成安裝後透過 pull 來下載模型,進行 RAG 需要 LLM Model 使用 qwen3:4b-instruct 以及 Embedding Model 使用 qwen3-embedding:0.6b

藉由 API 呼叫 ollama 測試 LLM 及 Embedding 功能是否正常,直接呼叫模型就會自動被載入。

$endpoint = "http://localhost:11434/api/"
$body = @'
{
  "model": "qwen3:4b-instruct",
  "messages": [
    {
      "role": "user",
      "content": "台北最高的建築物"
    }
  ],
  "stream": false
}
'@

Invoke-RestMethod -Uri ($endpoint + "chat") `
  -Method Post `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
$endpoint = "http://localhost:11434/api/"
$body = @'
{
  "model": "qwen3-embedding:0.6b",
  "prompt": "千里之行始於足下"
}
'@

Invoke-RestMethod -Uri ($endpoint + "embeddings") `
  -Method Post `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body ([System.Text.Encoding]::UTF8.GetBytes($body))

如果要確認 Ollama 安裝那些模型,可以透過以下指令:

ollama list

或者是透過 API:

(Invoke-RestMethod -Uri ($endpoint + "tags") `
    -Method Get -Headers @{ "Content-Type" = "application/json" }).models |
    Select-Object name, model, modified_at, size |
    Format-Table -AutoSize

向量資料庫

使用 chroma 作為向量資料庫。

uv pip install chromadb

注意 chroma 可能會要求特定的 Python 版本,可以透過 uv 來調整 Python Runtime。

uv python install 3.13
uv python pip 3.13

chromadb 的 persistent 是搭配 sqlite 將資料儲存在本機,而企業化的應用會考量將 metadata 及向量資料分開存放,metadata 儲存在 RDBMS,向量資料可以儲存在 chromadb 搭配的 Volume。

import ollama
import chromadb
import numpy as np
from sklearn.cluster import KMeans

# =====================================
# 1. 初始化 ChromaDB
# =====================================
chroma_client = chromadb.PersistentClient(path="./chroma_data")

collection = chroma_client.get_or_create_collection(
    name="ollama_persistent_collection",
    metadata={"description": "Persistent collection using Ollama embeddings"}
)

以下的文句是透過 LLM 產生,並指定三種類型:

幫我準備 18 條短句,分成三種主題,生成式 LLM、台北好吃的食物、健康的運動指南三類。我要來用於 RAG 應用,
幫我混合順序。只要回應不要解釋,其中幾句要是英文的。
# =====================================
# 2. 準備示範文檔
# =====================================
documents = [
    'Generative LLMs can accelerate content creation for enterprise teams.',
    '台北的牛肉麵湯頭醇厚,是許多人心中的必吃名單。',
    'A 30-minute brisk walk each day improves cardiovascular health.',
    '生成式 LLM 能協助快速整理內部文件並提升效率。',
    '士林夜市的豪大雞排外酥內嫩。',
    '重量訓練能有效提升基礎代謝率。',
    'LLMs are powerful in multi-turn reasoning tasks.',
    '饒河夜市的胡椒餅排隊人潮總是滿滿。',
    '規律做核心運動可改善姿勢並減少腰痛。',
    '使用生成式 LLM 可加快資料分析與洞察產生。',
    'Taipei’s mango shaved ice is a must-try in summer.',
    'HIIT(間歇性高強度訓練)能在短時間內有效燃脂。',
    '生成式 LLM 支援多語能力,有助跨國溝通。',
    '永康街的小籠包皮薄汁多。',
    'Stretching before and after workouts reduces injury risk.',
    'Generative LLMs can automate customer support responses.',
    '台北夜市的蔥抓餅香氣四溢。',
    '每週至少三次中強度運動有助維持良好健康。',
]

# =====================================
# 3. 使用 Ollama 模型生成向量
# =====================================
print("使用 Ollama 模型生成向量...")
model_name = "qwen3-embedding:0.6b"

embeddings = []
for i, doc in enumerate(documents):
    print(f"  處理文檔 {i+1}/{len(documents)}: {doc[:30]}...")
    try:
        response = ollama.embeddings(model=model_name, prompt=doc)
        embeddings.append(response['embedding'])
        print(f"    ✓ 向量長度: {len(response['embedding'])}")
    except Exception as e:
        print(f"  錯誤: {e}")
        print(f"  提示: 請確保已在本機執行: ollama pull {model_name}")
        exit(1)

# =====================================
# 4. 將文檔和向量存入 ChromaDB
# =====================================
print("\n將文檔存入 ChromaDB...")

collection.upsert(
    documents=documents,
    embeddings=embeddings,
    ids=[f"doc_{i}" for i in range(len(documents))],
    metadatas=[
        {"source": "document", "index": i, "model": model_name} 
          for i in range(len(documents))]
)

print(f"✓ 已成功存入 {len(documents)} 個文檔")

資料會儲存在 sqlite 的資料表,也可以透過 json dump 資料來查看。

# =====================================
# 5. 查詢示範
# =====================================
query_text = "吃太多食物會讓人變胖,但美味的牛肉麵還是讓人難以抗拒呀"
print("\n查詢示範...")
print(f"查詢文本: {query_text}")
# 生成查詢向量
query_embedding = ollama.embeddings(model=model_name, prompt=query_text)['embedding']
results = collection.query(
    query_embeddings=[query_embedding],
    n_results=7,
    include=['documents', 'metadatas', 'distances']
)
print("\n查詢結果:")
for i, (doc, meta, dist) in 
  enumerate(zip(results['documents'][0], results['metadatas'][0], results['distances'][0])):
    print(f"\n結果 {i+1}:")
    print(f"  文檔: {doc}")
    print(f"  元數據: {meta}")
    print(f"  距離: {dist:.4f}")

因為已經有文句的向量,可以驗證將文句進行自動分類的效果,以下使用 K-Means 演算法來進行分類。

# =====================================
# 6. 自動分類
# =====================================
# K-Means 演算法需要 numpy 和 scikit-learn
print("\n" + "="*50)
print("7. 自動分類所有文檔")
print("="*50)

# 從 ChromaDB 獲取所有文檔和向量
all_data = collection.get(include=['embeddings', 'documents'])
all_embeddings = np.array(all_data['embeddings'])
all_documents = all_data['documents']

# 設定要分成的類別數量
n_clusters = 3
print(f"將 {len(all_documents)} 個文檔分成 {n_clusters} 類...")

# 使用 K-Means 演算法進行分群
# n_init='auto' 是未來 scikit-learn 的預設值, 這裡設為 10 以避免警告
kmeans = KMeans(n_clusters=n_clusters, n_init=10)
kmeans.fit(all_embeddings)

# 整理並顯示分群結果
print("\n分群結果:")
# 建立一個字典來存放每個類別的文檔
clusters = {i: [] for i in range(n_clusters)}
for doc_index, cluster_label in enumerate(kmeans.labels_):
    clusters[cluster_label].append(all_documents[doc_index])

# 逐一印出每個類別的內容
for cluster_id, docs in sorted(clusters.items()):
    print(f"\n--- 第 {cluster_id + 1} 類 ---")
    for doc in docs:
        print(f"  - {doc}")

由於當初是自己透過 LLM 產生這三種類型的文句,因此透過 3 類去分類結果相當準確。但如果在不知道類別數量的情況下,則可以透過機器學習的 Elbow Method 或 Silhouette Score 等方法來決定最佳的類別數量。