請啟用 JavaScript 來查看內容

使用 RAG 打造個人知識庫,LlamaIndex 搭配 Gemini 模型

    前言

    現在各式 LLM (大型語言模型) 滿天飛,你有沒有想過讓模型可以參考你自己的資料來回答呢?
    就像一個屬於你的 AI 知識庫管家,有問必答,還會標示引用文件的來源,讓你還可以回去原始文件參考。

    今天要來帶大家使用 Python 中的 LlamaIndex 套件,搭配免費的 Gemini 模型 API,打造屬於你自己的 RAG (Retrieval Augmented Generation) 個人知識庫✨。

    文章前半部會先介紹 RAG 架構 (什麼是 RAG、RAG 流程),後半部再來使用 LlamaIndex 套件搭配 Gemini API 實際撰寫 RAG 架構程式碼。

    LlamaIndex
    LlamaIndex

    什麼是 RAG?

    RAG (Retrieval Augmented Generation,檢索增強生成) 是一種結合檢索搜尋和生成的自然語言處理架構。讓 LLM (大型語言模型)在生成回覆以前,從外部知識庫搜尋相關資料,並使用這些資訊生成更準確、更全面的答案。

    純粹的語言模型僅依賴於訓練語料,而 RAG 則能在生成時動態地加入額外知識,生成更準確、更相關、更具可靠性的輸出。


    來一個簡單的比喻,更容易理解:

    想像你是一位學生,要寫一篇關於台灣歷史的報告。

    一般 LLM 就像一個沒有外部資源的學生,只能憑藉自己的記憶和理解來寫報告,這可能會導致報告內容不準確、不全面,甚至出現錯誤。

    而 RAG 架構的學生,會先去圖書館查閱資料,會找到一些有關台灣歷史的資料,然後用這幾個相關資料來彙總、寫報告,這樣可以大大提高報告的準確性和全面性 (而且還能看到資料是從哪本書來的)。


    RAG 流程

    RAG 模型的架構主要可分成以下兩個階段:

    1. 索引 (Indexing):事前準備。從來源取得資料,並為其建立索引後儲存起來。
    2. 檢索和生成 (Retrieval and generation):使用者發問後,會從索引中搜尋相關資料,然後將其傳遞給模型並生成回覆。

    對應我下方自己畫的流程圖,灰色部分(A~C)就是「索引」、藍色部分(1~5) 就是「檢索和生成」。


    RAG 流程
    RAG 流程

    索引 (Indexing) 階段

    • A:將我們準備好的資料 (有可能是 公司內部文件、產品常見問答集、專業文獻、特定領域的專業知識 等等) 切分成較小的區塊(chunk)。因為區塊內容太多較難搜尋,而且 LLM 模型也有字數限制。
    • B:將區塊的內容透過 Embedding model 轉換成向量(一組數字),因為後續能比對資料間的向量距離,來判斷其相似程度。
    • C:將轉換好的向量 (連同原始資料) 儲存至向量資料庫。

    此流程只需要做一次就好,或者當原始資料有新增、更新時再執行即可。

    索引 (Indexing) 階段 流程 (圖片來源:LangChain 官網)
    索引 (Indexing) 階段 流程 (圖片來源:LangChain 官網)

    檢索和生成 (Retrieval and generation) 階段

    • 1:將使用者的問題一樣透過 Embedding model 轉換成向量。
    • 2、3:拿問題向量去向量資料庫中找出相關的區塊。
    • 4、5:將 "相關區塊" 與 "使用者的問題" 一起給 LLM 生成答案。
    索引 (Indexing) 階段 流程 (圖片來源:LangChain 官網)
    索引 (Indexing) 階段 流程 (圖片來源:LangChain 官網)

    使用 Promt Enginerring (提示工程) 或 Fine-tuning (微調) 不行嗎?

    • Promt Enginerring (提示工程):
      透過一些提示(Promt)技巧來引導模型,輸出較高品質、正確性的結果。
      例如:角色扮演、將複雜的任務拆分為更簡單的子任務、一步步思考……等等。

    • Fine-tuning (微調):
      在已經預訓練過(pre-trained)的模型基礎上,用自己準備的資料進一步訓練,讓模型的輸出能夠更符合我們的預期。
      例如:特定口吻、風格、格式、或加入自己的資料。


    RAG 相較於「Fine-tuning (微調)」或「Promt Enginerring (提示工程)」有幾點優勢:

    • 節省成本:不用像 Fine-tuning 需要訓練模型,節省時間成本、金錢成本(和硬體成本)。
    • 資料即時性:Fine-tuning 需要訓練模型,很難即時更新資料。
    • 資訊正確性:RAG 會參照外部資訊,有效減少虛假訊息,使得回答更加可信、準確。
    • 可信任性:RAG 回答可引用資訊來源,讓使用者可以審核答案,透明度高。
    • 安全、隱私性較高:RAG 是查找自己向量資料庫的知識,存取權限方面比較容易管控。相較之下,Fine-tuning 的較容易外洩。

    不過其實也不是只能選一種,你可以結合其中兩種、甚至三種,來讓模型達到更好、更符合自己的生成結果。


    什麼是 LlamaIndex?

    LlamaIndex 是一個資料框架,提供了抽象元件,可以更容易、更安全的使用 "資料提取"、"索引"、"查詢" 等功能。
    適用於基於 LLM 的應用程序,主要是 RAG 架構 (也就是我們上面介紹的)。

    LlamaIndex 支援 Python 和 Typescript,本篇文章會使用 Python 來示範,不過概念上是一樣的。

    LlamaIndex
    LlamaIndex

    撰寫程式

    需要先安裝 LlamaIndex 套件本身,以及我們要使用的 Gemini (LlamaIndex 已經有包好的介面可用):

    * 後來發現 Gemini 的 Embedding 不支援中文…所以改用 OpenAI 的 Embedding,因此最後的程式碼不需要 llama-index-embeddings-gemini,可不用安裝。

    1
    2
    
    pip install llama-index
    pip install llama-index-llms-gemini llama-index-embeddings-gemini llama-index-embeddings-openai
    

    LLM 和 Embedding model

    在開始建構 RAG 架構之前,我們先來分別試一下 LLMEmbedding model 是否都正常。

    * Gemini 各種模型介紹與限制,可以看這篇官方的說明頁面:https://ai.google.dev/models/gemini
    * 還沒有取得 Gemini API key 的人,可以參考我之前寫的文章:如何使用 Google 的 Gemini 模型 API?
    * Gemini API Key 可以像官方說明設定在環境變數中(GOOGLE_API_KEY),或者我這邊省麻煩直接寫在程式碼裡 (但就要注意不要不小心外洩了)。


    首先來試試 Gemini 本身 LLM,基本的 Complete 和 Chat 使用:

    * 以下參考官方提供的範例:https://github.com/run-llama/llama_index/blob/main/docs/docs/examples/llm/gemini.ipynb

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    from llama_index.llms.gemini import Gemini
    from llama_index.core.llms import ChatMessage
    
    
    GEMINI_API = "<YOUR_GEMINI_API>"
    
    # Complete
    response = Gemini(api_key=GEMINI_API).complete("誰是王建民?")
    print(response)
    # 王建民,是一名台灣職業棒球投手。他曾效力於美國職棒大聯盟(MLB)的紐約洋基隊、華盛頓國民隊......
    
    # 如果想要改用最新的 gemini 1.5 pro 或調整參數,可以像這樣設定:
    # response = Gemini(
    #     api_key=GEMINI_API,
    #     model_name="models/gemini-1.5-pro-latest",
    #     temperature=0.6,
    #     max_tokens=1024
    # ).complete("誰是王建民?")
    
    # Chat
    messages = [
        ChatMessage(role="user", content="誰是王建民?"),
        ChatMessage(role="assistant", content="王建民是臺灣職業棒球運動員,擔任投手,在美國職棒大聯盟9個球季累計68勝。"),
        ChatMessage(role="user", content="他是哪裡人?"),
    ]
    response = Gemini(api_key=GEMINI_API).chat(messages)
    print(response)
    # assistant: 臺南市   王建民於1980年3月31日出生於臺灣臺南市。
    
    # Streaming complete (串流的方式)
    response = Gemini(api_key=GEMINI_API).stream_complete("誰是王建民?")
    for r in response:
        print(r.text, end="")
    

    再來試試 Gemini 的 Embedding,將句子轉換為向量:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    # Embedding
    from llama_index.embeddings.gemini import GeminiEmbedding
    
    
    GEMINI_API = "<YOUR_GEMINI_API>"
    
    embed_model = GeminiEmbedding(api_key=GEMINI_API, model_name="models/embedding-001")
    # 或者想使用最新的模型
    # embed_model = GeminiEmbedding(api_key=GEMINI_API, model_name="models/text-embedding-004")
    embeddings = embed_model.get_text_embedding("『IT空間』是一個有關基礎 IT 技術教學與分享的部落格。")
    print(embeddings)
    # [0.042481236, 9.7126205e-05, -0.06401828, -0.03270765, 0.066297136, -0.019318093, ......
    

    沒問題~ 測試都正常。 (← 真的嗎…立旗)


    RAG

    一開始,先給大家看完整程式碼。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    import os.path
    from llama_index.core import (
        Settings,
        VectorStoreIndex,
        SimpleDirectoryReader,
        StorageContext,
        load_index_from_storage,
    )
    from llama_index.llms.gemini import Gemini
    # from llama_index.embeddings.gemini import GeminiEmbedding
    from llama_index.embeddings.openai import OpenAIEmbedding
    
    
    PERSIST_DIR = "./storage"
    GEMINI_API = "<YOUR_GEMINI_API>"
    OPENAI_API = "<YOUR_OPENAI_API>"
    
    # 更改全域配置
    # Settings.embed_model = GeminiEmbedding(model_name="models/embedding-001", api_key=GEMINI_API)
    Settings.embed_model = OpenAIEmbedding(api_key=OPENAI_API)
    Settings.llm = Gemini(api_key=GEMINI_API)
    
    
    if not os.path.exists(PERSIST_DIR):
        documents = SimpleDirectoryReader("docs").load_data()
        index = VectorStoreIndex.from_documents(documents)
        index.storage_context.persist(persist_dir=PERSIST_DIR)
    else:
        storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
        index = load_index_from_storage(storage_context)
    
    query_engine = index.as_query_engine()
    response = query_engine.query("動畫《葬送的芙莉蓮》中的芙莉蓮角色是由哪位聲優配音?")
    # response = query_engine.query("葬送的芙莉蓮是什麼?")
    print(response)
    

    這程式主要的部分是從官方文件來的:Starter Tutorial (OpenAI)

    可以看到,其實透過 LlamaIndex 框架來實作 RAG 架構,根本不用幾行程式碼。
    而且如果你是使用 OpenAI API,上方 Gemini 的部分甚至可以省略 (它預設是 OpenAI API)。還有中間 else 的部分也可以省略 (只是變成你每次執行都還要再 Embedding),所以實際上所需的程式碼更少。



    首先,我們先準備好原始資料,並放到 docs 資料夾內(這名稱是在程式碼內自訂的)。

    我就拿我最近在看、而且前陣子很火紅的日本動畫《葬送的芙莉蓮》當範例。
    我們去維基百科複製 葬送的芙莉蓮 的內容,並存到 docs 資料夾底下的 frieren.txt

    rag
     ├ docs
     │  └ frieren.txt
     │
     └ llamaindex_rag.py
    

    1
    2
    3
    4
    
    # 更改全域配置
    # Settings.embed_model = GeminiEmbedding(model_name="models/embedding-001", api_key=GEMINI_API)
    Settings.embed_model = OpenAIEmbedding(api_key=OPENAI_API)
    Settings.llm = Gemini(api_key=GEMINI_API)
    

    如同剛剛說的,LlamaIndex 框架預設的 LLM、Embedding model 都是採用 OpenAI API,如果你想要改變全域的配置,需透過 Settings 對象


    這邊我原本是打算 LLM 和 Embedding model 都使用 Gemini 的,結果我實際跑起來發覺結果很怪,每次找出來最相近的段落都是那兩個,網路查了才發現,Gemini 的 embedding-001 好像不支援中文……
    而且就算我改嘗試前幾天 Google 發布的新一代文字嵌入模型 text-embedding-004,也還是不行。

    沒辦法,最後在 Embedding 的部分還是改回使用 OpenAI 的 text-embedding-ada-002,所以也才同時需要 Gemini 和 OpenAI 的 API Key。

    * OpenAI 的 API key 申請可以參考我之前的文章 (OpenAI ChatGPT API 如何使用?(附上 Python 範例程式)),但因為已經過一年了,實際介面與步驟可能有點不太一樣。



     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    PERSIST_DIR = "./storage"
    
    ...
    
    if not os.path.exists(PERSIST_DIR):
        documents = SimpleDirectoryReader("docs").load_data()
        index = VectorStoreIndex.from_documents(documents)
        index.storage_context.persist(persist_dir=PERSIST_DIR)
    else:
        storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
        index = load_index_from_storage(storage_context)
    

    這邊的 if-else 是在第一次執行會去讀取 docs 資料夾下所有的檔案,將內容切割後做 Embedding (轉向量),並把結果儲存下來,避免每次執行都要花時間、花金錢做 Embedding。

    第二次之後執行,如果 ./storage 資料夾存在,我們就當已經做過 Embedding,直接從 ./storage 讀取向量資料。

    因此如果我們有修改、新增原始資料,需要把 ./storage 資料夾刪除,讓它再重新 Embedding 一次。


    1
    2
    3
    4
    
    query_engine = index.as_query_engine()
    response = query_engine.query("動畫《葬送的芙莉蓮》中的芙莉蓮角色是由哪位聲優配音?")
    # response = query_engine.query("葬送的芙莉蓮是什麼?")
    print(response)
    

    從原始資料讀取並轉換成向量之後,我們就可以開始詢問它問題。

    例如我問它「動畫《葬送的芙莉蓮》中的芙莉蓮角色是由哪位聲優配音?」,他會回答「種崎敦美」✨。
    非常正確~~🎉


    結語

    以上各個步驟的元件都有很多不同的可以替換,像是 LLM 可以換成 Azure OpenAI、Anthropic 的 Claude 3 Opus、生成超快的 Groq、本地跑 Ollama、甚至自訂 LLM 模型……等等 (詳細支援列表)。還有文件載入方面,也可以連接 PDF、CSV、HTML、Google Docs 等等多種來源。

    以上資訊可以在 官方文件 ExamplesLlamaHub 裡面找到。


    學習完最基礎的範例後,各位可以從官方文件的 常見問題 (FAQ) 來針對你感興趣的部分,進一步學習、嘗試。或者先去看看官方文件的 Learn 有針對以上不同的步驟做詳細的說明。


    我後來整理一篇「Query Transformations 技巧幫助 RAG 優化問題,提升檢索效果」。

    有時候我們的問題太廣、太模糊,
    導致找不出相關資料,甚至找到錯誤的資料,造成回應結果不靠譜。
    有一種做法是將問題先請 LLM 做 “改寫”,再送進去檢索,那就是「查詢轉換 (Query Transformations)」技巧。



    如果對於 生成式 AI 有興趣的讀者,記得追蹤『IT空間』FB 粉專,才不會錯過最新的發文通知呦~🔔




    如果想的太多,就會做的很少


    🔻 如果覺得喜歡,歡迎在下方獎勵我 5 個讚~
    分享

    Jia
    作者
    Jia
    軟體工程師 - Software Engineer