RAG 三部曲的最後一天。文件進去了(Day 18)、找出來了(Day 19),今天處理怎麼用與怎麼知道好不好。
後半段特別重要——因為 Day 19 提到的所有優化,你都需要一把尺才知道有沒有效。
「它答錯了,但看起來好專業。」
這是 RAG 系統最危險的失敗模式。使用者問一個知識庫裡沒有答案的問題,模型不會說「我不知道」,它會用檢索到的最接近的內容編一個聽起來很合理的答案。
比起明顯的胡說,這種「流暢的錯誤」更難被發現,也更容易造成實際傷害——因為使用者會相信它。
今天要做三件事:讓它有依據、讓它會拒答、讓你能量化它。
📊 職缺訊號
「評測、安全與品質」出現在 34.3% 的生成式 AI 職缺中,且是本期成長第二多的任務訊號(+1.7%)。這是市場成熟的訊號:當大家都做得出 demo 時,能證明品質的人才有價值。
昨天以二分法說明,今日提供更完整的排查決策樹:

圖 20-1:RAG 問題排查決策樹(示意流程)。從「檢索有沒有召回正確切塊」二分入手,左枝追資料與檢索,右枝追生成與提示。
這張圖建議存起來當工作手冊。 它的價值在於:把「RAG 答不好」這個模糊的抱怨,變成一連串可以逐一驗證的問題。
今天處理右枝——檢索對了,但生成端沒用好。三種典型症狀:
| 症狀 | 原因 | 解法 |
|---|---|---|
| 答案摻雜脈絡以外的內容 | 模型用了自己的預訓練知識 | 提示明確要求「僅依脈絡作答」 |
| 找不到答案時硬掰 | 沒有拒答機制 | 明確的拒答指令 + 檢索分數門檻(Day 19) |
| 引用的來源與內容對不上 | 沒有強制引用格式,或引用未驗證 | 結構化引用 + 程式驗證 |
對抗幻覺最有效的工程手段,不是讓使用者能夠檢查。

圖 20-2:可驗證答案的產生流程(示意流程)。關鍵是最後那個驗證步驟——沒有驗證的引用,本身也可能是幻覺。
📌 一個實務上踩過的坑
模型會編造引用編號。你給它 4 段脈絡,它可能標註「[5]」——一個不存在的來源。所以引用一定要程式驗證:檢查每個標註的編號是否落在實際提供的範圍內,不合法就重試。
RAG_SYSTEM_PROMPT = """你是企業內部知識助理。請嚴格遵守以下規則回答問題。
【資料來源】
以下是從公司知識庫檢索到的資料,每段前面有編號:
{context}
【回答規則】
1. 只能根據上述資料回答。不得使用你的其他知識,即使你認為那是對的。
2. 每一個事實陳述後面,必須以 [編號] 標註其來源,例如:特休可遞延一年 [2]。
3. 若上述資料不足以回答問題,直接回答:
「知識庫中沒有找到相關資料,建議洽詢 {fallback_contact}。」
不要嘗試推測或用常識補充。
4. 若資料之間有衝突,指出衝突並列出各自來源,不要自行判斷何者正確。
5. 用繁體中文回答,語氣專業但親切,長度以三段為限。
【使用者問題】
{question}"""
規則 4 是很多人沒想到的。 企業文件經常互相矛盾(舊版政策沒下架、不同部門的規定不一致)。讓模型指出衝突而不是自行選一個,是更誠實也更有用的行為——它把判斷權交還給知道脈絡的人。
"""answer.py —— 拒答不能只靠提示,要有程式層的防線。"""
NO_ANSWER = "知識庫中沒有找到相關資料,建議洽詢 IT 服務台(分機 1234)。"
async def answer(question: str, user):
# 防線一:檢索階段就判斷(Day 19 的分數門檻)
chunks = retrieve_and_rerank(question, user=user)
if not chunks:
return {"answer": NO_ANSWER, "sources": [], "refused": True}
context = "\n\n".join(f"[{i+1}] {c['text']}" for i, c in enumerate(chunks))
resp = await call_llm(RAG_SYSTEM_PROMPT.format(
context=context, question=question, fallback_contact="IT 服務台"))
# 防線二:驗證引用編號合法(模型會編造不存在的來源)
cited = {int(n) for n in re.findall(r"\[(\d+)\]", resp)}
if cited and not cited.issubset(set(range(1, len(chunks) + 1))):
log.warning("偵測到無效引用 %s,觸發重試", cited - set(range(1, len(chunks)+1)))
return await answer_with_stricter_prompt(question, chunks)
return {
"answer": resp,
"sources": [{"n": i + 1, "title": c["metadata"]["source"],
"url": c["metadata"].get("url"),
"heading": c["metadata"].get("heading_path")}
for i, c in enumerate(chunks) if (i + 1) in cited],
"refused": False,
}
💡 拒答率是一個要監控的指標
拒答率太低(< 2%)通常代表模型在硬掰;太高(> 30%)代表知識庫覆蓋不足或檢索門檻太嚴。把它畫成每日曲線,異常變動時去看發生什麼事——這是 RAG 系統最實用的健康指標之一。
RAG 的評測要分開量測檢索端與生成端,否則你不知道該修哪裡:
| 指標 | 衡量什麼 | 屬於 |
|---|---|---|
| Context Recall | 正確答案所需的資訊,有沒有被取回 | 檢索端 |
| Context Precision | 取回的內容中,相關的比例 | 檢索端 |
| Faithfulness | 答案是否被脈絡支持(幻覺指標) | 生成端 |
| Answer Relevancy | 答案有沒有切題回應問題 | 生成端 |
"""eval.py —— 最小可行的評測,不依賴任何框架。"""
import json
# 評測集:從真實使用者問題與客服工單累積,不要憑空想
# 每筆至少要有:question、ground_truth(人工確認的正確答案)、
# expected_sources(應該被檢索到的文件)
dataset = json.load(open("eval/golden_set.json"))
JUDGE_PROMPT = """判斷「答案」中的每個事實陳述,是否都能在「脈絡」中找到依據。
脈絡:{context}
答案:{answer}
回傳 JSON:{{"supported": <有依據的陳述數>, "total": <總陳述數>,
"unsupported_claims": ["<沒有依據的陳述>"]}}"""
async def evaluate(dataset):
results = []
for case in dataset:
chunks = retrieve_and_rerank(case["question"])
got_sources = {c["metadata"]["source"] for c in chunks}
# 檢索端:期望的來源有沒有被取回
recall = len(got_sources & set(case["expected_sources"])) \
/ max(len(case["expected_sources"]), 1)
out = await answer(case["question"], user=SYSTEM_USER)
# 生成端:用 LLM 評審算忠實度(Day 24 會談它的可信度怎麼驗證)
judge = await llm_judge(JUDGE_PROMPT.format(
context="\n".join(c["text"] for c in chunks), answer=out["answer"]))
faithfulness = judge["supported"] / max(judge["total"], 1)
results.append({"q": case["question"], "context_recall": recall,
"faithfulness": faithfulness, "refused": out["refused"]})
n = len(results)
print(f"Context Recall : {sum(r['context_recall'] for r in results)/n:.2f}")
print(f"Faithfulness : {sum(r['faithfulness'] for r in results)/n:.2f}")
print(f"拒答率 : {sum(r['refused'] for r in results)/n:.1%}")
return results
這段程式碼不到 40 行,但它讓你從「感覺不錯」變成「Context Recall 0.83、Faithfulness 0.91」。 有了數字,Day 19 的每一個優化才能被驗證。
評測集是 RAG 專案最有價值的資產,甚至超過程式碼——因為程式碼可以重寫,累積的評測案例不能。
| 階段 | 題數 | 來源 | 用途 |
|---|---|---|---|
| 起步 | 20–30 題 | 團隊自己想 + 幾個真實問題 | 建立基線,能跑就好 |
| 上線前 | 50–100 題 | 使用者訪談、客服工單、內部試用 | 品質門檻的依據 |
| 上線後 | 持續累積 | 每一次答錯都變成一題 | 回歸測試,防止修 A 壞 B |
必須包含的四類題目:
建立評測集之後,下一個問題是「什麼時候跑」。實務上的節奏:
| 時機 | 跑什麼 | 為什麼 |
|---|---|---|
| 每次改提示 | 全套評測集 | 提示的改動是全域性的,修 A 很容易壞 B |
| 每次改檢索參數 | 檢索端指標即可(快) | 生成端沒動,不需重跑昂貴的 LLM 評審 |
| 每次換模型 | 全套 + 人工抽查 | 模型行為的變化最難預測 |
| 知識庫大量更新後 | 全套 | 索引變了,檢索結果也會變 |
| 每週定期 | 全套 + 檢視新累積的失敗案例 | 抓出漸進式的退化 |
把「改提示要跑評測」做成 CI 的一部分,就是 Day 11 品質門檻在生成式 AI 的對應物:評測不過,PR 不能合併。這一步做到了,你的團隊就從「靠感覺調參」進化成「有依據地迭代」。
⚠️ 一個常見的自欺
用「模型生成的問題」當評測集,會得到虛高的分數——因為那些問題天生就貼合文件的用語。真實使用者的問法跟文件的寫法差很多(文件寫「年度特別休假」,使用者問「特休怎麼算」)。
評測集一定要有真實使用者的語料,哪怕只有 20 題。這 20 題的價值遠高於模型生成的 200 題。