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 人工智慧