Pembuatan agmentasi pengambilan (RAG)

Firebase Genkit menyediakan abstraksi yang membantu Anda mem-build pembuatan yang didukung pengambilan (RAG), serta plugin yang menyediakan integrasi dengan alat terkait.

Apa itu RAG?

Pembuatan yang ditingkatkan adalah teknik yang digunakan untuk menggabungkan sumber informasi tertentu menjadi respons LLM. Sangat penting untuk dapat melakukan Jadi, karena LLM biasanya dilatih dengan berbagai penggunaan praktis LLM sering kali memerlukan pengetahuan domain khusus (untuk Anda mungkin ingin menggunakan LLM untuk menjawab pertanyaan tentang produk perusahaan).

Salah satu solusinya adalah dengan menyesuaikan model menggunakan data yang lebih spesifik. Namun, lebih mahal baik dari segi biaya komputasi maupun upaya yang dibutuhkan untuk menyiapkan data pelatihan yang memadai.

Sebaliknya, RAG bekerja dengan menggabungkan sumber data eksternal ke dalam {i>prompt<i} di waktu yang diteruskan ke model. Misalnya, Anda bisa membayangkan {i>prompt<i} itu, "Apa hubungan Bart dengan Lisa?" mungkin diperluas ("ditambah") dengan menambahkan informasi yang relevan, sehingga memunculkan {i>prompt<i}, "Homer dan Anak Marge bernama Bart, Lisa, dan Maggie. Apa hubungan Bart kepada Lisa?"

Pendekatan ini memiliki beberapa manfaat:

  • Hal ini dapat lebih hemat biaya karena Anda tidak perlu melatih ulang model.
  • Anda dapat terus memperbarui sumber data dan LLM dapat segera membuat penggunaan informasi yang diperbarui.
  • Anda sekarang memiliki potensi untuk mengutip referensi dalam respons LLM Anda.

Di sisi lain, menggunakan RAG secara alami berarti perintah yang lebih panjang, dan beberapa LLM API biaya untuk setiap token input yang Anda kirim. Pada akhirnya, Anda harus mengevaluasi untung-rugi biaya untuk aplikasi Anda.

RAG adalah area yang sangat luas dan ada banyak teknik berbeda yang digunakan untuk mencapai RAG dengan kualitas terbaik. Framework Genkit inti menawarkan dua abstraksi utama untuk membantu Anda melakukan RAG:

  • Pengindeks: menambahkan dokumen ke "indeks".
  • {i>Embedders<i}: mengubah dokumen menjadi representasi vektor
  • Pengambil: mengambil dokumen dari "indeks", berdasarkan kueri.

Definisi ini dibuat secara luas karena Genkit tidak memiliki pendapat yang sama tentang apa itu "indeks" atau bagaimana dokumen diambil darinya. Khusus genkit menyediakan format Document dan yang lainnya ditentukan oleh retriever atau penyedia implementasi pengindeks.

Pengindeks

Indeks bertanggung jawab untuk melacak dokumen Anda sedemikian rupa sehingga Anda dapat dengan cepat mengambil dokumen yang relevan dengan kueri tertentu. Ini adalah sering dicapai menggunakan {i>database<i} vektor, yang mengindeks dokumen Anda menggunakan vektor multidimensi yang disebut {i>embedding<i}. Penyematan teks (buram) mewakili konsep yang diungkapkan oleh teks; ini dibuat menggunakan model ML tujuan khusus. Dengan mengindeks teks menggunakan embeddingnya, vektor dapat mengelompokkan teks yang terkait secara konseptual dan mengambil dokumen terkait dengan {i>string<i} teks baru (kueri).

Sebelum dapat mengambil dokumen untuk pembuatan, Anda harus menyerapnya ke dalam indeks dokumen. Alur penyerapan umum melakukan berikut ini:

  1. Bagi dokumen besar menjadi dokumen yang lebih kecil agar hanya relevan digunakan untuk meningkatkan kualitas perintah – “chunking”. Ini diperlukan karena banyak LLM memiliki jendela konteks terbatas, sehingga tidak praktis untuk sertakan seluruh dokumen dengan prompt.

    Genkit tidak menyediakan library pemotongan bawaan; Namun, ada beberapa library sumber yang kompatibel dengan Genkit.

  2. Membuat embeddings untuk setiap potongan. Tergantung pada {i>database<i} yang Anda gunakan, Anda mungkin secara eksplisit melakukannya dengan model pembuatan embedding, mungkin menggunakan generator embedding yang disediakan oleh database.

  3. Tambahkan potongan teks dan indeksnya ke {i>database<i}.

Anda dapat menjalankan alur penyerapan jarang atau hanya sekali jika Anda bekerja dengan sumber data yang stabil. Di sisi lain, jika Anda bekerja dengan data yang sering berubah, Anda dapat terus menjalankan alur penyerapan (untuk (misalnya, dalam pemicu Cloud Firestore, setiap kali dokumen diperbarui).

Penyemat

Penyematan adalah fungsi yang mengambil konten (teks, gambar, audio, dll.) dan membuat vektor numerik yang mengenkode makna semantik dari konten asli tersebut. Seperti disebutkan di atas, embeddings dimanfaatkan sebagai bagian dari proses pengindeksan. Namun, embedding juga dapat digunakan secara independen untuk membuat embedding tanpa indeks.

Pengambil

Pengambil adalah konsep yang merangkum logika yang terkait dengan jenis dokumen apa pun atau pengambilan. Kasus pengambilan yang paling populer biasanya termasuk pengambilan dari penyimpanan vektor, namun, di Genkit, retriever dapat berupa fungsi apa pun yang mengembalikan data.

Untuk membuat {i>taker<i}, Anda dapat menggunakan salah satu implementasi yang disediakan atau buat sendiri.

Pengindeks, pengambil, dan sematan yang didukung

Genkit menyediakan dukungan pengindeks dan retriever melalui sistem pluginnya. Tujuan plugin berikut didukung secara resmi:

Selain itu, Genkit mendukung penyimpanan vektor berikut melalui template kode, yang dapat Anda sesuaikan untuk konfigurasi database dan skema:

Dukungan model penyematan disediakan melalui plugin berikut:

{i>Plugin<i} Model
AI Generatif Google Penyematan teks tokek
Vertex AI Google Penyematan teks tokek

Menentukan Alur RAG

Contoh berikut menunjukkan cara mentransfer sekumpulan dokumen PDF menu restoran ke dalam {i>database<i} vektor dan mengambilnya untuk digunakan dalam aliran yang menentukan item makanan apa yang tersedia.

Menginstal dependensi

Dalam contoh ini, kita akan menggunakan library textsplitter dari langchaingo dan Library penguraian PDF ledongthuc/pdf:

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

Menentukan Pengindeks

Contoh berikut menunjukkan cara membuat pengindeks untuk menyerap koleksi dokumen PDF dan menyimpannya dalam database vektor lokal.

Fungsi ini menggunakan retriever kemiripan vektor berbasis file lokal yang disediakan oleh Genkit untuk pengujian dan pembuatan prototipe sederhana (jangan digunakan dalam produksi)

Membuat pengindeks

// 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)
}

Membuat konfigurasi pemotongan

Contoh ini menggunakan library textsplitter yang menyediakan teks sederhana untuk memecah dokumen menjadi segmen yang dapat diubah menjadi vektor.

Definisi berikut mengonfigurasi fungsi pemotongan untuk menampilkan dokumen segmen yang terdiri dari 200 karakter, dengan tumpang tindih antara potongan 20 karakter.

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

Pilihan pemotongan lainnya untuk {i>library<i} ini dapat ditemukan di Dokumentasi langchaingo.

Menentukan alur pengindeks Anda

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
}

Menjalankan alur pengindeks

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

Setelah menjalankan alur indexMenu, database vektor akan ditambahi dokumen dan siap digunakan dalam alur Genkit dengan langkah-langkah pengambilan.

Menentukan alur dengan pengambilan

Contoh berikut menunjukkan cara menggunakan retriever dalam alur RAG. Suka contoh pengindeks, contoh ini menggunakan {i> taker<i} vektor berbasis file dari Genkit, yang tidak boleh Anda gunakan dalam produksi.

    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()
        })

Menulis pengindeks dan retriever Anda sendiri

Anda juga bisa membuat retriever Anda sendiri. Hal ini berguna jika dokumen dikelola di penyimpanan dokumen yang tidak didukung di Genkit (misalnya: MySQL, Google Drive, dll.). Genkit SDK menyediakan metode fleksibel yang memungkinkan Anda memberikan kode kustom untuk mengambil dokumen.

Anda juga bisa menentukan retriever kustom yang dibuat di atas retriever yang ada di Genkit dan menerapkan teknik RAG tingkat lanjut (seperti pengurutan ulang atau prompt ) di atas.

Misalnya, Anda memiliki fungsi pemeringkatan ulang kustom yang ingin digunakan. Tujuan dalam contoh berikut mendefinisikan sebuah {i>custom retriever<i} yang menerapkan fungsi Anda ke {i>menu retriever<i} yang didefinisikan sebelumnya:

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
    },
)