Nel mondo dell'intelligenza artificiale, l'integrazione di modelli linguistici avanzati e database di informazioni è fondamentale per creare applicazioni potenti e intelligenti. In questo articolo vedremo un tutorial su come sviluppare un sistema di Retrieval-Augmented Generation (RAG) utilizzando LlamaIndex, OpenAI e Qdrant.
Prerequisiti
Abbiamo già discusso in articoli precedenti su:
- Cosa sia LlamaIndex e quali siano le sue funzionalità principali;
- Come installare il database vettoriale Qdrant.
Inizieremo quindi questo tutorial su LlamaIndex dando per scontato che si conoscano i concetti alla base del framework e che ci sia già installato Qdrant in locale (necessario per la seconda parte del tutorial).
LlamaIndex viene distribuita in due versioni per Python e per Node. Nel nostro tutorial useremo la versione Python quindi è importante avere l’interprete del linguaggio già installato prima di iniziare. Nel caso in cui non sia già presente è possibile scaricarlo al seguente link: https://www.python.org/downloads/
Installazione
LlamaIndex può essere installato mediante il gestore dei pacchetti Python eseguendo in console il comando:
pip install llama-index
Inoltre, siccome useremo OpenAi come LLM, è necessario valorizzare la variabile d’ambiente con la relativa API key:
export OPENAI_API_KEY=XXXXX
La chiave può essere generata dalla console di OpenAI:
Tutorial LlamaIndex con OpenAI
Una volta completata l’installazione del framework e la configurazione di OpenAI, possiamo iniziare a sviluppare un primo caso d’uso elementare.
Come esempio proveremo a creare un Agente RAG che sia in grado di fornire risposte all’utente sulla base delle informazioni contenute in un documento PDF. Più nello specifico, useremo un PDF contenente l’export del nostro precedente articolo sull’architettura RAG (che potete scaricare mediante l’apposita funzionalità del browser).
Come prima cosa creeremo una cartella sul filesystem che contenga:
- Una cartella “docs” con dentro il file PDF
- Un file “test.py” che conterrà il codice Python del nostro programma.
Il codice del nostro primo programma sarà il seguente:
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
docs = SimpleDirectoryReader("docs").load_data()
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()
print('Enter your question:')
q = input()
response = query_engine.query(q)
print(response)
Proviamo ora a commentarlo per spiegare il funzionamento del framework:
- Come prima cosa viene inizializzato un estrattore documentale di tipo SimpleDirectoryReader che legge tutti i documenti presenti all’interno della cartella “docs”. I documenti vengono quindi caricati in memoria e vengono normalizzati.
- Viene inizializzato il VectorStoreIndex che trasforma i documenti caricati allo step precedente in vettori (embeddings). Questi vettori vengono salvati in memoria insieme ad un’insieme di metadati che poi possono essere utilizzati per effettuare ricerche.
- L’indice viene quindi trasformato in un motore di Q&A che rappresenta il cuore dell’agente RAG.
Le istruzioni successive non fanno altro che ricevere una domanda tramite l’input dell’utente e processarla mediante il motore di Q&A per ottenere una risposta da restituire in output.
Come potete vedere, con pochissime istruzioni abbiamo realizzato un programma in grado di fornire risposte contestualizzate sulla base di uno o più documenti forniti in input. Questo semplice esempio ci permette di apprezzare a pieno la potenza di LlamaIndex.
Per testare il programma è sufficiente eseguirlo in console mediante il comando:
python3 test.py
Naturalmente se state usando una versione di Python diversa dalla 3 sarà necessario modificare il comando di conseguenza.
Provando a chiedere all’agente “what is a RAG architecture?” dovrebbe essere in grado di fornire una risposta soddisfacente.
Salvare gli embeddings in memoria
Il codice dell’esempio precedente presenta un problema abbastanza evidente in quanto ad ogni esecuzione andrà a ricalcolare i vettori di tutti i documenti passati in input. Fortunatamente LlamaIndex permette di salvare gli embeddings in un vectorstore per evitare di doverli ogni volta ricalcolare. Così facendo si possono risparmiare molti token.
Per utilizzare il vectorstore in-memory creeremo un programma “test2.py” come segue:
import os.path
from llama_index.core import (
VectorStoreIndex,
SimpleDirectoryReader,
StorageContext,
load_index_from_storage,
)
VECTORSTORE_DIR = "data";
if not os.path.exists(VECTORSTORE_DIR):
documents = SimpleDirectoryReader("docs").load_data()
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir=VECTORSTORE_DIR)
else:
storage_context = StorageContext.from_defaults(persist_dir=VECTORSTORE_DIR)
index = load_index_from_storage(storage_context)
query_engine = index.as_query_engine()
print('Enter your question:')
q = input()
response = query_engine.query(q)
print(response)
Eseguendolo noteremo che verrà creata una nuova cartella all’interno del nostro ambiente di lavoro all’interno della quale troveremo i file JSON che compongono l’indice generato.
La modifica principale che abbiamo apportato al codice è stato l’utilizzo dell’istruzione “index.storage_context.persist(…)” che permette di salvare l’indice in una cartella locale. Il programma va quindi a ricalcolare gli embeddings solo se non trova questa cartella già presente nel filesystem.
Integrazione con database vettoriali
L’approccio di salvare i dati in memoria può essere comodo e permette lo sviluppo rapido di applicazioni RAG ma quando i dati iniziano a diventare tanti è necessario adottare una soluzione meglio strutturata. Fortunatamente LlamaIndex supporta l’integrazione con numerosi vectorstore.
In questo nostro tutorial useremo Qdrant che abbiamo già visto in altre occasioni. Per poterlo usare è necessario installare l’apposito pacchetto Python che funge da connettore:
pip install llama-index-vector-stores-qdrant
Per salvare i vettori all’interno del vectorstore è sufficiente indicare al VectorStoreIndex che deve usare il QdrantVectorStore per persistere i documenti:
import qdrant_client
from llama_index.core import (
VectorStoreIndex,
SimpleDirectoryReader,
StorageContext,
)
from llama_index.vector_stores.qdrant import QdrantVectorStore
client = qdrant_client.QdrantClient(
host="localhost",
port=6333
)
vector_store = QdrantVectorStore(client=client, collection_name="llamaindex_collection")
storage_context = StorageContext.from_defaults(vector_store=vector_store)
documents = SimpleDirectoryReader("docs").load_data()
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
)
Ancora una volta il codice risulta molto semplice ed intuitivo. Ovviamente abbiamo dovuto definire un QdrantClient indicando l’host del server e la porta di connessione (6333 di default). Abbiamo inoltre definito una collection “llamaindex_collection” per salvare gli embeddings generati dal programma.
Eseguendo questo programma e accedendo alla console di Qdrant (http://localhost:6333/dashboard) sarà possibile verificare il caricamento dell’indice.
Una volta salvati i documenti nel vectorstore è possibile usarli come in quest’ultimo esempio:
import qdrant_client
from llama_index.core import (
VectorStoreIndex,
)
from llama_index.vector_stores.qdrant import QdrantVectorStore
client = qdrant_client.QdrantClient(
host="localhost",
port=6333
)
vector_store = QdrantVectorStore(client=client, collection_name="llamaindex_collection")
index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
query_engine = index.as_query_engine()
print('Enter your question:')
q = input()
response = query_engine.query(q)
print(response)
Al termine del tutorial possiamo dire di aver messo in pratica tutto ciò che serve per implementare un Agente RAG. La semplicità del codice e la rapidità con cui lo abbiamo sviluppato è veramente sorprendente e conferma che LlamaIndex è un framework molto potente e versatile.