Nella lezione precedente abbiamo completato la formazione di un database vettoriale su cui la nostra RAG sarà fondata. E' stata basata su un solo documento assolutamente insufficiente in un caso di produzione ma abbastanza per capire come funziona questo interessante meccanismo.
E' il momento di completarne il funzionamento, aggiungendo al database costruito un meccanismo di generazione di contenuti, la "G" di generation, l'ultima lettera dell'acronimo RAG.
Ciò che dovremo fare sarà ora implementare il recupero di documenti dal database per costruire un contesto di generazione su cui l'LLM dovrà lavorare. Questo è infatti il momento in cui fa ingresso, per la prima volta nel procedimento, un LLM. Useremo un modello GPT di OpenAI ma l'approccio potrà essere applicato a qualsiasi altro tipo di modello di Intelligenza Artificiale.
RAG: guidare la generazione
I passaggi necessari per il concatenamento dal database vettoriale al modello di generazione sarà focalizzato intorno al prompt che, come abbiamo già avuto ampiamente modo di discutere, non è semplicemente una "stringa da passare alla AI" come può sembrare da utenti neofiti ma un set di istruzioni complesso in cui spiegheremo all'LLM da dove partire, cosa generare e come fare tutto questo.
Nel nostro caso, il prompt verrà costruito dinamicamente con i seguenti passaggi:
- recuperiamo i documenti in base alla richiesta dell'utente (risultato della "retrieval", la "R" di RAG, in pratica);
- costruiamo un contesto aggregando i documenti recuperati, anche con una semplice concatenazione con simboli di "a capo";
- spieghiamo all'LLM che vogliamo generi una risposta basata sul contesto evitando di inserirvi informazioni prese da altre fonti. Se non trova la risposta nei documenti il modello deve dichiarare che non è in grado di rispondere. Ciò fornirà la vera forza di una RAG: lavorare solo limitatamente al territorio che le abbiamo preparato. Questo tuttavia non escluderà totalmente la possibilità dell'insorgere di allucinazioni di cui l'AI soffre.
L'ultimo punto rivela qualcosa di molto interessante a livello filosofico di una RAG: un prodotto AI non deve per forza essere onnisciente ma se, come in questo caso, è progettato per rispondere solo relativamente ad una certa base di conoscenza, senza fuoriuscire da questa, è assolutamente doveroso che ammetta di non avere a disposizione informazioni sufficienti per rispondere. Questo caso non solo non rappresenta un fallimento, in quanto parte del normale contratto di una RAG, ma offre anche spunti per l'ampliamento, se necessario, della knowledge base costruita.
Il codice
Usiamo come formazione del database il codice della lezione precedente riportandolo qui per praticità in maniera piuttosto "pulita".
Ricordiamo solo che le librerie Python da installare per il suo funzionamento sono:
pip install -U langchain langchain-community langchain-openai langchain-chroma wikipedia
mentre il codice che ci porterà ad avere un oggetto db con tutti documenti vettorializzati all'interno è:
from langchain_community.document_loaders import WikipediaLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
loader = WikipediaLoader(query="Roma", lang="it",load_max_docs=1)
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = splitter.split_documents(docs)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small", api_key="sk-...")
db = Chroma.from_documents(chunks, embedding=embeddings)
Avendo il database pronto, dobbiamo immediatamente scrivere il prompt:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template(
"""
Rispondi alle domande dell'utente utilizzando solo ed esclusivamente quello che hai trovato nel contesto
costruito sui documenti della RAG.
In questo caso il contesto è il seguente: {contesto}
La domanda dell'utente è questa: {domanda}
Se non conosci la risposta ammetti di non saper rispondere
"""
)
RAG: l'importanza delle istruzioni
Non sottovalutare mai questo punto! Sono fondamentali i documenti, i chunk, l'embedding ma istruzioni dettagliate nel prompt per l'LLM sono assolutamente fondamentali. Al suo interno deve essere fornito contesto e domanda ma anche indicazioni su come metterle in relazione per strutturare una risposta comprendendo anche tutto quello che concerne stile della risposta, capacità di segnalare la mancanza di informazioni e ruolo che deve assumere il modello.
A questo punto impostiamo modello e chain di concatenazione di prompt e LLM:
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
api_key="sk-..."
)
chain = prompt | llm
Quello che dobbiamo fare ora è:
- chiedere all'utente cosa vuole sapere (la variabile "domanda");
- recuperare i documenti in base alla domanda e costruire così il contenuto del contesto;
invocare il prompt passandogli domanda e contesto.
Prova di funzionamento
Facciamo una prova di funzionamento della RAG:
# RICHIESTA dell'utente
domanda = "Chi ha fondato Roma?"
# informazioni ottenute dal DATABASE
documenti = db.similarity_search(domanda, k=5)
# costruzione del CONTESTO partendo dai documenti
contesto = "\n\n".join(doc.page_content for doc in documenti)
# generazione della RISPOSTA
risposta = chain.invoke({
"contesto": contesto,
"domanda": domanda
})
print(f"Utente: {domanda}")
print(f"RAG: {risposta.content}")
ed otteniamo questo output:
Utente: Chi ha fondato Roma?
RAG: Roma è stata fondata secondo la tradizione il 21 aprile 753 a.C. da Romolo.
La risposta ci soddisfa e, come si sa, sarà necessario portare avanti la sperimentazione ma, nella prossima lezione, sarà il momento di comprendere come impacchettare la versione produzione di una RAG.
Se vuoi aggiornamenti su RAG: generazione della risposta inserisci la tua email nel box qui sotto: