4.6 生成內容中的來源歸屬
讓答案說明它是根據哪段資料,使用者才能查證,也才有辦法信任。
來源標註讓讀者可以回頭核對答案,也讓系統比較容易除錯。不過,只在句尾放一串連結還不夠;引用必須能對應到實際支撐該句話的資料。
為什麼要標來源
- 可以查證:使用者可以回頭看原文,確認答案沒有被誤解。
- 建立信任:知道答案有根據,而不是模型說了算。
- 方便除錯:答案不對的時候,從來源就能看出是資料本身錯了,還是模型讀錯了。
- 合規與責任:醫療、法律、學術這類場合,往往要求能追溯依據。
做法:編號加 metadata
整個做法接續前面各章的準備:
- 第 3 章:每一塊都帶著 metadata(來源檔名、頁數、標題)。
- 4.2、4.3:把資料放進提示詞時,每一段都編號並附上來源。
- 指示:要求模型用到哪一段,就標上編號,例如
[1]。 - 後處理:從答案裡找出編號,用編號對應回 metadata,顯示成使用者看得懂的來源。
範例
提示詞裡的資料:
[1](來源:員工手冊.pdf,第 12 頁)
員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。
[2](來源:員工手冊.pdf,第 13 頁)
年度結束時沒有休完的特別休假,公司應該折算成工資發給員工。
模型的答案:
沒休完的特休假,公司要折算成工資發給員工 [2]。
系統解析出 [2],查到對應的 metadata,顯示成:
來源:員工手冊.pdf,第 13 頁
後處理的程式
後處理要做的事:從答案找出所有的編號,對應回來源。更重要的是,驗證編號真的存在,因為模型有時會引用根本沒有的編號。
import re
def check_citations(answer, chunks):
"""找出答案裡的 [n],對應回來源;編號不存在的要警告"""
numbers = sorted({int(n) for n in re.findall(r"\[(\d+)\]", answer)})
for n in numbers:
if 1 <= n <= len(chunks):
c = chunks[n - 1]
print(f"[{n}] {c['source']} 第 {c['page']} 頁")
else:
print(f"[{n}] 警告:沒有這個編號的資料,模型可能亂編了引用")
用一個故意編造的答案測試,答案裡有 [2] 和一個不存在的 [5](只有兩段資料):
模型回答: 沒休完的特休假,公司要折算成工資發給員工 [2]。另外也可參考 [5]。
[2] 員工手冊.pdf 第 13 頁
[5] 警告:沒有這個編號的資料,模型可能亂編了引用
這個答案是我手寫的,用來示範後處理,不是真的模型輸出。
完整的可執行範例
下面是把 4.2 到 4.6 串起來的完整程式,不需要安裝任何套件。它組出完整的提示詞,並示範預算控制和引用檢查:
import re
SYSTEM = """你是公司的人資助理。請只根據下面「參考資料」回答問題。
規則:
1. 資料裡沒有答案時,直接說「資料中沒有提到」,不要猜。
2. 回答中用到哪一段資料,就在句子後面標上編號,例如 [1]。
3. 用繁體中文,簡短回答。"""
# 檢索器找回來的資料(已經依相似度排好,最相關的在前面)
retrieved = [
{"text": "員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。", "source": "員工手冊.pdf", "page": 12},
{"text": "年度結束時沒有休完的特別休假,公司應該折算成工資發給員工。", "source": "員工手冊.pdf", "page": 13},
{"text": "加班必須事先經主管同意。平日加班前兩小時,以 1.34 倍計算。", "source": "員工手冊.pdf", "page": 20},
]
def pick_within_budget(chunks, max_chars):
picked, used = [], 0
for chunk in chunks:
if used + len(chunk["text"]) > max_chars:
break
picked.append(chunk)
used += len(chunk["text"])
return picked
def build_prompt(question, chunks):
blocks = []
for number, chunk in enumerate(chunks, start=1):
blocks.append(f"[{number}](來源:{chunk['source']},第 {chunk['page']} 頁)\n{chunk['text']}")
context = "\n\n".join(blocks)
return f"{SYSTEM}\n\n# 參考資料\n{context}\n\n# 問題\n{question}"
def check_citations(answer, chunks):
numbers = sorted({int(n) for n in re.findall(r"\[(\d+)\]", answer)})
for n in numbers:
if 1 <= n <= len(chunks):
c = chunks[n - 1]
print(f"[{n}] {c['source']} 第 {c['page']} 頁")
else:
print(f"[{n}] 警告:沒有這個編號的資料,模型可能亂編了引用")
question = "沒休完的特休假會怎麼處理?"
chunks = pick_within_budget(retrieved, max_chars=70)
print(f"預算 70 字,放進了 {len(chunks)} 段(共檢索到 {len(retrieved)} 段)\n")
print(build_prompt(question, chunks))
answer = "沒休完的特休假,公司要折算成工資發給員工 [2]。另外也可參考 [5]。"
print("\n模型回答:", answer)
check_citations(answer, chunks)
來源歸屬的幾個層次
| 層次 | 做法 | 備註 |
|---|---|---|
| 列出參考了哪些文件 | 回答後面附上所有檢索到的來源 | 最簡單,但不知道哪句對應哪個來源 |
| 句子層級的引用 | 每個重點後面標編號 | 最常見,精準度和成本的平衡 |
| 引用原文片段 | 同時顯示引用的那一句原文 | 最容易查證,要額外處理 |
| 連結回原文位置 | 引用編號可以點開到原文的頁面或段落 | 使用體驗最好,需要保存足夠的位置資訊 |
要小心的地方
模型可能引用錯誤
- 編號不存在:像上面的例子,用程式驗證就能抓到。
- 編號存在但內容無法支撐說法:模型引用了
[2],但[2]並沒有說那句話。這比較難抓,可以用另一個模型判斷「這段資料能不能支撐這句話」;第 6 章的忠實度評估就是在處理這件事。
「有標來源」不等於「一定正確」
來源只是讓人有辦法查證,不代表答案就對。重要的場合,仍然需要使用者去看原文確認。
來源本身的問題
- 資料本身過期了,或是寫錯了。
- 資料是不可信的來源。
這要靠資料管理和 metadata(例如日期、版本、可信度標記)來處理。
權限
顯示來源時,要確認使用者有權限看那份文件。否則透過來源連結,可能洩漏不該看到的檔名或內容。