檢索增強生成(Retrieval-Augmented Generation)整理(2)
檢索實作
我們在前一篇整理RAG的理論脈絡,談到提出RAG的時間背景,以及理論架構。在本篇,我要進行實作。
RAG分成檢索、增強、生成三個領域。當時提出的時間點,LLM的上下文還不是很長,生成文字的能力還有待加強。但這些到了2025年顯然已經不再是弱點:各家LLM的上下文能力都在比長的,而生成出來的文字,無論流暢度、一致性和聚焦性,都比當年剛出爐的GPT-3好太多了。因此,增強和生成的功能已經用不太到,甚至…其實因為上下文的窗口太大,即使是新的文件,只要是文字,它也能馬上消化,找出需要的資訊。
所以,其實現在的問題變成資料本身。資料品質的好壞,決定最後檢索的結果。而生成文字,只是基於檢索結果的衍伸,差異不大。
不過,我認為如何讓模型照著我們所希望的方式檢索資料,是除了資料本身之外,另一個值得思考或努力的方向。因為同樣的一筆資料,不同的切入點,可能也會影響檢索的結果。這後面牽涉到領域知識、使用對象、習慣、以及工作流程、文化等等,不一而足。而越貼合實際需求的檢索流程,當然會讓LLM的輸出效果更有效率、更準確。
因此,我這次的實作,主要在檢索上面。基於實驗性質,我並沒有認真去設計貼合一般工作所需的檢索流程,而單純只是測試檢索的理論。甚至,更嚴格說,我也沒有花功夫去真正建立向量空間,或特地設計模型去建立它。我只是希望能依照RAG的精神,跑完檢索的流程。如果大家對這段也有興趣,那就歡迎跟我一起看下去。如果看到這邊,覺得這篇文章不是你要的,那可以跳出去了。
那我們就開始吧。
實作構想
整個流程大約是這樣:
萃取文本 => 分段(chunk) => 文字轉向量、正規化、用模型做索引 => 檢索。
接下來會依據各個部分做比較詳細的說明。
程式是用GPT協作的。裡面有一些不太好的程式寫作習慣,例如在function內巢狀嵌入function,參數互相使用,而內嵌的function在母function裡面也只用一次;或import module在function裡面。總之,是個難以維護,也不乾淨的程式碼。不過,做為一次性的小實驗,幫助理解概念,還是勉強可以的。如果要把這個專案放到正式使用,要重構的地方應該不少。
專案有上傳github,可以看到全部程式碼。
使用資料
其實資料量和分段,是我實作的時候第一個想到的問題。RAG通常是需要一定的文本數量(也要看每個文本的資訊量,但不能太少),而且分段的時候,每個段落的字數也不能太少,至少需要300-500字元(token)。
不過,我實在沒辦法短時間湊到這麼多文本,尤其是要圍繞單一主題。雖然可以用公開資料庫(例如維基百科),但又不想把規模搞太大,一些公開資料集,我並不清楚資料內容。百科全書的資料非常發散,如果要限縮主題,需要再做一些處理,而我只想趕快進到實驗的內容,簡單說就是懶。
所以我最後只挑了一個pdf檔案:來自本院的乳癌治療指引。這是一個pdf檔案,裡面內容格式很多樣化:有流程圖,也有表格、小段落文字等形式。檔案主要參考美國國家癌症整合網路(National Comprehensive Cancer Network,NCCN)所頒布的治療指引修改,所以兩者的檔案形式有些類似。有興趣的讀者可以直接到NCCN下載治療指引,免費註冊、免費下載。
萃取文本
由於pdf檔案裡面摻雜多樣化的格式,單純只用PDF讀取套件的話,會怕沒辦法把所有的資料都有系統的建立關聯,尤其是流程式和表格式,這兩種跟周圍文字有關聯,但並不是直接上下文關係的資料。因此,這邊多使用了OCR (Optical Character Recognization,光學文字辨識)的套件,將流程和表格的資料,連同關聯性(如箭頭、框線)一起截取,再轉成節點。利用擷取到的箭頭串聯節點關係。若是表格,則只將欄內文字轉成節點。
分段
依據段落文字或節點(來自於表格或流程圖)而有不同的處理方式:段落文字以每300-500字元分段,節點因為字數較少,以80-200字元分段。另外還設計了一個中英對照和標註:主要抓治療方式和劑量。不過我後來並沒有建立中英對照的json檔案,所以只能用英文純標註。
每個分段會被整理成一個個巢狀json結構。
段落:頁面、段落、文字
流程圖:文字、頁面、第幾頁的流程圖、節點編號
表格:文字、頁面、第幾頁的表格、節點編號
之後儲存起來。字元化(tokenization)的套件使用openAI開發的tiktoken(我一開始還以為是tiktok開發的= =)。
文字轉向量、正規化、用模型做索引
我使用的是multilingual-e5-base這個多語言模型,他是一個以attention為基礎的自然語言模型,並內建多種語言,所以不同語言的類似意思文字,轉換出來的向量會在空間上接近。這個好處是我不一定要用和資料相同的語言提問。這個模型有個特點:需要額外準備前綴(prefix),以提高搜尋的準確度。我們大部分是加了"passage:"(幾乎全部)。
轉成向量之後還是有做正規化處理。接著,利用FAISS來建立向量索引。FAISS是由meta開發的高維度向量檢索套件。
這樣前置工作就做完了,可以玩檢索了!
檢索
基本上就是調用前面輸出的檔案,以及模型。輸入的提問,利用模型轉成向量,也會經過正規化處理,然後再根據相似度(這邊是用cosine similarity)排序,挑出最高的N個回答。這邊輸出的回答,是片段。我沒有實作後續的生成環節,目的是為了知道搜尋的結果怎麼樣,而不想讓LLM自己潤飾答案,因為LLM本身很可能已擁有我要問的問題相關的知識,這樣最後的輸出會讓我看不出來,到底有沒有檢索到想要的東西。
我簡單問了一個問題:病人如果是ER(+),PR(+),HER-2(-) (Luminal A type),要怎麼治療?本來,使用multilanguage model,就是想要可以用中文問,他還是能找到答案。不過因為結果不理想,我還是使用英文提問。
在rank 3出現了答案:H/T(hormone therapy)。出處來自第五頁的流程圖。回去查原始檔:
確實有的。

乳癌的病人,若有賀爾蒙藥物的受體陽性,第一線治療是抗賀爾蒙藥物。
以上就是RAG檢索的相關實作。儘管在上下文極長,生成和檢索能力已經大幅強化的LLM時代,RAG可能已經不再被當初提出概念的原因所需要,但所衍生出來的檢索技術,我認為在某些場域還是具備應用價值。時至今日,仍然有許多圍繞在RAG相關的研究發表。我相信這不會只是因為捨不得為了現實,放下研究已久的技術,而是他仍然具備價值。有時候,技術的價值不在流行定義,而在人,或人怎麼使用。
謝謝看到這邊的大家。