RAG 完全指南(一):基礎概念與你的第一個 RAG 系統

前言

如果你曾經問過 ChatGPT 最新的新聞,它會告訴你它的知識有截止日期(knowledge cutoff)。
如果你問它你公司內部的文件,它完全不知道。

這是大型語言模型(LLM)的根本限制:訓練資料是靜態的

RAG(Retrieval-Augmented Generation) 就是解決這個問題的主流方法。它讓 LLM 在回答前先「查資料」,就像一個學生考試時可以翻開參考書——而不是完全靠記憶。

這個系列共五篇,帶你從基礎到進階,完整掌握 RAG 的設計與優化。


為什麼 LLM 需要 RAG?

LLM 的三大知識限制

問題說明
知識截止日期模型只知道訓練時間點之前的資訊
無法存取私有資料公司內部文件、資料庫、個人筆記都不在訓練集裡
幻覺(Hallucination)對不確定的問題,模型會「編造」聽起來合理的答案

解法比較

方案 A:Fine-tuning(微調)
  優點:模型真正「學會」知識
  缺點:成本高、資料需要大量、難以更新、模型大小增加

方案 B:RAG(檢索增強生成)
  優點:即時更新、成本低、可追溯來源
  缺點:需要維護向量資料庫、回答品質受檢索品質影響

結論:對大多數企業應用,RAG 是更實際的選擇。Fine-tuning 適合改變模型「風格」或「推理方式」,不適合注入大量知識。


RAG 的核心架構

一個標準的 RAG 系統分成兩個主要流程:

1. 索引流程(Indexing Pipeline)— 離線執行

原始文件(PDF、Word、網頁)
    ↓
文字擷取(Text Extraction)
    ↓
切塊(Chunking)— 將長文件切成小片段
    ↓
向量化(Embedding)— 將文字轉成數字向量
    ↓
存入向量資料庫(Vector Store)

2. 查詢流程(Query Pipeline)— 即時執行

使用者問題(Query)
    ↓
向量化(Query Embedding)
    ↓
向量搜尋(Similarity Search)— 找出最相關的文件片段
    ↓
組合 Prompt(Context + Question)
    ↓
LLM 生成答案(Generation)
    ↓
回傳給使用者

這個最基本的架構被稱為 Naive RAG(樸素 RAG)。


核心概念解釋

Embedding(向量嵌入)

Embedding 是把文字轉成一串數字(向量)的過程。
語意相近的文字,它們的向量在空間中也會很接近。

1# 概念示意
2"貓喜歡睡覺"  [0.12, -0.34, 0.87, ...]  # 768 維向量
3"狗喜歡跑步"  [0.15, -0.31, 0.72, ...]  # 語意不同,但都是動物,有些維度接近
4"量子物理"    [-0.88, 0.92, -0.11, ...] # 語意差很多,向量距離遠

向量相似度

最常用的相似度計算方式是餘弦相似度(Cosine Similarity)

1import numpy as np
2
3def cosine_similarity(vec_a, vec_b):
4    dot = np.dot(vec_a, vec_b)
5    norm = np.linalg.norm(vec_a) * np.linalg.norm(vec_b)
6    return dot / norm
7
8# 值域 -1 到 1,越接近 1 表示越相似

Chunking(切塊)

為什麼要切塊?

  1. LLM 的 context window 有限制
  2. 太長的文件會「稀釋」相關資訊,降低搜尋精準度
  3. 讓每個 chunk 專注在單一主題,提升匹配品質

實作:用 Python 建立你的第一個 RAG

環境安裝

1pip install openai chromadb tiktoken langchain-text-splitters

完整範例程式碼

  1import openai
  2import chromadb
  3from langchain_text_splitters import RecursiveCharacterTextSplitter
  4
  5# ---- 設定 ----
  6client = openai.OpenAI(api_key="your-api-key")
  7chroma_client = chromadb.Client()
  8collection = chroma_client.create_collection("my_docs")
  9
 10EMBED_MODEL = "text-embedding-3-small"
 11CHAT_MODEL  = "gpt-4o-mini"
 12
 13# ---- 工具函式 ----
 14
 15def get_embedding(text: str) -> list[float]:
 16    """將文字轉成向量"""
 17    response = client.embeddings.create(input=text, model=EMBED_MODEL)
 18    return response.data[0].embedding
 19
 20
 21def index_documents(docs: list[str]) -> None:
 22    """索引流程:切塊 → 向量化 → 存入向量 DB"""
 23    splitter = RecursiveCharacterTextSplitter(
 24        chunk_size=500,       # 每塊最多 500 字元
 25        chunk_overlap=50,     # 前後重疊 50 字元,避免斷句
 26    )
 27
 28    all_chunks = []
 29    for doc in docs:
 30        chunks = splitter.split_text(doc)
 31        all_chunks.extend(chunks)
 32
 33    embeddings = [get_embedding(chunk) for chunk in all_chunks]
 34
 35    collection.add(
 36        documents=all_chunks,
 37        embeddings=embeddings,
 38        ids=[f"chunk_{i}" for i in range(len(all_chunks))],
 39    )
 40    print(f"✅ 已索引 {len(all_chunks)} 個 chunks")
 41
 42
 43def retrieve(query: str, top_k: int = 3) -> list[str]:
 44    """查詢流程:向量搜尋,取出最相關的 chunks"""
 45    query_embedding = get_embedding(query)
 46    results = collection.query(
 47        query_embeddings=[query_embedding],
 48        n_results=top_k,
 49    )
 50    return results["documents"][0]  # 回傳 top_k 個文字片段
 51
 52
 53def generate_answer(query: str, context_chunks: list[str]) -> str:
 54    """組合 Prompt,讓 LLM 根據 context 回答"""
 55    context = "\n\n---\n\n".join(context_chunks)
 56
 57    prompt = f"""你是一個知識庫問答助手。請根據以下【參考資料】回答【問題】。
 58如果參考資料中沒有足夠資訊,請直接說「根據現有資料無法回答」,不要自行推測。
 59
 60【參考資料】
 61{context}
 62
 63【問題】
 64{query}
 65
 66【回答】"""
 67
 68    response = client.chat.completions.create(
 69        model=CHAT_MODEL,
 70        messages=[{"role": "user", "content": prompt}],
 71        temperature=0,  # RAG 場景建議設低,減少創意發揮
 72    )
 73    return response.choices[0].message.content
 74
 75
 76# ---- 主程式 ----
 77
 78def rag_pipeline(query: str) -> str:
 79    """完整的 RAG pipeline"""
 80    # Step 1: 檢索
 81    relevant_chunks = retrieve(query, top_k=3)
 82
 83    # Step 2: 生成
 84    answer = generate_answer(query, relevant_chunks)
 85
 86    return answer
 87
 88
 89# ---- 示範 ----
 90
 91if __name__ == "__main__":
 92    # 準備知識文件(實際場景可以是 PDF、Markdown、資料庫內容)
 93    documents = [
 94        """
 95        Python 是一種高階、直譯式程式語言,由 Guido van Rossum 在 1991 年發布。
 96        Python 的設計哲學強調程式碼的可讀性,使用縮排來表示程式碼區塊。
 97        Python 廣泛應用於資料科學、機器學習、網頁開發和自動化腳本。
 98        """,
 99        """
100        向量資料庫是專門儲存和搜尋向量嵌入的資料庫系統。
101        常見的向量資料庫包括:Chroma、Pinecone、Weaviate、Qdrant、Milvus。
102        向量資料庫使用近似最近鄰搜尋(ANN)演算法,可以在百萬筆資料中快速找到最相似的向量。
103        """,
104        """
105        RAG(Retrieval-Augmented Generation)是一種結合資訊檢索與文字生成的 AI 架構。
106        RAG 的主要優點是可以讓 LLM 存取外部知識庫,解決模型知識過時的問題。
107        RAG 系統通常由三個部分組成:文件索引、相似度搜尋、語言模型生成。
108        """,
109    ]
110
111    # 索引文件
112    index_documents(documents)
113
114    # 查詢
115    questions = [
116        "RAG 有哪些主要優點?",
117        "有哪些常見的向量資料庫?",
118        "Python 是誰發明的?",
119    ]
120
121    for q in questions:
122        print(f"\n❓ 問題:{q}")
123        answer = rag_pipeline(q)
124        print(f"💡 回答:{answer}")

預期輸出

✅ 已索引 3 個 chunks

❓ 問題:RAG 有哪些主要優點?
💡 回答:根據參考資料,RAG 的主要優點是可以讓 LLM 存取外部知識庫,解決模型知識過時的問題。

❓ 問題:有哪些常見的向量資料庫?
💡 回答:常見的向量資料庫包括:Chroma、Pinecone、Weaviate、Qdrant、Milvus。

❓ 問題:Python 是誰發明的?
💡 回答:Python 是由 Guido van Rossum 發明的,並於 1991 年發布。

Naive RAG 的局限性

這個基本實作已經可以運作,但在實際應用中會碰到幾個問題:

問題現象後續篇章
Chunking 策略粗糙語意被切斷,搜尋精準度低第二篇
只有語意搜尋關鍵字搜尋效果有時更好第三篇(混合搜尋)
單次查詢不夠複雜問題需要多次查詢才能拼湊完整答案第三篇(Multi-Query)
沒有 RerankingTop-K 結果可能不是最相關的第三篇(Reranker)
Context 太長塞入過多不相關 chunk,LLM 反而混淆第四篇(Context Compression)
沒有評估指標不知道 RAG 品質好不好第五篇

小結

這篇介紹了:

  • 為什麼需要 RAG:LLM 的知識限制
  • RAG 的核心架構:索引流程 vs 查詢流程
  • 關鍵概念:Embedding、向量相似度、Chunking
  • 第一個 RAG 實作:用 ChromaDB + OpenAI 建立完整 pipeline

下一篇我們會深入探討 Chunking 策略向量資料庫的選型,讓 RAG 的基礎打得更扎實。


系列導覽

  • 第一篇(本篇):基礎概念與第一個 RAG 系統
  • 第二篇:Chunking 策略與向量資料庫選型
  • 第三篇:進階檢索技術(混合搜尋、HyDE、Multi-Query、Reranker)
  • 第四篇:查詢優化與 Context 壓縮
  • 第五篇:生產級 RAG 評估與 Agentic RAG