5.5 結合檢索與生成
把 retrieve、build_prompt、generate 串成一個 ask 函式,並處理「找不到資料」的情況。
檢索器和生成器都完成了,現在把它們接起來。整個 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 的檢索鏈,也是同樣的三步。
差別只在於框架把這些步驟包起來,並提供許多可替換的元件。掌握這個結構後,換框架時也比較容易找到對應的位置。