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.

Instal dependensi untuk memproses PDF

npm install llm-chunk pdf-parse
npm i -D --save @types/pdf-parse

Menambahkan penyimpanan vektor lokal ke konfigurasi Anda

import {
  devLocalIndexerRef,
  devLocalVectorstore,
} from '@genkit-ai/dev-local-vectorstore';
import { textEmbeddingGecko, vertexAI } from '@genkit-ai/vertexai';

configureGenkit({
  plugins: [
    // vertexAI provides the textEmbeddingGecko embedder
    vertexAI(),

    // the local vector store requires an embedder to translate from text to vector
    devLocalVectorstore([
      {
        indexName: 'menuQA',
        embedder: textEmbeddingGecko,
      },
    ]),
  ],
});

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 { devLocalIndexerRef } from '@genkit-ai/dev-local-vectorstore';

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Membuat konfigurasi pemotongan

Contoh ini menggunakan library llm-chunk yang menyediakan pemisah teks sederhana untuk memecah dokumen menjadi segmen yang dapat diubah menjadi vektor.

Definisi berikut mengkonfigurasi fungsi pemotongan untuk menjamin segmen dokumen antara 1000 dan 2000 karakter, yang dipecah di akhir kalimat, dengan tumpang tindih antara potongan 100 karakter.

const chunkingConfig = {
  minLength: 1000,
  maxLength: 2000,
  splitter: 'sentence',
  overlap: 100,
  delimiters: '',
} as any;

Opsi pemotongan lainnya untuk library ini dapat ditemukan di dokumentasi llm-chunk.

Menentukan flow indexer Anda

import { index } from '@genkit-ai/ai';
import { Document } from '@genkit-ai/ai/retriever';
import { defineFlow, run } from '@genkit-ai/flow';
import { readFile } from 'fs/promises';
import { chunk } from 'llm-chunk';
import path from 'path';
import pdf from 'pdf-parse';
import * as z from 'zod';

export const indexMenu = defineFlow(
  {
    name: 'indexMenu',
    inputSchema: z.string().describe('PDF file path'),
    outputSchema: z.void(),
  },
  async (filePath: string) => {
    filePath = path.resolve(filePath);

    // Read the pdf.
    const pdfTxt = await run('extract-text', () =>
      extractTextFromPdf(filePath)
    );

    // Divide the pdf text into segments.
    const chunks = await run('chunk-it', async () =>
      chunk(pdfTxt, chunkingConfig)
    );

    // Convert chunks of text into documents to store in the index.
    const documents = chunks.map((text) => {
      return Document.fromText(text, { filePath });
    });

    // Add documents to the index.
    await index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

async function extractTextFromPdf(filePath: string) {
  const pdfFile = path.resolve(filePath);
  const dataBuffer = await readFile(pdfFile);
  const data = await pdf(dataBuffer);
  return data.text;
}

Menjalankan flow indexer

genkit flow:run indexMenu "'../pdfs'"

Setelah menjalankan alur indexMenu, database vektor akan diisi dengan dokumen dan siap digunakan dalam alur 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.

import { generate } from '@genkit-ai/ai';
import { retrieve } from '@genkit-ai/ai/retriever';
import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';
import { defineFlow } from '@genkit-ai/flow';
import { gemini15Flash } from '@genkit-ai/vertexai';
import * as z from 'zod';

// Define the retriever reference
export const menuRetriever = devLocalRetrieverRef('menuQA');

export const menuQAFlow = defineFlow(
  { name: 'menuQA', inputSchema: z.string(), outputSchema: z.string() },
  async (input: string) => {
    // retrieve relevant documents
    const docs = await retrieve({
      retriever: menuRetriever,
      query: input,
      options: { k: 3 },
    });

    // generate a response
    const llmResponse = await generate({
      model: gemini15Flash,
      prompt: `
    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.

    Question: ${input}
    `,
      context: docs,
    });

    const output = llmResponse.text();
    return output;
  }
);

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 dapat menentukan yang dibuat di atas retriever yang ada di Genkit dan menerapkan Teknik RAG (seperti pengurutan ulang atau ekstensi perintah) di bagian atas.

Pengambil Sederhana

Retriever sederhana memungkinkan Anda dengan mudah mengonversi kode yang ada ke dalam retriever:

import {
  defineSimpleRetriever,
  retrieve
} from '@genkit-ai/ai/retriever';
import { searchEmails } from './db';
import { z } from 'zod';

defineSimpleRetriever({
  name: 'myDatabase',
  configSchema: z.object({
    limit: z.number().optional()
  }).optional(),
  // we'll extract "message" from the returned email item
  content: 'message',
  // and several keys to use as metadata
  metadata: ['from', 'to', 'subject'],
} async (query, config) => {
  const result = await searchEmails(query.text(), {limit: config.limit});
  return result.data.emails;
});

Pengambil Kustom

import {
  CommonRetrieverOptionsSchema,
  defineRetriever,
  retrieve,
} from '@genkit-ai/ai/retriever';
import * as z from 'zod';

export const menuRetriever = devLocalRetrieverRef('menuQA');

const advancedMenuRetrieverOptionsSchema = CommonRetrieverOptionsSchema.extend({
  preRerankK: z.number().max(1000),
});

const advancedMenuRetriever = defineRetriever(
  {
    name: `custom/advancedMenuRetriever`,
    configSchema: advancedMenuRetrieverOptionsSchema,
  },
  async (input, options) => {
    const extendedPrompt = await extendPrompt(input);
    const docs = await retrieve({
      retriever: menuRetriever,
      query: extendedPrompt,
      options: { k: options.preRerankK || 10 },
    });
    const rerankedDocs = await rerank(docs);
    return rerankedDocs.slice(0, options.k || 3);
  }
);

(extendPrompt dan rerank adalah sesuatu yang harus Anda implementasikan sendiri, tidak disediakan oleh framework)

Dan kemudian Anda bisa menukar retriever Anda:

const docs = await retrieve({
  retriever: advancedRetriever,
  query: input,
  options: { preRerankK: 7, k: 3 },
});