4.3 上下文注入方法
把檢索到的資料放進提示詞,看起來只是貼上去,但放法和順序都有差。
上下文注入(context injection) 指的是把檢索到的資料,放進提示詞的過程。最簡單的做法就是直接串接,但有幾個細節會影響模型的表現。
最基本的做法:直接串接
把每一段資料依序放進提示詞,中間用分隔隔開:
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}"
這個做法簡單、可預測,是絕大多數 RAG 系統的起點。(完整可執行的版本在 4.4 和 4.5。)
每一段資料要怎麼包裝
單純貼上文字不夠,通常會加上:
- 編號:
[1]、[2],讓模型可以引用。 - 來源資訊:檔名、頁數、標題,來自第 3 章準備好的 metadata。
- 日期:資料可能有新舊版本,讓模型能判斷。
- 清楚的邊界:每一段用空行、分隔線或標籤隔開,避免兩段混在一起。
用標籤包起來的寫法,常見而且清楚:
<資料 編號="1" 來源="員工手冊.pdf" 頁數="12">
員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。
</資料>
資料的順序
放進提示詞的順序會影響結果。研究發現,當提示詞很長時,模型比較能有效利用放在開頭和結尾的資訊,夾在中間的容易被忽略。這個現象常被叫做「迷失在中間(lost in the middle)」,出自 Liu 等人 2023 年的研究。
實務上的做法:
- 最相關的放在最前面:也就是直接用檢索的相似度排序。
- 如果放很多段,可以考慮把最重要的放在最前面和最後面。
- 不要一次塞太多,資料越多,這個問題越明顯。
這個現象的程度會隨模型而不同,新的模型可能改善很多,所以最好用自己的資料測試。
其他注入的方式
去除重複
檢索回來的幾塊可能內容重疊(尤其是用了 overlap 的時候)。放進去之前先去重,節省空間。
合併相鄰的塊
如果檢索到的兩塊在原文裡本來就相連,可以合併成一段,保持上下文連貫,讀起來比兩段零碎的更自然。
補上前後文
只找到一小段時,可以把它前後相鄰的塊一起帶進來,提供完整的上下文。這就是 3.4 提到的「小塊找、大塊給」。
先壓縮再注入
把檢索到的內容先用 LLM 摘要,或只抽出和問題相關的句子,再放進提示詞。可以省空間、減少雜訊,但多一次模型呼叫,成本和延遲增加,而且摘要也可能漏掉細節。
分批處理
資料實在太多時,可以把它分成幾批,每批各自請模型回答,再把幾個答案合併。這類做法(有時叫 map-reduce)比較複雜,通常只在必要時使用。
位置:放在 system 還是 user
很多 LLM 的介面把訊息分成 system(系統指示)和 user(使用者訊息)。常見的放法:
- 指示放在 system:角色、規則。
- 參考資料和問題放在 user:因為每次提問的資料都不一樣。
不同的模型和服務對這兩個位置的處理方式不同,要看你使用的 API 說明。