Firebase Genkit fournit des abstractions qui vous aident à créer des flux de génération augmentée de récupération (RAG) ainsi que des plug-ins qui fournissent des intégrations avec les 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 éventail de documents, leur utilisation pratique nécessite souvent des connaissances spécifiques dans un domaine spécifique (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 votre source de données en continu 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 deux abstractions principales pour vous aider à effectuer une analyse RAG :
- Indexeurs: ajoutent des documents à un "index".
- Dispositifs d'intégration: transforme 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 larges à leur intention, car Genkit n'a pas de opinion sur ce qu'est un "index" ou sur la manière exacte dont les documents sont extraits de celui-ci. Genkit ne fournit qu'un format Document
. 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 (opaque) représente les concepts exprimés par un passage de texte. Ceux-ci sont générés à l'aide de modèles de ML à usage spécifique. 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 :
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 fragmentation intégrées. Toutefois, il existe des bibliothèques Open Source compatibles avec Genkit.
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.
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).
Outils d'intégration
Un outil d'intégration 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 prend en charge les indexeurs et les récupérateurs via son système de plug-ins. Les plug-ins suivants sont officiellement compatibles :
- Base de données vectorielle cloud Pinecone
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 :
- PostgreSQL avec
pgvector
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 des dépendances
Dans cet exemple, nous utiliserons la bibliothèque textsplitter
de langchaingo
et la bibliothèque d'analyse PDF ledongthuc/pdf
:
go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf
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
// 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)
}
Créer une configuration de fragmentation
Cet exemple utilise la bibliothèque textsplitter
, qui fournit un simple séparateur de texte pour diviser les documents en segments pouvant être vectorisés.
La définition suivante configure la fonction de segmentation pour renvoyer des segments de document de 200 caractères, avec un chevauchement de 20 caractères entre les segments.
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200),
textsplitter.WithChunkOverlap(20),
)
Pour en savoir plus sur le découpage de cette bibliothèque, consultez la documentation langchaingo
.
Définir votre flux d'indexation
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
}
Exécuter le flux d'indexation
genkit flow:run indexMenu "'menu.pdf'"
Après l'exécution du flux indexMenu
, la base de données vectorielle reçoit des documents et est 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.
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)))
})
Écrire vos propres indexeurs et récupérateurs
Vous pouvez également créer votre propre récupérateur. Cela est utile si vos documents sont gérés dans un magasin 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 récupérer 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 l'extension de requête) en plus.
Par exemple, supposons que vous souhaitiez utiliser une fonction de reclassement personnalisée. L'exemple suivant définit un récupérateur personnalisé qui applique votre fonction au récupérateur de menu défini précédemment :
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
},
)