WeHelp
Chroma 是一個輕量、開源的向量資料庫,專門為 AI 與大型語言模型(LLM)應用設計。
  1. 簡介、用途說明
  2. 下載、安裝、快速開始
  3. Chroma 運作模式
  4. 集合與資料管理
  5. 查詢相似資料
  6. RAG 檢索增強生成
RAG 檢索增強生成
以下是一個典型的整合方式: ```text 文件 → 切分 Chunk → Ollama Embed API → Chroma ↓ 使用者問題 → Ollama Embed API → Chroma 相似度檢索 ↓ 取回相關內容 ↓ Ollama LLM 生成答案 ``` 以下以 Python 為例,假設 Ollama Server 已在: ```text http://localhost:11434 ``` --- ## 1. 準備模型與套件 先下載一個 embedding 模型與生成模型: ```bash ollama pull nomic-embed-text ollama pull llama3.2 ``` 安裝 Python 套件: ```bash pip install ollama chromadb ``` 可以先確認 Ollama 是否正常: ```bash curl http://localhost:11434/api/tags ``` --- ## 2. Ollama Embed API Ollama 的 Embedding API 端點是: ```text POST http://localhost:11434/api/embed ``` 例如: ```bash curl http://localhost:11434/api/embed \ -H "Content-Type: application/json" \ -d '{ "model": "nomic-embed-text", "input": ["這是一段要轉換成向量的文字"] }' ``` 回傳內容大致如下: ```json { "model": "nomic-embed-text", "embeddings": [ [0.0123, -0.0456, 0.0789] ] } ``` 使用 Ollama Python SDK 時,可以直接呼叫: ```python import ollama response = ollama.embed( model="nomic-embed-text", input=["這是一段文字"] ) embeddings = response.embeddings ``` 注意:寫入 Chroma 與查詢時,必須使用同一個 embedding 模型,否則向量維度或語意空間可能不一致。 --- ## 3. 建立 Chroma 向量資料庫 下面的範例使用 Chroma 的持久化資料庫,資料會儲存在 `./chroma_db`: ```python import chromadb chroma_client = chromadb.PersistentClient( path="./chroma_db" ) collection = chroma_client.get_or_create_collection( name="knowledge_base", metadata={"hnsw:space": "cosine"}, embedding_function=None ) ``` 這裡設定: ```python embedding_function=None ``` 代表由我們自行呼叫 Ollama Embed API,而不是使用 Chroma 內建的 embedding 函式。 --- ## 4. 完整範例:建立 RAG 系統 建立 `rag_example.py`: ```python import hashlib from pathlib import Path import chromadb import ollama OLLAMA_HOST = "http://localhost:11434" EMBED_MODEL = "nomic-embed-text" CHAT_MODEL = "llama3.2" ollama_client = ollama.Client(host=OLLAMA_HOST) chroma_client = chromadb.PersistentClient( path="./chroma_db" ) collection = chroma_client.get_or_create_collection( name="knowledge_base", metadata={"hnsw:space": "cosine"}, embedding_function=None ) def get_embeddings(texts: list[str]) -> list[list[float]]: """ 使用 Ollama Embed API 將多段文字轉換成向量。 """ response = ollama_client.embed( model=EMBED_MODEL, input=texts ) # 新版 ollama SDK 通常可使用 response.embeddings if hasattr(response, "embeddings"): return response.embeddings # 兼容 dict 型態的回傳格式 return response["embeddings"] def split_text( text: str, chunk_size: int = 800, chunk_overlap: int = 100 ) -> list[str]: """ 簡單的文字切分器。 正式環境可以改用 RecursiveCharacterTextSplitter, 或依照段落、標題、頁面進行切分。 """ text = text.strip() if not text: return [] chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end].strip() if chunk: chunks.append(chunk) if end >= len(text): break start = end - chunk_overlap return chunks def make_id(source: str, chunk_index: int, text: str) -> str: """ 產生穩定且不重複的 chunk ID。 """ raw = f"{source}:{chunk_index}:{text}" return hashlib.sha256(raw.encode("utf-8")).hexdigest() def add_documents( documents: list[dict], batch_size: int = 32 ): """ documents 格式: [ { "text": "...", "source": "manual.txt" } ] """ all_chunks = [] all_metadatas = [] all_ids = [] for document in documents: source = document["source"] chunks = split_text(document["text"]) for index, chunk in enumerate(chunks): all_chunks.append(chunk) all_metadatas.append({ "source": source, "chunk_index": index }) all_ids.append(make_id(source, index, chunk)) for start in range(0, len(all_chunks), batch_size): batch_chunks = all_chunks[start:start + batch_size] batch_metadatas = all_metadatas[start:start + batch_size] batch_ids = all_ids[start:start + batch_size] embeddings = get_embeddings(batch_chunks) collection.upsert( ids=batch_ids, documents=batch_chunks, metadatas=batch_metadatas, embeddings=embeddings ) print(f"已寫入 {len(all_chunks)} 個文字區塊") def search( query: str, top_k: int = 4 ) -> list[dict]: """ 將使用者問題轉成 embedding, 再從 Chroma 找出最相似的文字區塊。 """ query_embedding = get_embeddings([query])[0] result = collection.query( query_embeddings=[query_embedding], n_results=top_k, include=["documents", "metadatas", "distances"] ) results = [] for document, metadata, distance in zip( result["documents"][0], result["metadatas"][0], result["distances"][0] ): results.append({ "document": document, "metadata": metadata, "distance": distance }) return results def generate_answer( question: str, retrieved_documents: list[dict] ) -> str: """ 將檢索到的內容組合成 Prompt, 再交給 Ollama 的生成模型回答。 """ context_parts = [] for index, item in enumerate(retrieved_documents, start=1): source = item["metadata"].get("source", "unknown") content = item["document"] context_parts.append( f"[來源 {index}: {source}]\n{content}" ) context = "\n\n".join(context_parts) prompt = f""" 你是一個知識庫問答助手。 請根據以下參考資料回答問題。 規則: 1. 只使用參考資料中的資訊。 2. 如果參考資料不足以回答,請明確說明「資料不足,無法確認」。 3. 不要自行捏造資料。 4. 回答最後列出使用的來源。 參考資料: {context} 問題: {question} """ response = ollama_client.chat( model=CHAT_MODEL, messages=[ { "role": "system", "content": "你是一個嚴謹的 RAG 問答助手。" }, { "role": "user", "content": prompt } ], options={ "temperature": 0 } ) if hasattr(response, "message"): return response.message.content return response["message"]["content"] def ask(question: str, top_k: int = 4) -> str: retrieved_documents = search(question, top_k=top_k) if not retrieved_documents: return "找不到相關資料。" return generate_answer( question, retrieved_documents ) if __name__ == "__main__": documents = [ { "source": "company_policy.txt", "text": """ 員工請假必須提前於人資系統提出申請。 一般事假至少需要提前一天申請。 病假可以在恢復工作後補交證明,但仍需依公司規定辦理。 """ }, { "source": "product_manual.txt", "text": """ 本產品支援使用 USB-C 充電。 建議使用輸出功率至少 30W 的電源供應器。 完整充電時間約為兩小時。 """ } ] # 建立索引 add_documents(documents) # 執行 RAG 問答 question = "病假需要提前申請嗎?" answer = ask(question, top_k=3) print("問題:", question) print("答案:") print(answer) ``` 執行: ```bash python rag_example.py ``` --- ## 5. RAG 的核心流程 ### 文件匯入 ```python chunks = split_text(document["text"]) embeddings = get_embeddings(chunks) ``` 文件會被切成多個區塊,每個區塊透過 Ollama 轉成向量。 ### 寫入 Chroma ```python collection.upsert( ids=ids, documents=chunks, metadatas=metadatas, embeddings=embeddings ) ``` Chroma 會保存: - 原始文字區塊 - 向量 - 文件來源 - 其他 metadata ### 查詢 ```python query_embedding = get_embeddings([query])[0] result = collection.query( query_embeddings=[query_embedding], n_results=4 ) ``` 使用者問題也要使用相同的 embedding 模型轉成向量,然後查找最相似的文件片段。 ### 生成答案 檢索結果被放入 Prompt: ```text 參考資料: [來源 1] ... 問題: ... ``` 再交給 Ollama 的生成模型回答。 --- ## 6. 從資料夾批次建立索引 如果要匯入多個 Markdown 或文字檔案,可以增加以下程式: ```python def load_documents_from_directory(directory: str): documents = [] for path in Path(directory).glob("**/*"): if path.suffix.lower() not in [".txt", ".md"]: continue text = path.read_text(encoding="utf-8") documents.append({ "source": str(path), "text": text }) return documents ``` 使用方式: ```python documents = load_documents_from_directory("./docs") add_documents(documents) ``` 若要處理 PDF、Word 或網頁,通常需要再加入: - PDF:`pypdf` - Word:`python-docx` - HTML:`BeautifulSoup` - OCR:Tesseract 或其他 OCR 模型 --- ## 7. 實務上需要注意的事項 ### 1. Embedding 模型必須一致 文件建立索引與查詢時,必須使用同一個模型: ```python EMBED_MODEL = "nomic-embed-text" ``` 不要用 A 模型建立向量,卻用 B 模型查詢。 ### 2. Chunk 不宜過大 一般可先嘗試: ```text chunk_size:500~1,000 字元 chunk_overlap:50~150 字元 ``` 實際大小要依文件類型與 LLM context window 調整。 ### 3. 儲存 metadata 建議保留: ```python { "source": "manual.pdf", "page": 12, "section": "安裝說明" } ``` 這樣回答時可以顯示來源,也方便除錯與過濾。 ### 4. 控制檢索數量 `top_k` 不一定越大越好: ```python search(question, top_k=4) ``` 太多不相關內容可能降低生成品質,通常可以從 3~8 開始測試。 ### 5. 可加入相似度門檻 Chroma 回傳的 `distance` 越小通常代表越相似,實務上可以過濾距離過大的結果: ```python results = [ item for item in results if item["distance"] < 0.8 ] ``` 實際門檻需依 embedding 模型與資料集測試,不應直接照抄。 ### 6. 大型系統可加入 Reranker 基本 RAG 流程是: ```text Embedding 檢索 → 直接交給 LLM ``` 更高品質的架構則是: ```text Embedding 初步檢索 → Reranker 重新排序 → LLM ``` 這可以降低檢索到不相關內容的問題。 --- ## 8. 最簡化的架構總結 最重要的三段程式如下: ```python # 1. 文件向量化 document_embeddings = ollama_client.embed( model="nomic-embed-text", input=document_chunks ).embeddings # 2. 寫入 Chroma collection.add( ids=ids, documents=document_chunks, embeddings=document_embeddings ) # 3. 查詢並交給 LLM query_embedding = ollama_client.embed( model="nomic-embed-text", input=[question] ).embeddings[0] result = collection.query( query_embeddings=[query_embedding], n_results=4 ) answer = ollama_client.chat( model="llama3.2", messages=[ { "role": "user", "content": f"請根據以下資料回答:\n{result}" } ] ) ``` 如此即可用 Ollama 提供本地 Embedding 與生成模型,再以 Chroma 負責向量儲存和相似度檢索,組成一個完整的本地 RAG 系統。
相關學習地圖、教學課程
Python 人工智慧
建議完成「Python 資料工程」教程後,繼續學習以下課程。