Genkit bietet Abstraktionsschichten, mit denen Sie RAG-Workflows (Retrieval Augmented Generation) erstellen können, sowie Plug-ins für die Einbindung in ähnliche Tools.
Was ist RAG?
Die Retrieval-Augmented Generation ist eine Methode, mit der externe Informationsquellen in die Antworten eines LLM einbezogen werden. 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 genaueren 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.
Bei RAG werden externe Datenquellen dagegen in einen Prompt eingefügt, wenn er an das Modell übergeben wird. Angenommen, der Prompt „Welche Beziehung hat Bart zu Lisa?“ könnte erweitert („augmentiert“) werden, indem einige relevante Informationen vorangestellt werden. Das würde zu dem Prompt „Die Kinder von Homer und Marge heißen Bart, Lisa und Maggie. Was ist die Beziehung zwischen Bart und Lisa?“
Dieser Ansatz bietet verschiedene Vorteile:
- Das kann kostengünstiger sein, da Sie das Modell nicht neu trainieren müssen.
- 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 anzugeben.
Andererseits bedeutet die Verwendung von RAG natürlich längere Prompts. Außerdem berechnen einige LLM API-Dienste Gebühren für jedes gesendete Eingabetoken. Letztendlich müssen Sie die Kostenabwägungen für Ihre Anwendungen bewerten.
RAG ist ein sehr breites Gebiet und es gibt viele verschiedene Techniken, um die beste RAG-Qualität zu erzielen. Das Genkit-Framework bietet zwei Hauptabstraktionen, die Sie bei der RAG-Analyse unterstützen:
- Indexierungsprogramme: Dokumente werden einem „Index“ hinzugefügt.
- Einbettung: Wandelt Dokumente in eine Vektordarstellung um.
- Retriever: Rufen anhand einer Abfrage Dokumente aus einem „Index“ ab.
Diese Definitionen sind absichtlich weit gefasst, da Genkit keine Meinung dazu hat, was ein „Index“ ist oder wie genau Dokumente daraus abgerufen werden. Genkit bietet nur ein Document
-Format und alles andere wird vom Anbieter der Abruf- oder Indexierungsimplementierung definiert.
Indexierungsprogramme
Der Index verwaltet Ihre Dokumente so, dass Sie bei einer bestimmten Suchanfrage schnell relevante Dokumente abrufen können. Am häufigsten wird dies mit einer Vektordatenbank erreicht, die Ihre Dokumente mithilfe von mehrdimensionalen Vektoren, sogenannten Einbettungen, indexiert. Eine Text-Embedding stellt (undurchsichtig) die Konzepte dar, die in einem Textabschnitt ausgedrückt werden. Diese 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 sie in Ihren Dokumentindex aufnehmen. 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.
Erstellen Sie Einbettungen für jeden Teil. 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 der Datenbank den Textblock und seinen Index hinzu.
Wenn Sie mit einer stabilen Datenquelle arbeiten, können Sie den Datenaufnahmevorgang selten oder nur einmal ausführen. Wenn Sie dagegen mit Daten arbeiten, die sich häufig ändern, können Sie den Datenaufnahmevorgang kontinuierlich ausführen, z. B. in einem Cloud Firestore-Trigger, wenn ein Dokument aktualisiert wird.
Embedder
Ein Einbettungstool ist eine Funktion, die Inhalte (Text, Bilder, Audio usw.) annimmt und einen numerischen Vektor erstellt, der die semantische Bedeutung des ursprünglichen Inhalts codiert. Wie bereits erwähnt, werden eingebettete Inhalte im Rahmen des Indexierungsvorgangs genutzt. Sie können jedoch auch unabhängig voneinander 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.
Sie können einen Retriever mit einer der bereitgestellten Implementierungen erstellen oder eine eigene erstellen.
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 Codevorlagen, die Sie für Ihre Datenbankkonfiguration und Ihr Schema anpassen können:
- PostgreSQL mit
pgvector
Die folgenden Plug-ins unterstützen das Einbetten von Modellen:
Plug-in | Modelle |
---|---|
Generative AI von Google | Texteinbettung |
RAG-Flow definieren
In den folgenden Beispielen wird gezeigt, wie Sie eine Sammlung von PDF-Dokumenten mit Restaurantmenüs in eine Vektordatenbank aufnehmen und für die Verwendung in einem Ablauf abrufen können, der bestimmt, welche Speisen verfügbar sind.
Abhängigkeiten installieren
In diesem Beispiel verwenden wir die textsplitter
-Bibliothek von langchaingo
und die ledongthuc/pdf
-Bibliothek zum Parsen von PDFs:
go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf
Indexer 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.
Dabei wird der lokale dateibasierte Vektorähnlichkeitsabruf verwendet, der in Genkit für einfache Tests und das Prototyping bereits implementiert ist. Verwenden Sie diese Funktion nicht in der Produktionsumgebung.
Indexer 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, 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)
}
Chunking-Konfiguration 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 findest du 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.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
}
Indexierungsvorgang ausführen
genkit flow:run indexMenu "'menu.pdf'"
Nach dem Ausführen des indexMenu
-Ablaufs wird die Vektordatenbank mit Dokumenten gefüllt und kann 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, 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),
})
Eigene Indexer und Retriever schreiben
Sie können auch einen eigenen Retriever erstellen. Das ist nützlich, wenn Ihre Dokumente in einem Dokumentenspeicher verwaltet werden, der in Genkit nicht unterstützt wird (z. B. MySQL oder Google Drive). 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 zum Neubewerten, die Sie verwenden möchten. Im folgenden Beispiel wird ein benutzerdefinierter Abrufmechanismus definiert, der Ihre Funktion auf den zuvor definierten Menüabrufmechanismus anwendet:
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
},
)