iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI Engineering

RE: 從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 20 篇

Day 20:RAG(三)生成與評測 — 引用、拒答與量化品質

  • 分享至 

  • xImage
  •  

RAG 三部曲的最後一天。文件進去了(Day 18)、找出來了(Day 19),今天處理怎麼用與怎麼知道好不好。

後半段特別重要——因為 Day 19 提到的所有優化,你都需要一把尺才知道有沒有效。

今天要解決的問題

「它答錯了,但看起來好專業。」

這是 RAG 系統最危險的失敗模式。使用者問一個知識庫裡沒有答案的問題,模型不會說「我不知道」,它會用檢索到的最接近的內容編一個聽起來很合理的答案。

比起明顯的胡說,這種「流暢的錯誤」更難被發現,也更容易造成實際傷害——因為使用者會相信它。

今天要做三件事:讓它有依據、讓它會拒答、讓你能量化它。

📊 職缺訊號

「評測、安全與品質」出現在 34.3% 的生成式 AI 職缺中,且是本期成長第二多的任務訊號(+1.7%)。這是市場成熟的訊號:當大家都做得出 demo 時,能證明品質的人才有價值。


一、現象:先分清楚是誰的錯

昨天以二分法說明,今日提供更完整的排查決策樹:

RAG 問題排查決策樹

圖 20-1:RAG 問題排查決策樹(示意流程)。從「檢索有沒有召回正確切塊」二分入手,左枝追資料與檢索,右枝追生成與提示。

這張圖建議存起來當工作手冊。 它的價值在於:把「RAG 答不好」這個模糊的抱怨,變成一連串可以逐一驗證的問題。

今天處理右枝——檢索對了,但生成端沒用好。三種典型症狀:

症狀 原因 解法
答案摻雜脈絡以外的內容 模型用了自己的預訓練知識 提示明確要求「僅依脈絡作答」
找不到答案時硬掰 沒有拒答機制 明確的拒答指令 + 檢索分數門檻(Day 19)
引用的來源與內容對不上 沒有強制引用格式,或引用未驗證 結構化引用 + 程式驗證

二、原理:讓答案「可驗證」

對抗幻覺最有效的工程手段,不是讓使用者能夠檢查。

image

圖 20-2:可驗證答案的產生流程(示意流程)。關鍵是最後那個驗證步驟——沒有驗證的引用,本身也可能是幻覺。

📌 一個實務上踩過的坑

模型會編造引用編號。你給它 4 段脈絡,它可能標註「[5]」——一個不存在的來源。所以引用一定要程式驗證:檢查每個標註的編號是否落在實際提供的範圍內,不合法就重試。

三、動手:生成端的三個實作

3.1 提示:紀律要寫死

RAG_SYSTEM_PROMPT = """你是企業內部知識助理。請嚴格遵守以下規則回答問題。

【資料來源】
以下是從公司知識庫檢索到的資料,每段前面有編號:

{context}

【回答規則】
1. 只能根據上述資料回答。不得使用你的其他知識,即使你認為那是對的。
2. 每一個事實陳述後面,必須以 [編號] 標註其來源,例如:特休可遞延一年 [2]。
3. 若上述資料不足以回答問題,直接回答:
   「知識庫中沒有找到相關資料,建議洽詢 {fallback_contact}。」
   不要嘗試推測或用常識補充。
4. 若資料之間有衝突,指出衝突並列出各自來源,不要自行判斷何者正確。
5. 用繁體中文回答,語氣專業但親切,長度以三段為限。

【使用者問題】
{question}"""

規則 4 是很多人沒想到的。 企業文件經常互相矛盾(舊版政策沒下架、不同部門的規定不一致)。讓模型指出衝突而不是自行選一個,是更誠實也更有用的行為——它把判斷權交還給知道脈絡的人。

3.2 拒答:兩道防線

"""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 系統最實用的健康指標之一。

3.3 評測:把「感覺不錯」變成數字

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

必須包含的四類題目:

  1. 標準題——知識庫裡有明確答案的(占多數)。
  2. 拒答題——知識庫裡沒有答案的(至少 15%)。這類題目最常被忘記,卻是防幻覺的關鍵。
  3. 多文件題——答案需要綜合兩份以上文件。
  4. 邊界題——文件互相矛盾、資訊過期、問題有歧義。

4.1 評測要跑多久一次

建立評測集之後,下一個問題是「什麼時候跑」。實務上的節奏:

時機 跑什麼 為什麼
每次改提示 全套評測集 提示的改動是全域性的,修 A 很容易壞 B
每次改檢索參數 檢索端指標即可(快) 生成端沒動,不需重跑昂貴的 LLM 評審
每次換模型 全套 + 人工抽查 模型行為的變化最難預測
知識庫大量更新後 全套 索引變了,檢索結果也會變
每週定期 全套 + 檢視新累積的失敗案例 抓出漸進式的退化

把「改提示要跑評測」做成 CI 的一部分,就是 Day 11 品質門檻在生成式 AI 的對應物:評測不過,PR 不能合併。這一步做到了,你的團隊就從「靠感覺調參」進化成「有依據地迭代」。

⚠️ 一個常見的自欺

用「模型生成的問題」當評測集,會得到虛高的分數——因為那些問題天生就貼合文件的用語。真實使用者的問法跟文件的寫法差很多(文件寫「年度特別休假」,使用者問「特休怎麼算」)。

評測集一定要有真實使用者的語料,哪怕只有 20 題。這 20 題的價值遠高於模型生成的 200 題。


今日小結

  • RAG 最危險的失敗是「流暢的錯誤」——比明顯的胡說更難發現、更容易造成傷害。
  • 對抗幻覺的工程手段不是要求模型「不要編」,而是讓答案可驗證:強制引用 + 程式驗證引用編號合法(模型會編造來源)。
  • 拒答要有兩道防線:檢索分數門檻(Day 19)+ 提示中的明確拒答指令。拒答率是要監控的健康指標(太低代表硬掰、太高代表覆蓋不足)。
  • 讓模型指出文件間的衝突而非自行判斷——把判斷權交還給人。
  • 評測要分開量測檢索端與生成端(Context Recall/Faithfulness),否則不知道該修哪裡。
  • 評測集是專案最有價值的資產:必含拒答題(≥15%)與真實使用者語料;每次答錯都變成一題。

延伸閱讀

  • 📄 RAGAS 文件——四個核心指標的定義與實作,本文的最小版本是它的簡化
  • 📄 Es et al., RAGAS: Automated Evaluation of Retrieval Augmented Generation (2023)——指標設計的原始論文

上一篇
Day 19:RAG(二)檢索端 — 混合檢索、重排序與查詢改寫
下一篇
Day 21:Agent(一) — 能用工作流就不要用代理
系列文
RE: 從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言