Firebase Genkit bietet Abstraktionen, die Ihnen helfen, retrieval-augmentierte Generationen zu erstellen RAG-Abläufe sowie Plug-ins, die Integrationen mit zugehörigen Tools ermöglichen.
Was ist RAG?
Retrieval-Augmented Generation ist eine Technik, mit der externe Informationsquellen in die Antworten eines LLM einbinden. Das ist wichtig, weil LLMs zwar in der Regel mit einem umfangreichen Material trainiert werden, ihre praktische Verwendung jedoch oft spezifisches Fachwissen erfordert. Sie können beispielsweise ein LLM verwenden, um Kundenfragen zu den Produkten Ihres Unternehmens zu beantworten.
Eine Lösung besteht darin, das Modell mit spezifischeren Daten zu optimieren. Dies kann jedoch sowohl in Bezug auf die Rechenkosten als auch in Bezug auf den Aufwand für die Vorbereitung geeigneter Trainingsdaten teuer sein.
Im Gegensatz dazu bezieht RAG externe Datenquellen in einen Prompt ein, an das Modell übergeben werden. Angenommen, der Prompt „Welche Beziehung hat Bart zu Lisa?“ könnte durch Einfügen relevanter Informationen erweitert („augmentiert“) werden. Das würde zu folgendem Prompt führen: „Die Kinder von Homer und Marge heißen Bart, Lisa und Maggie. In welcher Beziehung steht Bart? an Lisa?“
Dieser Ansatz bietet verschiedene Vorteile:
- Dies kann kostengünstiger sein, da das Modell nicht neu trainiert werden muss.
- Sie können Ihre Datenquelle kontinuierlich aktualisieren und das LLM kann die aktualisierten Informationen sofort verwenden.
- Sie haben jetzt die Möglichkeit, in den Antworten Ihres LLM Verweise zu zitieren.
Auf der anderen Seite bedeutet die Verwendung von RAG längere Prompts und einige LLM API Gebühren für jedes von Ihnen gesendete Eingabe-Token. Letztendlich müssen Sie die Kostenabwägungen für Ihre Anwendungen bewerten.
RAG ist ein sehr großer Bereich und es gibt viele verschiedene Techniken, die verwendet werden, die beste RAG-Qualität. Das zentrale Genkit-Framework bietet zwei Hauptabstraktionen, Ihnen bei der RAG-Unterstützung zu helfen:
- Indexierungstools: Dokumente einem „Index“ hinzufügen.
- Einbettung: Wandelt Dokumente in eine Vektordarstellung um
- Retriever: Rufen Sie anhand einer Abfrage Dokumente aus einem „Index“ ab.
Diese Definitionen sind absichtlich weit gefasst, da zu Genkit keine Meinung ist.
was für ein „Index“ oder wie genau Dokumente daraus abgerufen werden. Nur Genkit
stellt ein Document
-Format bereit und alles andere wird vom Retriever oder
Indexierungs-Implementierungsanbieter.
Indexierungstools
Der Index verwaltet Ihre Dokumente so, dass Sie bei einer bestimmten Suchanfrage schnell relevante Dokumente abrufen können. Am häufigsten wird dies mithilfe einer Vektordatenbank erreicht, die Ihre Dokumente mithilfe von mehrdimensionalen Vektoren, sogenannten Einbettungen, indexiert. Eine Texteinbettung stellt (undurchsichtig) die Konzepte dar, die in einem Textabschnitt ausgedrückt werden. Sie werden mithilfe von ML-Modellen für spezielle Zwecke generiert. Durch das Indexieren von Text mithilfe seiner Einbettung kann eine Vektordatenbank thematisch ähnlichen Text clustern und Dokumente abrufen, die sich auf einen neuen Textstring (die Suchanfrage) beziehen.
Bevor Sie Dokumente zum Generieren abrufen können, müssen Sie in Ihren Dokumentenindex aufzunehmen. Ein typischer Datenaufnahmevorgang umfasst die folgenden Schritte:
Große Dokumente in kleinere Dokumente aufteilen, damit nur relevante Teile verwendet werden, um Ihre Prompts zu ergänzen – „Chunking“. Das ist notwendig, da viele LLMs ein begrenztes Kontextfenster haben, sodass es nicht praktikabel ist, ganze Dokumente in einen Prompt aufzunehmen.
Genkit bietet keine integrierten Chunking-Bibliotheken. Es gibt jedoch Open-Source-Bibliotheken, die mit Genkit kompatibel sind.
Generieren Sie Einbettungen für jeden Block. Je nach verwendeter Datenbank können Sie dies explizit mit einem Modell zur Generierung von Einbettungen tun oder den von der Datenbank bereitgestellten Einbettungsgenerator verwenden.
Fügen Sie den Text-Chunk und seinen Index zur Datenbank hinzu.
Möglicherweise führen Sie den Datenaufnahmefluss selten oder nur einmal aus, wenn Sie mit einer stabilen Datenquelle. Wenn Sie dagegen mit Daten arbeiten, der sich häufig ändert, können Sie den Datenaufnahmefluss für Beispiel in einem Cloud Firestore-Trigger, wenn ein Dokument aktualisiert wird).
Einbettungspartner
Ein Einbettungstool ist eine Funktion, die Inhalte (Text, Bilder, Audio usw.) annimmt und einen numerischen Vektor erstellt, der die semantische Bedeutung der ursprünglichen Inhalte codiert. Wie bereits erwähnt, werden Embedder im Rahmen des Indexierungsvorgangs eingesetzt. Sie können aber auch unabhängig verwendet werden, um Einbettungen ohne Index zu erstellen.
Retriever
Ein Retriever ist ein Konzept, das Logik für jede Art von Dokumentabruf umfasst. Die gängigsten Abruffälle umfassen in der Regel den Abruf aus Vektorspeichern. In Genkit kann ein Retriever jedoch jede Funktion sein, die Daten zurückgibt.
Zum Erstellen eines Retrievers können Sie eine der bereitgestellten Implementierungen oder und erstellen Sie Ihre eigenen.
Unterstützte Indexer, Abrufprogramme und Embedder
Genkit bietet über sein Plug-in-System Unterstützung für Indexer und Retriever. Die folgenden Plug-ins werden offiziell unterstützt:
- Pinecone-Cloud-Vektordatenbank
Darüber hinaus unterstützt Genkit die folgenden Vektorspeicher über vordefinierte Code-Vorlagen, die Sie an Ihre Datenbankkonfiguration und Schema:
- PostgreSQL mit
pgvector
Die folgenden Plug-ins unterstützen das Einbetten von Modellen:
Plug-in | Modelle |
---|---|
Generative KI von Google | Gecko-Texteinbettung |
Google Vertex AI | Gecko-Texteinbettung |
RAG-Flow definieren
Die folgenden Beispiele zeigen, wie Sie eine Sammlung von PDF-Dokumenten der Speisekarte aufnehmen können in eine Vektordatenbank importieren und sie zur Verwendung in einem Ablauf abrufen, der bestimmt, welche Lebensmittel verfügbar sind.
Abhängigkeiten installieren
In diesem Beispiel verwenden wir die textsplitter
-Bibliothek von langchaingo
und
die PDF-Parsing-Bibliothek ledongthuc/pdf
:
go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf
Indexierung definieren
Im folgenden Beispiel wird gezeigt, wie Sie einen Indexer erstellen, um eine Sammlung von PDF-Dokumenten aufzunehmen und in einer lokalen Vektordatenbank zu speichern.
Sie verwendet den lokalen dateibasierten Vektorähnlichkeitsabruf dass Genkit sofort einsatzfähig ist, um einfache Tests und Prototyping durchzuführen ( in der Produktion verwenden)
Indexierung erstellen
// 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)
if err != nil {
log.Fatal(err)
}
err = vertexai.Init(ctx, g, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
g,
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder(g, "text-embedding-004"),
},
)
if err != nil {
log.Fatal(err)
}
Aufteilungskonfiguration erstellen
In diesem Beispiel wird die textsplitter
-Bibliothek verwendet, die einen einfachen Textsplitter zum Aufteilen von Dokumenten in Segmente bietet, die vektorisiert werden können.
In der folgenden Definition wird die Segmentierungsfunktion so konfiguriert, dass Dokumentsegmente mit 200 Zeichen zurückgegeben werden, wobei sich die Segmente um 20 Zeichen überschneiden.
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200),
textsplitter.WithChunkOverlap(20),
)
Weitere Optionen für das Chunking dieser Bibliothek finden Sie in der langchaingo
-Dokumentation.
Indexierungsablauf definieren
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.WithIndexerDocs(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
}
Indexierungsvorgang ausführen
genkit flow:run indexMenu "'menu.pdf'"
Nach dem Ausführen des indexMenu
-Ablaufs wird die Vektordatenbank mit
Dokumente und können in Genkit-Abläufen mit Abrufschritten verwendet werden.
Ablauf mit Abruf definieren
Das folgende Beispiel zeigt, wie Sie einen Retriever in einem RAG-Ablauf verwenden können. Wie im Beispiel für den Indexer wird in diesem Beispiel der dateibasierte Vektorabruf von Genkit verwendet, der nicht für die Produktion geeignet ist.
ctx := context.Background()
g, err := genkit.Init(ctx)
if err != nil {
log.Fatal(err)
}
err = vertexai.Init(ctx, g, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
model := vertexai.Model(g, "gemini-1.5-flash")
_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
g,
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder(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.
docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
Query: 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.
return genkit.GenerateText(ctx, g,
ai.WithModel(model),
ai.WithMessages(
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)))
})
Eigene Indexierungs- und Abrufe schreiben
Sie können auch Ihren eigenen Retriever erstellen. Dies ist nützlich, wenn Ihre Dokumente werden in einem Dokumentenspeicher verwaltet, der in Genkit nicht unterstützt wird (z. B. MySQL, Google Drive usw.). Das Genkit SDK bietet flexible Methoden, mit denen Sie benutzerdefinierten Code zum Abrufen von Dokumenten angeben können.
Sie können auch benutzerdefinierte Retriever definieren, die auf vorhandenen Retrievern in Genkit aufbauen, und erweiterte RAG-Techniken wie die Neubewertung oder Prompterweiterung anwenden.
Angenommen, Sie haben eine benutzerdefinierte Funktion zur Neubewertung, die Sie verwenden möchten. Die Im folgenden Beispiel wird ein benutzerdefinierter Retriever definiert, der Ihre Funktion auf den den zuvor definierten Menü-Retriever:
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.
response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
Query: req.Query,
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
},
)