L'ottimizzazione dei prompt per i modelli linguistici di grandi dimensioni (LLM) sta evolvendo da un processo manuale basato su tentativi ed errori a un approccio scientifico e automatizzato. In questo tutorial, vedremo come utilizzare GEPA, un framework di evoluzione riflessiva, per trasformare prompt deboli in istruzioni altamente performanti per risolvere complessi problemi aritmetici.

Configurazione dell'ambiente: GEPA e LiteLLM

Il primo passo consiste nell'installazione delle librerie necessarie. Utilizzeremo GEPA per gestire il ciclo evolutivo del prompt e LiteLLM per standardizzare le chiamate alle API di diversi modelli linguistici, garantendo flessibilità tra i vari provider.

python
!pip install -q gepa litellm import os, re, json, random, getpass, textwrap import litellm import gepa.optimize_anything as oa from gepa.optimize_anything import ( optimize_anything, GEPAConfig, EngineConfig, ReflectionConfig, ) litellm.suppress_debug_info = True if not os.environ.get("OPENAI_API_KEY"): os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ") TASK_LM = "openai/gpt-4o-mini" REFLECTION_LM = "openai/gpt-4.1" MAX_METRIC_CALLS = 100

In questa configurazione, distinguiamo due ruoli fondamentali:

  • Task LM: Il modello (es. GPT-4o-mini) incaricato di risolvere effettivamente il problema matematico.

  • Reflection LM: Un modello tipicamente più potente (es. GPT-4.1 o modelli di ragionamento avanzati) che analizza i fallimenti del Task LM per riscrivere e migliorare il prompt.

Costruzione di un benchmark aritmetico deterministico

Per ottimizzare un prompt in modo efficace, serve un set di dati affidabile. Creiamo un generatore di problemi aritmetici "deterministici" (ovvero con una soluzione univoca e calcolabile programmaticamente) che copra diverse casistiche: sconti commerciali, calcolo di distanze di viaggio, gestione del portafoglio e operazioni a catena.

python
def make_problems(n, seed=0): rng = random.Random(seed) out = [] for _ in range(n): t = rng.choice(["discount", "travel", "wallet", "chain"]) if t == "discount": unit = rng.choice([40, 60, 80, 120]) qty = rng.choice([5, 6, 8, 10]) disc = rng.choice([10, 20, 25, 50]) total = unit * qty gold = total - total * disc // 100 q = (f"A shop sells notebooks at {unit} rupees each. You buy {qty} " f"notebooks and get a {disc}% discount on the total bill. " f"How many rupees do you pay in total?") elif t == "travel": s1, h1 = rng.choice([40, 50, 60]), rng.choice([2, 3]) s2, h2 = rng.choice([30, 45, 70]), rng.choice([1, 2, 3]) gold = s1 * h1 + s2 * h2 q = (f"A car drives at {s1} km/h for {h1} hours, then at {s2} km/h " f"for {h2} hours. What is the total distance travelled, in km?") elif t == "wallet": tens = rng.choice([3, 5, 7, 9]) fifties= rng.choice([2, 4, 6]) spent = rng.choice([50, 80, 110, 150]) gold = tens * 10 + fifties * 50 - spent q = (f"You have {tens} ten-rupee notes and {fifties} fifty-rupee " f"notes. You spend {spent} rupees. How many rupees are left?") else: x = rng.choice([6, 9, 12, 15]); y = rng.choice([4, 7, 10]); z = rng.choice([3, 8, 11]) gold = x * 2 - y + z q = (f"Start with the number {x}. Double it, then subtract {y}, " f"then add {z}. What number do you end with?") out.append({"question": q, "answer": gold}) return out all_problems = make_problems(18, seed=42) random.Random(1).shuffle(all_problems) trainset = all_problems[:12] valset = all_problems[12:] print(f"Dataset: {len(trainset)} train / {len(valset)} val problems\n")

Dividiamo i dati in un training set (utilizzato da GEPA per l'ottimizzazione iterativa) e un validation set (testato solo alla fine per verificare che il prompt funzioni bene anche su problemi mai visti prima).

Definizione dell'evaluator e feedback strutturato

Il cuore dell'ottimizzazione riflessiva è la capacità di fornire feedback "azionabile". Non basta dire all'IA che la risposta è sbagliata; bisogna spiegare perché. L'evaluator analizza l'output del modello, estrae la risposta finale (identificata dal formato #### <numero>) e assegna un punteggio basato sulla precisione del calcolo e del formato.

python
def build_system_prompt(candidate: dict) -> str: return (f"{candidate['instructions']}\n\n" f"OUTPUT FORMAT RULES:\n{candidate['format_rules']}") def parse_answers(text: str): formatted = re.search(r"####\s*(-?\d+)", text) all_nums = re.findall(r"-?\d+", text) fmt_val = int(formatted.group(1)) if formatted else None last_val = int(all_nums[-1]) if all_nums else None return fmt_val, last_val def evaluate(candidate: dict, example: dict): system = build_system_prompt(candidate) raw = call_task_lm(system, example["question"]) gold = example["answer"] fmt_val, last_val = parse_answers(raw) if fmt_val is not None and fmt_val == gold: score, fb = 1.0, "Correct and correctly formatted." elif fmt_val is not None and fmt_val != gold: score, fb = 0.0, (f"WRONG ANSWER. You output '#### {fmt_val}' but the correct answer is {gold}.") elif last_val == gold: score, fb = 0.5, (f"Right number but FORMAT VIOLATION. Use '#### <integer>'.") else: score, fb = 0.0, (f"WRONG. Correct answer is {gold}. Likely a reasoning slip.") side_info = {"feedback": fb, "problem": example["question"], "model_output": raw[:500]} return score, side_info

Questo feedback strutturato permette a GEPA di capire se il modello fallisce per mancanza di logica (errori matematici) o per inosservanza del formato richiesto.

Esecuzione del ciclo evolutivo con GEPA

Iniziamo con un prompt "seme" estremamente semplice: "Risolvi il problema matematico. Dai la risposta.". È un prompt volutamente debole che funge da linea di base (baseline).

python
seed_candidate = { "instructions": "Solve the math problem.", "format_rules": "Give the answer.", } objective = ( "Evolve a system prompt so a small LLM reliably solves multi-step arithmetic " "word problems AND always ends with exactly '#### <integer>'." ) config = GEPAConfig( engine=EngineConfig(max_metric_calls=MAX_METRIC_CALLS, parallel=True), reflection=ReflectionConfig(reflection_lm=REFLECTION_LM), ) result = optimize_anything( seed_candidate=seed_candidate, evaluator=evaluate, dataset=trainset, valset=valset, objective=objective, config=config, )

Durante l'esecuzione, GEPA tenta diverse varianti del prompt. Se una variante migliora il punteggio medio, diventa la base per la "prole" successiva. Il processo continua fino al raggiungimento dell'obiettivo o all'esaurimento del budget di chiamate.

Guida passo-passo

Processo di Ottimizzazione con GEPA

  1. 1
    InizializzazioneSi definisce un prompt di partenza e si valutano le prestazioni iniziali sui set di train e validazione.
  2. 2
    Ciclo EvolutivoGEPA chiama il Task LM sui problemi. L`evaluator analizza i risultati e produce feedback.
  3. 3
    RiflessioneIl Reflection LM usa il feedback per identificare punti deboli (es. mancanza di passaggi intermedi) e propone modifiche alle istruzioni.
  4. 4
    Selezione e ValidazioneIl framework seleziona il candidato migliore basato sui punteggi medi e lo valida sul set di dati `held-out` per evitare l`overfitting.

Analisi dei risultati e validazione finale

Al termine del processo, estraiamo il best_candidate. Spesso si nota come il Reflection LM aggiunga istruzioni che forzano il modello a mostrare il proprio ragionamento passo-passo (Chain of Thought) o a eseguire una fase di verifica interna prima di scrivere la riga finale con il formato rigoroso.

python
best = result.best_candidate print(" Seed prompt score:", eval_set(seed_candidate, valset, "val-seed")[0]) print(" GEPA prompt score:", eval_set(best, valset, "val-gepa")[0])

Il miglioramento delle prestazioni tra il prompt iniziale e quello ottimizzato sul set di validazione dimostra che il sistema ha imparato strategie di istruzione generalizzabili, anziché limitarsi a memorizzare le risposte del training set.

Il nostro verdetto
4.5/ 5

Verdetto Tecnico

GEPA si conferma uno strumento potente per chiunque debba gestire pipeline LLM complesse. La capacità di evolvere prompt multi-componente basandosi su feedback strutturato riduce drasticamente il tempo dedicato alla scrittura manuale delle istruzioni, portando a risultati più robusti e meno soggetti a errori di formato.

Posso usare GEPA con modelli locali?
Sì, tramite LiteLLM è possibile collegare GEPA a modelli locali come Llama 3 o Mistral eseguiti tramite server compatibili con le API OpenAI.
Quanti esempi servono per una buona ottimizzazione?
Anche con pochi esempi (10-20) si possono ottenere miglioramenti significativi, purché i problemi siano rappresentativi della varietà che il modello dovrà affrontare.
GEPA funziona per compiti non matematici?
Assolutamente sì. Può essere usato per l`estrazione di dati, la sintesi di testi o la generazione di codice, a patto di definire un evaluator capace di dare un punteggio all`output.

Fonti e riferimenti

  1. Repository Ufficiale GEPA su GitHub
  2. Tutorial e Notebook Completo Marktechpost