檢索增強生成 (RAG)

Genkit 提供抽象概念,協助您建構檢索增強生成 (RAG) 流程,以及可與相關工具整合的外掛程式。

什麼是 RAG?

檢索增強生成是一種技術,可將外部資訊來源納入 LLM 回覆。這點很重要,因為雖然 LLM 通常會以廣泛的素材訓練,但實際使用 LLM 時,通常需要具備特定領域的知識 (例如,您可能想使用 LLM 來回答客戶對貴公司產品的疑問)。

解決方法之一是使用更具體的資料微調模型。不過,無論是運算成本,還是準備足夠訓練資料所需的努力,都可能會讓您付出高昂的代價。

相較之下,RAG 會在提示傳遞至模型時,將外部資料來源整合至提示中。舉例來說,您可以想像,提示「Bart 與 Lisa 的關係為何?」可能會透過在前面加上一些相關資訊來擴充 (「擴增」),進而產生「Homer 和 Marge 的孩子分別是 Bart、Lisa 和 Maggie。What is Bart's relationship to Lisa?

這種方法有幾個優點:

  • 您不必重新訓練模型,因此成本效益更高。
  • 您可以持續更新資料來源,而 LLM 可以立即使用更新後的資訊。
  • 你現在可以在 LLM 的回覆中引用參考資料。

另一方面,使用 RAG 自然會導致提示內容變長,而部分 LLM API 服務會針對您傳送的每個輸入符號收費。最終,您必須評估應用程式的成本取捨。

RAG 涵蓋的範圍非常廣泛,因此有許多不同的技術可用於取得最佳品質的 RAG。Genkit 核心架構提供兩個主要抽象概念,協助您執行 RAG:

  • 索引器:將文件新增至「索引」。
  • 嵌入者:將文件轉換為向量表示法
  • 檢索器:在指定查詢時,從「索引」中擷取文件。

這些定義的範圍很廣,因為 Genkit 不對「索引」的定義或從中擷取文件的方式有任何意見。Genkit 只提供 Document 格式,其他所有內容都由擷取器或索引器實作供應器定義。

索引器

索引負責追蹤您的文件,讓您在指定查詢時,能快速擷取相關文件。這項作業通常會使用向量資料庫完成,該資料庫會使用稱為嵌入的多維向量為文件建立索引。文字嵌入 (不透明) 代表文字段落所表達的概念,這些概念是使用專用機器學習模型產生。透過使用嵌入內容為文字建立索引,向量資料庫就能將概念上相關的文字分組,並擷取與新文字串 (查詢) 相關的文件。

您必須先將文件匯入文件索引,才能擷取文件以供產生作業使用。一般攝入流程會執行以下操作:

  1. 將大型文件分割成較小的文件,以便只使用相關部分來擴充提示 (稱為「分割」)。這是必要的做法,因為許多 LLM 的內容窗格有限,因此無法在提示中加入整份文件。

    Genkit 不提供內建的分割程式庫,但有可與 Genkit 相容的開放原始碼程式庫。

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

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

如果您使用的是穩定的資料來源,可能不常執行擷取流程,甚至只執行一次。另一方面,如果您要處理經常變更的資料,可以持續執行攝入流程 (例如在 Cloud Firestore 觸發事件中,每當文件更新時)。

嵌入者

嵌入器是一種函式,可接收內容 (文字、圖片、音訊等),並建立數值向量,對原始內容的語意進行編碼。如上所述,嵌入程式會在索引處理程序中使用。不過,也可以單獨使用這些函式,在沒有索引的情況下建立嵌入資料。

獵犬

擷取器是一種概念,用來封裝與任何類型文件擷取相關的邏輯。最常見的擷取用途通常包括從向量儲存庫擷取資料。不過,在 Genkit 中,擷取器可以是任何傳回資料的函式。

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

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

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

此外,Genkit 透過預先定義的程式碼範本支援下列向量儲存庫,您可以根據資料庫設定和結構定義自訂這些儲存庫:

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

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

定義 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()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

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

建立分割設定

這個範例使用 textsplitter 程式庫,該程式庫提供簡單的文字分割器,可將文件分割為可向量化的片段。

下列定義會將區塊函式設為傳回 200 個字元的文件片段,且每個區塊之間有 20 個字元的重疊部分。

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

如要進一步瞭解此程式庫的更多分割選項,請參閱 langchaingo 說明文件

定義索引器流程

genkit.DefineFlow(
    g, "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 = ai.Index(ctx, menuPDFIndexer, ai.WithDocs(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 流程中使用 retriever。與索引器範例一樣,這個範例使用 Genkit 的檔案式向量擷取器,但不建議在正式版中使用。

ctx := context.Background()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

model := googlegenai.VertexAIModel(g, "gemini-1.5-flash")

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

genkit.DefineFlow(
  g, "menuQA",
  func(ctx context.Context, question string) (string, error) {
    // Retrieve text relevant to the user's question.
    resp, err := ai.Retrieve(ctx, menuPdfRetriever, ai.WithTextDocs(question))


    if err != nil {
        return "", err
    }

    // Call Generate, including the menu information in your prompt.
    return genkit.GenerateText(ctx, g,
        ai.WithModelName("googleai/gemini-2.0-flash"),
        ai.WithDocs(resp.Documents),
        ai.WithSystem(`
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.`)
        ai.WithPrompt(question),
  })

編寫自己的索引器和擷取器

您也可以自行建立擷取器。如果您的文件是在 Genkit 不支援的文件儲存庫中管理,這項功能就很實用 (例如 MySQL、Google 雲端硬碟等)。Genkit SDK 提供彈性方法,讓您提供用於擷取文件的自訂程式碼。

您也可以定義自訂擷取器,在 Genkit 中建構現有擷取器,並套用進階 RAG 技術 (例如重新排序或提示擴充功能)。

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

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}

advancedMenuRetriever := genkit.DefineRetriever(
    g, "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.
        resp, err := ai.Retrieve(ctx, menuPDFRetriever,
            ai.WithDocs(req.Query),
            ai.WithConfig(ocalvec.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
    },
)