Go 部落格

使用 Go 構建由 LLM 驅動的應用

Eli Bendersky
2024 年 9 月 12 日

過去一年中,LLMs(大型語言模型)及其相關工具(如嵌入模型)的功能顯著增強,越來越多的開發者正在考慮將 LLMs 整合到其應用中。

由於 LLMs 通常需要專用硬體和大量計算資源,它們最常被打包為提供 API 訪問的網路服務。領先的 LLMs(如 OpenAI 或 Google Gemini)的 API 就是這樣工作的;即使是像 Ollama 這樣的“自託管”LLM 工具,也會將 LLM 封裝在一個 REST API 中供本地使用。此外,在應用中利用 LLMs 的開發者通常需要輔助工具,如向量資料庫,這些工具也最常部署為網路服務。

換句話說,由 LLM 驅動的應用與許多現代雲原生應用非常相似:它們需要對 REST 和 RPC 協議、併發性和效能有出色的支援。這些恰好是 Go 擅長的領域,使其成為編寫由 LLM 驅動的應用的絕佳語言。

這篇博文透過一個簡單的由 LLM 驅動的應用示例來講解如何使用 Go。文章首先描述了演示應用要解決的問題,然後展示了該應用的幾個變體,它們都完成相同的任務,但使用了不同的包來實現。這篇博文演示所需的所有程式碼均可在網上獲取

用於問答的 RAG 伺服器

一種常見的由 LLM 驅動的應用技術是 RAG——檢索增強生成(Retrieval Augmented Generation)。RAG 是為特定領域的互動定製 LLM 知識庫最具可擴充套件性的方法之一。

我們將使用 Go 構建一個 RAG 伺服器。這是一個為使用者提供兩種操作的 HTTP 伺服器

  • 將文件新增到知識庫
  • 就此知識庫向 LLM 提問

在典型的實際場景中,使用者會將文件語料庫新增到伺服器,然後向其提問。例如,公司可以用內部文件填充 RAG 伺服器的知識庫,並利用它為內部使用者提供由 LLM 驅動的問答功能。

下圖展示了我們的伺服器與外部世界的互動

RAG server diagram

除了使用者傳送 HTTP 請求(上述兩種操作)外,伺服器還會與以下物件互動

  • 一個嵌入模型,用於計算提交的文件和使用者問題的向量嵌入
  • 一個向量資料庫,用於高效儲存和檢索嵌入。
  • 一個 LLM,用於基於從知識庫收集的上下文進行提問。

具體來說,伺服器向用戶公開了兩個 HTTP 端點

/add/: POST {"documents": [{"text": "..."}, {"text": "..."}, ...]}: 向伺服器提交一系列文字文件,以新增到其知識庫。對於此請求,伺服器會執行以下操作

  1. 使用嵌入模型計算每個文件的向量嵌入。
  2. 將文件及其向量嵌入儲存在向量資料庫中。

/query/: POST {"content": "..."}: 向伺服器提交問題。對於此請求,伺服器會執行以下操作

  1. 使用嵌入模型計算問題的向量嵌入。
  2. 使用向量資料庫的相似性搜尋功能,在知識資料庫中找到與問題最相關的文件。
  3. 使用簡單的提示工程技術,將步驟 (2) 中找到的最相關文件作為上下文來重新表述問題,並將其傳送給 LLM,然後將 LLM 的答案返回給使用者。

我們的演示使用的服務包括

將這些服務替換為其他等效服務應該非常簡單。事實上,伺服器的第二和第三個變體就是關於這個的!我們將從直接使用這些工具的第一個變體開始。

直接使用 Gemini API 和 Weaviate

Gemini API 和 Weaviate 都提供了便利的 Go SDK(客戶端庫),我們的第一個伺服器變體直接使用了這些庫。此變體的完整程式碼在此目錄中

我們不會在這篇博文中重現全部程式碼,但這裡有一些閱讀時需要記住的注意事項

結構:程式碼結構對於任何用 Go 編寫過 HTTP 伺服器的人來說都會很熟悉。Gemini 和 Weaviate 的客戶端庫會初始化,並將客戶端儲存在一個狀態值中,該狀態值會傳遞給 HTTP 處理器。

路由註冊:使用 Go 1.22 中引入的路由增強功能,設定我們伺服器的 HTTP 路由非常簡單

mux := http.NewServeMux()
mux.HandleFunc("POST /add/", server.addDocumentsHandler)
mux.HandleFunc("POST /query/", server.queryHandler)

併發性:我們伺服器的 HTTP 處理器會透過網路訪問其他服務並等待響應。這對於 Go 來說不是問題,因為每個 HTTP 處理器都在自己的 goroutine 中併發執行。這個 RAG 伺服器可以處理大量併發請求,並且每個處理器的程式碼是線性且同步的。

批次 API:由於 /add/ 請求可能會提供大量文件新增到知識庫,伺服器利用嵌入(embModel.BatchEmbedContents)和 Weaviate 資料庫(rs.wvClient.Batch)的批次 API 來提高效率。

使用 LangChain for Go

我們的第二個 RAG 伺服器變體使用 LangChainGo 完成相同的任務。

LangChain 是一個流行的 Python 框架,用於構建由 LLM 驅動的應用。LangChainGo 是其 Go 等效版本。該框架提供了一些工具,可以將應用構建為模組化元件,並透過一個通用 API 支援許多 LLM 提供商和向量資料庫。這使得開發者可以編寫適用於任何提供商的程式碼,並且可以非常輕鬆地更改提供商。

此變體的完整程式碼在此目錄中。閱讀程式碼時,您會注意到兩點

首先,它比前一個變體要短一些。LangChainGo 負責將向量資料庫的完整 API 封裝在通用介面中,初始化和處理 Weaviate 所需的程式碼更少。

其次,LangChainGo API 使得切換提供商相當容易。假設我們想用另一個向量資料庫替換 Weaviate;在前一個變體中,我們必須重寫所有與向量資料庫互動的程式碼以使用新的 API。有了像 LangChainGo 這樣的框架,我們就不再需要這樣做。只要 LangChainGo 支援我們感興趣的新向量資料庫,我們應該只需替換伺服器中的幾行程式碼,因為所有資料庫都實現了一個通用介面

type VectorStore interface {
    AddDocuments(ctx context.Context, docs []schema.Document, options ...Option) ([]string, error)
    SimilaritySearch(ctx context.Context, query string, numDocuments int, options ...Option) ([]schema.Document, error)
}

使用 Genkit for Go

今年早些時候,谷歌推出了Genkit for Go——一個用於構建由 LLM 驅動的應用的新開源框架。Genkit 與 LangChain 有一些共同特點,但在其他方面有所不同。

與 LangChain 一樣,它提供了可以由不同提供商實現(作為外掛)的通用介面,從而使得在提供商之間切換更加簡單。然而,它並不試圖規定不同的 LLM 元件如何互動;相反,它專注於生產功能,如提示管理和工程,以及整合開發者工具的部署。

我們的第三個 RAG 伺服器變體使用 Genkit for Go 完成相同的任務。其完整程式碼在此目錄中

此變體與 LangChainGo 的變體相當相似——使用用於 LLM、嵌入器和向量資料庫的通用介面,而不是直接的提供商 API,使得在提供商之間切換更加容易。此外,使用 Genkit 將由 LLM 驅動的應用部署到生產環境要容易得多;我們沒有在我們的變體中實現這一點,但如果您感興趣,請隨意閱讀文件

總結 - 使用 Go 構建由 LLM 驅動的應用

本文中的示例僅展示了使用 Go 構建由 LLM 驅動的應用的可能性。它演示了用相對較少的程式碼構建一個強大的 RAG 伺服器有多麼簡單;最重要的是,由於 Go 的一些基本特性,這些示例具備了相當程度的生產就緒性。

與 LLM 服務互動通常意味著向網路服務傳送 REST 或 RPC 請求,等待響應,然後基於響應向其他服務傳送新的請求等等。Go 在所有這些方面都表現出色,為管理併發性和處理複雜網路服務提供了強大的工具。

此外,Go 作為雲原生語言的出色效能和可靠性使其成為實現 LLM 生態系統中更基礎構建塊的自然選擇。例如,可以看看 OllamaLocalAIWeaviateMilvus 等專案。

下一篇文章:別名有何含義?
上一篇文章:分享您使用 Go 開發的反饋
部落格索引