Retrieval-Augmented Generation (RAG)

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:

  1. 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.

  2. 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.

  3. 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:

Inoltre, Genkit supporta i seguenti archivi vettoriali tramite modelli di codice, che puoi personalizzare per la configurazione del database schema:

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

Code-first framework for orchestrating, deploying, and monitoring generative AI workflows.

Aggiornamento: Nov 6, 2024

Code-first framework for orchestrating, deploying, and monitoring generative AI workflows.

Aggiornamento: Jul 16, 2024

Code-first framework for orchestrating, deploying, and monitoring generative AI workflows.

Aggiornamento: Jan 13, 2025