Firebase Genkit fornisce astrazioni che ti aiutano a creare flussi di Retrieval Augmented Generation (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 poterlo fare perché, sebbene gli LLM vengano in genere addestrati su un'ampia gamma di materiale, il loro utilizzo pratico 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 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.
La RAG è un'area molto ampia e esistono molte tecniche diverse per ottenere una RAG di qualità superiore. Il framework Genkit di base offre tre astratti principali per aiutarti a eseguire la 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 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 flusso di importazione tipico 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 "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 embedding per ogni chunk. A seconda del database in uso, puoi farlo esplicitamente con un modello di generazione di embedding o 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, 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.
Retriever
Un retriever è un concetto che racchiude la logica relativa a qualsiasi tipo di recupero di documenti. I casi di recupero più comuni in genere includono il recupero da datastore 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:
- Repository di vettori Cloud Firestore
- Vertex AI Vector Search
- Database vettoriale Chroma DB
- Database vettoriale cloud Pinecone
Inoltre, Genkit supporta i seguenti repository 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 | Incorporamento di testo di Gecko |
Google Vertex AI | Incorporamento di testo di Gecko |
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 per l'elaborazione dei PDF
npm install llm-chunk pdf-parse @genkit-ai/dev-local-vectorstore
npm i -D --save @types/pdf-parse
Aggiungere un repository di vettori locale alla configurazione
import {
devLocalIndexerRef,
devLocalVectorstore,
} from '@genkit-ai/dev-local-vectorstore';
import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai';
import { z, genkit } from 'genkit';
const ai = genkit({
plugins: [
// vertexAI provides the textEmbedding004 embedder
vertexAI(),
// the local vector store requires an embedder to translate from text to vector
devLocalVectorstore([
{
indexName: 'menuQA',
embedder: textEmbedding004,
},
]),
],
});
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 similarità vettoriale basato su file locale fornito da Genkit out-of-the-box per test e prototipazione semplici (non da utilizzare in produzione)
Crea l'indice
export const menuPdfIndexer = devLocalIndexerRef('menuQA');
Crea configurazione di chunking
Questo esempio utilizza la libreria llm-chunk
, che fornisce un semplice divisore di testo per suddividere i documenti in segmenti che possono essere vettorizzati.
La definizione seguente configura la funzione di suddivisione in blocchi per garantire un segmento di documento compreso tra 1000 e 2000 caratteri, suddiviso alla fine di una frase, con una sovrapposizione tra blocchi di 100 caratteri.
const chunkingConfig = {
minLength: 1000,
maxLength: 2000,
splitter: 'sentence',
overlap: 100,
delimiters: '',
} as any;
Altre opzioni di suddivisione per questa libreria sono disponibili nella documentazione di llm-chunk.
Definisci il flusso dell'indice
import { Document } from 'genkit/retriever';
import { chunk } from 'llm-chunk';
import { readFile } from 'fs/promises';
import path from 'path';
import pdf from 'pdf-parse';
async function extractTextFromPdf(filePath: string) {
const pdfFile = path.resolve(filePath);
const dataBuffer = await readFile(pdfFile);
const data = await pdf(dataBuffer);
return data.text;
}
export const indexMenu = ai.defineFlow(
{
name: 'indexMenu',
inputSchema: z.string().describe('PDF file path'),
outputSchema: z.void(),
},
async (filePath: string) => {
filePath = path.resolve(filePath);
// Read the pdf.
const pdfTxt = await run('extract-text', () =>
extractTextFromPdf(filePath)
);
// Divide the pdf text into segments.
const chunks = await run('chunk-it', async () =>
chunk(pdfTxt, chunkingConfig)
);
// Convert chunks of text into documents to store in the index.
const documents = chunks.map((text) => {
return Document.fromText(text, { filePath });
});
// Add documents to the index.
await ai.index({
indexer: menuPdfIndexer,
documents,
});
}
);
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 l'esempio di indicizzatore, questo esempio utilizza il recupero di vettori basato su file di Genkit, che non devi utilizzare in produzione.
import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';
// Define the retriever reference
export const menuRetriever = devLocalRetrieverRef('menuQA');
export const menuQAFlow = ai.defineFlow(
{ name: 'menuQA', inputSchema: z.string(), outputSchema: z.string() },
async (input: string) => {
// retrieve relevant documents
const docs = await ai.retrieve({
retriever: menuRetriever,
query: input,
options: { k: 3 },
});
// generate a response
const { text } = await ai.generate({
prompt: `
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.
Question: ${input}`,
docs,
});
return text;
}
);
Scrivi i tuoi indicizzatori e recuperatori
È 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 basati su quelli esistenti in Genkit e applicare tecniche RAG avanzate (come il ricalcolo del ranking o le estensioni dei prompt).
Simple Retrievers
I retriever semplici ti consentono di convertire facilmente il codice esistente in retriever:
import { z } from "genkit";
import { searchEmails } from "./db";
ai.defineSimpleRetriever(
{
name: "myDatabase",
configSchema: z
.object({
limit: z.number().optional(),
})
.optional(),
// we'll extract "message" from the returned email item
content: "message",
// and several keys to use as metadata
metadata: ["from", "to", "subject"],
},
async (query, config) => {
const result = await searchEmails(query.text, { limit: config.limit });
return result.data.emails;
}
);
Recuperatori personalizzati
import {
CommonRetrieverOptionsSchema,
} from 'genkit/retriever';
import { z } from 'genkit';
export const menuRetriever = devLocalRetrieverRef('menuQA');
const advancedMenuRetrieverOptionsSchema = CommonRetrieverOptionsSchema.extend({
preRerankK: z.number().max(1000),
});
const advancedMenuRetriever = ai.defineRetriever(
{
name: `custom/advancedMenuRetriever`,
configSchema: advancedMenuRetrieverOptionsSchema,
},
async (input, options) => {
const extendedPrompt = await extendPrompt(input);
const docs = await ai.retrieve({
retriever: menuRetriever,
query: extendedPrompt,
options: { k: options.preRerankK || 10 },
});
const rerankedDocs = await rerank(docs);
return rerankedDocs.slice(0, options.k || 3);
}
);
(extendPrompt
e rerank
sono elementi che dovrai implementare autonomamente,
non forniti dal framework)
A questo punto, puoi semplicemente sostituire il tuo retriever:
const docs = await ai.retrieve({
retriever: advancedRetriever,
query: input,
options: { preRerankK: 7, k: 3 },
});
Reranker e recupero in due fasi
Un modello di ricoordinamento, noto anche come cross-encoder, è un tipo di modello che, dato un query e un documento, restituisce un punteggio di somiglianza. Utilizziamo questo punteggio per riordinare i documenti in base alla pertinenza alla nostra query. Le API di riordinamento prendono un elenco di documenti (ad esempio l'output di un retriever) e riordinano i documenti in base alla loro pertinenza alla query. Questo passaggio può essere utile per perfezionare i risultati e assicurarsi che le informazioni più pertinenti vengano utilizzate nel prompt fornito a un modello generativo.
Esempio di riassegnazione del ranking
Un riassegnatore in Genkit è definito con una sintassi simile a quella dei retriever e degli indicizzatori. Ecco un esempio di utilizzo di un riassegnatore in Genkit. Questo flusso assegna un nuovo ranking a un insieme di documenti in base alla loro pertinenza alla query fornita utilizzando un riassegnatore di ranking predefinito di Vertex AI.
const FAKE_DOCUMENT_CONTENT = [
'pythagorean theorem',
'e=mc^2',
'pi',
'dinosaurs',
'quantum mechanics',
'pizza',
'harry potter',
];
export const rerankFlow = ai.defineFlow(
{
name: 'rerankFlow',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.array(
z.object({
text: z.string(),
score: z.number(),
})
),
},
async ({ query }) => {
const documents = FAKE_DOCUMENT_CONTENT.map((text) =>
({ content: text })
);
const rerankedDocuments = await ai.rerank({
reranker: 'vertexai/semantic-ranker-512',
query: ({ content: query }),
documents,
});
return rerankedDocuments.map((doc) => ({
text: doc.content,
score: doc.metadata.score,
}));
}
);
Questo riassegnatore utilizza il plug-in genkit di Vertex AI con semantic-ranker-512
per assegnare un punteggio e classificare i documenti. Più alto è il punteggio, più il documento è pertinente alla query.
Reranker personalizzati
Puoi anche definire ricollocatori personalizzati in base al tuo caso d'uso specifico. Questa operazione è utile quando devi rieseguire il ranking dei documenti utilizzando la tua logica personalizzata o un modello personalizzato. Ecco un semplice esempio di definizione di un riassegnatore personalizzato:
export const customReranker = ai.defineReranker(
{
name: 'custom/reranker',
configSchema: z.object({
k: z.number().optional(),
}),
},
async (query, documents, options) => {
// Your custom reranking logic here
const rerankedDocs = documents.map((doc) => {
const score = Math.random(); // Assign random scores for demonstration
return {
...doc,
metadata: { ...doc.metadata, score },
};
});
return rerankedDocs.sort((a, b) => b.metadata.score - a.metadata.score).slice(0, options.k || 3);
}
);
Una volta definito, questo riassegnatore personalizzato può essere utilizzato come qualsiasi altro riassegnatore nei flussi RAG, offrendoti la flessibilità di implementare strategie di riassegnazione avanzate.