EN

5.5 結合檢索與生成

把 retrieve、build_prompt、generate 串成一個 ask 函式,並處理「找不到資料」的情況。

RAG下一篇 →

檢索器和生成器都完成了,現在把它們接起來。整個 RAG 流程,在程式裡就是依序呼叫三個函式。

整個程式的地圖:上排是建索引,下排是每次提問時依序呼叫的函式

一條「鏈」

在 RAG 框架裡,這種把幾個步驟依序串起來的東西,常被叫做鏈(chain)。自己寫的話,就是一個普通的函式:

def ask(question):
    hits = retrieve(question)                # ① 檢索
    if not hits:                             # ② 找不到夠相關的資料
        return "資料中沒有提到", []
    prompt = build_prompt(question, hits)    # ③ 增強:組成提示詞
    answer = generate(prompt)                # ④ 生成
    return answer, hits

整個 RAG 的主流程,就是這幾行。它回傳兩個東西:答案,和用到的資料(hits),後者讓呼叫端可以顯示來源。

處理「找不到資料」

注意第 ② 步:如果檢索過濾之後一筆都不剩,就直接回答「資料中沒有提到」,連模型都不呼叫。

這樣做的好處:

  • 省錢也省時間:不用呼叫模型。
  • 避免幻覺:沒有資料可以參考時,模型最容易亂編。直接擋掉,最保險。
  • 行為可預期:這種情況的回答是程式決定的,不靠模型自己判斷。

不過,這個擋法依賴門檻值設得好不好。門檻太嚴會擋掉其實有答案的問題,太鬆則擋不住無關的問題。所以提示詞裡仍然要保留「資料裡沒有就說沒有」這條規則,兩道防線一起用。

加上紀錄

在 ask 裡,建議把中間的結果都記錄或印出來:

def ask(question):
    print("問題:", question)
    hits = retrieve(question)
    for n, h in enumerate(hits, 1):
        print(f"檢索 [{n}] 距離 {h['distance']:.3f}|{h['source']} > {h['headings']}")
    ...

RAG 出錯時,第一個要問的是:檢索到了什麼?送給模型的提示詞是什麼? 沒有這些紀錄,就只能猜。

這條流程可以怎麼擴充

基本版跑通之後,常見的擴充方向:

想改善的地方 做法 對應章節
追問聽不懂(「那加班呢?」) 先把追問改寫成完整的獨立問題,再檢索 4.4
找得不夠準 混合檢索、重排序 2.1、2.4
資料常變動 排程更新索引 3.6
要有來源 解析答案中的編號,顯示來源 4.6
要知道好不好 建立評估流程 第 6 章
多使用者 檢索時用 metadata 過濾權限 3.5

不需要一開始就全部做。先把最基本的做出來,用真實的問題去測,看哪裡不夠好,再針對性地加。

和框架的對照

前面 5.1 提到的框架,做的其實就是這件事:

  • LlamaIndex 的 query_engine.query(...),內部也是:檢索、組提示詞、呼叫模型。
  • LangChain 的檢索鏈,也是同樣的三步。

差別只在於框架把這些步驟包起來,並提供許多可替換的元件。掌握這個結構後,換框架時也比較容易找到對應的位置。

← 全部文章