In questa lezione, mettiamo tutto insieme quanto imparato fino a questo momento sulle RAG, partendo dal caricamento di un documento (solo uno a titolo di esempio) per arrivare al suo inserimento, sotto forma di set di vettori numerici, in un database vettoriale. Lo scopo del nostro esperimento sarà quello di eseguire query sul database, passando una richiesta e ricevendo quelle che il database reputa le frasi più affini del documento a quanto da noi indicato.
Come si può immaginare, l'esperimento potrà essere esteso, per esercizio, ad un numero maggiore di documenti per poter saggiare maggiormente la capacità di risposta del database. Ora però concentriamoci su un punto cardine.
Dalla progettazione della RAG alla messa in produzione
In questo esempio, otterremo delle risposte dal nostro database ma questo non basterà per una RAG in produzione. Ricordiamo infatti che tutto ciò che verrà estratto dal database vettoriale non costituirà la risposta per l'utente ma il contesto di ragionamento dell'LLM. La "G" di RAG sta per Generation, quindi l'atto finale consisterà nella risposta che un LLM da noi scelto genererà basandosi in parte su ciò che verrà estratto dal database vettoriale e in parte da tutto il resto degli elementi che inseriremo nel Prompt che useremo per indirizzare correttamente il modello a comporre la "vera" risposta per l'utente.
Questa considerazione è importante per capire non solo come nelle prossime lezioni concluderemo il nostro percorso di apprendimento delle RAG ma anche per comprendere quale sia il vero scopo di questa lezione: non formulare una risposta perfetta per l'utente ma selezionare le migliori frasi che mettano l'LLM in condizione di ragionare bene.
Alimentazione della RAG
Costruiamo il nostro procedimento di alimentazione della RAG. Seguiamo questi passaggi:
- preleviamo un documento da Wikipedia riguardante un determinato argomento;
- creiamo i chunk;
- vettorizziamo i documenti;
- inseriamo tutti i vettori nel database scelto (diciamo Chroma);
- eseguiamo una query.
Il codice di esempio
Ecco il codice (basato sui package langchain_community==0.3.27, langchain-openai==0.3.27, langchain-chroma==0.2.6, wikipedia) in cui i commenti ci guideranno tra le singole operazioni:
from langchain_community.document_loaders import WikipediaLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# raccogliamo un solo documento da Wikipedia su un argomento che ci interessa
loader = WikipediaLoader(query="Roma", lang="it",load_max_docs=1)
docs = loader.load()
doc = docs[0]
# questo stamperà i metadati del documento tra cui l'URL del documento scelto
print(doc.metadata)
print(f"Titolo del documento: {doc.metadata['title']}")
print(f"Lunghezza del documento: {len(doc.page_content)} caratteri")
# Splittiamo i documenti (l'unico, in questo caso)
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = splitter.split_documents(docs)
# verifichiamo in quante componenti è stato scisso
print(f"Numero di chunk ottenuti: {len(chunks)} chunk")
# procediamo alla vettorializzazione del documento con un modello OpenAI
embeddings = OpenAIEmbeddings(model="text-embedding-3-small",
api_key="sk-...")
# Memorizzazione dei vettori su Chroma
db = Chroma.from_documents(chunks, embedding=embeddings)
# eseguiamo la nostra prima query
query = "Chi ha fondato Roma?"
risultati = db.similarity_search_with_score(query, k=5)
# stampa dei risultati
for x in risultati:
print(x[0].page_content)
Esecuzione e richiesta
Dopo l'esecuzione e la richiesta "Chi ha fondato Roma?" vediamo che la prima risposta fornita - pertanto quello giudicato più simile dal database vettoriale - riporta il testo Fondata secondo la tradizione il 21 aprile 753 a.C. da Romolo (sebbene scavi recenti presso il Lapis niger indichino la... che inquadra esattamente ciò che abbiamo richiesto.
I parametri inseriti in fase di splitting creano dei chunk non troppo estesi e ciò spesso può non essere un vantaggio, soprattutto con il rischio di estrarre porzioni parziali di testo. L'importante è che i segmenti di documento che questi rappresentano contengano sufficienti informazioni da poter supportare bene il ragionamento dell'LLM.
Il database al momento è stato custodito in memoria in modo che ad ogni esecuzione sia riscritto. Ciò è ideale per un esercizio in cui la procedura viene ripetuta più volte ma la preparazione di una RAG in produzione vedrà il salvataggio del database su disco in modo che al momento del lancio del software non dovremo ripartire dai documenti ma sarà sufficiente avere il database già popolato.
Per fare in modo che il database sia memorizzato su disco, passo fondamentale per la messa in produzione, con Chroma è sufficiente aggiungere un parametro persist_directory riportante il nome del database:
db = Chroma.from_documents(chunks,
embedding=embeddings,
persist_directory="chroma_store")
Detto questo, siamo pronti per completare la compilazione di una RAG e sperimentarne una sua versione per la fase di produzione.
Se vuoi aggiornamenti su Implementazione di una RAG inserisci la tua email nel box qui sotto: