Generowanie rozszerzone przez wyszukiwanie w zapisanych informacjach (RAG)

Genkit udostępnia abstrakcje, które ułatwiają tworzenie przepływów RAG, oraz wtyczki umożliwiające integrację z powiązanymi narzędziami.

Co to jest RAG?

Generowanie wspomagane przez wyszukiwanie to technika wykorzystywana do włączania zewnętrznych źródeł informacji do odpowiedzi LLM. Jest to ważne, ponieważ modele LLM są zwykle trenowane na podstawie szerokiego zbioru materiałów, ale ich praktyczne zastosowanie często wymaga znajomości konkretnej dziedziny (np. możesz chcieć używać modelu LLM do odpowiadania na pytania klientów dotyczące produktów Twojej firmy).

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

W tym przypadku zewnętrzne źródła danych są uwzględniane w promptach w momencie ich przekazywania do modelu. Wyobraźmy sobie na przykład prompt „Jaki jest związek Barta z Lisą?”. Może on zostać rozszerzony („wzbogacony”) o odpowiednie informacje, dzięki czemu prompt będzie brzmiał: „Dzieci Homera i Marge to Bart, Lisa i Maggie. „Jaki jest związek Barta z Lisą?”

Takie podejście ma kilka zalet:

  • Może to być bardziej opłacalne, ponieważ nie musisz ponownie trenować modelu.
  • Możesz stale aktualizować źródło danych, a LLM może natychmiast korzystać z zaktualizowanych informacji.
  • Teraz możesz zacytować źródła w odpowiedziach na pytania dotyczące LLM.

Z drugiej strony korzystanie z RAG oznacza dłuższe prompty, a niektóre usługi interfejsu LLM API pobierają opłaty za każdy wysłany token wejściowy. Ostatecznie musisz ocenić koszty związane z Twoimi aplikacjami.

RAG to bardzo szeroka dziedzina, a do uzyskania najlepszej jakości RAG służy wiele różnych technik. Podstawowa platforma Genkit oferuje 2 główne abstrakcje, które ułatwiają wykonywanie operacji RAG:

  • Indeksatory: dodawanie dokumentów do „indeksu”.
  • Embedders: przekształca dokumenty w postać wektorową
  • Retrievers: pobiera dokumenty z „indeksu” na podstawie zapytania.

Te definicje są celowo ogólne, ponieważ Genkit nie ma zdania na temat tego, czym jest „indeks” ani jak dokładnie dokumenty są z niego pobierane. Genkit udostępnia tylko format Document, a wszystko inne jest definiowane przez dostawcę implementacji wyszukiwarki lub indeksatora.

Indeksatory

Indeks odpowiada za śledzenie dokumentów w taki sposób, aby można było szybko pobrać odpowiednie dokumenty w odpowiedzi na konkretne zapytanie. Najczęściej odbywa się to za pomocą bazy danych wektorów, która indeksuje dokumenty za pomocą wielowymiarowych wektorów zwanych wektorami dystrybucyjnymi. Umieszczenie tekstu (nieprzejrzyście) przedstawia pojęcia wyrażone przez fragment tekstu; są one generowane za pomocą modeli ML do specjalnych celów. Dzięki indeksowaniu tekstu za pomocą jego uczenia się, baza danych wektorowej może grupować teksty o powiązaniach pojęciowych i pobierać dokumenty powiązane z nowym ciągiem tekstowym (zapytaniem).

Zanim będzie można pobrać dokumenty na potrzeby generowania, trzeba je zaimportować do indeksu dokumentów. Typowy proces przetwarzania obejmuje te czynności:

  1. Podziel duże dokumenty na mniejsze, aby do wzbogacania promptów były używane tylko odpowiednie fragmenty (tzw. „chunking”). Jest to konieczne, ponieważ wiele modeli LLM ma ograniczone okno kontekstu, co uniemożliwia uwzględnienie całych dokumentów z promptem.

    Genkit nie udostępnia wbudowanych bibliotek dzielenia na części, ale dostępne są biblioteki open source, które są z nim zgodne.

  2. wygenerować embeddingi dla każdego fragmentu; W zależności od używanej bazy danych możesz to zrobić za pomocą modelu generowania wektorów lub skorzystać z generatora wektorów udostępnianego przez bazę danych.

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

Jeśli pracujesz ze stabilnym źródłem danych, możesz uruchamiać przetwarzanie rzadko lub tylko raz. Jeśli jednak pracujesz z danymi, które często się zmieniają, możesz ciągle uruchamiać przetwarzanie (np. w wyzwalaczu Cloud Firestore, gdy dokument zostanie zaktualizowany).

Umieszczacze

Funkcja embedder przyjmuje dane (tekst, obrazy, dźwięk itp.) i tworzy wektor liczbowy, który koduje znaczenie semantyczne pierwotnych danych. Jak wspomnieliśmy powyżej, dostawcy usług wstawiania treści są wykorzystywani w ramach procesu indeksowania. Można ich jednak używać niezależnie do tworzenia wektorów bez indeksu.

Retrievery

Retriever to koncepcja, która zawiera logikę związaną z dowolnym rodzajem wyszukiwania dokumentów. Najczęstsze przypadki wyszukiwania to zwykle wyszukiwanie z magazynów wektorów. W Genkit funkcja retriever może jednak być dowolną funkcją zwracającą dane.

Aby utworzyć funkcję pobierania, możesz użyć jednej z dostępnych implementacji lub utworzyć własną.

Obsługiwane indeksatory, pobierający i wstawiający

Genkit obsługuje indeksator i wyszukiwarkę za pomocą systemu wtyczek. Te wtyczki są oficjalnie obsługiwane:

  • Pinecone – wektorowa baza danych w chmurze

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

Obsługa modela wektora dystrybucyjnego jest dostępna w tych wtyczkach:

Wtyczka Modele
Generatywna AI od Google Wektoryzacja tekstu

Definiowanie przepływu RAG

Poniższe przykłady pokazują, jak można przetworzyć kolekcję dokumentów PDF z menu restauracji do bazy danych wektorów i pobrać je do użycia w procesie, który określa, jakie produkty spożywcze są dostępne.

Instalowanie zależności

W tym przykładzie użyjemy biblioteki textsplitter z poziomu langchaingo oraz biblioteki do parsowania plików PDF ledongthuc/pdf:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

Definiowanie indeksatora

W tym przykładzie pokazujemy, jak utworzyć indeksator, który przetwarza zbiór dokumentów PDF i zapisuje je w lokalnej bazie danych wektorów.

Korzysta on z lokalnego modułu wyszukiwania podobieństwa wektorów w plikach, który Genkit udostępnia domyślnie na potrzeby prostego testowania i tworzenia prototypów. Nie używaj tego w wersji produkcyjnej.

Tworzenie indeksatora

// 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()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(g, "menuQA",
      localvec.Config{Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004")})
if err != nil {
    log.Fatal(err)
}

Tworzenie konfiguracji podziału na fragmenty

W tym przykładzie użyto biblioteki textsplitter, która udostępnia prosty rozdzielacz tekstowy do dzielenia dokumentów na segmenty, które można wektoryzować.

W tej definicji funkcja dzielenia na części jest skonfigurowana tak, aby zwracać segmenty dokumentu o długości 200 znaków, z nakładaniem się na 20 znaków.

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

Więcej opcji podziału tej biblioteki znajdziesz w dokumentacji langchaingo.

Definiowanie procesu indeksowania

genkit.DefineFlow(
    g, "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.WithDocs(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
}

Uruchamianie procesu indeksowania

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

Po uruchomieniu procesu indexMenu baza danych wektorów zostanie zasilona dokumentami i będzie gotowa do użycia w procesach Genkit z krokami wyszukiwania.

Definiowanie przepływu danych z odzyskiwaniem

Z tego przykładu dowiesz się, jak używać funkcji pobierania w ramach procesu RAG. Podobnie jak przykład indeksatora, ten przykład używa funkcji pobierania wektorów opartej na plikach Genkit, której nie należy używać w środowisku produkcyjnym.

ctx := context.Background()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

model := googlegenai.VertexAIModel(g, "gemini-1.5-flash")

_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
    g, "menuQA", localvec.Config{Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004")},
)
if err != nil {
    log.Fatal(err)
}

genkit.DefineFlow(
  g, "menuQA",
  func(ctx context.Context, question string) (string, error) {
    // Retrieve text relevant to the user's question.
    resp, err := ai.Retrieve(ctx, menuPdfRetriever, ai.WithTextDocs(question))


    if err != nil {
        return "", err
    }

    // Call Generate, including the menu information in your prompt.
    return genkit.GenerateText(ctx, g,
        ai.WithModelName("googleai/gemini-2.0-flash"),
        ai.WithDocs(resp.Documents),
        ai.WithSystem(`
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.`)
        ai.WithPrompt(question),
  })

pisać własne indeksatory i wyszukiwarki;

Możesz też utworzyć własnego retrievera. Jest to przydatne, jeśli dokumenty są zarządzane w magazynie dokumentów, który nie jest obsługiwany przez Genkit (np. MySQL, Dysk Google itp.). Pakiet SDK Genkit udostępnia elastyczne metody, które umożliwiają podanie niestandardowego kodu do pobierania dokumentów.

Możesz też zdefiniować niestandardowe moduły pobierania, które będą opierać się na istniejących modułach w Genkit i stosować zaawansowane techniki RAG (takie jak ponowne rankingowanie lub rozszerzenie prompta).

Załóżmy na przykład, że masz niestandardową funkcję ponownego rankingu, której chcesz użyć. W tym przykładzie definiujemy niestandardowy moduł pobierania, który stosuje Twoją funkcję do zdefiniowanego wcześniej modułu pobierania menu:

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}

advancedMenuRetriever := genkit.DefineRetriever(
    g, "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.
        resp, err := ai.Retrieve(ctx, menuPDFRetriever,
            ai.WithDocs(req.Query),
            ai.WithConfig(ocalvec.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
    },
)