開發日誌 #2:向量化與檢索系統建置
完成智能分片後,下一步是將文本轉換為向量,並建立高效的檢索系統。這個階段的核心任務是:
- 選擇合適的 Embedding 模型
- 整合 ChromaDB 向量資料庫
- 實現高效的語義檢索
Embedding 模型選擇
需求分析
我們的需求很明確:
- ✅ 中文優化:招標文件都是繁體中文,需要專為中文設計的模型
- ✅ 模型輕量:希望能在一般電腦上跑,不需要高階 GPU
- ✅ 檢索效果:準確率要高,能理解專業術語
- ✅ 本地部署:資料安全,不能外洩
模型評估
經過調研,我選定了 BAAI/bge-small-zh-v1.5:
| 模型 | 向量維度 | 模型大小 | 適用場景 |
|---|---|---|---|
| bge-small-zh-v1.5 | 512 | ~100MB | ⭐ 中小型應用,平衡性能與效率 |
| bge-base-zh-v1.5 | 768 | ~400MB | 更高準確率,需要更多資源 |
| bge-large-zh-v1.5 | 1024 | ~1GB | 最高準確率,需要 GPU |
選擇理由:
- 專為中文優化,支援繁體中文
- 模型較小(約 100MB),載入快速
- 向量維度適中(512),儲存空間友善
- CPU 即可運行,無需 GPU
模型測試
先做個簡單的相似度測試:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 測試文本
text1 = "數位雙生智能火場鑑識輔助系統"
text2 = "智慧科技消防全民防救災整備研發計畫"
# 生成向量
emb1 = model.encode(text1)
emb2 = model.encode(text2)
# 計算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity = cosine_similarity([emb1], [emb2])[0][0]
print(f"相似度: {similarity:.4f}")結果:
相似度: 0.5867
不錯!雖然兩個文本字面上不同,但模型能理解它們都與消防系統相關。
ChromaDB 整合
為什麼選擇 ChromaDB?
市面上的向量資料庫很多(Pinecone, Weaviate, Qdrant, Milvus…),我選擇 ChromaDB 的原因:
✅ 簡單易用:API 設計直觀,幾行程式碼就能上手 ✅ 內建持久化:自動儲存到磁碟,重啟後無需重新載入 ✅ Python 原生:完美整合 Python 生態系 ✅ 適合中小型專案:對於我們的文檔量(數百到數千個分片)效能足夠
向量化流程設計
def embed_to_vectordb(chunks_folder: str):
"""將分片向量化並存入 ChromaDB"""
# 1. 載入分片數據
with open(f"{chunks_folder}/chunks.json", 'r', encoding='utf-8') as f:
chunks = json.load(f)
print(f"📦 載入 {len(chunks)} 個分片")
# 2. 載入 Embedding 模型
print("🔄 載入 Embedding 模型...")
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 3. 批次生成向量
print("🔢 生成向量...")
texts = [chunk['text'] for chunk in chunks]
embeddings = model.encode(texts, batch_size=32, show_progress_bar=True)
# 4. 建立 ChromaDB 集合
client = chromadb.PersistentClient(path=f"{chunks_folder}/vectordb")
collection_name = f"collection_{uuid.uuid4().hex[:16]}"
collection = client.create_collection(name=collection_name)
# 5. 批次插入向量
print("💾 存入向量資料庫...")
collection.add(
embeddings=embeddings.tolist(),
documents=texts,
metadatas=[chunk['metadata'] for chunk in chunks],
ids=[chunk['chunk_id'] for chunk in chunks]
)
print("✅ 向量化完成!")實際執行結果
$ python embed_to_vectordb.py "rag/01 華電聯網服務建議書_20251012_124248"
📦 載入 360 個分片
🔄 載入 Embedding 模型...
模型載入完成 (BAAI/bge-small-zh-v1.5)
🔢 生成向量...
Batches: 100%|████████████| 12/12 [00:11<00:00, 1.05batch/s]
💾 存入向量資料庫...
├─ 集合名稱: collection_a7f3e9c2d8b14f56
├─ 向量數量: 360
├─ 向量維度: 512
└─ 儲存路徑: rag/.../vectordb/
✅ 向量化完成!
⏱️ 總耗時: 12.3 秒技術難題與解決
難題 3:ChromaDB 集合名稱限制
問題描述:
一開始我想用原始檔名作為集合名稱:
collection_name = "rag_01 華電聯網服務建議書_20251012_124248"
collection = client.create_collection(name=collection_name)結果報錯:
ValueError: Expected collection name that matches pattern:
[a-zA-Z0-9._-]{3,63}
Got: rag_01 華電聯網服務建議書_20251012_124248
ChromaDB 不支援中文和空格!
解決方案:
使用 UUID 作為集合名稱,原始名稱存在 metadata:
import uuid
# 生成 UUID 作為集合名稱
collection_name = f"collection_{uuid.uuid4().hex[:16]}"
# 原始名稱存在 metadata
collection = client.create_collection(
name=collection_name,
metadata={
"original_name": "rag_01 華電聯網服務建議書_20251012_124248",
"source_file": "doc/01 華電聯網服務建議書.pdf",
"created_at": datetime.now().isoformat()
}
)
# 記錄 UUID 供後續查詢使用
with open(f"{chunks_folder}/vectordb_info.txt", 'w', encoding='utf-8') as f:
f.write(f"Collection UUID: {collection_name}\n")
f.write(f"Original Name: rag_01 華電聯網服務建議書_20251012_124248\n")難題 4:Metadata 不能有 None 值
問題描述:
插入向量時出現錯誤:
TypeError: Failed to extract enum MetadataValue
'NoneType' object cannot be converted to 'PyString'原因是某些分片的 heading 或 page_number 可能是 None,而 ChromaDB 不接受 None 值。
解決方案:
在插入前過濾掉 None 值:
def clean_metadata(metadata: dict) -> dict:
"""清理 metadata,移除 None 值"""
return {k: v for k, v in metadata.items() if v is not None}
# 使用時
metadatas = [clean_metadata(chunk['metadata']) for chunk in chunks]
collection.add(
embeddings=embeddings.tolist(),
documents=texts,
metadatas=metadatas,
ids=[chunk['chunk_id'] for chunk in chunks]
)或者更優雅的方式,在建立 metadata 時就只添加非 None 的欄位:
metadata = {}
if chunk_metadata.get('section'):
metadata['section'] = chunk_metadata['section']
if chunk_metadata.get('heading'):
metadata['heading'] = chunk_metadata['heading']
if chunk_metadata.get('page_number'):
metadata['page_number'] = chunk_metadata['page_number']檢索系統實現
查詢接口設計
def query_vectordb(chunks_folder: str, query: str, top_k: int = 5):
"""查詢向量資料庫"""
# 1. 載入資料庫
client = chromadb.PersistentClient(path=f"{chunks_folder}/vectordb")
# 2. 讀取 collection UUID
with open(f"{chunks_folder}/vectordb_info.txt", 'r', encoding='utf-8') as f:
collection_name = f.readline().split(': ')[1].strip()
collection = client.get_collection(name=collection_name)
# 3. 載入 Embedding 模型
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 4. 生成查詢向量
query_embedding = model.encode(query)
# 5. 執行檢索
results = collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=top_k
)
# 6. 格式化輸出
return format_results(results)檢索效果測試
查詢問題:「數位雙生智能火場鑑識輔助系統的架構是什麼」
檢索結果:
🔍 查詢: 數位雙生智能火場鑑識輔助系統的架構是什麼
📊 找到 5 個相關結果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【結果 1】相似度: 0.6748
章節: 二. 數位雙生智能火場鑑識輔助系統
標題: 系統架構說明
頁碼: 45
內容:
本系統採用多層架構設計,包含:
1. 使用者介面層 - 提供 Web-based 操作介面
2. 應用服務層 - 使用 ASP.NET Core 建置 RESTful API
3. 資料存取層 - 整合多種資料庫系統
4. 基礎設施層 - 包含雲端服務與資料治理機制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【結果 2】相似度: 0.6733
章節: 壹、專案說明 > 一. 本專案規劃重點摘要
標題: (一) 系統建置概述
內容:
運用數位雙生(Digital Twin)技術建置火場智能鑑識輔助系統,
整合 3D 建模、IoT 感測、AI 分析等技術...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【結果 3】相似度: 0.6255
章節: 參、專案需求規劃 > 一. 整體系統架構
內容:
本系統以「數位雙生智能火場鑑識輔助系統」平台為基礎,
採用微服務架構,各模組間透過 API Gateway 進行通訊...
效果評估:
✅ 準確率高:前三個結果都直接命中「系統架構」相關內容 ✅ 語義理解:即使問題用「架構是什麼」,也能找到「系統架構說明」 ✅ 章節定位:完整的章節路徑讓用戶能快速找到原文位置 ✅ 相似度合理:0.67 的相似度顯示高度相關
互動式查詢模式
為了方便測試,我還加入了互動式查詢:
def interactive_query(chunks_folder: str):
"""互動式查詢模式"""
print("🤖 進入互動式查詢模式(輸入 'exit' 離開)\n")
while True:
query = input("🔍 查詢 > ").strip()
if query.lower() == 'exit':
print("👋 再見!")
break
if not query:
continue
results = query_vectordb(chunks_folder, query, top_k=5)
display_results(results)
print()使用示範:
$ python query_vectordb.py "rag/01 華電聯網服務建議書_20251012_124248"
🤖 進入互動式查詢模式(輸入 'exit' 離開)
🔍 查詢 > 系統架構
📊 找到 5 個相關結果
[顯示結果...]
🔍 查詢 > 專案團隊成員
📊 找到 5 個相關結果
[顯示結果...]
🔍 查詢 > exit
👋 再見!性能分析
向量化性能
| 指標 | 數據 |
|---|---|
| 分片數量 | 360 個 |
| Batch Size | 32 |
| 總批次數 | 12 批次 |
| 單批次耗時 | ~1 秒 |
| 總耗時 | ~12 秒 |
| 平均速度 | ~30 chunks/秒 |
檢索性能
| 指標 | 數據 |
|---|---|
| 向量維度 | 512 |
| 資料庫大小 | ~5 MB |
| 查詢耗時 | ~1 秒 |
| Top-K | 5 |
| 相似度計算 | Cosine Similarity |
資源使用
| 資源 | 使用量 |
|---|---|
| 記憶體 | ~1.5 GB (含模型) |
| 磁碟空間 | ~100 MB (模型) + ~5 MB (資料庫) |
| CPU | Intel i5 即可 |
| GPU | 非必需 |
數據持久化
向量化完成後的檔案結構:
rag/01 華電聯網服務建議書_20251012_124248/
├── chunks.json # 原始分片數據
├── chunks/ # 分片文字檔
├── vectordb/ # ⭐ ChromaDB 資料庫
│ ├── chroma.sqlite3 # SQLite 資料庫
│ └── [其他 ChromaDB 檔案]
└── vectordb_info.txt # ⭐ 資料庫資訊
vectordb_info.txt 內容:
Collection UUID: collection_a7f3e9c2d8b14f56
Original Name: rag_01 華電聯網服務建議書_20251012_124248
Source File: doc/01 華電聯網服務建議書.pdf
Created At: 2025-10-12T14:32:15
Vector Count: 360
Vector Dimension: 512
Model: BAAI/bge-small-zh-v1.5
這樣即使重新啟動,也能快速載入資料庫進行查詢,無需重新向量化。
階段總結
第二階段完成了向量化與檢索系統:
✅ 完成項目:
- Embedding 模型選擇與測試
- ChromaDB 整合與持久化
- 高效的語義檢索系統
- 互動式查詢介面
✅ 技術突破:
- 解決 ChromaDB 中文命名限制
- 處理 metadata None 值問題
- 實現高效批次向量化
- 完整的持久化方案
📊 性能表現:
- 360 個分片向量化:~12 秒
- 查詢回應時間:~1 秒
- 檢索準確率:優秀
🎯 下一步: 整合本地 LLM (Ollama),實現完整的 RAG 問答系統。