Generowanie rozszerzone przez wyszukiwanie w zapisanych informacjach (RAG)

Firebase Genkit udostępnia abstrakcje, które ułatwiają tworzenie generacji rozszerzonego zakresu danych przepływy RAG oraz wtyczki umożliwiające integrację z powiązanymi narzędziami.

Co to jest RAG?

Generacja oparta na pobieraniu to technika wykorzystywana do uwzględniania źródeł informacji w odpowiedziach LLM. Ważne jest, by móc Chociaż duże modele językowe są zwykle trenowane na praktyczne zastosowania modeli LLM często wymagają specjalistycznej wiedzy z jego dziedziny (na Możesz na przykład użyć modelu LLM, aby odpowiadać na pytania klientów, Pytania dotyczące usług firmy).

Jednym z rozwiązań jest dostrojenie modelu przy użyciu bardziej szczegółowych danych. Jednak może być kosztowny zarówno pod względem kosztów mocy obliczeniowej, jak i nakładu pracy aby przygotować odpowiednie dane treningowe.

RAG działa natomiast, uwzględniając zewnętrzne źródła danych w prompcie czas przesyłania do modelu. Wyobraź sobie na przykład prompt, „Jaki jest związek Bartka z Lisą?” mogą zostać rozszerzone przez poprzedzają pewne istotne informacje i wyświetla się komunikat: „Homer Dzieci Magdy mają imiona Bart, Lisa i Maggie. Jaka jest relacja Bartka? do Lizy?”.

Takie podejście ma kilka zalet:

  • Może to być bardziej opłacalne, ponieważ nie trzeba ponownie trenować modelu.
  • Możesz stale aktualizować źródło danych, a LLM od razu korzystanie ze zaktualizowanych informacji.
  • Teraz masz możliwość powołania się na odniesienia w odpowiedziach LLM.

Z kolei używanie RAG oznacza w naturalny sposób dłuższe prompty i niektóre interfejsy LLM API. opłaty za każdy wysłany token wejściowy są naliczane. Ostatecznie należy ocenić obniżenia kosztów aplikacji.

RAG to bardzo szeroki obszar. Istnieje wiele różnych technik służących do najwyższej jakości. Podstawowa platforma Genkit opiera się na 2 głównych abstrakcjach może pomóc RAG-om:

  • Indeksy: dodawanie dokumentów do „indeksu”.
  • Umieszczanie: przekształca dokumenty do reprezentacji wektorowej
  • Moduły pobierania: pobieranie dokumentów z „indeksu” dla zapytania.

Te definicje są celowo szerokie, ponieważ firma Genkit nie ma o niej opinii co to znaczy „indeks” czyli jak dokładnie pobierane są z niego dokumenty. Tylko Genkit udostępnia format Document, a pozostałe są zdefiniowane przez retrievera lub dostawcy implementacji usługi indeksującej.

Indeksujący

Indeks odpowiada za śledzenie dokumentów w sposób można szybko pobrać odpowiednie dokumenty dla konkretnego zapytania. To najbardziej często można osiągnąć za pomocą wektorowej bazy danych, która indeksuje dokumenty za pomocą wielowymiarowych wektorów dystrybucyjnych. Osadzanie tekstu (nieprzezroczyste) reprezentuje koncepcje wyrażone przez fragment tekstu; są generowane za pomocą specjalnych modeli ML. Indeksowanie tekstu za pomocą jego wektora dystrybucyjnego baza danych jest w stanie grupować koncepcyjnie powiązany tekst i pobierać dokumenty lub powiązane z nowym ciągiem tekstowym (zapytaniem).

Aby móc pobrać dokumenty na potrzeby ich wygenerowania, musisz: ich pozyskanie do indeksu dokumentów. Typowy proces pozyskiwania danych :

  1. Podziel duże dokumenty na mniejsze, aby uwzględnić tylko te, które są istotne są wykorzystywane do rozszerzania promptów, Jest to konieczne ponieważ wiele LLM ma ograniczone okno kontekstu, przez co niepraktyczne jest aby uwzględnić całe dokumenty z promptem,

    Genkit nie udostępnia wbudowanych bibliotek do dzielenia na fragmenty; jednak istnieją otwarte i biblioteki źródłowe kompatybilne z Genkit.

  2. Wygeneruj wektory dystrybucyjne dla każdego fragmentu. W zależności od używanej bazy danych możesz to zrobić jawnie za pomocą modelu generowania wektora dystrybucyjnego lub może użyć generatora wektorów dystrybucyjnych udostępnianego przez bazę danych.

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

Jeśli pracujesz, proces pozyskiwania możesz uruchamiać nieregularnie lub tylko raz. ze stabilnym źródłem danych. Jeśli natomiast pracujesz z danymi, często się zmienia, możesz stale uruchamiać proces pozyskiwania (na przykład np. w aktywatorze Cloud Firestore za każdym razem, gdy dokument jest aktualizowany).

Umieszczanie

Umieszczanie na stronie 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 wspomnieliśmy powyżej, w procesie indeksowania wykorzystywane są obiekty osadzone, ale można ich też używać niezależnie do tworzenia wektorów dystrybucyjnych bez indeksu.

retrievery

retriever to koncepcja uwzględniająca logikę związaną z dowolnym dokumentem. pobieranie danych. Najczęściej są to pobrania z ale w programie Genkit retriever może być dowolną funkcją zwracającą dane.

Aby utworzyć retrievera, możesz użyć jednej z podanych implementacji lub tworzyć własne.

Obsługiwane moduły indeksujące, pobrane oraz umieszczone

Genkit zapewnia obsługę indeksowania i pobierania za pomocą systemu wtyczek. następujące wtyczki są oficjalnie obsługiwane:

Dodatkowo Genkit obsługuje następujące magazyny wektorowe za pomocą wstępnie zdefiniowanych szablonów kodu, które można dostosowywać pod kątem konfiguracji bazy danych schemat:

Obsługa modelu umieszczania jest zapewniana przez te wtyczki:

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

Definiowanie przepływu RAG

Poniższe przykłady pokazują, jak można pozyskać kolekcję dokumentów PDF z menu restauracji do bazy danych wektorowych i pobierać je do wykorzystania w procesie, który określa, które produkty spożywcze są dostępne.

Instalowanie zależności

W tym przykładzie użyjemy biblioteki textsplitter z witryn langchaingo oraz Biblioteka analizy plików PDF ledongthuc/pdf:

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

Definiowanie indeksującego

Poniższy przykład pokazuje, jak utworzyć mechanizm indeksujący do pozyskiwania kolekcji dokumentów PDF i przechowywanie ich w lokalnej bazie danych wektorowych.

Wykorzystuje narzędzie do pobierania podobieństw wektorowych na podstawie lokalnego pliku. które firma Genkit od razu udostępnia gotowe do testowania i tworzenia prototypów (nie wykorzystania w środowisku produkcyjnym)

Tworzenie indeksującego

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

err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
    log.Fatal(err)
}
err = localvec.Init()
if err != nil {
    log.Fatal(err)
}

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

Utwórz konfigurację dzielenia na fragmenty

W tym przykładzie użyto biblioteki textsplitter, która zawiera prosty tekst aby podzielić dokumenty na segmenty, które można poddać wektorze.

Poniższa definicja konfiguruje funkcję dzielenia na fragmenty, która zwraca dokument po 200 znaków z nakładaniem się po 20 znaków.

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

Więcej opcji dzielenia na fragmenty w tej bibliotece znajdziesz w dokumentacja langchaingo.

Zdefiniuj przepływ robota indeksującego

genkit.DefineFlow(
    "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 = menuPDFIndexer.Index(ctx, &ai.IndexerRequest{Documents: 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 indeksującego

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

Po uruchomieniu procesu indexMenu wektorowa baza danych zostanie zainicjowana ciągiem dokumentów i gotowych do użycia w przepływach Genkit wraz z instrukcjami pobierania.

Definiowanie procesu z pobieraniem

Poniższy przykład pokazuje, jak można użyć retrievera w przepływie RAG. Polub z indeksem – w tym przykładzie użyto opartego na plikach narzędzia do pobierania wektorów wektorowych Genkit, których nie należy używać w środowisku produkcyjnym.

    ctx := context.Background()

    err := vertexai.Init(ctx, &vertexai.Config{})
    if err != nil {
        log.Fatal(err)
    }
    err = localvec.Init()
    if err != nil {
        log.Fatal(err)
    }

    model := vertexai.Model("gemini-1.5-pro")

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

    genkit.DefineFlow(
        "menuQA",
        func(ctx context.Context, question string) (string, error) {
            // Retrieve text relevant to the user's question.
            docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
                Document: ai.DocumentFromText(question, nil),
            })
            if err != nil {
                return "", err
            }

            // Construct a system message containing the menu excerpts you just
            // retrieved.
            menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
            for _, doc := range docs.Documents {
                menuInfo.Content = append(menuInfo.Content, doc.Content...)
            }

            // Call Generate, including the menu information in your prompt.
            resp, err := model.Generate(ctx, &ai.GenerateRequest{
                Messages: []*ai.Message{
                    ai.NewSystemTextMessage(`
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.`),
                    menuInfo,
                    ai.NewUserTextMessage(question),
                },
            }, nil)
            if err != nil {
                return "", err
            }

            return resp.Text()
        })

Stwórz własne roboty indeksujące i mechanizmy retrievery

Możesz też stworzyć własnego retrievera. Jest to przydatne, jeśli dokumenty są zarządzane w magazynie dokumentów, który nie jest obsługiwany w Genkit (np. MySQL, Dysku Google itp.). Genkit SDK udostępnia elastyczne metody, udostępniasz niestandardowy kod pobierania dokumentów.

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

Załóżmy na przykład, że masz niestandardową funkcję ponownego pozycjonowania, której chcesz użyć. Ten przykład zdefiniował własny program do pobierania, który stosuje Twoją funkcję do program do pobierania menu zdefiniowane wcześniej:

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}
advancedMenuRetriever := ai.DefineRetriever(
    "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.
        response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
            Document: req.Document,
            Options:  localvec.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
    },
)