Firebase Genkit fornisce astrazioni che consentono di creare generazioni potenziate dal recupero (RAG) e plug-in che forniscono integrazioni con gli strumenti correlati.
Che cos'è la RAG?
La generazione aumentata del recupero è una tecnica utilizzata per incorporare fonti di informazioni nelle risposte di un LLM. È importante saper fare perché, mentre gli LLM sono in genere addestrati su un ampio corpus di l'uso pratico degli LLM richiede spesso una conoscenza settoriale specifica (ad Ad esempio, potresti voler usare un LLM per rispondere alle domande dei clienti domande su prodotti aziendali).
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, puoi immaginare il prompt, "Qual è la relazione di Bart con Lisa?" potrebbe essere espanso ("aumentato") di anteponendo alcune informazioni pertinenti, generando il prompt "Homer I figli di Marge si chiamano Bart, Lisa e Maggie. Qual è il rapporto tra Bart e Lisa?"
Questo approccio offre 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 alcune API LLM per ogni token di input che invii. 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 il RAG:
- Indexer: aggiungi 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 deliberatamente generiche perché Genkit non è ben definito
che "indice" o il modo in cui i documenti vengono recuperati esattamente. Solo Genkit
fornisce un formato Document
e tutto il resto è definito dal retriever o
di implementazione dell'indicizzatore.
Indicizzatori
L'indice è responsabile del monitoraggio dei tuoi documenti in modo da poter recuperare rapidamente i documenti pertinenti in base a una query specifica. Questo è il numero massimo spesso eseguito mediante un database vettoriale, che indicizza i documenti utilizzando vettori multidimensionali, chiamati incorporamenti. Un'evidenziazione del testo (in modo opaco) rappresenta i concetti espressi da un passaggio di testo; questi vengono generati utilizzando modelli ML speciali. Indicizzando il testo tramite la sua incorporamento, un vettore è in grado di raggruppare testi concettualmente correlati e recuperare documenti relative a una nuova stringa di testo (la query).
Prima di poter recuperare i documenti per la generazione, devi importarle 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 aumentare i prompt, ovvero "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 chunking integrate, ma sono disponibili librerie open source compatibili con Genkit.
Genera incorporamenti per ogni blocco. A seconda del database che utilizzi, puoi farlo esplicitamente con un modello di generazione dell'incorporamento, potrebbe usare il generatore di incorporamento fornito dal database.
Aggiungi il blocco 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, puoi eseguire continuamente il flusso di importazione (ad esempio, in un trigger Cloud Firestore, ogni volta che un documento viene aggiornato).
Incorporatori
Un incorporamento è una funzione che prende i contenuti (testo, immagini, audio e così via) e crea un vettore numerico che codifica il significato semantico del contenuto originale. Come accennato in precedenza, gli incorporatori vengono sfruttati nell'ambito del processo di indicizzazione, ma possono anche essere usati in modo indipendente per creare incorporamenti senza un indice.
Recuperatori
Un retriever è un concetto che incapsula la logica relativa a qualsiasi tipo di documento recupero. I casi di recupero più frequenti in genere includono Vectorstore, tuttavia in Genkit un retriever può essere qualsiasi funzione che restituisce dati.
Per creare un retriever, puoi usare una delle implementazioni fornite oppure e crearne uno tuo.
Indicizzatori, retriever e embedder supportati
Genkit offre supporto per indexer e retriever attraverso il proprio sistema di plug-in. La i seguenti plug-in sono ufficialmente supportati:
- Database vettoriale cloud Pinecone
Inoltre, Genkit supporta i seguenti archivi vettoriali tramite modelli di codice, che puoi personalizzare per la configurazione del database schema:
- PostgreSQL con
pgvector
Il supporto del modello di incorporamento viene fornito tramite i seguenti plug-in:
Plug-in | Modelli |
---|---|
IA generativa di Google | Incorporamento di testo di Gecko |
Google Vertex AI | Incorporamento del testo Geco |
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 indexer per importare una raccolta di documenti PDF e archiviarli in un database vettoriale locale.
Utilizza il retriever di similarità vettoriale basato su file locale fornito da Genkit per test e prototipazione semplici (non da utilizzare in produzione).
Crea l'indicizzatore
// 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)
}
Crea configurazione di chunking
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),
)
Ulteriori opzioni di chunking 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.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
}
Esegui il flusso dell'indice
genkit flow:run indexMenu "'menu.pdf'"
Dopo aver eseguito il flusso indexMenu
, il database vettoriale verrà distribuito con
documenti pronti per essere utilizzati nei flussi Genkit con passaggi di recupero.
Definisci 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 devi utilizzare in produzione.
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)))
})
Scrivi i tuoi indexer e retriever personali
È 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 offre metodi flessibili che consentono fornisci un codice personalizzato per recuperare i documenti.
Puoi anche definire recuperatori personalizzati basati su quelli esistenti in Genkit e applicare tecniche RAG avanzate (come il ranking o l'estensione del prompt).
Ad esempio, supponi di voler utilizzare una funzione personalizzata di riposizionamento. 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.
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
},
)