RAG
本文章會分享筆者在參與COSCUP的問答bot的開發經驗。
COSCUP是由台灣開放原始碼社群推動的年度研討會,每年都有眾多志工投入籌備工作。由於COSCUP擁有豐富的文件,但志工們經常需要翻閱過往資料,這為工作增加了負擔。本專案旨在減輕志工的工作量,利用RAG(Retrieval Augmented Generation)技術打造一個AI bot,以便將部分文件查閱工作轉化為與bot互動的形式。
bot使用範例
本文大致分為三個部分
RAG (Retrieval-Augmented Generation)旨在提供LLM生成回答的方法,使其能夠在回答問題時,不僅依賴於模型本身的知識,還可以檢索相關的外部資料,提供更準確且相關性更高的答案。相比單純使用LLM生成答案,RAG能夠有效降低模型的幻覺(hallucination)問題,並提升回答的可靠性。
以下為一個經典的RAG流程
https://github.com/chatchat-space/Langchain-Chatchat
首先需要把文件存入資料庫中,通常會使用向量資料庫(Vector Store),當使用者提出一個問題時,RAG 首先會從大型數據庫中找到與問題相關的文本片段,然後將這些片段作為上下文輸入到生成模型中,生成最終的回答。
其中會有幾個關鍵
由於我們的文件大多是繁體中文,因此LLM選擇breeze-7b。
langchain為一個框架,使用langchain有優點但也有缺點。
優點
能夠非常快速的將基本RAG流程搭建起來,example,並且可以快速的與langsmith結合,langsmith能提供清楚明瞭的log。
缺點
由於高度抽象化導致後續維護變複雜,許多文章也在討論是否需要使用langchain,如這篇文章。
我們採用了三種來源
mattermost為類似slack的通訊軟體
並且支援4種格式: md, txt, docx, pdf
我們在langchain提供的不同loader建立了一個class,能夠指定資料夾並載入支援的檔案格式。
我們只抓github倉庫內的md文件,直接使用GithubFileLoader
。
必須設置token。
https://github.com/settings/tokens?type=beta
本地文件與github repo的性質類似,都是將一個長文檔抓下來。然而,在處理聊天訊息時,我們需要將分散的訊息組合成段落,且聊天訊息的資訊密度通常較低。為了確保內容的準確性,我們選擇抓取特定人物在特定頻道的發言,並向前後各延伸抓取三則相關訊息,以此組合成完整的段落。
訊息會組成如下的對話段落,並存入資料庫內。
抓取mattermost訊息需要設置token,並且只能抓取已加入的頻道。
https://api.mattermost.com/
完整code: https://github.com/COSCUP/polyhistor/blob/main/vectorDB/read_mattermost.py
因為username是額外一個api,所以使用了cache機制讓同樣的id不需要抓第二次username。
Chunking 是指將文件分成多個文字段落並存入資料庫中。由於這些切分後的文字段落會被納入後續的 prompt,因此其長度至關重要。過長的段落可能導致超出 LLM的輸入限制,而過短的段落則可能無法提供足夠的有效信息。
目前chunking方法能分為5個levels:
chunking方法能夠同時採用,意即同一文件可經過不同的chunking方法存在資料庫內
本專案只使用到Document Specific Splitting
單純的使用字符長度切分,並且可設置重疊長度。
在 Character Splitting 上加入分隔符號的判斷,預設會判斷
對不同文檔採用不同的方式,例如: md文件使用header
以下只介紹概念
由於embedding能代表語意資訊,若能以句子為單位,利用前後句的相關性為chunking段落依據,當前後關聯差距過大時,就為分為不同的chunk。
langchain實作: https://python.langchain.com/v0.2/docs/how_to/semantic-chunker/
持續地問LLM現在需不需要分段,甚至是將原文摘要等等。
這會耗費非常多的token。
由於這兩個方法會使用到model,會花費相對多的時間。
Embedding model會將切分好的段落轉變成embedding,接著存入資料庫內。而embedding的好壞會直接影響Retriever的結果,因此選擇一個好的embedding model是非常重要的。
由於我們的文件大多為繁體中文,因此我們從C_MTEB的leaderboard上挑選測試的model:
我們採用的指標為Recall,測試找到多少正確出處,並且為了貼近實際的使用情境,一道問題可能會對應多個出處。
https://en.wikipedia.org/wiki/Precision_and_recall
Result
Model | Recall |
---|---|
dztech/bge-large-zh:v1.5 | 0.702 |
herald/dmeta-embedding-zh | 0.729 |
milkey/m3e | 0.810 |
chevalblanc/acge_text_embedding | 0.837 |
根據測試出來的結果,我們選擇chevalblanc/acge_text_embedding
為embedding model。
我們的測試方法只有驗證文件出處,而非確切的段落,因此有可能會出現檢索到正確的文件,但是是錯誤的段落。
我們採用MultiQuery retriever方法來檢索文檔,在傳統retriever領域中,query expansion與qeury rewriting是非常常用的技術,這兩種技術都是為了提高query的檢索正確性,而現在我們能使用 LLM 來做這個步驟,如下圖,我們能利用 LLM 將一個 query 擴增到數個與原 query 相似的query,保證檢索結果的多樣性。
https://medium.com/@kbdhunga/advanced-rag-multi-query-retriever-approach-ad8cd0ea0f5b
此方法在langchain已經有實作。
docs
我們選擇自己實作,並分為 multiqueryChain 與 answerchain
multiqueryChain
我們定義了一個處理multiqueryChain結果的function
answerChain
接著將兩個chain合併
我們使用的vectorstore為Qdrant
當文檔數量過多時,如果每次更新文件時都需要重新轉成embedding再存入資料庫內,會非常的耗時。因此我們會偵測檔案是否有變動,當文件更新後,我們才會再把文檔切分並存入。
整體流程如下:
我們會利用檔案的路徑與檔案內容的hash值來做是否存入的根據。檔案路徑因為資料庫本身就會存,因此實作相當容易,但hash值計算必須自己實現。
目前只有實現本地文件與github repo的策略,本地文件需要自己計算hash值如下,github有提供SHA可供比對。
特別感謝BOB、小畢、Katy、jimmy與iris對本專案的貢獻。本專案是一個開源項目,歡迎有興趣的朋友加入我們的開發行列。