Retrieval-augmented generation (RAG)

Firebase Genkit menyediakan abstraksi yang membantu Anda membangun flow retrieval-augmented generation (RAG), serta plugin yang menyediakan integrasi dengan alat terkait.

Apa itu RAG?

Retrieval-augmented generation adalah teknik yang digunakan untuk menggabungkan sumber informasi eksternal ke dalam respons LLM. Sangat penting untuk dapat melakukan hal ini, karena meskipun LLM biasanya dilatih pada materi yang luas, penggunaan praktis LLM sering kali memerlukan pengetahuan domain tertentu (misalnya, Anda mungkin ingin menggunakan LLM untuk menjawab pertanyaan tentang produk perusahaan).

Salah satu solusinya adalah dengan menyesuaikan model menggunakan data yang lebih spesifik. Namun, biaya untuk hal ini tidak sedikit, 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 prompt pada saat diteruskan ke model. Misalnya, Anda bisa membayangkan prompt, "Apa hubungan Bart dengan Lisa?" dapat diperluas ("diaugmentasi") dengan menambahkan beberapa informasi yang relevan, sehingga menghasilkan prompt, "Anak-anak Homer dan Marge bernama Bart, Lisa, dan Maggie. Apa hubungan Bart dengan 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 menggunakan informasi terbaru.
  • Anda sekarang memiliki potensi untuk mengutip referensi dalam respons LLM Anda.

Di sisi lain, prompt akan lebih lama apabila menggunakan RAG, dan beberapa layanan LLM API mengenakan biaya untuk setiap token input yang Anda kirim. Pada akhirnya, Anda harus mengevaluasi untung-rugi biaya untuk aplikasi Anda.

RAG adalah bidang 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:

  • Indexer: menambahkan dokumen ke "indeks".
  • Embedder: mengubah dokumen menjadi representasi vektor
  • Retriever: mengambil dokumen dari "indeks", berdasarkan kueri.

Definisi ini sengaja dibuat luas karena Genkit tidak mempunyai pendapat mengenai apa itu "indeks" atau bagaimana tepatnya dokumen diambil darinya. Genkit hanya menyediakan format Document dan yang lainnya ditentukan oleh penyedia implementasi retriever atau indexer.

Indexer

Indeks bertanggung jawab untuk melacak dokumen Anda sedemikian rupa sehingga Anda dapat dengan cepat mengambil dokumen yang relevan berdasarkan permintaan tertentu. Ini sering dilakukan dengan menggunakan database vektor, yang mengindeks dokumen Anda menggunakan vektor multidimensi yang disebut embeddings. Embedding teks (secara buram) mewakili konsep yang diungkapkan oleh suatu bagian teks; ini dihasilkan menggunakan model ML dengan tujuan khusus. Dengan mengindeks teks menggunakan embedding-nya, database vektor dapat mengelompokkan teks yang terkait secara konseptual dan mengambil dokumen yang terkait dengan string teks baru (kueri).

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

  1. Membagi dokumen besar menjadi dokumen yang lebih kecil sehingga hanya bagian relevan yang digunakan untuk meningkatkan kualitas prompt – “pemotongan”. Hal ini diperlukan karena banyak LLM memiliki jendela konteks terbatas, sehingga tidak praktis untuk menyertakan seluruh dokumen dengan prompt.

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

  2. Membuat embeddings untuk setiap potongan. Tergantung pada database yang Anda gunakan, Anda mungkin secara eksplisit melakukannya dengan model pembuatan embedding, atau Anda mungkin menggunakan pembuat embedding yang disediakan oleh database.

  3. Tambahkan potongan teks dan indeksnya ke database.

Anda tidak perlu menjalankan flow penyerapan secara sering atau hanya sekali jika Anda bekerja dengan sumber data yang stabil. Di sisi lain, jika Anda bekerja dengan data yang sering berubah, Anda mungkin akan cukup sering menjalankan flow penyerapan (misalnya, dalam pemicu Cloud Firestore, setiap kali dokumen diperbarui).

Embedder

Embedder 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, embedder dimanfaatkan sebagai bagian dari proses pengindeksan. Namun, embedder juga dapat digunakan secara independen untuk membuat embeddings tanpa indeks.

Retriever

Retriever adalah konsep yang merangkum logika yang terkait dengan segala jenis pengambilan dokumen. Kasus pengambilan yang paling populer biasanya termasuk pengambilan dari penyimpanan vektor. Namun, di Genkit, retriever dapat berupa fungsi apa pun yang menampilkan data.

Untuk membuat retriever, Anda dapat menggunakan salah satu implementasi yang disediakan atau membuatnya sendiri.

Indexer, retriever, dan embedder yang didukung

Genkit menyediakan dukungan indexer dan retriever melalui sistem plugin-nya. Plugin berikut ini didukung secara resmi:

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

Dukungan model embedding disediakan melalui plugin berikut:

Plugin Model
AI Generatif Google Embedding teks Gecko
Vertex AI Google Embedding teks Gecko

Menentukan Flow RAG

Contoh berikut menunjukkan cara menyerap kumpulan dokumen PDF menu restoran ke dalam database vektor dan mengambilnya untuk digunakan dalam flow 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 Indexer

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

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

Membuat indexer

// 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 pemisah teks sederhana untuk memecah dokumen menjadi segmen-segmen yang dapat divektorkan.

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

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

Pilihan pemotongan lainnya untuk library ini dapat ditemukan di dokumentasi langchaingo.

Menentukan flow indexer 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 = ai.Index(ctx, menuPDFIndexer, ai.WithIndexerDocs(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 flow indexer

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

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

Menentukan flow dengan pengambilan

Contoh berikut menunjukkan cara menggunakan retriever dalam flow RAG. Seperti contoh indexer, contoh ini menggunakan retriever vektor berbasis file 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-flash")

	_, 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.
			return ai.GenerateText(ctx, model,
				ai.WithMessages(
					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)))
		})

Menulis indexer 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 dibangun di atas retriever yang ada di Genkit dan menerapkan teknik RAG lanjutan (seperti pemeringkatan ulang atau ekstensi prompt) di atasnya.

Misalnya, Anda memiliki fungsi pemeringkatan ulang khusus yang ingin digunakan. Contoh berikut menentukan retriever khusus yang menerapkan fungsi Anda ke menu retriever yang ditentukan 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
	},
)