檢索增強生成 (RAG)

Firebase Genkit 提供抽象化技術,協助打造受檢索使用者的系統 (RAG) 流程,以及提供相關工具整合的外掛程式。

什麼是 RAG?

「檢索增強生成」是一種將外部 IP 位址 將資訊來源擷取到 LLM 的回覆重點在於 因為 LLM 通常 需要特定領域知識才能實際使用大型語言模型 例如,「大型語言模型」有關您的 公司產品)。

其中一種解決方法是使用更具體的資料微調模型,不過, 可能昂貴的運算成本和所需工作 準備足夠的訓練資料

相反地,RAG 會在 傳送到模型的時間比如說,你可以想像 「小巴與 Lisa 的關係是什麼?」可能是展開 (「擴增」) 前面加上一些相關資訊,導致使用者看到「家鄉和 瑪姬的孩子分別命名為 Bart、Lisa 和 Maggie。什麼是 Bart 的關係 給 Lisa ?」

這種方法有幾個優點:

  • 比較有效率,因為您不需要重新訓練模型。
  • 您可以持續更新資料來源,LLM 就能立即 更新資訊的使用方式
  • 現在,您可以在 LLM 的回覆中引用參考資料。

另一方面,使用 RAG 也能自然產生較長的提示,而部分 LLM API 服務則會根據您傳送的每個輸入符記付費。最後,您必須評估 進而節省應用程式的成本

RAG 非常廣泛,可以利用許多不同技術來達成 這裡是最高品質的 RAGGenkit 架構提供兩大抽象層 如何進行 RAG:

  • 索引器:將文件加入「索引」。
  • 嵌入器:將文件轉換為向量表示法
  • 擷取:針對特定查詢,從「索引」擷取文件。

這些定義非常廣泛,因為 Genkit 不會有人對 何謂「索引」或如何從中擷取文件僅限 Genkit 提供 Document 格式,其他則是由擷取器或 索引實作提供者。

索引器

索引負責追蹤文件 快速擷取特定查詢的相關文件大部分 通常透過向量資料庫建立文件索引 稱為嵌入的多維度向量嵌入文字 (不透明) 以文字段落表示的概念;然後根據模型 運用特殊用途的機器學習模型運用嵌入功能建立文字索引 資料庫能將概念上相關的文字叢集 與新的文字字串 (查詢) 相關。

如要擷取用於生成文件的文件,您必須完成以下工作: 將這些檔案擷取至文件索引典型的擷取流程 包括:

  1. 將大型文件分割成多個小文件,只顧及需求 部分則會用於增強提示,也就是「分塊」此為必要步驟 因為很多 LLM 的脈絡窗口有限 透過提示收錄整份文件

    Genkit 不提供內建的分塊程式庫。不過 並與 Genkit 相容的原始碼程式庫

  2. 為每個區塊產生嵌入。視您使用的資料庫而定 您也可以明確使用嵌入生成模型 可以使用資料庫提供的嵌入產生器

  3. 將文字區塊及其索引新增至資料庫。

您可能會不常執行擷取流程,或只在工作時執行一次 並選用可靠的資料來源另一方面,如果您在處理資料 經常變更,則可以持續執行 例如在 Cloud Firestore 觸發條件中更新文件時)。

嵌入程式

嵌入程式會擷取內容 (文字、圖像、音訊等) 並建立數字向量,藉此對原始內容的語意含義進行編碼。如前所述,系統在建立索引的過程中會使用嵌入器,但也可以單獨用於建立沒有索引的嵌入。

擷取

擷取器是一種概念,會封裝與任何類型文件相關的邏輯 和擷取。最常見的擷取案例通常包含擷取自 但在 Genkit 中,可傳回資料的函式可以是擷取器。

如要建立擷取器,您可以使用所提供的其中一種實作項目,或 您可以自行建立

支援的索引器、擷取器和嵌入器

Genkit 透過外掛程式系統提供索引和擷取器支援。 已正式支援下列外掛程式:

此外,Genkit 透過預先定義的向量商店,支援下列向量商店 程式碼範本,您可以為資料庫設定 結構定義:

支援嵌入模型透過下列外掛程式提供:

外掛程式 模型
Google 生成式 AI Gecko 文字嵌入
Google Vertex AI Gecko 文字嵌入

定義 RAG 流程

以下範例說明如何擷取一系列餐廳菜單 PDF 文件 並擷取到向量資料庫內,然後擷取這些資料,便能在決定哪些食品商品可供兌換的流程中使用。

安裝依附元件

在這個範例中,我們會使用來自 langchaingotextsplitter 程式庫, ledongthuc/pdf PDF 剖析程式庫:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

定義索引器

以下範例說明如何建立索引器來擷取 PDF 文件集合 並儲存至本機向量資料庫

使用本機檔案型向量相似度擷取器 Genkit 提供立即可用的功能,方便您進行簡單測試和原型設計 (不會 用於實際工作環境)

建立索引器

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
    log.Fatal(err)
}
err = localvec.Init()
if err != nil {
    log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
    "menuQA",
    localvec.Config{
        Embedder: vertexai.Embedder("text-embedding-004"),
    },
)
if err != nil {
    log.Fatal(err)
}

建立分塊設定

本範例使用提供簡單文字的 textsplitter 程式庫 分割器可以將文件拆解成可以向量化的區段。

下列定義會設定分塊函式以傳回文件 片段由 200 個字元組成,並與 20 個字元的區塊重疊

splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithChunkSize(200),
    textsplitter.WithChunkOverlap(20),
)

如需這個程式庫的分割選項,請前往 langchaingo 說明文件

定義索引器流程

genkit.DefineFlow(
    "indexMenu",
    func(ctx context.Context, path string) (any, error) {
        // Extract plain text from the PDF. Wrap the logic in Run so it
        // appears as a step in your traces.
        pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
            return readPDF(path)
        })
        if err != nil {
            return nil, err
        }

        // Split the text into chunks. Wrap the logic in Run so it
        // appears as a step in your traces.
        docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
            chunks, err := splitter.SplitText(pdfText)
            if err != nil {
                return nil, err
            }

            var docs []*ai.Document
            for _, chunk := range chunks {
                docs = append(docs, ai.DocumentFromText(chunk, nil))
            }
            return docs, nil
        })
        if err != nil {
            return nil, err
        }

        // Add chunks to the index.
        err = menuPDFIndexer.Index(ctx, &ai.IndexerRequest{Documents: docs})
        return nil, err
    },
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
    f, r, err := pdf.Open(path)
    if f != nil {
        defer f.Close()
    }
    if err != nil {
        return "", err
    }

    reader, err := r.GetPlainText()
    if err != nil {
        return "", err
    }

    bytes, err := io.ReadAll(reader)
    if err != nil {
        return "", err
    }
    return string(bytes), nil
}

執行索引工具流程

genkit flow:run indexMenu "'menu.pdf'"

執行 indexMenu 流程後,系統會用 文件,並可透過擷取步驟在 Genkit 流程中使用。

定義擷取的流程

以下範例說明如何在 RAG 流程中使用擷取器。喜歡 索引器範例,這個範例會使用 Genkit 檔案式向量擷取器 我們不應該在實際工作環境中使用

    ctx := context.Background()

    err := vertexai.Init(ctx, &vertexai.Config{})
    if err != nil {
        log.Fatal(err)
    }
    err = localvec.Init()
    if err != nil {
        log.Fatal(err)
    }

    model := vertexai.Model("gemini-1.5-pro")

    _, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
        "menuQA",
        localvec.Config{
            Embedder: vertexai.Embedder("text-embedding-004"),
        },
    )
    if err != nil {
        log.Fatal(err)
    }

    genkit.DefineFlow(
        "menuQA",
        func(ctx context.Context, question string) (string, error) {
            // Retrieve text relevant to the user's question.
            docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
                Document: ai.DocumentFromText(question, nil),
            })
            if err != nil {
                return "", err
            }

            // Construct a system message containing the menu excerpts you just
            // retrieved.
            menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
            for _, doc := range docs.Documents {
                menuInfo.Content = append(menuInfo.Content, doc.Content...)
            }

            // Call Generate, including the menu information in your prompt.
            resp, err := model.Generate(ctx, &ai.GenerateRequest{
                Messages: []*ai.Message{
                    ai.NewSystemTextMessage(`
You are acting as a helpful AI assistant that can answer questions about the
food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question. If you don't know, do not
make up an answer. Do not add or change items on the menu.`),
                    menuInfo,
                    ai.NewUserTextMessage(question),
                },
            }, nil)
            if err != nil {
                return "", err
            }

            return resp.Text()
        })

編寫自己的索引和擷取器

此外,您也可以建立自己的擷取器。如果您的 Genkit 不支援文件存放區中的管理文件,例如: MySQL、Google 雲端硬碟等)。Genkit SDK 提供靈活的方法 為您提供用來擷取文件的自訂程式碼。

您也可以定義以現有擷取器為基礎建構的自訂擷取器 並應用進階 RAG 技術 (例如重新排名或提供提示) 。

舉例來說,假設您想使用自訂重新排名函式, 以下範例定義一個自訂擷取器,可將函式套用至 先前定義的選單擷取器

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}
advancedMenuRetriever := ai.DefineRetriever(
    "custom",
    "advancedMenuRetriever",
    func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
        // Handle options passed using our custom type.
        opts, _ := req.Options.(CustomMenuRetrieverOptions)
        // Set fields to default values when either the field was undefined
        // or when req.Options is not a CustomMenuRetrieverOptions.
        if opts.K == 0 {
            opts.K = 3
        }
        if opts.PreRerankK == 0 {
            opts.PreRerankK = 10
        }

        // Call the retriever as in the simple case.
        response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
            Document: req.Document,
            Options:  localvec.RetrieverOptions{K: opts.PreRerankK},
        })
        if err != nil {
            return nil, err
        }

        // Re-rank the returned documents using your custom function.
        rerankedDocs := rerank(response.Documents)
        response.Documents = rerankedDocs[:opts.K]

        return response, nil
    },
)