Génération augmentée de récupération (RAG)

Firebase Genkit fournit des abstractions qui vous aident à créer des flux de génération avec récupération (RAG), ainsi que des plug-ins qui permettent d'intégrer des outils associés.

Qu'est-ce que le RAG ?

La génération augmentée par récupération est une technique utilisée pour intégrer des sources d'informations externes dans les réponses d'un LLM. Il est important de pouvoir le faire, car, bien que les LLM soient généralement entraînés sur un large corpus de données, leur utilisation pratique nécessite souvent des connaissances spécifiques du domaine (par exemple, vous pouvez utiliser un LLM pour répondre aux questions des clients sur les produits de votre entreprise).

Une solution consiste à affiner le modèle à l'aide de données plus spécifiques. Toutefois, cela peut être coûteux à la fois en termes de coût de calcul et d'efforts nécessaires pour préparer des données d'entraînement adéquates.

En revanche, le RAG consiste à intégrer des sources de données externes dans une requête au moment où elle est transmise au modèle. Par exemple, vous pouvez imaginer que l'invite "Quelle est la relation de Bart avec Lisa ?" peut être étendue ("augmentée") en ajoutant des informations pertinentes au début, ce qui donne l'invite "Les enfants d'Homer et Marge s'appellent Bart, Lisa et Maggie. Quelle est la relation de Bart avec Lisa ?"

Cette approche présente plusieurs avantages :

  • Cela peut être plus rentable, car vous n'avez pas besoin de réentraîner le modèle.
  • Vous pouvez mettre à jour en permanence votre source de données, et le LLM peut immédiatement utiliser les informations mises à jour.
  • Vous pouvez désormais citer des références dans les réponses de votre LLM.

En revanche, l'utilisation du RAG implique naturellement des requêtes plus longues, et certains services d'API LLM facturent chaque jeton d'entrée que vous envoyez. En fin de compte, vous devez évaluer les compromis sur les coûts pour vos applications.

La classification RAG est un domaine très vaste, et de nombreuses techniques différentes sont utilisées pour obtenir la meilleure qualité de classification. Le framework Genkit de base propose trois abstractions principales pour vous aider à effectuer des RAG:

  • Indexeurs: ajoutent des documents à un "index".
  • Embeddings: transforment les documents en représentation vectorielle
  • Récupérateurs : récupèrent des documents à partir d'un "index", en fonction d'une requête.

Ces définitions sont volontairement larges, car Genkit n'a pas d'opinion sur ce qu'est un "index" ni sur la manière exacte dont les documents sont récupérés à partir de celui-ci. Genkit ne fournit qu'un format Document, et tout le reste est défini par le fournisseur d'implémentation du récupérateur ou de l'indexeur.

Indexeurs

L'index est chargé de suivre vos documents de manière à pouvoir récupérer rapidement les documents pertinents en fonction d'une requête spécifique. Pour ce faire, il est généralement recommandé d'utiliser une base de données vectorielle, qui indexe vos documents à l'aide de vecteurs multidimensionnels appelés embeddings. Une représentation vectorielle continue de texte représente (de manière opaque) les concepts exprimés par un passage de texte. Ils sont générés à l'aide de modèles de ML à usage spécial. En indexant le texte à l'aide de son empreinte, une base de données vectorielle peut regrouper le texte conceptuellement associé et récupérer les documents associés à une nouvelle chaîne de texte (la requête).

Avant de pouvoir récupérer des documents à des fins de génération, vous devez les ingérer dans votre index de documents. Un flux d'ingestion type effectue les opérations suivantes :

  1. Divisez les documents volumineux en documents plus petits afin que seules les parties pertinentes soient utilisées pour enrichir vos requêtes (segmentation). Cela est nécessaire, car de nombreux LLM ont une fenêtre de contexte limitée, ce qui rend difficile l'inclusion de documents entiers avec une invite.

    Genkit ne fournit pas de bibliothèques de segmentation intégrées. Toutefois, des bibliothèques Open Source compatibles avec Genkit sont disponibles.

  2. Générez des représentations vectorielles continues pour chaque segment. Selon la base de données que vous utilisez, vous pouvez le faire explicitement avec un modèle de génération d'embeddings ou utiliser le générateur d'embeddings fourni par la base de données.

  3. Ajoutez le bloc de texte et son index à la base de données.

Vous pouvez exécuter votre flux d'ingestion de manière peu fréquente ou une seule fois si vous travaillez avec une source de données stable. En revanche, si vous travaillez avec des données qui changent fréquemment, vous pouvez exécuter le flux d'ingestion en continu (par exemple, dans un déclencheur Cloud Firestore, chaque fois qu'un document est mis à jour).

Intégrateurs

Un outil d'embedding est une fonction qui prend un contenu (texte, images, audio, etc.) et crée un vecteur numérique qui encode la signification sémantique du contenu d'origine. Comme indiqué ci-dessus, les encodeurs sont utilisés dans le processus d'indexation. Toutefois, ils peuvent également être utilisés indépendamment pour créer des représentations vectorielles continues sans indice.

Retrievers

Un récupérateur est un concept qui encapsule la logique liée à tout type de récupération de documents. Les cas de récupération les plus courants incluent généralement la récupération à partir de magasins de vecteurs. Toutefois, dans Genkit, un récupérateur peut être n'importe quelle fonction qui renvoie des données.

Pour créer un récupérateur, vous pouvez utiliser l'une des implémentations fournies ou créer la vôtre.

Indexeurs, récupérateurs et intégrateurs compatibles

Genkit est compatible avec l'indexeur et le récupérateur via son système de plug-in. Les plug-ins suivants sont officiellement compatibles:

De plus, Genkit est compatible avec les magasins de vecteurs suivants via des modèles de code prédéfinis, que vous pouvez personnaliser pour votre configuration et votre schéma de base de données:

La prise en charge des modèles d'embeddings est assurée par les plug-ins suivants :

Plug-in Modèles
IA générative de Google Embedding textuel Gecko
Google Vertex AI Embedding textuel Gecko

Définir un flux RAG

Les exemples suivants montrent comment ingérer une collection de documents PDF de menus de restaurant dans une base de données vectorielle et les récupérer pour les utiliser dans un flux qui détermine les plats disponibles.

Installer les dépendances pour traiter les PDF

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

Ajouter un magasin de vecteurs local à votre configuration

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,
      },
    ]),
  ],
});

Définir un indexeur

L'exemple suivant montre comment créer un indexeur pour ingérer une collection de documents PDF et les stocker dans une base de données vectorielle locale.

Il utilise le récupérateur de similarité vectorielle basé sur des fichiers locaux que Genkit fournit prêt à l'emploi pour les tests et le prototypage simples (ne pas utiliser en production).

Créer l'indexeur

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Créer une configuration de segmentation

Cet exemple utilise la bibliothèque llm-chunk, qui fournit un séparateur de texte simple pour diviser les documents en segments pouvant être vectorisés.

La définition suivante configure la fonction de segmentation pour garantir un segment de document compris entre 1 000 et 2 000 caractères, divisé à la fin d'une phrase, avec un chevauchement entre les segments de 100 caractères.

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

Pour en savoir plus sur le découpage de cette bibliothèque, consultez la documentation llm-chunk.

Définir votre flux d'indexation

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

Exécuter le flux de l'indexeur

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

Après l'exécution du flux indexMenu, la base de données vectorielle sera enrichie de documents et prête à être utilisée dans les flux Genkit avec des étapes de récupération.

Définir un flux avec récupération

L'exemple suivant montre comment utiliser un récupérateur dans un flux RAG. Comme l'exemple d'indexeur, cet exemple utilise le récupérateur de vecteurs basé sur des fichiers de Genkit, que vous ne devez pas utiliser en production.

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

Écrire vos propres indexeurs et récupérateurs

Vous pouvez également créer votre propre récupérateur. Cette option est utile si vos documents sont gérés dans un entrepôt de documents non compatible avec Genkit (par exemple, MySQL, Google Drive, etc.). Le SDK Genkit fournit des méthodes flexibles qui vous permettent de fournir du code personnalisé pour extraire des documents. Vous pouvez également définir des récupérateurs personnalisés qui s'appuient sur les récupérateurs existants dans Genkit et appliquer des techniques avancées de génération augmentée de récupération (telles que le reclassement ou les extensions de requête) en plus.

Simple Retrievers

Les récupérateurs simples vous permettent de convertir facilement du code existant en récupérateurs:

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

Récupérateurs personnalisés

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 et rerank sont des éléments que vous devrez implémenter vous-même, et non fournis par le framework)

Vous pouvez ensuite remplacer votre récupérateur:

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

Reclassement et récupération en deux étapes

Un modèle de reclassement, également appelé encodeur croisé, est un type de modèle qui, étant donné une requête et un document, renvoie un score de similarité. Nous utilisons ce score pour réorganiser les documents en fonction de leur pertinence par rapport à notre requête. Les API de reclassement prennent une liste de documents (par exemple, la sortie d'un outil de récupération) et réorganisent les documents en fonction de leur pertinence par rapport à la requête. Cette étape peut être utile pour affiner les résultats et s'assurer que les informations les plus pertinentes sont utilisées dans l'invite fournie à un modèle génératif.

Exemple de reclassement

Dans Genkit, un outil de réajustement de classement est défini à l'aide d'une syntaxe semblable à celle des outils de récupération et d'indexation. Voici un exemple d'utilisation d'un outil de réajustement dans Genkit. Ce flux réorganise un ensemble de documents en fonction de leur pertinence par rapport à la requête fournie à l'aide d'un outil de réorganisation Vertex AI prédéfini.

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

Ce reranker utilise le plug-in genkit Vertex AI avec semantic-ranker-512 pour évaluer et classer les documents. Plus le score est élevé, plus le document est pertinent par rapport à la requête.

Reclasseurs personnalisés

Vous pouvez également définir des reclasseurs personnalisés en fonction de votre cas d'utilisation spécifique. Cela est utile lorsque vous devez réorganiser les documents à l'aide de votre propre logique personnalisée ou d'un modèle personnalisé. Voici un exemple simple de définition d'un outil de reclassement personnalisé:

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

Une fois défini, ce rerankeur personnalisé peut être utilisé comme n'importe quel autre rerankeur dans vos flux RAG, ce qui vous permet d'implémenter des stratégies de reclassement avancées.