Pembuatan agmentasi pengambilan (RAG)

Firebase Genkit menyediakan abstraksi yang membantu Anda mem-build alur pembuatan retrieval-augmented production (RAG), serta plugin yang menyediakan integrasi dengan alat terkait.

Apa itu RAG?

Pembuatan augmentasi pengambilan adalah teknik yang digunakan untuk menggabungkan sumber informasi eksternal ke dalam respons LLM. Penting untuk dapat melakukannya karena, meskipun LLM biasanya dilatih dengan materi yang luas, penggunaan praktis LLM sering kali memerlukan pengetahuan domain khusus (misalnya, Anda mungkin ingin menggunakan LLM untuk menjawab pertanyaan pelanggan mengenai produk perusahaan Anda).

Salah satu solusinya adalah menyesuaikan model menggunakan data yang lebih spesifik. Namun, hal ini bisa menjadi mahal dalam hal biaya komputasi dan dalam hal upaya yang diperlukan untuk menyiapkan data pelatihan yang memadai.

Sebaliknya, RAG bekerja dengan menggabungkan sumber data eksternal ke dalam prompt pada saat diteruskan ke model. Misalnya, Anda dapat membayangkan dialog, "Apa hubungan Bart dengan Lisa?" dapat diperluas ("ditambah") dengan menambahkan beberapa informasi yang relevan, sehingga menghasilkan perintah, "Anak Homer dan Marge bernama Bart, Lisa, dan Maggie. Apa hubungan Bart dengan Lisa?"

Pendekatan ini memiliki beberapa manfaat:

  • Cara ini dapat lebih hemat biaya karena Anda tidak perlu melatih ulang model.
  • Anda dapat terus memperbarui sumber data dan LLM dapat langsung menggunakan informasi yang diperbarui.
  • Sekarang Anda bisa mengutip referensi dalam respons LLM.

Di sisi lain, menggunakan RAG biasanya berarti permintaan akan lebih panjang, dan beberapa layanan LLM API akan mengenakan biaya untuk setiap token input yang Anda kirim. Pada akhirnya, Anda harus mengevaluasi kompromi 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".
  • Embedder: mengubah dokumen menjadi representasi vektor
  • Pengambil: mengambil dokumen dari "indeks", yang diberi kueri.

Definisi ini memiliki tujuan yang luas karena Genkit tidak memiliki opini tentang apa yang dimaksud dengan "indeks" atau bagaimana dokumen diambil darinya. Genkit hanya menyediakan format Document dan lainnya ditentukan oleh penyedia implementasi retriever atau pengindeks.

Pengindeks

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

Agar dapat mengambil dokumen untuk tujuan pembuatan, Anda harus menyerapnya ke dalam indeks dokumen. Alur penyerapan umum melakukan hal-hal berikut:

  1. Bagi dokumen besar menjadi dokumen yang lebih kecil sehingga hanya bagian yang relevan yang digunakan untuk meningkatkan kualitas perintah Anda – "chunking". 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, tersedia library open source yang kompatibel dengan Genkit.

  2. Membuat embeddings untuk setiap potongan. Bergantung pada database yang digunakan, Anda dapat melakukannya secara eksplisit dengan model pembuatan embedding, atau Anda dapat menggunakan generator embedding yang disediakan oleh database.

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

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

Penyemat

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

Pengambil

Pengambilan adalah konsep yang mengenkapsulasi logika yang terkait dengan segala jenis pengambilan dokumen. Kasus pengambilan yang paling populer biasanya mencakup pengambilan dari penyimpanan vektor. Namun, dalam Genkit, retriever dapat berupa fungsi apa pun yang menampilkan data.

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

Pengindeks, pengambil, dan penyemat yang didukung

Genkit menyediakan dukungan pengindeks dan pengambil melalui sistem pluginnya. Plugin berikut didukung secara resmi:

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

Dukungan model penyematan disediakan melalui plugin berikut:

Pengaya Model
AI Generatif Google Penyematan teks Gecko
Vertex AI Google Penyematan teks Gecko

Mendefinisikan Alur RAG

Contoh berikut menunjukkan cara menyerap kumpulan dokumen PDF menu restoran ke dalam database vektor dan mengambilnya untuk digunakan dalam alur yang menentukan item makanan yang tersedia.

Menginstal 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 Pengindeks

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

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

Membuat pengindeks

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 membagi dokumen menjadi beberapa segmen yang dapat divektorkan.

Definisi berikut mengkonfigurasi fungsi pemotongan untuk menjamin segmen dokumen antara 1000 dan 2000 karakter, pecah di akhir kalimat, dengan tumpang tindih di 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.

Tentukan alur pengindeks

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 alur pengindeks

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 alur dengan pengambilan

Contoh berikut menunjukkan cara menggunakan retriever dalam alur RAG. Seperti contoh pengindeks, contoh ini menggunakan vector retriever 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 { geminiPro } 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: geminiPro,
      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 pengindeks dan pengambil Anda sendiri

Anda juga dapat membuat retriever sendiri. Hal ini berguna jika dokumen Anda dikelola di penyimpanan dokumen yang tidak didukung di Genkit (misalnya: MySQL, Google Drive, dll.). Genkit SDK menyediakan metode fleksibel yang memungkinkan Anda menyediakan kode kustom untuk mengambil dokumen. Anda juga dapat menentukan pengambil kustom yang dibuat berdasarkan pengambil yang ada di Genkit dan menerapkan teknik RAG lanjutan (seperti pengurutan ulang atau ekstensi perintah) di atasnya.

{i>Simple Retriever<i}

Simple Retriever memungkinkan Anda mengonversi kode yang ada menjadi retriever dengan mudah:

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)

Selanjutnya, Anda dapat menukar retriever Anda:

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