Retrieval-Augmented Generation (RAG)

Firebase Genkit fornisce astrazioni che ti aiutano a creare flussi di generazione basata su recupero (RAG), nonché plug-in che forniscono integrazioni con strumenti correlati.

Che cos'è la RAG?

La Retrieval-Augmented Generation è una tecnica utilizzata per integrare fonti di informazioni esterne nelle risposte di un LLM. È importante essere in grado di farlo perché, mentre gli LLM sono in genere addestrati su un'ampia gamma di materiali, l'uso pratico degli LLM richiede spesso una conoscenza specifica 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 è la relazione di Bart con 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.

RAG è un'area molto ampia e sono utilizzate molte tecniche diverse per ottenere la RAG di migliore qualità. Il framework Genkit di base offre due principali astratti 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 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 fatto spesso utilizzando 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 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 flusso di importazione tipico svolge quanto segue:

  1. Suddividi i documenti di grandi dimensioni in documenti più piccoli in modo da utilizzare solo le parti pertinenti per migliorare 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.

  2. Genera incorporamenti per ogni blocco. A seconda del database che stai utilizzando, potresti eseguire questa operazione in modo esplicito con un modello di generazione dell'incorporamento o utilizzare il generatore di incorporamento fornito dal database.

  3. 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, puoi 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 embedder vengono utilizzati nell'ambito del processo di indicizzazione, ma possono essere utilizzati anche in modo indipendente per creare embedding senza un indice.

Recuperatori

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 da archivi vettoriali, tuttavia in Genkit un retriever può essere qualsiasi funzione che restituisca 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:

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:

Il supporto del modello di embedding viene fornito tramite i seguenti plug-in:

Plug-in Modelli
IA generativa di Google Incorporamento del testo Geco
Google Vertex AI Incorporamento di testo di Gecko

Definizione di un flusso RAG

I seguenti esempi mostrano come importare una raccolta di documenti PDF del menù di un ristorante in un database vettoriale e recuperarli per utilizzarli in un flusso che determina quali alimenti sono 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 retriever di similitudine vettoriale basato su file locale fornito da Genkit pronto all'uso per test e prototipazione semplici (non utilizzare 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()

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

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 seguente definizione 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(
	"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à seminato con i documenti e sarà pronto per essere utilizzato nei flussi di Genkit con i passaggi di recupero.

Definire un flusso con recupero

L'esempio seguente mostra come utilizzare un retriever in un flusso RAG. Come nell'esempio dell'indicizzatore, questo esempio utilizza il retriever vettoriale basato su file di Genkit, che non dovresti utilizzare in produzione.

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

Scrivi i tuoi indicizzatori e recuperatori

È anche possibile creare un proprio retriever. Questo è utile se i documenti sono gestiti in un archivio documenti non supportato in Genkit (ad esempio 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 := 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
	},
)