Retrieval-Augmented Generation (RAG)

Firebase Genkit bietet Abstraktionsschichten, mit denen Sie Retrieval Augmented Generation (RAG)-Abläufe erstellen können, sowie Plug-ins für die Integration mit ähnlichen Tools.

Was ist RAG?

Retrieval-Augmented Generation ist eine Technik, mit der externe Informationsquellen in die Antworten eines LLM eingebunden werden. Es ist wichtig, dass Sie dies tun können, da LLMs in der Regel mit einer Vielzahl von Materialien geschult werden. Der praktische Einsatz von LLMs erfordert jedoch häufig spezifisches Domänenwissen (z. B. können Sie 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 durch Einfügen relevanter Informationen erweitert („augmentiert“) 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 Hauptabstraktionsebenen, die Sie bei der RAG-Analyse unterstützen:

  • Indexierungstools: Dokumente einem „Index“ hinzufügen.
  • 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.

Indexierungstools

Der Index verwaltet Ihre Dokumente so, dass Sie bei einer bestimmten Suchanfrage schnell relevante Dokumente abrufen können. Dies erreichen Sie am häufigsten mit einer Vektordatenbank, die Ihre Dokumente mit mehrdimensionalen Vektoren indexiert, die als Einbettungen bezeichnet werden. Eine Texteinbettung stellt (nicht transparent) 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 sie in Ihren Dokumentindex aufnehmen. Ein typischer Datenaufnahmevorgang umfasst die folgenden Schritte:

  1. 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.

  2. 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.

  3. 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.

Einbettungsdienste

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 verwendet. Sie können jedoch 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. Zu den gängigsten Abrufvorgängen gehört normalerweise der 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:

Darüber hinaus unterstützt Genkit die folgenden Vektorspeicher über vordefinierte Codevorlagen, die Sie für Ihre Datenbankkonfiguration und Ihr Schema anpassen können:

Die folgenden Plug-ins unterstützen das Einbetten von Modellen:

Plug-in Modelle
Generative AI von Google Gecko-Texteinbettung
Google Vertex AI Gecko-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 festlegt, 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ähnlichkeits-Abruf verwendet, den Genkit direkt für einfache Tests und Prototyping bereitstellt (nicht in der Produktion verwenden).

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

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

Chunking-Konfiguration erstellen

In diesem Beispiel wird die Bibliothek textsplitter verwendet, die einen einfachen Text-Splitter bietet, um Dokumente in Segmente aufzuteilen, 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(
	"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 Dokumenten per Seed versehen 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()

	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-flash")

	_, 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.
			return ai.GenerateText(ctx, 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 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 möchten eine benutzerdefinierte Funktion zur Neubewertung des Rankings verwenden. 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 := 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
	},
)