Generowanie rozszerzone przez wyszukiwanie (RAG)

Firebase Genkit udostępnia abstrakcje, które ułatwiają tworzenie przepływów generowania RAG z użyciem technologii rozszerzonego pobierania, a także wtyczki umożliwiające integrację z powiązanymi narzędziami.

Co to jest RAG?

Generowanie rozszerzone przez wyszukiwanie to technika używana do uwzględniania zewnętrznych źródeł informacji w odpowiedziach LLM. Jest to bardzo ważne, ponieważ choć modele LLM są zwykle trenowane na szerokiej gamie materiałów, praktyczne ich zastosowanie często wymaga wiedzy z konkretnej dziedziny (LLM możesz na przykład używać LLM, aby odpowiadać na pytania klientów dotyczące produktów Twojej firmy).

Jednym z rozwiązań jest dostrojenie modelu przy użyciu bardziej szczegółowych danych. Może to być jednak kosztowne zarówno pod względem kosztów mocy obliczeniowej, jak i wysiłku niezbędnego do przygotowania odpowiednich danych treningowych.

Z kolei RAG działa przez uwzględnianie zewnętrznych źródeł danych w prompcie w momencie przekazywania go do modelu. Na przykład prompt „Jaka jest relacja Bartka z Lisą?” może zostać rozszerzony („rozszerzona”) przez dodanie odpowiednich informacji, co spowoduje pojawienie się promptu: „Dzieci Homer i Marge nazywają się Bart, Lisa i Maggie. Jaki jest związek Bartka z Lizą?

Takie podejście ma kilka zalet:

  • Może to być bardziej opłacalne, ponieważ nie trzeba ponownie uczyć modelu.
  • Źródło danych możesz stale aktualizować, a LLM może od razu korzystać ze zaktualizowanych informacji.
  • Teraz możesz cytować odwołania w odpowiedziach swojego LLM.

Z drugiej strony użycie RAG w naturalny sposób oznacza dłuższe prompty, a niektóre opłaty za usługi interfejsu LLM API są naliczane za każdy wysłany token wejściowy. Ostatecznie musisz ocenić korzyści związane z kosztami w przypadku Twoich aplikacji.

RAG to bardzo szeroki obszar i stosuje się wiele różnych technik, aby osiągnąć najlepszą jakość RAG. Podstawowa platforma Genkit zawiera 2 główne abstrakcje ułatwiające prowadzenie RAG:

  • Indeksy: dodawanie dokumentów do „indeksu”.
  • Umieszczanie: przekształcanie dokumentów w reprezentację wektorową
  • Moduły pobierania: pobieranie dokumentów z „indeksu” po wywołaniu zapytania.

Definicje te są celowo ogólne, ponieważ Genkit nie ma opinii na temat tego, czym jest „indeks” ani jak dokładnie są z niego pobierane dokumenty. Genkit udostępnia tylko format Document, a wszystko inne jest definiowane przez dostawcę implementacji modułu pobierania lub indeksowania.

Indeksy

Indeks odpowiada za śledzenie dokumentów w taki sposób, aby można było szybko pobierać odpowiednie dokumenty po konkretnym zapytaniu. Najczęściej robi się to za pomocą bazy danych wektorów, która indeksuje dokumenty za pomocą wielowymiarowych wektorów zwanych wektorami dystrybucyjnymi. Osadzanie tekstu (niewyraźnie) odzwierciedla koncepcje wyrażone przez fragment tekstu. Są one generowane za pomocą specjalnych modeli ML. Indeksując tekst przez jego umiejscowienie, wektorowa baza danych może grupować koncepcyjnie powiązany tekst i pobierać dokumenty powiązane z nowym ciągiem tekstowym (zapytaniem).

Zanim pobierzesz dokumenty w celu wygenerowania, musisz je przetworzyć w indeksie dokumentów. Typowy proces przetwarzania wygląda tak:

  1. Dziel duże dokumenty na mniejsze dokumenty, tak by do uzupełniania promptów używać tylko odpowiednich części. Jest to konieczne, ponieważ wiele modeli LLM ma ograniczone okno kontekstu, przez co uwzględnienie całych dokumentów za pomocą promptu jest niepraktyczne.

    Genkit nie udostępnia wbudowanych bibliotek do fragmentowania, ale dostępne są biblioteki open source, które są zgodne z Genkit.

  2. Wygeneruj wektory dystrybucyjne dla każdego fragmentu. W zależności od używanej bazy danych możesz to zrobić bezpośrednio za pomocą modelu generowania reprezentacji właściwościowych lub możesz użyć generatora umieszczania dostępnego w bazie danych.

  3. Dodaj fragment tekstu i jego indeks do bazy danych.

Przepływ pozyskiwania danych możesz uruchamiać rzadko lub tylko raz, jeśli pracujesz na stabilnym źródle danych. Z drugiej strony, jeśli pracujesz z danymi, które często się zmieniają, proces pozyskiwania danych może być uruchamiany w sposób ciągły (na przykład w aktywatorze Cloud Firestore, gdy dokument jest aktualizowany).

Umieszczanie

Umieszczanie to funkcja, która pobiera treść (tekst, obrazy, dźwięk itp.) i tworzy wektor numeryczny, który koduje semantyczne znaczenie oryginalnej treści. Jak wspomniano powyżej, elementy osadzone są wykorzystywane w ramach procesu indeksowania, ale można ich też używać niezależnie do tworzenia wektorów dystrybucyjnych bez indeksu.

Pobierający

Termin „ retriever” obejmuje logikę związaną z pobieraniem dokumentów z dowolnych źródeł. Najpopularniejszymi przypadkami pobierania są zwykle pobieranie danych z magazynów wektorów, ale w Genkitie pobieraniem może być dowolna funkcja, która zwraca dane.

Aby utworzyć moduł pobierania, możesz użyć jednej z podanych implementacji lub utworzyć własną.

Obsługiwane roboty indeksujące, pobierania i obiekty osadzone

Genkit zapewnia obsługę narzędzia do indeksowania i pobierania za pomocą systemu wtyczek. Oficjalnie obsługiwane są te wtyczki:

Dodatkowo Genkit obsługuje te magazyny wektorów za pomocą wstępnie zdefiniowanych szablonów kodu, które możesz dostosować do konfiguracji i schematu bazy danych:

Te wtyczki umożliwiają obsługę modelu umieszczania:

Wtyczka Modele
Generatywna AI od Google Osadzanie tekstu gekon
Google Vertex AI Osadzanie tekstu gekon

Definiowanie przepływu RAG

W podanych niżej przykładach pokazujemy, jak można przetworzyć kolekcję dokumentów PDF z menu restauracji do bazy danych wektorowych i pobrać je na potrzeby przepływu danych, który określa, jakie pozycje są dostępne.

Instalowanie zależności do przetwarzania plików PDF

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

Dodaj lokalny magazyn wektorów do konfiguracji

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

Zdefiniuj indekser

Przykład poniżej pokazuje, jak utworzyć indekser do pozyskiwania kolekcji dokumentów PDF i przechowywania ich w lokalnej bazie danych wektorów.

Wykorzystuje działający na podstawie plików lokalny moduł pobierania podobieństw wektorów, który jest gotowy do użytku w narzędziu Genkit do prostego testowania i tworzenia prototypów (nie należy ich używać w środowisku produkcyjnym).

Tworzenie indeksatora

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

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Utwórz konfigurację podziału na fragmenty

W tym przykładzie korzystamy z biblioteki llm-chunk, która pozwala łatwo podzielić tekst na segmenty, które można poddawać wektorom.

Poniższa definicja konfiguruje funkcję fragmentacji w celu zapewnienia gwarancji fragmentu dokumentu zawierającego od 1000 do 2000 znaków, podzielony na koniec zdania i nakładający się fragment o długości 100 znaków.

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

Więcej opcji podziału na fragmenty znajdziesz w dokumentacji fragmentu llm.

Zdefiniuj proces indeksowania

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;
}

Uruchamianie procesu indeksowania

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

Po uruchomieniu przepływu indexMenu baza danych wektorowych zostanie zapełniona dokumentami i będzie gotowa do użycia w przepływach Genkit z krokami pobierania.

Definiowanie procesu z pobieraniem

Poniższy przykład pokazuje, jak można użyć modułu pobierania w przepływie RAG. Podobnie jak w przykładzie indeksatora, w tym przykładzie używany jest oparty na plikach wektorowy moduł pobierania danych Genkit, którego nie należy używać w środowisku produkcyjnym.

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

Pisanie własnych indeksatorów i mechanizmów pobierania

Możesz też utworzyć własny plik retriever. Jest to przydatne, gdy dokumenty są zarządzane w magazynie dokumentów, który nie jest obsługiwany przez Genkit (np. MySQL, Dysk Google itp.). Pakiet Genkit SDK udostępnia elastyczne metody udostępniania niestandardowego kodu do pobierania dokumentów. Możesz też zdefiniować niestandardowe żądania pobierania, które bazują na istniejących modułach do pobierania w Genkit, i stosować do nich zaawansowane techniki RAG (np. ponowne pozycjonowanie lub rozszerzenia promptów).

Proste moduły pobierania

Proste moduły do pobierania pozwalają łatwo przekonwertować istniejący kod na moduły:

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

Niestandardowe moduły pobierające

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 i rerank to coś, co trzeba wdrożyć samodzielnie – nie są dostępne w ramach platformy)

Potem wystarczy wymienić na inny retrievera:

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