RAG 是什麼?
RAG(Retrieval-Augmented Generation)讓 LLM 在回答前先從外部知識庫檢索相關文檔,再基於檢索結果生成答案。這解決了 LLM 的兩個核心問題:
- 知識截止日期:LLM 只知道訓練數據中的信息
- 幻覺 (Hallucination):LLM 可能編造不存在的事實
用戶提問 → Embedding → 向量搜索 → Top-K 文檔 → LLM 生成 → 帶來源的答案
核心組件拆解
1. Embedding 模型選擇
| 模型 | 維度 | 語言 | 成本 | 適用場景 |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | 多語言 | $0.02/1M tokens | 通用、成本敏感 |
| text-embedding-3-large | 3072 | 多語言 | $0.13/1M tokens | 高精度需求 |
| bge-large-zh-v1.5 | 1024 | 中文 | 免費 (本地) | 中文場景首選 |
| bge-m3 | 1024 | 多語言 | 免費 (本地) | 多語言混合 |
| jina-embeddings-v3 | 1024 | 多語言 | 免費 (API) | 長文檔 (8K tokens) |
建議:中文為主用 bge-large-zh-v1.5,多語言用 bge-m3,API 方案用 text-embedding-3-small。
# Ollama 本地 Embedding
ollama pull nomic-embed-text
# 或 bge-m3
ollama pull bge-m3
2. Chunking 策略
這是最容易被忽略但最關鍵的環節。錯誤的 Chunking 直接導致檢索失敗。
| 策略 | 適合場景 | 優點 | 缺點 |
|---|---|---|---|
| 固定大小 (500 tokens) | 通用 | 簡單、可預測 | 可能在句子中間截斷 |
| 語義分割 (按段落/章節) | 結構化文檔 | 保持語義完整性 | 塊大小不一致 |
| 遞歸分割 (按 \n\n → \n → 。) | 混合文檔 | 自適應 | 實現複雜 |
| 句子窗口 | 精確檢索 | 每句獨立 | 丟失上下文 |
最佳實踐:
- Chunk size: 256-512 tokens(太小丟失上下文,太大稀釋語義)
- Overlap: 10-20%(確保關鍵信息不被截斷)
- 使用 LangChain 的
RecursiveCharacterTextSplitter
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
chunks = splitter.split_text(document)
3. 向量數據庫對比
| 方案 | 部署 | 規模 | 適合場景 |
|---|---|---|---|
| Chroma | 本地/嵌入式 | <100K docs | 原型/個人項目 |
| Qdrant | 本地/Docker | 百萬級 | 生產環境 |
| Milvus | 分佈式 | 億級 | 企業級 |
| Pinecone | 雲端 SaaS | 無上限 | 不想管基礎設施 |
| pgvector | PostgreSQL 擴展 | 百萬級 | 已有 PG 的項目 |
推薦路徑:原型用 Chroma → 生產用 Qdrant Docker → 超大規模用 Milvus。
# Qdrant 快速啟動
docker run -p 6333:6333 qdrant/qdrant
4. Rerank(重排序)
初次向量檢索後,用更精確的 Cross-encoder 對 Top-K 結果重新排序,大幅提升準確度。
| 模型 | 語言 | 說明 |
|---|---|---|
| bge-reranker-v2-m3 | 多語言 | 開源最佳 |
| Cohere Rerank | 多語言 | API,效果最好 |
| bge-reranker-large | 中/英 | 中文場景推薦 |
為什麼需要 Rerank? 向量相似度 ≠ 語義相關性。Rerank 用 Cross-encoder 逐對比較 query 和 document,更精準。
# 典型流程
# 1. 向量檢索 → 20 個候選
candidates = vector_db.search(query, top_k=20)
# 2. Rerank → 取前 5
from FlagEmbedding import FlagReranker
reranker = FlagReranker('BAAI/bge-reranker-v2-m3')
scores = reranker.compute_score([[query, doc] for doc in candidates])
top_5 = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]
# 3. LLM 生成
answer = llm.generate(query, context=top_5)
完整 RAG Pipeline 代碼
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.llms import Ollama
# 1. 加載文檔
with open("knowledge_base.txt") as f:
text = f.read()
# 2. Chunking
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(text)
# 3. Embedding + 存入向量庫
embeddings = OllamaEmbeddings(model="bge-m3")
vectorstore = Chroma.from_texts(chunks, embeddings)
# 4. 檢索
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
docs = retriever.get_relevant_documents("什麼是 RAG?")
# 5. 生成
llm = Ollama(model="qwen2.5:7b")
context = "\n".join([d.page_content for d in docs])
answer = llm.invoke(f"基於以下文檔回答問題:\n{context}\n\n問題:什麼是 RAG?")
常見踩坑
| 坑 | 原因 | 解法 |
|---|---|---|
| 檢索結果不相關 | Chunk size 太大或太小 | 調優 256-512,加 overlap |
| 檢索速度慢 | 未建索引 | Qdrant/Milvus 建 HNSW 索引 |
| Embedding 成本高 | 每次查詢都調 API | 本地部署 bge-m3 |
| 答案幻覺 | LLM 無視檢索結果 | Prompt 中強制引用來源 |
| 中英混合查詢失敗 | Embedding 模型不支持 | 用 bge-m3 多語言模型 |