自訂 RAG 解決方案的實用範例

實際案例展示量身設計的檢索增強生成系統,如何解決複雜的企業知識問題。

近年 RAG 有時聲名不佳:有人認為它極其簡單(入門確實如此,但要擴展規模就沒那麼容易);也有人認為它已被「代理式系統」取代(但不少這類系統稍加細看,很快便會發現與 RAG 十分相似……)。

本文透過一兩個實例,說明我們如何應對以下常見挑戰:

  • 處理文字與數值混合的資料,以及這類資料為何令簡單的 RAG 失效:關鍵字互相重疊,而數字本身不帶語義。

  • 為何以摘要優先的嵌入設計有幫助:先為每個區塊產生簡短的描述性摘要,再以該摘要進行嵌入和查詢。

  • 如何產生情境摘要:加入上層文件的脈絡,避免形態相近的統計資料互相混淆。

  • 何時依靠程式碼和 Pydantic 模型:如需保留逐字內容,便應結合自訂程式碼及/或 Pydantic 模型與 LLM 呼叫,以提高可靠性。

建構自訂 RAG 解決方案

基本原理

從客戶支援機械人到內部知識助理,各類工具都由 RAG 系統驅動。

其背後一般包含以下步驟:

  1. 將來源文件分割成區塊

  2. 將每個區塊嵌入向量空間

  3. 查詢時檢索排名最高的 K 個區塊

  4. 根據這些區塊產生答案

LangChain、LlamaIndex、OpenAI Filestore 等熱門工具套件,令這些步驟變得近乎輕而易舉。但在實際管線中,資料不會只有密集文字,而基本的 RAG 可能難以應付其他資料。以下各節會展示資料挑戰的具體例子,並隨複雜程度增加,逐步建構解決方案。

情況變得棘手時

  1. 當資料不只有文字時(其實十分常見)

以遊戲情境中的以下資料區塊為例:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

嵌入之所以有效,是因為模型透過語義和文法,學會了詞語之間的關係。上述資料混合了文字和數字;一旦離開這個特定情境,數字與詞語之間便沒有任何關係。因此,這個資料區塊基本上只是一些略帶描述性的詞語, 後面接着若干隨機數字。

如果我們只有這一類資料,其實不成問題,因為仍可利用少數描述性詞語的嵌入 進行檢索(或直接使用 text-to-SQL)。然而,如果這個區塊埋藏在大量文字密集的區塊之中,而那些區塊也出現相同詞語,又會怎樣?例如:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

現在假設我們想檢索「啟用 Draconic Ascension 後的攻擊距離是多少?」我們很可能無法檢索到所需的相關區塊,因為它被淹沒在其他含有相同關鍵字的區塊之中。

問題根源在於,我們無法有效區分這些資料區塊,儘管它們包含同一主題下不同類型的資訊。我們能否以某種方式 豐富或強化資料?當然可以:smile:

  1. 透過摘要豐富資料,沒錯,你沒有看錯

我們可先產生摘要來描述資料內容,而非直接嵌入區塊本身,然後以摘要進行嵌入和檢索。到產生答案時,我們仍會使用與摘要連結的原始資料。

因此,我們會為上述兩個區塊範例產生以下摘要:

  1. 攻擊距離、速度及傷害的統計資料(預設狀態及啟用 Draconic Ascension 後)。

  2. Draconic Ascension 的說明及詳情,包括啟動條件、視覺效果和背景故事。

然後,我們也會擴充查詢,使其與摘要「對齊」。例如,把「啟用 Draconic Ascension 後的攻擊距離是多少?」改成「啟用 Draconic Ascension 後,攻擊距離的 統計數據是多少?」當檢索查詢來自技術領域以外的使用者,並以~~「自由發揮」的~~一般自然語言提問時,這一點尤其重要。畢竟,他們既不知道也不在意 RAG 如何運作,才能盡量提高精確率和召回率。

展示情況何時會變得更棘手的圖表。

  1. 不要斷章取義(人生一般也適用)

接下來的情境,是要處理大量看起來相同的資料區塊,例如:

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

如果沿用同一方法,試想查詢「角色 X 的攻擊距離是多少?」由於剛才產生的摘要看起來也十分相似,我們只能靠運氣猜測。那麼,如何區分它們?

答案很簡單:提供脈絡。我們只需在資料區塊中加入其上層文件的參照,例如本例的 {”角色”: “X”}。這樣,即使也有角色 Y 和 Z 的相同資料,我們仍能準確檢索角色 X 的正確資料。

不過,更理想且更具通用性的方法,是為區塊產生情境摘要。換言之,我們不只為 資料區塊本身產生摘要,而是同時傳入其上層文件和區塊, 以產生整體情境摘要,並在摘要中說明該區塊與 上層文件的關係,例如:

  1. 此區塊提供角色 X 的……詳細統計資料。此區塊顯示 X 在攻擊速度方面的優勢,並以此連繫完整文件……

  2. 此區塊提供角色 Y 的……詳細統計資料。此區塊顯示 Y 使用特殊能力後提升的數值,並以此連繫完整文件……

  3. 此區塊提供角色 Z 的……詳細統計資料。此區塊顯示 Z 的數值十分適合在團隊 對戰中擔任坦克,並以此連繫完整文件……

對上述例子而言,這種方法(部分靈感來自 Anthropic)可能顯得過於複雜,但對於脫離脈絡後容易被誤解的區塊卻非常有效。此外,它提供適用於所有區塊的統一方法,令工程管線保持整潔。

展示情況何時會變得更棘手的圖表。

  1. 需要做到~~事事控制~~嚴謹時

我們通常會取得完整資料,再將其分割成區塊供 RAG 系統使用。這個例子則略有不同:資料已被分割,但區塊劃分不當。它們只是從邏輯區塊隨機截取的部分,實際上需要重新組合。邏輯區塊是指本應自然歸在一起的內容,例如文件的一個小節或意思連貫的段落。

展示情況何時會變得更棘手的圖表。

最初,我們把所有資料送入一次 LLM 呼叫,要求它按理解分組,再傳回分組後的內容。LLM 應該很擅長這項工作,對吧?答案是既對,也不對。

我們在其他幾次經驗中也發現,當要求完整而精確的內容時,LLM 往往會敷衍處理,表現並不可靠,尤其是在脈絡很長的情況下。這其實完全合理。但對這個特定使用案例而言,這是無法接受的,因為我們確實需要逐字保留準確內容,不能摘要,也不能遺漏原文任何部分。任何細節都不能遺漏。

至於「對」的部分,則是它確實能出色地理解零碎區塊的語義和結構。前提是它願意逐字傳回內容。真麻煩:/

那麼,如何善用 LLM 的長處,同時避開它不可靠的部分?我們請來了可靠的老朋友:程式碼(也就是自訂 Python 函數)。 再加上一個「簡單得不能再簡單」的 Pydantic 模型。解決方案如下:

  • 逐一處理各個段落,同時維護目前的邏輯區塊

  • 處理每個段落時向 LLM 詢問:此段落是否屬於目前的邏輯區塊?按照 Pydantic 模型回答「是」或「否」。

  • 如回答「是」,便將該段落附加至區塊;如回答「否」,則輸出已完成的目前邏輯區塊,再以該段落建立新區塊。

展示情況何時會變得更棘手的圖表。

相比一次處理全部內容,這種做法當然會多用一些 token;但對這個以準確保留內容為首要目標的特定使用案例而言,少量額外成本非常值得。

這個解決方案非常簡單,卻遵循一項重要原則:需要嚴謹結果時,不應只依賴 LLM,畢竟它們本質上是概率模型。

利用自訂程式碼或函數及 Pydantic 模型,既可充分發揮 LLM 的能力,也能取得可預測而可靠的結果。

總結

建構生成式 AI 解決方案,既是工程挑戰,也是 AI 挑戰。希望這些例子能啟發你應對自己的獨特挑戰。如想進一步了解工程優先的生成式 AI 解決方案,請參閱有關路由器式代理系統設計的網誌文章。

作者

Cynthia Yu