Dataset Sintetici e Reverse Generation

Dataset Sintetici e Reverse Generation

Nello sviluppo di architetture (RAG) la validazione delle performance rappresenta spesso un collo di bottiglia. Nelle fasi iniziali, è comune fare affidamento su una validazione manuale, verificando le risposte del sistema su un numero limitato di documenti.

In scenari di produzione, questo approccio presenta una serie criticità:

  • Non è scalabile: Il testing non è sostenibile su Knowledge Base estese;
  • Assenza di Regression Testing: Senza un benchmark è impossibile determinare oggettivamente se una modifica, come un cambio nel chunk size o un aggiornamento del modello di embedding, abbia effettivamente migliorato o peggiorato il sistema.
  • Mancanza di Ground Truth: Possedere i documenti finali non è sufficiente per costruire un dataset di test, questi forniscono la conoscenza ma mancano le coppie domanda-risposta verificate (Ground Truth) necessarie per misurare l'accuratezza.

Dataset Sintetici e Reverse Generation

La risposta a queste problematiche risiede nell'utilizzo di dataset sintetici, ovvero informazioni generate tramite modelli di intelligenza artificiale dotati di reasoning.

Anziché affidarsi all'intervento umano per scrivere migliaia di domande, si implementa una pipeline di "Reverse Generation", in cui l'IA viene utilizzata per testare l'IA stessa.

Il concetto chiave è l'inversione del flusso:

  1. Si fornisce all'LLM (denominato "Dataset Builder") i frammenti di testo;
  2. Si istruisce il modello a formulare la domanda che un utente porrebbe per ottenere quella specifica informazione;
  3. Il modello estrae la risposta corretta, creando una coppia QA (Question-Answer) che costituisce il Golden Dataset.

Questo processo trasforma una documentazione passiva in un asset di testing attivo.

Implementazione Tecnica

La costruzione di un Dataset Builder richiede scelte architetturali precise per garantire la qualità del dato in uscita.

Chunking e Overlap

A differenza della fase di retrieval standard, durante la generazione del dataset l'overlap dei chunk viene impostato a 0 per garantire che ogni frammento di testo sia unico.

Questo evita la creazione di domande duplicate e su frasi ripetute a cavallo di due segmenti, massimizzando la varietà del dataset.

Output Strutturato

Gli LLM sono motori probabilistici e tendono a variare il formato di output. Per l'integrazione in pipeline automatizzate, è essenziale forzare una struttura dati rigida.

Librerie come Pydantic possono essere usate per obbligare il modello a restituire un oggetto JSON validato.

Un parser inietta automaticamente le istruzioni di formattazione nel prompt, assicurando che l'output sia programmaticamente utilizzabile dallo script di valutazione.

Sampling e API Economy

Generare un dataset completo su migliaia di pagine comporta un significativo carico computazionale.

Per ridurre i costi e l'overhead, si adotta una strategia di Random Sampling, in cui una percentuale rappresentativa di chunk viene selezionata casualmente per iterazione (ad esempio, 5/10%), riducendo così il numero di chiamate API necessarie.

Questo approccio è dettato anche da vincoli di API Economy. Un ciclo di test completo su 100 domande implica centinaia di chiamate API in pochi minuti:

  1. Generazione della domanda;
  2. Risposta dell'Agente;
  3. Valutazione del Giudice.

L'uso esclusivo di modelli "Large" (come Claude) renderebbe i costi poco sostenibili (salvo casi specifici). L'architettura prevede quindi un mix di modelli: modelli efficienti per la generazione massiva , riservando i modelli più performanti  per il ruolo di "Giudice".

È fondamentale evitare il Self-Evaluation Bias: se lo stesso modello viene utilizzato per generare domande, rispondere e valutare, il sistema tenderà a premiarsi eccessivamente.

Codice Python

Nelle ultime settimane ho esplorato e studiato Langchain, un framework versatile per l’integrazione e la gestione di modelli linguistici in flussi di lavoro.

Ho usato Python, principalmente per la sua semplicità e flessibilità, rispetto ad altri linguaggi, per implementare il processo di generazione e valutazione di un dataset sintetico.

E' consigliato creare un ambiente virtuale Python per mantenere isolate le dipendenze del progetto.

Per semplificare l'installazione delle librerie necessarie, puoi utilizzare un file requirements.txt, che include tutte le dipendenze del progetto.

.env

GOOGLE_API_KEY=GOOGLEAPIKEY

setup_vectordb.py

import os
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings

load_dotenv() # caricamento delle variabili d'ambiente

# Configurazione
path_cartella = "./documenti_pdf"
index_path = "faiss_index_locale"

print("CARICAMENTO PDF")
if not os.path.exists(path_cartella):
    print(f"❌ Errore: La cartella non è presente {path_cartella}")
    exit()

loader = DirectoryLoader(path_cartella, glob="*.pdf", loader_cls=PyPDFLoader)
docs = loader.load()

# Pulizia nomi file
for doc in docs:
    filename = os.path.basename(doc.metadata.get("source", ""))
    doc.metadata["source"] = filename
    doc.page_content = f"FONTE DOCUMENTO: {filename}\n\n{doc.page_content}"

print(f"✅ Caricate {len(docs)} pagine")

# Chunking 
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)

# Embedding e salvataggio 
print("🧠 Calcolo Embeddings...")
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")

db = FAISS.from_documents(splits, embeddings)
db.save_local(index_path)

print(f"🎉 Database salvato in: {index_path}")

evaluate_agent.py

import json
import os
import re
from dotenv import load_dotenv

load_dotenv()

from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.tools.retriever import create_retriever_tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# Setup agente
print("⚙️ Inizializzazione Agente...")

# Verifica indice
if not os.path.exists("faiss_index_locale"):
    print("❌ ERRORE CRITICO: Manca l'indice vettoriale 'faiss_index_locale'.")
    exit()

# Caricamento local DB
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = FAISS.load_local("faiss_index_locale", embeddings, allow_dangerous_deserialization=True)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

tool_pdf = create_retriever_tool(
    retriever,
    "cerca_nei_pdf",
    "Strumento per cercare informazioni nei documenti PDF."
)
tools = [tool_pdf]

# Dichiarazione del modello
llm_agent = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)

# Preparazione del promtp template
prompt = ChatPromptTemplate.from_messages([
    ("system", "Sei un assistente. Rispondi usando i documenti. Cita la fonte."),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = create_tool_calling_agent(llm_agent, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False) 

# Setup del validatore
llm_judge = ChatGoogleGenerativeAI(model="gemini-2.5-pro", temperature=0)

def valuta_risposta(domanda, risposta_attesa, risposta_agente):
    """
    Chiede all'LLM Giudice di dare un voto.
    """
    prompt_giudice = f"""
    Agisci come un esaminatore imparziale.
    
    DOMANDA: {domanda}
    RISPOSTA CORRETTA (Reference): {risposta_attesa}
    RISPOSTA AGENTE (Student): {risposta_agente}
    
    COMPITO:
    Valuta se la risposta dell'Agente contiene le informazioni chiave della Risposta Corretta.
    Ignora differenze di stile o formattazione.
    
    Assegna un voto da 0 a 10.
    Rispondi SOLO con il numero.
    """
    
    try:
        return llm_judge.invoke(prompt_giudice).content.strip()
    except Exception as e:
        return f"ERRORE GIUDICE: {e}"

# Ciclo di valutazione
file_dataset = "dataset_format.json"

if not os.path.exists(file_dataset):
    print(f"❌ Errore: Non trovo {file_dataset}.")
    exit()

with open(file_dataset, "r", encoding="utf-8") as f:
    dataset = json.load(f)

print(f"\n🧪 INIZIO TEST AUTOMATICO SU {len(dataset)} DOMANDE\n")
print("-" * 60)

voti_totali = []

for i, item in enumerate(dataset):
    q = item["question"]
    truth = item["answer"]
    
    print(f"📝 Test {i+1}: {q}")
    
    # Risposte dell'agente
    try:
        res_agente = agent_executor.invoke({"input": q})["output"]
    except Exception as e:
        res_agente = f"ERRORE AGENTE (Crash): {e}"

    # Valutazione del giudice
    voto_raw = valuta_risposta(q, truth, res_agente)
    
    # Pulizia e debug del voto
    # Estrarre il primo numero trovato nella stringa
    match = re.search(r'\d+', str(voto_raw))
    
    if match:
        voto = int(match.group())
        # Protezione: se il voto è fuori scala viene normalizzato
        if voto > 10: voto = 0
    else:
        voto = 0
    
    voti_totali.append(voto)
    
    # Stampa risultati
    print(f"🤖 Agente: {res_agente[:100]}...") 
    print(f"✅ Attesa: {truth[:100]}...")
    print(f"👀 GIUDICE RAW: '{voto_raw}'") # risposta RAW per troubleshooting
    print(f"🏆 VOTO FINALE: {voto}/10")
    print("-" * 60)

# Report finale
media = sum(voti_totali) / len(voti_totali) if voti_totali else 0

print(f"\n📊 RISULTATO FINALE")
print(f"Media Voti: {media:.1f}/10")

if media > 8:
    print("🟢 Il sistema funziona bene.")
elif media > 5:
    print("🟡 Il sistema va affinato")
else:
    print("🔴 Ci sono problemi tecnico o di contenuto.")

Il seguente codice non è stato scritto da uno sviluppatore professionista. Prima di utilizzarlo è altamente consigliato sottoporlo a un'analisi approfondita e a una revisione per garantirne l'efficienza e la sicurezza.

LLM-as-a-Judge

Una volta generato il JSON del Golden Dataset, una pipeline di valutazione chiude il cerchio:

  1. Retrieval & Generation: L'Agente risponde alla domanda sintetica;
  2. Judging: Un LLM "Giudice" confronta la risposta dell'Agente;
  3. Scoring: Viene assegnato un voto (0-10) basato sull'accuratezza semantica.

Tuttavia, un semplice numero non racconta tutta la storia. Un sistema RAG può fallire in punti distinti :

  • Context Recall (Recupero): La domanda è: "L'Agente ha trovato il PDF giusto?"

    • Errore tipico: Il retrieval ha fallito nel recuperare i chunk pertinenti e l'Agente risponde "Non lo so" o fornisce informazioni generiche.
  • Faithfulness (Fedeltà/Allucinazioni): La domanda è: "L'Agente ha inventato cose non scritte nel PDF?"

    • Errore tipico: Il retrieval ha funzionato ma il modello generativo allucina in parte nella risposta.
  • Answer Relevancy (Pertinenza): La domanda è: "L'Agente ha risposto alla domanda o ha divagato?"

    • Errore tipico: L'Agente recita parti corrette del documento ma non indirizza lo specifico intento dell'utente.

Gestire la Complessità

L'adozione di una struttura a lista risponde a precisi requisiti, superando i limiti della semplice corrispondenza univoca 1:1 tra domanda e singolo frammento di testo.

Per questo motivo, lo standard prevede l'uso di array passages.

{
  "id": int,
  "question": string,
  "answer": string,
  "passages": [
    {
      "content": string,
      "document_path": string,
      "start_char": int,
      "end_char": int
    }
  ]
}

 

In scenari più complessi la formulazione di una risposta corretta potrebbe richiedere logiche "multi-hop", dove è necessario aggregare e correlare informazioni dislocate in punti distanti del corpus documentale, ad esempio combinando un requisito presente a pagina 1 con una specifica tecnica descritta a pagina 30 (o addirittura in file diversi).

Questo array permette di mappare puntualmente tutte le fonti necessarie, abilitando una misurazione granulare: il sistema di validazione può così verificare se l'Agente ha effettivamente recuperato tutti i frammenti pertinenti elencati e non solo se ha fornito la risposta finale corretta.

Inoltre, la persistenza dei metadati posizionali è funzionale alla User Experience, permettendo alle interfacce di frontend di evidenziare le frasi originali, offrendo all'utente un riscontro immediato sulla provenienza dei dati.

Riferimenti

Related Posts

Gemini CLI - L'IA nel Terminale

Gemini CLI - L'IA nel Terminale

Introduzione pratica per usare l'AI di Google nel tuo flusso di lavoro quotidiano attraverso il terminale

Chunking - Il cuore invisibile dell'IA

Chunking - Il cuore invisibile dell'IA

Fondamenti di chunking e gestione del contesto

Ricerca Vettoriale con Azure AI Search

Ricerca Vettoriale con Azure AI Search

Fondamenti della ricerca semantica

Azure nel Banking - Architettura AKS di classe Enterprise

Azure nel Banking - Architettura AKS di classe Enterprise

Case Study

Come ho scritto del codice grazie all'IA

Come ho scritto del codice grazie all'IA

Un approccio vibe coding per sperimentare con l’IA nella scrittura di codice in un contesto cloud reale

Copilot Studio & Azure AI - Creare chatbot evoluti RAG-driven

Copilot Studio & Azure AI - Creare chatbot evoluti RAG-driven

Guida tecnica per l'integrazione di Copilot Studio e Azure AI con esempio avanzato

Copilot Studio & Azure AI - Creare chatbot su misura per la tua knowledge base

Copilot Studio & Azure AI - Creare chatbot su misura per la tua knowledge base

Guida tecnica per l'integrazione di Copilot Studio e Azure AI con esempio semplice

Migrazione da TIBCO Mashery ad Azure API Management

Migrazione da TIBCO Mashery ad Azure API Management

Case Study