LLM/RAG Zoomcamp 第一堂筆記:Part 1

這篇筆記是我請 Claude 針對課程第一部分(introduction)所作的翻譯(YouTube 影片 的翻譯)。目的是少打一些字,可以加快自己整理的速度。

比較奇怪的是,原始字幕文字檔中並沒有程式,為什麼翻譯後卻含程式?

到底是 Claude 太聰明,自己去 GitHub 找到對應程式?還是幻覺超譯?等我看完後再為大家解惑。

▌1.1 - LLM 與 RAG 介紹

》LLM Zoomcamp 課程介紹

大家好,歡迎來到我們的課程,這是我們第一個模組的第一個單元。這門課程叫做 LLM Zoomcamp,在這門課程中我們將學習 LLM 的實際應用,特別是我們將把注意力集中在 RAG(檢索增強生成)上。我稍後會談到這些變化的含義以及我們具體要做什麼。

我想先從解釋我們將在整個課程中使用的問題開始。這將是我們貫穿整個課程的實際問題。在我們的 DataTalks.Club 社群中,我們有多門課程,這個 LLM Zoomcamp 是我們的第五門課程。

通常在我們的課程中,我們都有常見問題集(FAQ),這些問題在影片中沒有答案,或者答案不容易找到。我們有這些文件,我快速打開其中一個。

在這些文件中我們有常見問題集,格式是先有一個章節,然後是問題,接著是答案,就是這樣的問答形式。我們有相當多的問題,在這個特定的資料工程 Zoomcamp 文件中,我們有 321 頁這樣的答案。

通常我們要求學生在去 Slack 頻道、群組或課程頻道之前,先檢查這個文件。當然,學生們大部分的問題都能在這裡找到答案,但問題是要找到你需要的資訊並不容易。有 321 個問題,你如何真正找到你需要的資訊呢?這並不簡單。

這就是為什麼我們要使用這些 FAQ 的資料,從三門課程的三個 FAQ 中取得資料,我們將建立一個問答系統。給定一個來自潛在學生的問題,會使用這些 FAQ 文件來回答學生的問題。這就是我們要在課程結束時建立的東西。

它將是一個簡單的表單,你輸入一個問題,然後得到答案。我們將如何做到這一點呢?我們將使用 LLM 和 RAG。什麼是 LLM 和 RAG,這正是我們在這個模組中要討論的內容。

》理解 LLM

LLM 代表大型語言模型(Large Language Model)。我們可以從語言模型開始說起。語言模型是根據你到目前為止輸入的詞語來預測下一個標記、下一個詞語的東西。

想像一下你的手機,你打開 WhatsApp 想要傳訊息給某人,比如說你想傳訊息給你的朋友。你開始輸入「how」空格「i」空格,手機通常會建議你下一個邏輯詞語是「are」,因為通常你輸入的是「how are you」。它識別出「how are」,所以邏輯上下一個詞語是「you」,或者「how are there」,也許你想問「how are the things」。它會建議你通常跟在「how are」後面的常見詞語。

在你的手機上,這對你來說是個人化的,你的手機使用語言模型來做這件事。通常是像樸素貝葉斯(naive Bayes)這樣簡單的東西,只是根據你迄今為止輸入的內容來預測下一個詞語。這些只是簡單的,或者我們可以稱之為小型語言模型。它們沒有很多參數,相當簡單,相較於大型語言模型來說也相當「愚蠢」。

通常的簡單語言模型能夠很好地完成它們的工作,但大型語言模型之所以被稱為「大型」是因為它們很龐大,有大量的參數,數十億、數百億的參數,並且在大量的資料上進行訓練。

它們本質上做的事情是一樣的——根據之前的詞語預測下一個詞語,但它們做這件事的方式讓人感覺像在與人類對話,至少如果你使用 ChatGPT 這樣的東西的話。感覺就像你在與一個智能存在對話,因為它理解你問的問題並給出答案。

但在底層,這是一個擁有大量參數的語言模型,在大量資料上進行訓練。

讓我畫一個 LLM。它會是這樣的一個框。在內部它們使用神經網路,比如 Transformer,但我們實際上不會深入討論這些。在這門課程中,我們不會涵蓋 LLM 背後的理論,我們不會試圖查看 LLM 的內部,我們將把它們視為黑盒子。

對我們來說,這是一個超級聰明的東西,可以理解你問的問題並給出有意義的答案。這就是我們如何使用它的,至於裡面是什麼,對我們在這個特定課程中來說是次要的。

通常 LLM 接收一個輸入,我們談論的是輸入為文字的簡單情況,我們通常稱這個輸入為提示(prompt)。這是進入 LLM 的內容,輸出是某種答案。

提示可能看起來像這樣,例如這是我們在課程中要使用的——一個問題,比如「我如何註冊課程?」然後是基於什麼來回答問題的上下文,會有一些上下文,然後是答案。之後我們可能就留在那裡,所以答案冒號,就是這樣。

記住我談到的語言模型,語言模型做的是完成輸入並找到下一個邏輯術語或下一個邏輯詞語。所以它期望基於上下文,期望對問題有一個答案,或者我們期望它這樣做,所以它會試圖做到這一點。

這是一個提示的例子。現代 LLM 如 ChatGPT,你實際上不需要這樣做,不需要在最後寫「答案」,但這只是一個例子。答案可以是實際的答案,所有跟在這後面的內容。

》探索 RAG

接下來我們要談論的是 RAG。RAG 代表檢索增強生成(Retrieval Augmented Generation)。這裡有兩個有趣的東西:生成和檢索。我們使用檢索來增強生成。

檢索就是搜尋,生成就是 LLM。LLM 生成文字,它們使用搜尋來增強這個文字的生成。這到底意味著什麼呢?

我有幾個例子說明為什麼我們需要 RAG,為什麼我們需要增強我們的生成與搜尋。

我問了 ChatGPT 一個問題:「我如何烹飪鮭魚?」這是一個直接的問題,ChatGPT 給出了非常全面的答案,有很多烹飪鮭魚的方法。這是一個我們不需要搜尋的完美例子——我們有一個問題,LLM 知道答案,LLM 給了我們答案。

但如果我們想問它關於課程的問題會怎樣?我問「我現在加入課程太晚了嗎?」但 LLM 不知道我們在談論什麼課程,什麼時候太晚,這到底意味著什麼。然後它說「我可以幫助你,但我需要知道更多細節」。

我們想要使用搜尋的原因實際上是為了回答這個問題,給它更多資訊,給它更多上下文,這樣 LLM 就能夠給出答案。

這是另一個例子,比如「我如何註冊課程?」它同樣不知道我們問的是什麼,但它只是給出一些無意義的答案。

另一個我們在 ChatGPT 中使用檢索的好例子是我們可以問它查找如何註冊資料工程 Zoomcamp。在這裡我們明確地給它一個指令去執行搜尋,然後基於搜尋的結果給我們答案。這是一個 RAG 的例子,我們用從搜尋中得到的上下文來增強生成,這更類似於我們要建立的東西。

搜尋是 RAG 的第一個組件,我們有某種知識庫,然後另一個組件是實際的 LLM。讓我們說我們有一個用戶,課程的學生,或者只是用戶。用戶有一個問題,比如「我如何註冊課程?」

在 RAG 中我們首先做的是將這個查詢或問題發送到知識庫。知識庫有一些文章,我們的 FAQ 是一個知識庫。它有一些文章,一些答案,一些問題和這些問題的答案。可能有關於如何註冊課程的條目,所以我們檢索這些條目。

讓我們說這些是文件 D1、D2 等等到 D5。讓我們說我們檢索五個文件。這些文件現在有上下文。記住我展示的例子,「我如何註冊課程」或「我現在加入課程太晚了嗎?」LLM 沒有概念,但現在這些文件為 LLM 提供了上下文來找出正確的答案。

現在使用這些文件,我們創建一個提示。我們把這個提示和上下文變成這些文件 D1 到 D5。這些是我們收到的文件,現在我們將這個提示發送到 LLM。

LLM 接收提示,它有問題,它有上下文,現在基於問題和上下文,它可以生成我們返回給用戶的答案。

這就是我所說的 RAG 框架,我們如何將資料庫、知識庫(我們可以稱之為資料庫)和 LLM 一起使用來完成我們想要的事情——如何為我們的查詢添加上下文。

實際上這個資料庫和這個 LLM 可以是任何東西。例如,在課程中,在這個特定模組中,我們首先會使用一個玩具搜尋引擎,一個超級簡單的搜尋引擎來說明這個想法,但稍後在同一個模組中,我們將用 Elasticsearch 替換這個搜尋引擎。

稍後在課程中,我們不僅會使用文字搜尋,還會使用其他搜尋方式,特別是向量搜尋。這裡的東西可以是任何東西,不必是一個特定的技術。在這個框架中,你可以輕鬆地用另一個工具替換它們,看看哪個效果更好。

對於 LLM 也是如此。在這個模組中我們將使用 OpenAI,但不一定要是 OpenAI,你可以使用一些開源的 LLM,你只需要用另一個替換一個。當然你需要調整提示,因為不同的 LLM 期望提示稍微不同。

就是這樣。我們有框架,我們替換或插入一些特定的資料庫,一些特定的 LLM,我們看看它如何工作,然後在課程結束時,我們還會將其放入一些好的 UI 中,比如 Streamlit 或類似的東西。

這就是你要學習的,這就是你要做的,然後在課程結束時,你將在自己的知識庫上實現類似的東西。我真的很期待這門課程,祝你玩得開心!

▌1.2 - 配置你的環境

》介紹

大家好,在這個影片中我想向你展示如何配置你的環境。在這個特定的影片中,我想使用 GitHub Codespaces 來展示如何配置它,但你不必使用它,你可以跟著影片做,在你的本地機器上做所有你需要的事情。

我不會展示如何安裝 Docker,你需要自己做,因為 Codespaces 已經有 Docker,我們將專注於安裝這個特定模組需要的函式庫。

或者你再次如我所說,你不必使用 Codespaces,它是一個非常方便的工具,但你也可以為這個模組使用 Google Colab、Sagemaker Cloud,或任何其他筆記本提供商,或者你可以在本地運行。

對於第二個模組,我們需要 GPU,現在我們不需要 GPU。對於第二個模組我們需要,所以如果你有訪問帶有 GPU 的機器的權限,那很好,你應該使用它。如果你沒有,這是我們將在下一個模組中討論的事情。

現在回到 Codespaces,我將向你展示如何從頭開始設置一個簡單的環境,它是免費的。你應該到你的 GitHub 帳戶創建一個新的儲存庫,給它一些名稱,我將稱之為 llm-zoomcamp。它應該是公開的,因為你會提交你的筆記本,你的作業在這裡,然後你需要在答案表單中分享作業的鏈接。

許可證我們不關心,gitignore 應該是 Python,讓我們創建一個儲存庫。一旦儲存庫創建完成,我們點擊 code,這裡有兩個標籤:Local 和 Codespaces。我只是點擊在線創建一個 codespace。

現在它將在瀏覽器中啟動 Visual Studio Code,所以它正在準備環境。我們不想使用瀏覽器,所以我已經在我的電腦上有 Visual Studio Code 桌面版。如果你想跟著這個教程,你也應該有它。再次,如果你已經有我們稍後要安裝的工具,你不必這樣做。

現在讓我在 Visual Studio Code 桌面版中打開它。我點擊這裡,然後點擊在 Visual Studio Code 桌面版中打開。如果這是你第一次運行 Codespaces,Visual Studio Code 應該在一段時間後要求你為 Codespaces 安裝擴展。如果沒有,你需要手動做這件事。

我已經按 Ctrl+Tilda 打開命令行,這裡我們已經有 Docker 可以運行 hello world,所以 Docker、Docker Compose 已經在這裡,我們有 Python。

》Anaconda 安裝

我將向你展示兩種準備環境的方法。第一種方法是使用我們這裡已經有的 Python,我們需要安裝我們需要的函式庫。然後在影片的第二部分,我將向你展示如何使用 Anaconda 安裝,使用 Anaconda 你不需要安裝任何函式庫,你可以只下載 Anaconda,這就是你需要的全部。

對於安裝函式庫,我們將使用 pip install,然後有一堆我們需要安裝的函式庫:

  • tqdm(這是一個進度管理器,非常方便)
  • jupyter notebook,我將指定版本 7.1.2
  • openai(與 OpenAI API 通信)
  • elasticsearch(我們將首先使用一個簡單的玩具搜尋引擎,但在模組結束時我們將用 Elasticsearch 替換它)
  • scikit-learn
  • pandas

當我們稍後使用 Anaconda 時,我們將需要安裝 tqdm、openai 和 elasticsearch,它已經有 Jupyter,已經有 scikit-learn,已經有 pandas。

》Elasticsearch 配置

當我們安裝時,我們需要有一個 OpenAI 的金鑰,因為我們會使用它的 LLM GPT。我將到 platform.openai.com,你應該在那裡註冊(如果你還沒有的話)。這有點半可選,我們也會展示如何在沒有 OpenAI 的情況下運行它,但為了簡單起見,也為了這個模組,你應該有一個帳戶。

然後我們需要到 API keys,我將創建一個新的 API 金鑰,我將稱之為 course,創建一個秘密金鑰。我當然會在錄製這個影片後刪除這個金鑰,我不希望你使用它。但現在你會看到我的金鑰,但這絕對不應該發生——你不能暴露你的金鑰,提交到 Git 或向不信任的人展示。

現在我們需要取得金鑰,我寫:

export OPENAI_API_KEY=你的金鑰

現在我們在我們的環境中有了金鑰。

》訪問 Jupyter Notebook

我只是啟動 Jupyter notebook。如果你像我一樣在 Codespaces 中運行,你應該注意到這個 ports 標籤得到一個,這意味著它檢測到有一個端口 8888 在 Codespaces 上運行,它自動將這個端口轉發到我們的本地機器,這意味著現在我們可以使用我們的瀏覽器訪問 Jupyter notebook。

我將輸入 localhost:8888,它要求 token,我們可以複製這整個東西或如果端口對你來說不同,你可以複製這個 token。我們有 Jupyter,現在我點擊新建 Python 3,它會自動打開。

我將隱藏這裡,然後我們可以測試事情是否工作,我將 import openai,所以它工作,現在我們可以向 OpenAI 發送一個簡單的請求。

from openai import OpenAI

client = OpenAI()

當我們創建客戶端時,我們可以明確地在這裡提供 API 金鑰,或者如果我們不提供它,它會從我們創建的環境變數中讀取。

我們可以快速檢查我是否真的做了這件事:

import os
os.environ.keys()

我們應該有 openai 金鑰,這是正確的金鑰。我們可以直接放金鑰,或者如果我們設置了這個環境變數,我們不需要做任何事情。

現在我們可以說我們想要使用 chat API:

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user", 
        "content": "is it too late to join the course"
    }]
)

然後我們可以看回復是什麼:

response.choices[0].message.content

這是答案。當然它不知道我指的是什麼課程,因此它提供了這個答案。

》替代環境設置

現在我將向你展示用 Anaconda 準備環境的不同方法,這有點容易。我們到 docs.anaconda 找到 Linux 安裝,我將選擇最新的一個。注意這實際上相當大,幾乎 1GB,因為 Anaconda 中有很多東西,我們坦率地說大部分都不需要,但為了簡單起見,我們只是下載、解包和使用。

我複製鏈接並下載:

wget [anaconda_download_link]

你也可以使用 miniconda 而不是 Anaconda,我也會展示如何做這個。Miniconda 要小得多。

bash Miniconda3-latest-Linux-x86_64.sh

安裝後,當我創建一個新的 shell,我們這裡有 base,意味著我們激活了基礎環境。現在我們可以使用 which python 來看 python 來自哪裡,python 來自 miniconda。

》最後步驟

現在你知道如何準備環境。也許最後一件事,我將向你展示如何提交你的更改。這是我們工作的文件,假設這是你的作業,當然給它一個有意義的名稱。

我們可以創建一個適當的目錄:

mkdir 01-intro

然後把它放在那裡,給它一個好名稱,比如 homework.ipynb。

git add .
git commit -m "homework 1"
git push

現在它在 Git 中,我們已經推送到 Git,我們已經準備好環境。

▌1.3 - 檢索和搜尋

》檢索介紹

歡迎回來,在這個影片中我們要談論檢索。我想先再談論一下 RAG 框架。記住在 RAG 框架中我們有兩個組件:資料庫和 LLM。

這裡對於資料庫,我們將使用一個簡單的搜尋引擎,這個搜尋引擎是我們在其中一個預備課程工作坊中實現的。如果你到課程儲存庫,你可以看到這裡的工作坊「實現搜尋引擎」,有一個影片你可以跟著做,還有一個 GitHub 儲存庫。

在那個工作坊中我們實現了一個搜尋引擎,它是一個非常簡單的記憶體內搜尋引擎,它不是生產就緒的,只是為了說明搜尋引擎如何工作。現在我們要使用這個簡單的搜尋引擎,稍後在模組中我們將用另一個替換它,用 Elasticsearch,但這裡我只想說明原理。

現在我們要做的是取得這個搜尋引擎,取得 FAQ 文件的資料,將資料放入搜尋引擎並執行簡單搜尋。稍後我們將使用結果放入 LLM 並得到我們問題的答案。這是第一步。

》準備文件

我們要使用這個實現,這是 minsearch.py 檔案,實現了搜尋功能。我要下載這個檔案,它不是可以 pip 安裝的,所以我只是下載檔案並將其用作套件。

我已經有我的 Jupyter 在這裡運行,我將開始一個新的筆記本,我將稱之為 rag-intro。第一步我將使用 wget 命令下載這個搜尋引擎實現:

!wget https://raw.githubusercontent.com/alexeygrigorev/minsearch/main/minsearch.py
import minsearch

這是我們實現的搜尋引擎。現在我們要取得 FAQ 文件,我已經解析了它們,所以它們是 JSON 格式。

我們有每個課程的 JSON 物件,在 JSON 物件內部我們有不同的文件,如問題、文件的章節,文字是問題的答案。如果你想知道我如何提取這些文件,有另一個 IPython 筆記本,你可以看到我如何做到的。

這是我用來提取文件的程式碼,這是你稍後可以用於自己專案的東西,如果你也有想要用於提取資料的 Google 文件。

現在我們只是取得這個 JSON 檔案,我們需要載入這個資料並將其放入我們的搜尋引擎:

import json

with open('documents.json', 'rt') as f_in:
    docs_raw = json.load(f_in)

結構如我所說,對於每個課程我們有巢狀結構的文件。現在我想要擺脫這種巢狀性,我只想有一個包含字典的列表,一個包含文件的列表。我想創建,保持這個但只是添加另一個欄位 course:

documents = []

for course in docs_raw:
    for doc in course['documents']:
        doc['course'] = course['course']
        documents.append(doc)

現在我們的文件應該包含有 course、question、section(文件部分所屬)、實際問題和這個問題答案的文件記錄。

》使用 minsearch 函式庫索引文件

現在我們想要索引它,為此在這個 minsearch 函式庫中我們有索引:

from minsearch import Index

index = Index(
    text_fields=["question", "text", "section"],
    keyword_fields=["course"]
)

我們需要指定兩件事:哪些欄位是文字欄位,哪些欄位是關鍵字欄位。對於關鍵字欄位,我們可以進行過濾。例如,如果我們想要將搜尋限制為只有資料工程 Zoomcamp 課程的,這會類似於 SQL 查詢,我們做 WHERE course = 'data-engineering-zoomcamp'

對於文字欄位,這些是我們用於執行搜尋的欄位,這將是 question、text 和 section 欄位。

現在我們創建索引,我們可以用 fit 來擬合:

index.fit(documents)

如果你熟悉 scikit-learn API,它們通常有這個 fit 方法,這正是我們要使用的。現在它已經分析了所有文件,它知道什麼是文字欄位,什麼是關鍵字,所以現在我們可以使用這個搜尋引擎來執行這個查詢。

》為查詢檢索文件

現在讓我們執行搜尋:

q = 'the course has already started can I still enroll'

results = index.search(
    query=q,
    boost={'question': 3.0},
    num_results=5
)

這裡我們有幾個參數:

  • 實際的查詢
  • boost:當我們認為其中一個文字欄位比其他欄位更重要時,我們可以提升這個欄位。我們說如果 question 欄位包含詞語,它比 answer 包含這些詞語更重要。我們給 question 欄位 3 倍的提升。
  • num_results:返回的結果數量

現在我們執行搜尋,這是結果。最相關的文件是「是的,你可以,你將無法提交一些作業」。實際上這正是我們試圖找到答案的同一個問題:「課程已經開始了我還能加入嗎?」稍微不同,因為我們使用了 enroll 而不是 join,但我們仍然能夠檢索到這個文件。

順便說一下,我們注意到這裡我們實際上檢索到了來自另一個 FAQ 的記錄,不是來自資料工程 FAQ。這意味著我們還需要添加一個過濾器,這是這裡的另一個參數:

results = index.search(
    query=q,
    filter_dict={'course': 'data-engineering-zoomcamp'},
    boost={'question': 3.0},
    num_results=5
)

我們可以說我們只對資料工程 Zoomcamp 課程的文件感興趣,只給我關於這個課程的文件。現在結果應該不同,所以現在我們只有資料工程 Zoomcamp 的答案。

再次,第一個答案非常相關:「我還能在開始日期後加入課程嗎?」這就是答案。

現在我們實現了第一步,我們有一個查詢,現在我們已經索引了我們的知識庫,所以現在我們可以向這個知識庫詢問上下文。現在我們認為對於這個查詢,這些是最相關的文件,我們接下來要做的是將這些文件放入 LLM 並將其用作上下文。

我們想要建立一個看起來像這樣的提示,這是我們在下一個影片中要做的。

▌1.4 - 使用 GPT-4o 生成答案

》介紹

歡迎回來,在這個影片中我們將談論生成。之前我們用搜尋引擎索引了我們的文件,現在我們可以使用用戶查詢執行搜尋。讓我快速向你展示我們到目前為止建立的東西。

我們有一個查詢「課程已經開始了我還能註冊嗎?」,從我們的知識庫我們可以檢索答案。現在我們想做的是取得這些來自我們知識庫的潛在文件,可能包含或可能不包含答案的文件,我們想將它們作為用戶問題的上下文放入 LLM。

我們現在需要形成一個提示並將其發送到 OpenAI 或其他 LLM。在這個特定的影片中,我們將使用 OpenAI 作為 LLM,我們將使用 GPT-4o,最新的,它也相當快,與 GPT-4 相比相當便宜。

我應該已經配置了金鑰,所以現在我可以:

from openai import OpenAI

client = OpenAI()

我不需要做任何事情,因為我把金鑰放在環境變數中。

現在我們將再次使用 chat completion API:

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": q
    }]
)

》回應分析

讓我們看看回應是什麼:

response.choices[0].message.content

這是回應,它再次只是給出一些非常通用的答案。現在我們想要取得這個內容上下文,我們從知識庫檢索的答案,並將其放在提示中,希望現在我們的 LLM 將包含上下文的內容並給出更好的答案。

》建立提示模板

我們的提示可以看起來像這樣:

prompt_template = """
You're a course teaching assistant. Answer the question based on the CONTEXT from the FAQ database. 
Use only the facts from the CONTEXT when answering the question.

QUESTION: {question}

CONTEXT:
{context}

If the CONTEXT doesn't contain the answer, output "I don't know".
""".strip()

通常,好的提示工程實踐是我們需要給它一個角色。我們可以說「你是一個課程教學助理」。這是我們分配給 LLM 的角色,通常我們需要為更高級的 LLM 如 GPT-4、GPT-3.5 做這件事。

對於一些較小的 LLM,我們可能不一定需要這樣做。這更像是藝術和科學,因為你只需要做很多實驗,看看什麼對你最有效。首先只是試錯,但稍後在課程中我們還會談論不同的評估指標,所以你實際上能夠數值化評估你的提示表現如何。

》建立上下文

現在讓我們建立上下文:

context = ""

for doc in results:
    context = context + f"section: {doc['section']}\nquestion: {doc['question']}\nanswer: {doc['text']}\n\n"

我從一個空字串開始,我們只是遍歷我們的文件,對於每個文件我們建立內容。

》獲得答案

現在我們可以將所有東西放在一起:

prompt = prompt_template.format(question=q, context=context)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": prompt
    }]
)

print(response.choices[0].message.content)

這裡它說「是的,即使課程已經開始,你仍然可以註冊和完成作業,但要注意最終專案的截止日期」。

》結論

我們剛剛完成的是我們取得了從我們知識庫檢索的文件,這是我們的上下文,基於那個上下文我們建立了一個提示,我們將那個提示發送到我們的 LLM,我們得到了我們返回給用戶的答案。

》探索 OpenAI 的替代方案

我看到人們在 Slack 中問的一個非常常見的問題是我們是否可以使用除了 OpenAI 之外的任何其他東西。是的,我們將在模組二中涵蓋開源模型,但還有其他服務我們可以使用而不是 OpenAI,它們提供非常相似的服務,如 API,你不需要自己託管,你可以只發送一個提示並得到回應,非常類似於 OpenAI。

我想在這個影片中談論替代方案,我想請你提供不同替代方案的列表。我已經在聊天中、在 Slack 中看到一些,讓我們一起建立一個替代方案列表。

為此我在我們的第一模組中創建了一個 markdown 頁面,這是 OpenAI 替代方案。我已經添加了一個 Mistral AI。如果你到 Mistral,有這個 La plateforme,你可以註冊免費試用,他們會給你大約 5 歐元的計劃。

還有許多許多其他像 Mistral 這樣的服務,我們也可以使用而不是 OpenAI。如果出於某些原因你不想註冊創建 OpenAI 帳戶,或者有提供一些免費信用的服務,你可以在課程期間使用,意味著你將能夠免費完成課程。

讓我們在這裡建立一個列表,發送你的拉取請求,你可以自由探索和嘗試其他東西。我已經在 Slack 中看到一些其他平台,我不記得名稱,有人說你也可以使用例如 AWS,我認為服務叫做 Bedrock,他們也有一堆 LLM,然後在線找到免費的 AWS 信用(如 25 美元)相當容易。

所以讓我們建立這樣的列表,我們不必將自己限制在只有 OpenAI。

▌1.5 - RAG 流程:清理和模組化程式碼

》介紹

歡迎回來,在這個影片中我想向你展示如何清理我們到目前為止寫的程式碼,使其模組化,使其更易於使用,然後在我們進行課程時稍後替換不同的組件。

這是 RAG 框架:我們發送查詢到我們的索引,到我們的搜尋(這是一個 minsearch 實現),然後我們建立提示,將其發送到 LLM(在我們的情況下是 GPT-4o),然後得到答案。

這是我們在過去幾個影片中做的實現。所以這裡我們索引一切,我們檢索結果,我們建立提示,然後我們將提示發送到 OpenAI。

現在我想將所有東西放在一起,所以首先我想清理它,使其更模組化,將所有東西放在一起並將其分解為簡單的函數。

》搜尋

首先我將從一個叫做 search 的函數開始:

def search(query):
    results = index.search(
        query=query,
        filter_dict={'course': 'data-engineering-zoomcamp'},
        boost={'question': 3.0},
        num_results=5
    )
    
    return results

這是我們的搜尋函數。讓我快速檢查它是否工作:

search("how do I run kafka")

它檢索一些關於 Kafka 的問題,我們將答案限制為只有資料工程 Zoomcamp 課程。

》建立提示

然後下一個函數是我們想要將這些文件、結果轉換為提示:

def build_prompt(query, search_results):
    prompt_template = """
You're a course teaching assistant. Answer the question based on the CONTEXT from the FAQ database. 
Use only the facts from the CONTEXT when answering the question.

QUESTION: {question}

CONTEXT:
{context}

If the CONTEXT doesn't contain the answer, output "I don't know".
""".strip()

    context = ""
    
    for doc in search_results:
        context = context + f"section: {doc['section']}\nquestion: {doc['question']}\nanswer: {doc['text']}\n\n"
    
    prompt = prompt_template.format(question=query, context=context).strip()
    return prompt

》LLM

現在讓我也把調用 GPT 的邏輯放入:

def llm(prompt):
    response = client.chat.completions.create(
        model='gpt-4o',
        messages=[{"role": "user", "content": prompt}]
    )
    
    return response.choices[0].message.content

》RAG 流程

所以我們輸入用戶查詢,我們執行搜尋,我們從搜尋結果建立提示,我們從 LLM 得到答案,這正是這個流程的實現。

現在我們用函數表達了每個步驟,讓我們嘗試執行:

query = 'how do I enroll the course has just started how do I enroll'

》除錯

讓我們玩一下這個,我稍微改變提示。實際上我們可能需要除錯,讓我們看看搜尋結果是否實際包含問題。

讓我檢查查詢應該是 query 而不是 q:

def build_prompt(query, search_results):
    # ... (固定了變數名稱)
    prompt = prompt_template.format(question=query, context=context).strip()
    return prompt

》RAG 流程函數

現在我們可以將所有東西放入一個簡單的函數:

def rag(query):
    search_results = search(query)
    prompt = build_prompt(query, search_results)
    answer = llm(prompt)
    return answer

現在甚至更簡單,我們只需要:

rag('the course has already started can I still enroll')

現在我們已經將所有東西放在一起,你可以看到現在相當直接地玩不同的組件。所以現在例如如果我們想使用 Elasticsearch 而不是 minsearch,我們需要做的就是用其他東西替換這個函數,我們就會有 Elasticsearch。

或者如果我們想要有不同的 LLM,我們會替換這個函數,我們可能需要稍微改變這個,但現在我們可以將這個 RAG 框架放入程式碼中,現在更容易理解到底發生了什麼,這些函數中的每一個都是獨立的,所以我們可以隨意插拔。

好的,就這樣,在下一個影片中我想向你展示如何用 Elasticsearch 替換 minsearch。

▌1.6 - 使用 Elasticsearch 搜尋

》介紹

歡迎回來,這是 LLM Zoomcamp,在這個影片中我們將用 Elasticsearch 替換我們之前使用的玩具搜尋引擎。

快速回顧我們到目前為止做了什麼:我們有一個知識庫,我們用一個小的函式庫搜尋引擎索引它,現在用用戶查詢我們可以檢索相關文件,建立提示並將這個提示發送到 LLM,LLM 會給我們答案。我們已經有了這個程式碼。

這是我們之前實現的,其中一個東西,像搜尋使用我們在預備課程工作坊中實現的玩具搜尋引擎。現在我們想用適當的搜尋系統,用 Elasticsearch 替換它。

但為什麼我們首先使用這個玩具搜尋引擎呢?你可能想知道,這是因為稍後在課程中我們可能使用像 Colab 這樣的工具,在那裡運行 Elasticsearch 不容易,所以有一些在記憶體中運行並給出好結果的東西是一件好事,另外我們想看看如何實現搜尋引擎。

這就是為什麼我們之前使用它,但現在讓我們用你實際上可能在實踐中使用的真實東西替換它。

》運行 Elasticsearch

首先我們想要運行 Elasticsearch。現在我在我的 Codespaces 中,我有這個運行 Elasticsearch 的 Docker 命令:

docker run -it \
    --rm \
    --name elasticsearch \
    -p 9200:9200 \
    -p 9300:9300 \
    -e "discovery.type=single-node" \
    -e "xpack.security.enabled=false" \
    docker.elastic.co/elasticsearch/elasticsearch:8.4.3

Elasticsearch 將在這兩個端口上運行。現在它將下載 Docker 映像並啟動 Elasticsearch。

我們需要檢查我們需要的端口是否被轉發,我們將使用端口 9200。我將創建另一個終端並執行一個簡單的 HTTP GET 請求到 localhost:9200。

curl localhost:9200

我們看到一個回應,這意味著它工作。

》索引資料

現在讓我們首先索引我們這裡的這些文件。關於 Elasticsearch 的事情是它實際上是持久的,意味著與這個玩具搜尋引擎不同,當我們的機器停止,當我們完成這個 Jupyter notebook 過程時,我們下次啟動時基本上需要重建整個索引。

但當涉及到 Elasticsearch 時,它將所有資料保存在磁碟上,下次我們啟動 Elasticsearch 時,它將擁有我們需要的所有資料。

現在讓我們看看如何從我們的 Jupyter 使用它:

from elasticsearch import Elasticsearch

es_client = Elasticsearch('http://localhost:9200')

我們可以通過執行這個 info 函數檢查事情是否工作:

es_client.info()

好的,它工作,我們可以從我們的 Jupyter 連接到我們在 Docker 中運行的 Elasticsearch。

現在我們需要創建一個索引。Elasticsearch 中的索引就像關係資料庫中的表,它與我們之前用 minsearch 創建的非常相似。通常語法相當複雜,所以我已經準備了這個查詢:

index_settings = {
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 0
    },
    "mappings": {
        "properties": {
            "text": {"type": "text"},
            "section": {"type": "text"},
            "question": {"type": "text"},
            "course": {"type": "keyword"} 
        }
    }
}

這裡有趣的是屬性,course 是 keyword,我們將進行過濾,其餘的是 text 類型。這與我們用 minsearch 做的非常相似——前三個是 text,course 是 keyword。

現在我們需要給索引一個名稱:

index_name = "course-questions"

es_client.indices.create(index=index_name, body=index_settings)

索引被創建了,我們現在可以用這個索引索引資料。方法是我們使用 Elasticsearch 客戶端,然後我們有 index 函數:

from tqdm.auto import tqdm

for doc in tqdm(documents):
    es_client.index(index=index_name, document=doc)

我們基本上需要遍歷我們擁有的所有文件,對於每個文件用 Elasticsearch 索引它。我也喜歡添加進度條,這個操作可能需要一些時間。

》查詢資料

現在我們可以查詢這個資料,查詢我必須提前準備,因為它相當複雜:

def elastic_search(query):
    search_query = {
        "size": 5,
        "query": {
            "bool": {
                "must": {
                    "multi_match": {
                        "query": query,
                        "fields": ["question^3", "text", "section"],
                        "type": "best_fields"
                    }
                },
                "filter": {
                    "term": {
                        "course": "data-engineering-zoomcamp"
                    }
                }
            }
        }
    }
    
    response = es_client.search(index=index_name, body=search_query)
    
    result_docs = []
    for hit in response['hits']['hits']:
        result_docs.append(hit['_source'])
    
    return result_docs

讓我解釋這裡發生什麼。有兩個組件:

  1. 過濾組件:與我們之前做的非常相似,就像這個 SELECT 查詢,其中 course 是 data-engineering-zoomcamp
  2. 文字匹配:注意這個 ^3,意味著 question 比 text 或 section 欄位重要 3 倍

大小是 5,所以我們只會得到 5 個答案。

這是我們發送到 Elasticsearch 的搜尋查詢,我們使用客戶端的 search 方法,我們需要指定索引名稱和查詢。

結果被隱藏在很多巢狀結構下,我們需要先到 hits,然後在 hits 裡面我們需要再次有 hits,對於每個 hit 我們實際上需要進入並到達 source。

》更新 RAG 流程

現在讓我們將這個放入函數中:

def elastic_search(query):
    # ... (上面的實現)
    return result_docs

讓我們快速測試它:

elastic_search("the course has already started can I still enroll")

我們得到一些結果,現在讓我們快速調整我們的 RAG 函數:

def rag(query):
    search_results = elastic_search(query)
    prompt = build_prompt(query, search_results)
    answer = llm(prompt)
    return answer

我只是用 elastic_search 替換 search,現在當我執行 RAG 時,我們應該經過相同的 RAG 流程並得到答案。

這就是我們如何替換一個組件,我們用適當的搜尋引擎替換我們的玩具搜尋引擎,你可以看到這是高度模組化的,所以我們可以輕鬆地交換所有組件。稍後在課程中我們還會向你展示如何用開源的替換我們使用的這個 OpenAI LLM。

好的,這個影片就到這裡,我們可能會有一個影片展示如何在本地運行它,很可能我們會把它留到下週。祝你玩得開心,再見!