為什麼要學 RAG?
最近在做一個專案,需要讓 LLM 回答關於特定文件的問題。直接用 ChatGPT 的問題是:
- 它不知道我的私有資料
- 知識有截止日期
- 容易產生幻覺
RAG(Retrieval-Augmented Generation)正好解決這些問題——先從知識庫檢索相關內容,再讓 LLM 基於這些內容生成回答。
RAG 的核心概念
簡單說,RAG 的流程是:
- 文件處理:把文件切成小塊(chunks)
- 向量化:用 embedding 模型把文字轉成向量
- 儲存:存到向量資料庫
- 檢索:用戶提問時,找出最相關的 chunks
- 生成:把相關內容餵給 LLM,生成回答
用 LangChain 實作
LangChain 提供了很好的抽象層。以下是我的實作過程。
1. 安裝依賴
pip install langchain langchain-openai langchain-chroma
pip install langchain-text-splitters langchain-community
2. 載入文件
from langchain_community.document_loaders import DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 載入 markdown 文件
loader = DirectoryLoader('./docs', glob="**/*.md")
documents = loader.load()
# 切分文件
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)
print(f"切分成 {len(chunks)} 個 chunks")
3. 建立向量儲存
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# 建立 embeddings
embeddings = OpenAIEmbeddings()
# 儲存到 Chroma
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
4. 建立檢索鏈
from langchain_openai import ChatOpenAI
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
# 初始化 LLM
llm = ChatOpenAI(model="gpt-4o")
# 建立 prompt
prompt = ChatPromptTemplate.from_messages([
("system", "根據以下內容回答問題。如果不知道答案,就說不知道。\n\n{context}"),
("human", "{input}")
])
# 建立檢索鏈
document_chain = create_stuff_documents_chain(llm, prompt)
retrieval_chain = create_retrieval_chain(
vectorstore.as_retriever(),
document_chain
)
# 測試
response = retrieval_chain.invoke({"input": "這份文件主要在講什麼?"})
print(response["answer"])
踩過的坑
Chunk Size 很重要
一開始設太大(2000),檢索結果太雜;設太小(200),上下文不完整。最後發現 800-1000 是比較好的起點。
Overlap 不能省
沒有 overlap 的話,如果重要資訊剛好被切斷,就會漏掉。建議設 chunk_size 的 10-20%。
不同文件類型要分開處理
PDF、Markdown、HTML 的結構不同,混在一起效果不好。我後來改成各自處理,再合併。
進階優化
混合搜尋
純語義搜尋有時會漏掉關鍵字匹配。可以結合 BM25:
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
ensemble_retriever = EnsembleRetriever(
retrievers=[vectorstore.as_retriever(), bm25_retriever],
weights=[0.7, 0.3]
)
重新排序
檢索出來的結果可以再用 reranker 排序,提高相關性:
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
compressor = CohereRerank(model="rerank-multilingual-v3.0")
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever()
)
向量資料庫選擇
| 資料庫 | 優點 | 缺點 | 適合場景 |
|---|---|---|---|
| Chroma | 簡單、本地 | 不適合大規模 | 開發測試 |
| Pinecone | 託管、快速 | 要錢 | 生產環境 |
| Weaviate | 功能豐富 | 較複雜 | 進階需求 |
| Qdrant | 開源、效能好 | 自己維護 | 自建服務 |
我的建議:開發用 Chroma,生產看預算選 Pinecone 或 Qdrant。
總結
RAG 的核心概念不難,但魔鬼在細節:
- Chunk 策略很重要,多實驗
- 混合搜尋比純語義搜尋好
- Reranker 能顯著提升品質
- 根據資料量選擇適合的向量資料庫
LangChain 讓這些實作變簡單,但理解底層原理才能做出好的調優。