今天進入生成式 AI 職缺中出現頻率最高的主題:RAG(48.1%)。
因為它太重要,我用三天處理:今天講索引端(文件怎麼進去)、明天講檢索端(怎麼找出來)、後天講生成與評測(怎麼用、怎麼知道好不好)。
如果你只能讀一天,讀今天——因為多數 RAG 系統的品質上限,在文件進資料庫的那一刻就決定了。
一個真實的抱怨:
「我們把公司 3,000 份文件都放進去了,但它還是常常答不出來。是不是要換更強的模型?」
換模型幾乎不會解決這個問題。因為問題通常出在:使用者問「請假規定是什麼」,而檢索取回的是三段被攔腰切斷的段落——第一段結尾寫「請假規定如下:」,第二段是表格的中間三列,第三段從「……但主管得視情況調整」開始。
模型拿到這種脈絡,只能靠猜。這不是模型的問題,是索引的問題。
📊 職缺訊號
「RAG 與企業知識庫」出現在 48.1% 的生成式 AI 職缺中,是最高的任務訊號。值得注意的是 JD 的用詞已經從早期的「熟悉 RAG」進化為「優化檢索品質」「處理企業文件的權限與更新」——市場已經過了「會做 RAG」的階段,進入「會把 RAG 做好」的階段。
先建立全局:

圖 18-1:RAG 的兩條管線(示意架構)。兩者共用同一個嵌入模型是硬性要求——換模型就必須重建索引。
今天處理上半部。 這半部的特性是:做錯了,下半部再怎麼優化都補不回來。
切塊(chunking)看起來很無聊,卻是 RAG 品質最大的單一變因。

圖 18-2:四種切塊策略對同一份文件的切法比較(示意對照)。固定大小會攔腰切斷語意;遞迴切分尊重段落句讀;結構感知沿標題切;父子塊以小塊檢索、大塊生成。
| 策略 | 做法 | 適合 | 問題 |
|---|---|---|---|
| 固定大小 | 每 500 字元切一刀 | 快速原型 | 攔腰切斷表格、清單、句子(開頭那個案例) |
| 遞迴切分 | 優先在段落、換行、句號處切 | 一般文件的合理預設 | 對結構化文件仍不夠好 |
| 結構感知 | 沿標題階層切(H1/H2/H3) | 技術文件、法規、SOP | 需要文件本身結構良好 |
| 父子塊 | 用小塊做檢索,命中後回傳所屬的大塊 | 企業文件的最佳解 | 實作較複雜、儲存量較大 |
父子塊為什麼是最佳解? 因為它同時解決兩個矛盾的需求:
用小塊命中、回傳大塊,兩者兼得。
"""chunk.py —— 結構感知切塊,保留標題階層作為 metadata。"""
import re
from langchain_text_splitters import RecursiveCharacterTextSplitter
def split_markdown(text: str, source: str):
"""依標題切段,並把標題路徑寫進 metadata(明天檢索時會用到)。"""
chunks, heading_stack, buf = [], [], []
def flush():
if buf:
body = "\n".join(buf).strip()
if body:
chunks.append({
"text": " > ".join(heading_stack) + "\n" + body, # ← 標題放進內文
"metadata": {
"source": source,
"heading_path": " > ".join(heading_stack),
"level": len(heading_stack),
},
})
buf.clear()
for line in text.split("\n"):
m = re.match(r"^(#{1,4})\s+(.*)", line)
if m:
flush()
level = len(m.group(1))
heading_stack[:] = heading_stack[:level - 1] + [m.group(2).strip()]
else:
buf.append(line)
flush()
# 過長的段落再用遞迴切分,並保留重疊避免切斷語意
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=120,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
)
out = []
for c in chunks:
for piece in splitter.split_text(c["text"]):
out.append({"text": piece, "metadata": c["metadata"]})
return out
兩個關鍵設計:
人事規章 > 請假規定 > 特休)。這讓一個孤立的段落也帶著它的上下文,對嵌入品質的提升非常明顯。separators 要含中文標點。預設的分隔符是為英文設計的,中文文件用預設值會切得很難看。"""embed.py —— 嵌入模型的選擇與使用。"""
from sentence_transformers import SentenceTransformer
# 中文場景的選型考量:多語支援、序列長度上限、維度(影響儲存與速度)
MODEL_NAME = "BAAI/bge-m3" # 多語、支援長序列,中文表現穩定
model = SentenceTransformer(MODEL_NAME)
def embed(texts: list[str], is_query: bool = False):
"""注意:部分模型對「查詢」與「文件」需要不同的前綴,用錯會顯著掉分。"""
if is_query and "bge" in MODEL_NAME:
texts = [f"為這個句子生成表示以用於檢索相關文章:{t}" for t in texts]
return model.encode(texts, normalize_embeddings=True)
⚠️ RAG 的第一條硬性規則
建立索引與查詢時,必須使用同一個模型的同一個版本。 換了模型(甚至只是升版)就必須重建整個索引,因為新舊向量不在同一個空間裡,相似度計算完全沒有意義。
實務做法:把嵌入模型名稱與版本寫進索引的 metadata,服務啟動時比對,不一致就拒絕啟動——寧可起不來,也不要靜默地給出爛結果。

圖 18-3:HNSW 索引的運作直觀(示意架構)。上層稀疏圖快速逼近目標區域,逐層下降後在底層稠密圖精細搜尋——以可調的精度損失換取數量級的速度提升。
要理解的核心概念是「近似」:HNSW 是近似最近鄰演算法,它不保證找到真正最相似的前 k 筆。這個取捨由參數控制(ef_search 越大越準也越慢),而多數人不知道自己在做這個取捨。
"""index.py —— 用 Chroma 建立索引(本機可跑,適合起步)。"""
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(
name="company_docs",
metadata={"hnsw:space": "cosine", "embedding_model": MODEL_NAME}, # 記下模型
)
def index_documents(chunks):
collection.add(
ids=[f"{c['metadata']['source']}#{i}" for i, c in enumerate(chunks)],
embeddings=embed([c["text"] for c in chunks]).tolist(),
documents=[c["text"] for c in chunks],
metadatas=[{
**c["metadata"],
"dept": c["metadata"].get("dept", "all"), # ← 權限過濾用(Day 27)
"updated_at": "2026-07-25", # ← 增量更新用
} for c in chunks],
)
| 向量庫 | 適合 | 注意 |
|---|---|---|
| Chroma | 原型、小型專案(< 100 萬筆) | 本機即可跑,正式環境需評估 |
| pgvector | 已有 PostgreSQL、要與關聯資料一起查 | 中等規模最務實的選擇 |
| Qdrant/Milvus | 大規模、需要進階過濾與分散式 | 需要獨立維運 |
| 雲端託管(各家向量服務) | 要少維運 | 資料主權需評估(Day 27) |
教學範例都用乾淨的 Markdown,真實企業文件不是這樣。三個必然遇到的問題:
| 難題 | 症狀 | 處理方向 |
|---|---|---|
| PDF 解析 | 表格變成亂碼、雙欄排版讀成交錯的句子、掃描檔沒有文字層 | 用版面感知的解析工具;掃描檔先做 OCR;表格單獨處理成 Markdown 表格 |
| 權限 | 業務看到人資的薪資文件 | metadata 帶部門/機密等級,在檢索階段就過濾(Day 27 詳談) |
| 更新 | 文件已刪除,助理還在引用 | 建立增量更新管線,刪除必須連動——這是最常被漏掉的一環 |
💡 給起步者的最小可行索引
別一開始就追求完美。第一版這樣做就好:
- 只放一個部門、格式最乾淨的 50 份文件(不要一次 3,000 份)。
- 用結構感知切塊,
chunk_size=800、overlap=120。- metadata 至少放:
source(來源檔名與頁碼)、dept(權限)、updated_at(更新)。- 先驗證這 50 份的檢索品質(明天講怎麼量),再擴大。
範圍小而品質好的 RAG,遠比範圍大而品質差的有用。 後者會讓使用者第三次答錯後就再也不用了——而使用者的信任一旦失去,比技術問題難修得多。
索引建好了,明天處理檢索端:為什麼純向量檢索會漏掉「型號 A-2350」這種精確關鍵字、混合檢索與重排序各能提升多少、以及那個所有 RAG 系統都會遇到的問題——使用者問「那它的保固呢」,系統完全不知道「它」是什麼。
我也會給一張「每加一層優化,品質上升多少、延遲增加多少」的取捨圖。