Genkit fornisce astrazioni che ti aiutano a creare flussi di generazione basata sul recupero (RAG), nonché plug-in che forniscono integrazioni con strumenti correlati.
Che cos'è la RAG?
La Retrieval-Augmented Generation è una tecnica utilizzata per incorporare fonti di informazioni esterne nelle risposte di un LLM. È importante poterlo fare perché, sebbene gli LLM vengano in genere addestrati su un ampio corpus di materiale, l'uso pratico degli LLM richiede spesso conoscenze specifiche del dominio (ad esempio, potresti voler utilizzare un LLM per rispondere alle domande dei clienti sui prodotti della tua azienda).
Una soluzione è ottimizzare il modello utilizzando dati più specifici. Tuttavia, questo può essere costoso sia in termini di costi di calcolo sia in termini di impegno necessario per preparare dati di addestramento adeguati.
Al contrario, la RAG funziona incorporando origini dati esterne in un prompt al momento in cui viene passata al modello. Ad esempio, potresti immaginare che il prompt "Qual è il rapporto tra Bart e Lisa?" possa essere ampliato ("aumentato") anteponendo alcune informazioni pertinenti, ottenendo il prompt "I figli di Homer e Marge si chiamano Bart, Lisa e Maggie. Qual è il rapporto tra Bart e Lisa?"
Questo approccio presenta diversi vantaggi:
- Può essere più conveniente perché non devi addestrare nuovamente il modello.
- Puoi aggiornare continuamente l'origine dati e il modello LLM può utilizzare immediatamente le informazioni aggiornate.
- Ora hai la possibilità di citare riferimenti nelle risposte del tuo LLM.
D'altra parte, l'utilizzo di RAG comporta naturalmente prompt più lunghi e alcuni servizi API LLM addebitano un costo per ogni token di input inviato. Alla fine, devi valutare i compromessi in termini di costi per le tue applicazioni.
La RAG è un'area molto ampia e esistono molte tecniche diverse per ottenere una RAG di qualità superiore. Il framework Genkit di base offre due astratti principali per aiutarti a eseguire RAG:
- Indexer: aggiungono documenti a un "indice".
- Incorporatori: trasformano i documenti in una rappresentazione vettoriale
- Retriever: recuperano i documenti da un "indice", in base a una query.
Queste definizioni sono ampie intenzionalmente perché Genkit non ha opinioni su cosa sia un "indice" o su come vengono recuperati esattamente i documenti. Genkit fornisce solo un formato Document
e tutto il resto è definito dal provider di implementazione del retriever o dell'indice.
Indexer
L'indice è responsabile del monitoraggio dei tuoi documenti in modo da poter recuperare rapidamente i documenti pertinenti in base a una query specifica. Questo viene spesso ottenuto utilizzando un database vettoriale, che indicizza i documenti utilizzando vettori multidimensionali chiamati embedding. Un'evidenziazione del testo (in modo opaco) rappresenta i concetti espressi da un passaggio di testo; questi vengono generati utilizzando modelli ML specifici. Indicizzando il testo utilizzando il relativo embedding, un database vettoriale è in grado di raggruppare il testo concettualmente correlato e recuperare i documenti correlati a una nuova stringa di testo (la query).
Prima di poter recuperare i documenti a scopo di generazione, devi importarli nell'indice dei documenti. Un tipico flusso di importazione svolge quanto segue:
Suddividi i documenti di grandi dimensioni in documenti più piccoli in modo da utilizzare solo le parti pertinenti per migliorare i prompt, ovvero il "chunking". Questo è necessario perché molti LLM hanno una finestra di contesto limitata, il che rende impraticabile includere interi documenti con un prompt.
Genkit non fornisce librerie di suddivisione in blocchi integrate, ma sono disponibili librerie open source compatibili con Genkit.
Genera embedding per ogni chunk. A seconda del database in uso, puoi farlo esplicitamente con un modello di generazione di embedding o puoi utilizzare il generatore di embedding fornito dal database.
Aggiungi il frammento di testo e il relativo indice al database.
Se utilizzi un'origine dati stabile, puoi eseguire il flusso di importazione di rado o una sola volta. D'altra parte, se lavori con dati che cambiano di frequente, potresti eseguire continuamente il flusso di importazione (ad esempio, in un trigger Cloud Firestore, ogni volta che un documento viene aggiornato).
Incorporatori
Un embedder è una funzione che prende i contenuti (testo, immagini, audio e così via) e crea un vettore numerico che codifica il significato semantico dei contenuti originali. Come accennato in precedenza, gli inserzionisti vengono utilizzati nell'ambito del processo di indexing. Tuttavia, possono essere utilizzati anche in modo indipendente per creare embedding senza un indice.
Retriever
Un retriever è un concetto che incapsula la logica relativa a qualsiasi tipo di recupero di documenti. I casi di recupero più comuni in genere includono il recupero dai magazzini di vettori. Tuttavia, in Genkit un retriever può essere qualsiasi funzione che restituisce dati.
Per creare un retriever, puoi utilizzare una delle implementazioni fornite o crearne una personalizzata.
Indicizzatori, retriever e embedder supportati
Genkit fornisce il supporto per gli indicizzatori e i retriever tramite il proprio sistema di plug-in. I seguenti plug-in sono supportati ufficialmente:
- Database vettoriale cloud Pinecone
Inoltre, Genkit supporta i seguenti store di vettori tramite modelli di codice predefiniti, che puoi personalizzare in base alla configurazione e allo schema del database:
- PostgreSQL con
pgvector
Il supporto del modello di embedding viene fornito tramite i seguenti plug-in:
Plug-in | Modelli |
---|---|
IA generativa di Google | Embedding di testo |
Definire un flusso RAG
Gli esempi riportati di seguito mostrano come importare una raccolta di documenti PDF del menu di un ristorante in un database di vettori e recuperarli per utilizzarli in un flusso che determina quali sono gli articoli disponibili.
Installa le dipendenze
In questo esempio utilizzeremo la libreria textsplitter
di langchaingo
e la libreria di analisi del PDF ledongthuc/pdf
:
go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf
Definisci un indicizzatore
L'esempio seguente mostra come creare un indicizzatore per importare una raccolta di documenti PDF e archiviarli in un database di vettori locale.
Utilizza il recupero della somiglianza vettoriale basato su file locale fornito da Genkit out-of-the-box per test e prototipazione semplici. Non utilizzare questo in produzione.
Crea l'indice
// 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, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
log.Fatal(err)
}
if err = localvec.Init(); err != nil {
log.Fatal(err)
}
menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(g, "menuQA",
localvec.Config{Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004")})
if err != nil {
log.Fatal(err)
}
Crea configurazione di suddivisione in blocchi
Questo esempio utilizza la libreria textsplitter
, che fornisce un semplice suddivisore di testo per suddividere i documenti in segmenti che possono essere vettorizzati.
La definizione seguente configura la funzione di suddivisione in blocchi in modo da restituire segmenti di documento di 200 caratteri, con una sovrapposizione tra blocchi di 20 caratteri.
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200),
textsplitter.WithChunkOverlap(20),
)
Altre opzioni di suddivisione per questa libreria sono disponibili nella documentazione di langchaingo
.
Definisci il flusso dell'indice
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.WithDocs(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
}
Esegui il flusso dell'indice
genkit flow:run indexMenu "'menu.pdf'"
Dopo aver eseguito il flusso indexMenu
, il database vettoriale verrà seminato con i documenti e sarà pronto per essere utilizzato nei flussi Genkit con i passaggi di recupero.
Definire un flusso con recupero
L'esempio seguente mostra come utilizzare un retriever in un flusso RAG. Come l'esempio di indicizzatore, questo esempio utilizza il recupero di vettori basato su file di Genkit, che non deve essere utilizzato in produzione.
ctx := context.Background()
g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
log.Fatal(err)
}
if err = localvec.Init(); err != nil {
log.Fatal(err)
}
model := googlegenai.VertexAIModel(g, "gemini-1.5-flash")
_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
g, "menuQA", localvec.Config{Embedder: googlegenai.VertexAIEmbedder(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.
resp, err := ai.Retrieve(ctx, menuPdfRetriever, ai.WithTextDocs(question))
if err != nil {
return "", err
}
// Call Generate, including the menu information in your prompt.
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-2.0-flash"),
ai.WithDocs(resp.Documents),
ai.WithSystem(`
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.`)
ai.WithPrompt(question),
})
Scrivere indici e recuperatori personalizzati
È anche possibile creare un proprio retriever. Questa opzione è utile se i tuoi documenti vengono gestiti in un repository di documenti non supportato in Genkit (ad es. MySQL, Google Drive e così via). L'SDK Genkit fornisce metodi flessibili che ti consentono di fornire codice personalizzato per il recupero dei documenti.
Puoi anche definire recuperatori personalizzati che si basano su quelli esistenti in Genkit e applicare tecniche RAG avanzate (come il ranking o l'estensione del prompt).
Ad esempio, supponiamo che tu abbia una funzione di ricoordinamento personalizzata che vuoi utilizzare. Il seguente esempio definisce un retriever personalizzato che applica la funzione al retriever del menu definito in precedenza:
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.
resp, err := ai.Retrieve(ctx, menuPDFRetriever,
ai.WithDocs(req.Query),
ai.WithConfig(ocalvec.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
},
)