Retrieval-augmented generation (RAG)

Firebase Genkit menyediakan abstraksi yang membantu Anda membuat 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 tiga 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. Bergantung 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. 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 membuat 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 untuk memproses PDF

npm install llm-chunk pdf-parse @genkit-ai/dev-local-vectorstore
npm i -D --save @types/pdf-parse

Menambahkan penyimpanan vektor lokal ke konfigurasi Anda

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

const ai = genkit({
  plugins: [
    // vertexAI provides the textEmbedding004 embedder
    vertexAI(),

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

Menentukan Indexer

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

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

Membuat indexer

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Membuat konfigurasi pemotongan

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

Definisi berikut mengonfigurasi fungsi pemotongan untuk menjamin segmen dokumen antara 1.000 dan 2.000 karakter, yang dipecah di akhir kalimat, dengan tumpang tindih antara potongan sebanyak 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 { Document } from 'genkit/retriever';
import { chunk } from 'llm-chunk';
import { readFile } from 'fs/promises';
import path from 'path';
import pdf from 'pdf-parse';

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

export const indexMenu = ai.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 ai.index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

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.

import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';

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

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

    // generate a response
   const { text } = await ai.generate({
      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}`,
      docs,
    });

    return text;
  }
);

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

Pengambil Sederhana

Pengambil sederhana memungkinkan Anda mengonversi kode yang ada menjadi pengambil dengan mudah:

import { z } from "genkit";
import { searchEmails } from "./db";

ai.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,
} from 'genkit/retriever';
import { z } from 'genkit';

export const menuRetriever = devLocalRetrieverRef('menuQA');

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

const advancedMenuRetriever = ai.defineRetriever(
  {
    name: `custom/advancedMenuRetriever`,
    configSchema: advancedMenuRetrieverOptionsSchema,
  },
  async (input, options) => {
    const extendedPrompt = await extendPrompt(input);
    const docs = await ai.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 terapkan sendiri, bukan disediakan oleh framework)

Kemudian, Anda dapat mengganti pengambil:

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

Pengurut Ulang dan Pengambilan Dua Tahap

Model pemeringkatan ulang — juga dikenal sebagai cross-encoder — adalah jenis model yang, dengan kueri dan dokumen, akan menghasilkan skor kemiripan. Kami menggunakan skor ini untuk mengurutkan ulang dokumen berdasarkan relevansinya dengan kueri kita. Reranker API mengambil daftar dokumen (misalnya output pengambil) dan mengurutkan ulang dokumen berdasarkan relevansinya dengan kueri. Langkah ini dapat berguna untuk menyesuaikan hasil dan memastikan bahwa informasi yang paling relevan digunakan dalam perintah yang diberikan ke model generatif.

Contoh Reranker

Pengurut ulang di Genkit ditentukan dalam sintaksis yang mirip dengan pengambil dan pengindeksan. Berikut adalah contoh penggunaan reranker di Genkit. Alur ini mengurutkan ulang kumpulan dokumen berdasarkan relevansinya dengan kueri yang diberikan menggunakan reranker Vertex AI yang telah ditentukan sebelumnya.

const FAKE_DOCUMENT_CONTENT = [
  'pythagorean theorem',
  'e=mc^2',
  'pi',
  'dinosaurs',
  'quantum mechanics',
  'pizza',
  'harry potter',
];

export const rerankFlow = ai.defineFlow(
  {
    name: 'rerankFlow',
    inputSchema: z.object({ query: z.string() }),
    outputSchema: z.array(
      z.object({
        text: z.string(),
        score: z.number(),
      })
    ),
  },
  async ({ query }) => {
    const documents = FAKE_DOCUMENT_CONTENT.map((text) =>
       ({ content: text })
    );

    const rerankedDocuments = await ai.rerank({
      reranker: 'vertexai/semantic-ranker-512',
      query:  ({ content: query }),
      documents,
    });

    return rerankedDocuments.map((doc) => ({
      text: doc.content,
      score: doc.metadata.score,
    }));
  }
);

Pengurut ulang ini menggunakan plugin genkit Vertex AI dengan semantic-ranker-512 untuk memberi skor dan peringkat dokumen. Makin tinggi skornya, makin relevan dokumen tersebut dengan kueri.

Pengurut Ulang Kustom

Anda juga dapat menentukan pemeringkat ulang kustom agar sesuai dengan kasus penggunaan tertentu. Hal ini berguna saat Anda perlu mengurutkan ulang dokumen menggunakan logika kustom atau model kustom Anda sendiri. Berikut adalah contoh sederhana untuk menentukan pemeringkat ulang kustom:

export const customReranker = ai.defineReranker(
  {
    name: 'custom/reranker',
    configSchema: z.object({
      k: z.number().optional(),
    }),
  },
  async (query, documents, options) => {
    // Your custom reranking logic here
    const rerankedDocs = documents.map((doc) => {
      const score = Math.random(); // Assign random scores for demonstration
      return {
        ...doc,
        metadata: { ...doc.metadata, score },
      };
    });

    return rerankedDocs.sort((a, b) => b.metadata.score - a.metadata.score).slice(0, options.k || 3);
  }
);

Setelah ditentukan, pemeringkat ulang kustom ini dapat digunakan seperti pemeringkat ulang lainnya dalam alur RAG, sehingga memberi Anda fleksibilitas untuk menerapkan strategi pemeringkat ulang lanjutan.