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 permettent d'intégrer des outils associés.
Qu'est-ce que la 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 documents, 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, peut s'avérer coûteux en termes de coûts 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 la requête, "Quelle est la relation entre Bart et Lisa ?" peut être étendu ("augmenté") en des informations pertinentes, ce qui entraîne l'invite « Homer et Les enfants de Marge s'appellent Bart, Lisa et Maggie. Quelle est la relation entre Bart à Lisa ? »
Cette approche présente plusieurs avantages :
- Cela peut s'avérer 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 avez désormais la possibilité de 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 de coût de vos applications.
La génération augmentée de récupération est un domaine très vaste, et de nombreuses techniques différentes sont utilisées pour obtenir la meilleure qualité possible. Le framework Genkit de base propose deux abstractions principales pour vous aider à effectuer une analyse RAG :
- Indexeurs: ajoutent des documents à un "index".
- intégrateurs: transforment les documents en représentation vectorielle.
- Retrievers: récupèrent des documents dans 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.
Outils d'indexation
L'index assure le suivi de vos documents de telle sorte que vous pouvez récupérer rapidement les documents pertinents pour une requête spécifique. Il s'agit de généralement réalisée à l'aide d'une base de données vectorielle, qui indexe vos documents en utilisant de vecteurs multidimensionnels appelés représentations vectorielles continues. Représentation vectorielle continue de texte (opaque) représente les concepts exprimés par un passage de texte ; celles-ci sont générées à l'aide de modèles de ML à usage spécifique. En indexant le texte à l'aide de sa représentation vectorielle continue, un vecteur est capable de regrouper du texte lié au concept et de récupérer des documents liée à une nouvelle chaîne de texte (la requête).
Avant de pouvoir récupérer des documents en vue de les générer, vous devez de 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). C'est nécessaire car de nombreux LLM ont une fenêtre de contexte limitée, ce qui rend difficile inclure des documents entiers avec une requête.
Genkit ne fournit pas de bibliothèques de fragmentation intégrées. Cependant, il existe des de bibliothèques sources disponibles 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 peu fréquemment 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 intégrateur est une fonction qui prend du 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 outils de représentation vectorielle continue sont exploités dans le cadre du processus d'indexation. Toutefois, ils peuvent également être utilisés indépendamment pour créer des représentations vectorielles continues sans index.
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 Cependant, dans Genkit, un récupérateur peut correspondre à n'importe quelle fonction renvoyant des données.
Pour créer un récupérateur, vous pouvez utiliser l'une des implémentations fournies ou créer le 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 :
- Base de données vectorielle dans le cloud Pinecone
En outre, Genkit prend en charge les magasins de données vectorielles suivants via des modèles de code personnalisables, que vous pouvez personnaliser pour la configuration de votre base de données et schéma:
- PostgreSQL avec
pgvector
La prise en charge des modèles d'intégration 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 de 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 des tests et un 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()
g, err := genkit.Init(ctx)
if err != nil {
log.Fatal(err)
}
err = vertexai.Init(ctx, g, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
g,
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder(g, "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 séparateur de texte simple pour diviser les documents en segments pouvant être vectorisés.
La définition suivante configure la fonction de fragmentation pour renvoyer des documents segments de 200 caractères, avec un chevauchement entre des blocs de 20 caractères.
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'indexeur
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.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 données
et prêts à être utilisés 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()
g, err := genkit.Init(ctx)
if err != nil {
log.Fatal(err)
}
err = vertexai.Init(ctx, g, &vertexai.Config{})
if err != nil {
log.Fatal(err)
}
err = localvec.Init()
if err != nil {
log.Fatal(err)
}
model := vertexai.Model(g, "gemini-1.5-flash")
_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
g,
"menuQA",
localvec.Config{
Embedder: vertexai.Embedder(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.
docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
Query: 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 genkit.GenerateText(ctx, g,
ai.WithModel(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)))
})
Écrivez vos propres indexeurs et récupérateurs
Il est également possible de créer votre propre retriever. 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 aussi définir des récupérateurs personnalisés s'appuyant sur des récupérateurs existants dans Genkit et appliquer des techniques RAG avancées (comme le reclassement ou ) en haut.
Par exemple, supposons que vous souhaitiez utiliser une fonction de reclassement personnalisée. La l'exemple suivant définit un récupérateur personnalisé qui applique votre fonction au Menu "Retriever" défini précédemment:
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.
response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
Query: req.Query,
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
},
)