近來 RAG 有時名聲不佳:有人如今認為它極其簡單(入門確實如此,但要擴展就沒那麼容易),也有人認為它已被「代理式系統」超越(但許多代理式系統稍加探究,很快就會發現其實與 RAG 非常相似……)。
本文透過一兩個實作範例,說明我們如何處理以下常見挑戰:
處理文字與數值混合的資料,以及這類資料為何會讓簡易 RAG 失效:關鍵字彼此重疊,數字本身則沒有語意。
為何採用摘要優先的嵌入設計會有幫助:先為每個區塊產生簡短的描述性摘要,再以該摘要進行嵌入與查詢。
如何產生具上下文的摘要:納入上層文件的上下文,避免形式相似的統計資料混淆。
何時應依賴程式碼與 Pydantic 模型:若逐字保留內容至關重要,可將自訂程式碼及/或 Pydantic 模型與 LLM 呼叫搭配使用,以確保可靠性。
基本原理
從客服機器人到內部知識助理,各種應用背後都有 RAG 系統提供支援。
在底層,通常會執行以下步驟:
將來源文件切分成區塊
將每個區塊嵌入向量空間
查詢時檢索排名前 K 的區塊
以這些區塊為依據生成答案
LangChain、LlamaIndex、OpenAI Filestore 等熱門工具組,讓這些步驟幾乎變得輕而易舉。但在實際的資料管線中,遇到的資料不會只有密集文字,而基礎 RAG 往往難以應付。接下來,我們會以具體範例說明資料方面的挑戰,並隨著複雜度增加,逐步建構解決方案。
當資料不只是文字時(其實很常見)
請看看以下遊戲情境中的資料區塊:
JSON
嵌入之所以有效,是因為模型透過語意和文法,學會了詞語之間的關係。上述資料混合了文字與數字;一旦脫離這個特定情境,數字與文字之間便毫無關聯。因此,可以說這個資料區塊基本上只是一些略具描述性的詞語,後面接著若干隨機數字。
如果我們只有這類資料,其實不成問題,因為仍可利用少數描述性詞語的嵌入進行檢索(或者直接使用 text-to-SQL)。但如果這個區塊埋在大量文字密集的區塊中,而且那些區塊也出現相同詞語呢?例如:
JSON
現在,假設我們想檢索「啟用 Draconic Ascension 時的攻擊範圍是多少?」我們很可能無法找出所需的相關區塊,因為它淹沒在其他含有相同關鍵字的區塊雜訊中。
根本問題在於:即使這些資料區塊針對同一主題提供不同類型的資訊,我們也無法有效區分它們。我們能否以某種方式豐富或強化這些資料?當然可以:smile:
用摘要來豐富資料,沒錯,你沒看錯
我們不直接嵌入區塊本身,而是先產生一段摘要來描述資料內容,再根據摘要進行嵌入與檢索。到了生成步驟,我們仍會使用與摘要連結的原始資料。
因此,針對上面的兩個區塊範例,我們可以產生如下摘要:
攻擊範圍、速度與傷害的統計資料(預設狀態及啟用 Draconic Ascension 時)。
Draconic Ascension 的說明與詳細資料,包括啟用條件、視覺效果及背景故事。
接著,我們也會擴充查詢,使其與摘要「對齊」。例如,我們會把「啟用 Draconic Ascension 時的攻擊範圍是多少?」改成「啟用 Draconic Ascension 時,攻擊範圍的統計資料是什麼?」當檢索查詢來自非技術領域的使用者,且他們以~~「自由發揮」的~~一般自然語言提問時,這點尤其重要。畢竟,他們不會知道或在意 RAG 如何運作,才能將精確率/召回率提升至最高。


別斷章取義(人生通常也適用)
接下來的情況,是要處理大量外觀相同的資料區塊,如下所示:
Plain Text
如果沿用相同方法,想像一下我們提出:「角色 X 的攻擊範圍是多少?」由於剛才產生的摘要也非常相似,我們只能靠運氣猜測。那麼,要如何區分它們?
答案很簡單:提供上下文。我們可以直接在資料區塊中加入其上層文件的參照;以本例而言,就是 {”角色”: “X”}。如此一來,即使也有角色 Y 和 Z 的相同資料,我們仍能準確檢索角色 X 的正確資料。
不過,更好且更具通用性的方法,是為區塊產生具上下文的摘要。也就是說,我們不只為資料區塊本身產生摘要,而是同時傳入其上層文件和區塊,以產生整體上下文摘要;摘要會說明這個區塊在上層文件中的作用。例如:
此區塊提供角色 X 的……詳細統計資料。此區塊藉由呈現 X 在攻擊速度方面的優勢……融入完整文件。
此區塊提供角色 Y 的……詳細統計資料。此區塊藉由呈現 Y 使用特殊能力後提升的屬性……融入完整文件。
此區塊提供角色 Z 的……詳細統計資料。此區塊藉由呈現 Z 十分適合在團隊對戰中擔任坦克的屬性……融入完整文件。
這種方法(部分靈感來自 Anthropic)用於上述範例或許顯得小題大作,但對於可能因「脫離上下文」而被誤解的區塊非常有效。此外,它還提供一套適用於所有區塊的統一方法,讓工程管線保持簡潔。


需要~~控制一切~~嚴謹處理時
通常,我們取得的是完整資料,再將其切分為區塊供 RAG 系統使用。這個範例略有不同:資料雖然已切分成區塊,但切得很差。這些區塊只是從邏輯區塊中隨機截取的片段,實際上必須重新合併。「邏輯區塊」是指本來就應放在一起的一段內容,例如文件的小節或語意連貫的段落。


第一次嘗試處理這些資料時,我們將所有內容放進一次 LLM 呼叫,要求它自行判斷如何分組,再傳回分組後的內容。LLM 應該很擅長這件事,對吧?嗯,對,也不對。
我們在其他幾次經驗中也發現,當你要求完整且精確的內容時,LLM 往往會偷懶,而且不太可靠,尤其是在上下文很長的情況下。這完全可以理解。但對這個特定使用情境來說,這是致命缺陷,因為我們確實需要逐字保留精確內容:不能摘要,也不能略過原始內容的任何部分。任何細節都不能遺漏。
至於「對」的部分,則是它確實非常擅長理解零碎區塊的語意與結構。前提是它不會拒絕逐字引述原文。真是的:/
那麼,我們要如何善用 LLM 的長處,同時避開它不可靠的部分?我們轉而求助老朋友:程式碼(其實就是自訂 Python 函式)。 以及一個「簡單到不能再簡單」的 Pydantic 模型。解決方案如下:
逐一處理各個段落,同時維護目前的邏輯區塊
處理每個段落時,都詢問 LLM:這個段落是否屬於目前的邏輯區塊?依照 Pydantic 模型回答「是」或「否」。
若是,就把該段落附加至區塊;若否,則輸出已完成的目前邏輯區塊,再以該段落建立新區塊。


當然,這種方法使用的權杖比一次處理完整內容稍多,但在這個以精確保留內容為首要目標的特定使用情境中,這筆(小額)額外成本非常值得。
這個解決方案非常簡單,卻遵循了一項重要原則:需要嚴謹處理時,不應完全依賴 LLM,畢竟它們本質上是機率式系統。
透過自訂程式碼/函式與 Pydantic 模型,我們既能充分發揮 LLM 的能力,也能獲得可預測且可靠的結果。
建構生成式 AI 解決方案,既是 AI 挑戰,也是工程挑戰。希望這些範例能啟發你解決自己獨特的挑戰。若想進一步瞭解工程優先的生成式 AI 解決方案,請參閱我們介紹路由器式代理系統設計的文章。