Retrieval-Augmented Generation (RAG)

Firebase Genkit bietet Abstraktionsschichten, mit denen Sie RAG-Workflows (Retrieval Augmented Generation) erstellen können, sowie Plug-ins für die Integration mit ähnlichen 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, für die praktische Anwendung von LLMs jedoch oft spezifisches Fachwissen erforderlich ist. Sie können beispielsweise einen 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 der RAG-Methode werden externe Datenquellen 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 der 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 drei Hauptabstraktionen, die Sie bei der RAG 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 mithilfe einer Vektordatenbank erreicht, die Ihre Dokumente mithilfe von 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. 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.

  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.

Einbettungspartner

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 Embedder im Rahmen des Indexierungsvorgangs eingesetzt. Sie können aber auch unabhängig verwendet werden, um ohne Index Einbettungen 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 eine der bereitgestellten Implementierungen verwenden 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

Die folgenden Beispiele zeigen, 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 für die Verarbeitung von PDFs installieren

npm install llm-chunk pdf-parse @genkit-ai/dev-local-vectorstore
npm i -D --save @types/pdf-parse

Lokalen Vektorspeicher zur Konfiguration hinzufügen

import {
  devLocalIndexerRef,
  devLocalVectorstore,
} from '@genkit-ai/dev-local-vectorstore';
import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai';
import { z, genkit } from 'genkit';

const ai = genkit({
  plugins: [
    // vertexAI provides the textEmbedding004 embedder
    vertexAI(),

    // the local vector store requires an embedder to translate from text to vector
    devLocalVectorstore([
      {
        indexName: 'menuQA',
        embedder: textEmbedding004,
      },
    ]),
  ],
});

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.

Es verwendet den lokalen dateibasierten Vektorähnlichkeitsabruf, den Genkit für einfache Tests und Prototypen bereitstellt (nicht in der Produktion verwenden).

Indexer erstellen

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Chunking-Konfiguration erstellen

In diesem Beispiel wird die llm-chunk-Bibliothek verwendet, die einen einfachen Textsplitter zum Aufteilen von Dokumenten in Segmente bietet, die vektorisiert werden können.

Mit der folgenden Definition wird die Segmentierungsfunktion so konfiguriert, dass ein Dokumentsegment zwischen 1.000 und 2.000 Zeichen lang ist, am Ende eines Satzes geteilt wird und eine Überlappung zwischen den Segmenten von 100 Zeichen besteht.

const chunkingConfig = {
  minLength: 1000,
  maxLength: 2000,
  splitter: 'sentence',
  overlap: 100,
  delimiters: '',
} as any;

Weitere Optionen für das Chunking dieser Bibliothek finden Sie in der llm-chunk-Dokumentation.

Indexierungsablauf definieren

import { Document } from 'genkit/retriever';
import { chunk } from 'llm-chunk';
import { readFile } from 'fs/promises';
import path from 'path';
import pdf from 'pdf-parse';

async function extractTextFromPdf(filePath: string) {
  const pdfFile = path.resolve(filePath);
  const dataBuffer = await readFile(pdfFile);
  const data = await pdf(dataBuffer);
  return data.text;
}

export const indexMenu = ai.defineFlow(
  {
    name: 'indexMenu',
    inputSchema: z.string().describe('PDF file path'),
    outputSchema: z.void(),
  },
  async (filePath: string) => {
    filePath = path.resolve(filePath);

    // Read the pdf.
    const pdfTxt = await run('extract-text', () =>
      extractTextFromPdf(filePath)
    );

    // Divide the pdf text into segments.
    const chunks = await run('chunk-it', async () =>
      chunk(pdfTxt, chunkingConfig)
    );

    // Convert chunks of text into documents to store in the index.
    const documents = chunks.map((text) => {
      return Document.fromText(text, { filePath });
    });

    // Add documents to the index.
    await ai.index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

Indexierungsvorgang ausführen

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

Nach dem Ausführen des indexMenu-Workflows wird die Vektordatenbank mit Dokumenten gefüllt und kann in Genkit-Workflows 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.

import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';

// Define the retriever reference
export const menuRetriever = devLocalRetrieverRef('menuQA');

export const menuQAFlow = ai.defineFlow(
  { name: 'menuQA', inputSchema: z.string(), outputSchema: z.string() },
  async (input: string) => {
    // retrieve relevant documents
    const docs = await ai.retrieve({
      retriever: menuRetriever,
      query: input,
      options: { k: 3 },
    });

    // generate a response
   const { text } = await ai.generate({
      prompt: `
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.

Question: ${input}`,
      docs,
    });

    return text;
  }
);

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 (z. B. Neubewertung oder Prompt-Erweiterungen) anwenden.

Simple Retrievers

Mit einfachen Retrievern können Sie vorhandenen Code ganz einfach in Retriever konvertieren:

import { z } from "genkit";
import { searchEmails } from "./db";

ai.defineSimpleRetriever(
  {
    name: "myDatabase",
    configSchema: z
      .object({
        limit: z.number().optional(),
      })
      .optional(),
    // we'll extract "message" from the returned email item
    content: "message",
    // and several keys to use as metadata
    metadata: ["from", "to", "subject"],
  },
  async (query, config) => {
    const result = await searchEmails(query.text, { limit: config.limit });
    return result.data.emails;
  }
);

Benutzerdefinierte Abrufprogramme

import {
  CommonRetrieverOptionsSchema,
} from 'genkit/retriever';
import { z } from 'genkit';

export const menuRetriever = devLocalRetrieverRef('menuQA');

const advancedMenuRetrieverOptionsSchema = CommonRetrieverOptionsSchema.extend({
  preRerankK: z.number().max(1000),
});

const advancedMenuRetriever = ai.defineRetriever(
  {
    name: `custom/advancedMenuRetriever`,
    configSchema: advancedMenuRetrieverOptionsSchema,
  },
  async (input, options) => {
    const extendedPrompt = await extendPrompt(input);
    const docs = await ai.retrieve({
      retriever: menuRetriever,
      query: extendedPrompt,
      options: { k: options.preRerankK || 10 },
    });
    const rerankedDocs = await rerank(docs);
    return rerankedDocs.slice(0, options.k || 3);
  }
);

(extendPrompt und rerank müssen Sie selbst implementieren, werden nicht vom Framework bereitgestellt)

Anschließend können Sie den Retriever einfach austauschen:

const docs = await ai.retrieve({
  retriever: advancedRetriever,
  query: input,
  options: { preRerankK: 7, k: 3 },
});

Reranker und zweistufiger Abruf

Ein Modell für die Neubewertung, auch als Cross-Encoder bezeichnet, ist ein Modell, das anhand einer Suchanfrage und eines Dokuments einen Ähnlichkeitsscore zurückgibt. Anhand dieser Bewertung werden die Dokumente nach Relevanz für die Suchanfrage neu angeordnet. Reranker-APIs nehmen eine Liste von Dokumenten (z. B. die Ausgabe eines Retrievers) und ordnen die Dokumente basierend auf ihrer Relevanz für die Suchanfrage neu an. Dieser Schritt kann hilfreich sein, um die Ergebnisse zu optimieren und dafür zu sorgen, dass die relevantesten Informationen im Prompt verwendet werden, der einem generativen Modell bereitgestellt wird.

Beispiel für den Reranker

Ein Reranker in Genkit wird in einer ähnlichen Syntax wie Retriever und Indexer definiert. Hier ist ein Beispiel für die Verwendung eines Rerankers in Genkit. Bei diesem Ablauf werden eine Reihe von Dokumenten anhand ihrer Relevanz für die angegebene Suchanfrage mithilfe eines vordefinierten Vertex AI-Neubewerters neu bewertet.

const FAKE_DOCUMENT_CONTENT = [
  'pythagorean theorem',
  'e=mc^2',
  'pi',
  'dinosaurs',
  'quantum mechanics',
  'pizza',
  'harry potter',
];

export const rerankFlow = ai.defineFlow(
  {
    name: 'rerankFlow',
    inputSchema: z.object({ query: z.string() }),
    outputSchema: z.array(
      z.object({
        text: z.string(),
        score: z.number(),
      })
    ),
  },
  async ({ query }) => {
    const documents = FAKE_DOCUMENT_CONTENT.map((text) =>
       ({ content: text })
    );

    const rerankedDocuments = await ai.rerank({
      reranker: 'vertexai/semantic-ranker-512',
      query:  ({ content: query }),
      documents,
    });

    return rerankedDocuments.map((doc) => ({
      text: doc.content,
      score: doc.metadata.score,
    }));
  }
);

Dieser Reranker verwendet das Vertex AI Genkit-Plug-in mit semantic-ranker-512, um Dokumente zu bewerten und zu ranken. Je höher der Wert, desto relevanter ist das Dokument für die Suchanfrage.

Benutzerdefinierte Neubewertung

Sie können auch benutzerdefinierte Reranker für Ihren spezifischen Anwendungsfall definieren. Das ist hilfreich, wenn Sie Dokumente mithilfe Ihrer eigenen benutzerdefinierten Logik oder eines benutzerdefinierten Modells neu sortieren möchten. Hier ein einfaches Beispiel für die Definition eines benutzerdefinierten Rerankers:

export const customReranker = ai.defineReranker(
  {
    name: 'custom/reranker',
    configSchema: z.object({
      k: z.number().optional(),
    }),
  },
  async (query, documents, options) => {
    // Your custom reranking logic here
    const rerankedDocs = documents.map((doc) => {
      const score = Math.random(); // Assign random scores for demonstration
      return {
        ...doc,
        metadata: { ...doc.metadata, score },
      };
    });

    return rerankedDocs.sort((a, b) => b.metadata.score - a.metadata.score).slice(0, options.k || 3);
  }
);

Nach der Definition kann dieser benutzerdefinierte Reranker wie jeder andere Reranker in Ihren RAG-Abläufen verwendet werden. So können Sie flexibler erweiterte Strategien für die Neubewertung implementieren.