iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
AI Engineering

從零搞懂 RAG 評測之旅系列 第 25 篇

DAY25. Embedding 與向量資料庫

  • 分享至 

  • xImage
  •  

昨天我們把文件切成了 chunks。今天要讓這些 chunks 變得「可搜尋」:用 bge-m3 把每個 chunk 轉成向量,存進 Chroma 向量資料庫,並做第一次相似度搜尋。做完之後,你就能輸入一個問題,看到系統從知識庫撈出最相關的段落,這正是 RAG 裡「R(Retrieval)」的部分。

今日目標

  1. 用 bge-m3 產生向量,並確認 Ollama 運作正常
  2. 建立 Chroma 向量資料庫並持久化到硬碟
  3. 做相似度搜尋,觀察檢索結果
  4. 避免每次執行都重新計算向量

Step 1:測試 Embedding 模型

先確認 Ollama 正在執行(可以開啟 http://localhost:11434 檢查),再在 build_rag.py 加入設定與測試函式:

import os

from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma

EMBED_MODEL = "bge-m3"
OLLAMA_URL = "http://127.0.0.1:11434"
PERSIST_DIR = "chroma_db"
COLLECTION_NAME = "rag-tutorial"


def get_embeddings():
    return OllamaEmbeddings(model=EMBED_MODEL, base_url=OLLAMA_URL)


def check_embeddings():
    vector = get_embeddings().embed_query("什麼是 Git?")
    print(f"向量維度:{len(vector)}")
    print(f"前 5 個數值:{vector[:5]}")

暫時把 __main__ 區塊改成:

if __name__ == "__main__":
    check_embeddings()

執行後,你會看到一串 1024 維的向量(bge-m3 的輸出維度)。第一次呼叫需要先把模型載入記憶體,會比較慢,之後就快了。

如果出現連線錯誤,通常是 Ollama 沒有啟動,或 bge-m3 還沒下載,用 ollama list 確認一下。

Step 2:建立向量資料庫

繼續在 build_rag.py 加入建庫的函式:

def get_vectorstore():
    return Chroma(
        collection_name=COLLECTION_NAME,
        embedding_function=get_embeddings(),
        persist_directory=PERSIST_DIR,
        collection_configuration={"hnsw": {"space": "cosine"}},
    )


def build_vectorstore(chunks):
    # 先清掉舊的 collection,避免重複執行時資料重複寫入
    get_vectorstore().delete_collection()
    vectorstore = get_vectorstore()
    vectorstore.add_documents(chunks)
    return vectorstore

幾個重點:

項目 說明
collection_name 向量資料庫裡的「資料表」名稱,一個專案可以有多個 collection
embedding_function 指定用哪個模型產生向量,建庫與查詢必須使用同一個模型,否則向量空間不同,距離沒有意義
persist_directory 資料存放的資料夾,DAY23 已經把 chroma_db/ 加進 .gitignore
{"hnsw": {"space": "cosine"}} 指定用餘弦距離計算相似度。Chroma 預設是 L2 距離,文字 Embedding 通常搭配餘弦距離,所以我們明確指定

另外一個容易踩到的坑:add_documents 每次執行都會「新增」資料。如果不先清除,重複執行幾次,同一個 chunk 就會在庫裡出現好幾份,檢索結果會被重複內容佔滿。所以 build_vectorstore 一開始先刪除舊的 collection。

把 __main__ 改成:

if __name__ == "__main__":
    docs = load_documents()
    chunks = split_documents(docs)
    vectorstore = build_vectorstore(chunks)
    print(f"已建立向量庫,共 {len(chunks)} 個 chunks")

執行後,專案裡會出現 chroma_db/ 資料夾,向量已經存進硬碟。

Step 3:相似度搜尋

加入一個搜尋函式,把結果整理成容易閱讀的格式:

def search(vectorstore, query: str, k: int = 3):
    results = vectorstore.similarity_search_with_score(query, k=k)
    print(f"\n問題:{query}")
    for rank, (doc, score) in enumerate(results, 1):
        source = os.path.basename(doc.metadata["source"])
        preview = doc.page_content.replace("\n", " ")[:60]
        print(f"  [{rank}] 距離 {score:.4f} | {source} | {preview}...")

similarity_search_with_score 回傳的分數是距離,不是相似度。使用餘弦距離時,它等於「1 減去餘弦相似度」,所以數字越小代表越相似。

把 __main__ 改成:

if __name__ == "__main__":
    docs = load_documents()
    chunks = split_documents(docs)
    vectorstore = build_vectorstore(chunks)

    # 下述需針對 data 資料夾下的文件進行提問
    search(vectorstore, "201 和 200 狀態碼有什麼差別?")
    search(vectorstore, "git revert 和 git reset 有什麼不同?")
    search(vectorstore, "為什麼索引會拖慢寫入速度?")
    search(vectorstore, "容器和虛擬機有什麼差異?")

你應該會看到,每個問題排名第一的結果,都來自對應主題的文件(HTTP、Git、資料庫、Docker),這代表 Embedding 確實抓到了語意。請特別觀察三件事:

  1. 排名第一的 chunk,內容是否真的能回答問題
  2. 第 2、3 名是否來自同一份文件,還是混入了不相關的文件
  3. 距離數字的差距:第一名與後面名次差得多不多

Step 4:避免重複計算向量

每次執行都重新計算所有 chunks 的向量,既慢又沒必要。向量已經存在 chroma_db/ 裡了,之後只要直接載入即可。把 __main__ 改成最終版本:

REBUILD = False  # 修改文件內容或切割參數後,才需要改成 True

if __name__ == "__main__":
    if REBUILD or not os.path.exists(PERSIST_DIR):
        docs = load_documents()
        chunks = split_documents(docs)
        vectorstore = build_vectorstore(chunks)
        print(f"已建立向量庫,共 {len(chunks)} 個 chunks")
    else:
        vectorstore = get_vectorstore()
        count = len(vectorstore.get()["ids"])
        print(f"已載入既有向量庫,共 {count} 筆")

    search(vectorstore, "201 和 200 狀態碼有什麼差別?")

第一次執行會建庫,之後再執行就會直接載入,速度明顯變快。

想一想:問一個知識庫裡沒有的問題

最後做一個小實驗,試試看問知識庫完全沒有的內容:

search(vectorstore, "今天台中的天氣如何?")

你會發現,系統依然回傳了 3 筆結果,只是距離會比前面的問題大。向量搜尋永遠會回傳「最接近的 k 筆」,不管它們到底相不相關。

小結

今天完成了 RAG 的檢索環節,明天要把檢索接上語言模型:建立 retriever,用 qwen3:8b 根據檢索到的內容生成答案,完成第一版完整的 RAG。


上一篇
DAY24. 文件載入與切割
系列文
從零搞懂 RAG 評測之旅 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言