CodeGym /Corsi /ChatGPT Apps /Test di carico leggeri e qualità dei dati del feed

Test di carico leggeri e qualità dei dati del feed

ChatGPT Apps
Livello 17 , Lezione 4
Disponibile

1. Perché alla ChatGPT App servono test di carico?

Nel web classico il load testing spesso si associa all’immagine «milioni di RPS, cluster gigantesco, pizza per gli SRE». Per la ChatGPT App e i server MCP la realtà è più semplice e, per fortuna, più economica. In linea di massima siete già familiari con gli SLO, ma vediamo come SLO/observability e qualità del feed interagiscono sotto carico.

La caratteristica principale: ChatGPT aspetta il completamento di un tool call per continuare a generare la risposta. L’utente vede un bel flusso di token, ma appena il modello decide di invocare uno strumento, la magia dello streaming finisce — finché il backend non risponde. Se il vostro server MCP o ACP a volte risponde in 8–10 secondi invece dei 2–4 secondi target, l’UX si trasforma da «assistente magico» a «l’ennesimo sito lento».

C’è anche un budget di timeout rigido: per le chiamate agli strumenti OpenAI mantiene un limite superiore nell’ordine di alcune decine di secondi (i numeri esatti dipendono dalla modalità, ma conviene pensare a 30–60 secondi, e per l’UX — idealmente 5–10 secondi). Se al picco di carico i vostri tool calls iniziano a rientrare in 25–30 secondi, siete formalmente ancora nel limite, ma dal punto di vista dell’utente siete già «rotti».

Secondo aspetto: ci importa meno un RPS astratto e più la concorrenza. Per un’App dallo Store è realistico avere 50–100 utenti attivi contemporanei; è proprio questo che vogliamo verificare, non «se resiste a 50k RPS di un sintetico GET /health».

E infine, la ChatGPT App è uno stack:

flowchart LR
  User --> ChatGPT
  ChatGPT -->|tools/call| MCP["Server MCP GiftGenius"]
  MCP --> DB["Database con il feed dei regali"]
  MCP --> ACP["Checkout / backend ACP"]
  ACP --> PSP["PSP / Stripe"]

Se non verifichiamo come questo stack si comporta sotto un carico contenuto ma realistico, qualsiasi campagna promozionale o il finire in una raccolta dello Store può rapidamente trasformarlo nella slide «come non fare prodotti LLM».

In questa lezione per «test di carico leggeri» intendiamo brevi run (di solito 1–10 minuti) che verificano:

  • se il sistema regge l’online di picco atteso;
  • se p95/p99 della latenza non scappano oltre gli SLO;
  • se non compaiono errori, timeout e rate limit dalle API esterne.

In parallelo guarderemo l’altro lato della qualità — i dati del product feed (di seguito semplicemente «feed»), senza i quali nessun GiftGenius sarà né «Gift» né «Genius».

In questa lezione prima ci occupiamo dei test di carico leggeri per MCP/ACP (cosa e come caricare, quali metriche osservare), poi li caliamo nell’observability (latenza, errori, risorse, webhooks e log) e nella seconda parte parliamo della qualità del feed e di come sotto carico possa sorprendentemente colpire.

2. Cosa caricare: non ChatGPT, ma le proprie API

Fissiamo un’idea per non confonderla dopo: eseguiamo il load testing direttamente sul nostro backend — server MCP, endpoint ACP, webhooks — e non tramite l’UI di ChatGPT.

Ci sono vari motivi.

  • Primo, il risparmio. Se inviate tool calls reali tramite ChatGPT, pagherete i token e allo stesso tempo sbatterete contro i limiti di ChatGPT, anche se state testando il vostro codice.
  • Secondo, la prevedibilità. Con le chiamate dirette a /mcp o /api/checkout controllate lo scenario, senza dipendere dalla decisione del modello se invocare lo strumento in quel momento o meno.
  • Terzo, la trasparenza. Sotto carico volete vedere chiaramente: ecco 2000 richieste verso MCP in 5 minuti, ecco la distribuzione della latenza, ecco il grafico della CPU. Se fate passare il carico tramite ChatGPT, lo strato aggiuntivo di rumore e limiti complica soltanto il quadro.

Set tipico di endpoint per un test di carico di GiftGenius:

  • endpoint del server MCP che implementa i tool JSON‑RPC (/mcp o simile);
  • uno‑due endpoint ACP per creare e finalizzare il checkout (in modalità sandbox del PSP);
  • eventualmente — un endpoint che gestisce i webhooks del PSP, per vedere come si comporta al picco di eventi.

Assumiamo di avere un backend Next.js 16 con il server MCP accessibile su /api/mcp e un server ACP con l’endpoint /api/checkout/create.

3. Mini‑scenario di smoke‑load per GiftGenius

I nostri product manager credono in un futuro roseo e dicono: «Il picco realistico è di 50 utenti contemporanei, ognuno entra, sceglie un regalo e a volte arriva al pagamento».

Per un test di carico leggero basta modellare, diciamo, 30–50 «utenti virtuali» (VU), ciascuno dei quali esegue la sequenza:

  1. Chiamata dello strumento giftgenius.search_gifts (ricerca dei regali per profilo e budget).
  2. Chiamata a giftgenius.get_gift_details per un paio di articoli dal risultato.
  3. (A volte) chiamata all’endpoint ACP create_checkout_session per un articolo.

Tutto questo direttamente via HTTP verso i nostri MCP/ACP, senza ChatGPT.

Chiamata JSON‑RPC al MCP

Esempio di body della richiesta al MCP (semplificato):

const body = {
  jsonrpc: "2.0",
  id: "test-" + Math.random(),
  method: "tools/call",
  params: {
    toolName: "giftgenius.search_gifts",
    arguments: {
      occasion: "birthday",
      budget: 50,
      interests: ["sport", "books"],
    },
  },
};

Nel progetto reale la struttura può variare leggermente, ma il principio è lo stesso: un metodo JSON‑RPC, dentro — tool e argomenti.

4. Scriviamo un semplice script di carico in TypeScript

Come primo passo implementiamo la parte più semplice del nostro scenario — la chiamata a giftgenius.search_gifts verso il MCP. Facciamo prima uno script Node.js minimale in TypeScript che invia tali richieste a /api/mcp e misura la latenza, poi aggiungeremo il checkout e percorsi più complessi.

Client HTTP di base

Supponiamo di avere un .env con MCP_URL=http://localhost:3000/api/mcp.

// scripts/loadTest.ts
import "dotenv/config";

const MCP_URL = process.env.MCP_URL!;

async function callSearchGifts() {
  const body = {
    jsonrpc: "2.0",
    id: `search-${Date.now()}-${Math.random()}`,
    method: "tools/call",
    params: {
      toolName: "giftgenius.search_gifts",
      arguments: { occasion: "birthday", budget: 50 },
    },
  };

  const started = Date.now();
  const res = await fetch(MCP_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const latencyMs = Date.now() - started;
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return latencyMs;
}

Qui si può anche aggiungere un parsing semplice della risposta JSON, ma ai fini di latenza/tasso di errore è sufficiente.

Esecuzione concorrente di più richieste

Dobbiamo controllare il numero di richieste contemporanee. Per semplicità prendiamo un numero fisso di «utenti virtuali» e chiediamo a ciascuno di eseguire N richieste in sequenza.

async function runVirtualUser(iterations: number) {
  const latencies: number[] = [];
  for (let i = 0; i < iterations; i++) {
    try {
      const ms = await callSearchGifts();
      latencies.push(ms);
    } catch (e) {
      console.error("Error in VU:", e);
      latencies.push(-1); // contrassegniamo l'errore
    }
  }
  return latencies;
}

Ora possiamo avviare, ad esempio, 20 di questi utenti virtuali:

async function main() {
  const users = 20;
  const iterations = 10;

  const tasks = Array.from({ length: users }, () =>
    runVirtualUser(iterations),
  );

  const results = await Promise.all(tasks);
  const all = results.flat();
  // ...calcolo delle metriche
}

main().catch((e) => console.error(e));

Questo genererà circa 200 chiamate al MCP, parte delle quali verranno eseguite in parallelo, cioè con una concorrenza sufficientemente alta.

Calcolo di p95 e del tasso di errore

Aggiungiamo una piccola utility per il calcolo del percentile e degli errori. Promemoria: p95 è il valore sotto il quale rientra il 95% delle richieste.

function percentile(values: number[], p: number) {
  const sorted = values.filter(v => v >= 0).sort((a, b) => a - b);
  if (!sorted.length) return 0;
  const idx = Math.floor((p / 100) * (sorted.length - 1));
  return sorted[idx];
}

function errorRate(values: number[]) {
  const total = values.length;
  const errors = values.filter(v => v < 0).length;
  return (errors / total) * 100;
}

E in main aggiungiamo l’output:

const p95 = percentile(all, 95);
const p99 = percentile(all, 99);
const errRate = errorRate(all);

console.log(`Total: ${all.length}`);
console.log(`p95: ${p95} ms, p99: ${p99} ms`);
console.log(`Error rate: ${errRate.toFixed(2)}%`);

Avete ora uno script di smoke‑load minimale che potete eseguire in locale o su staging prima del rilascio. Non toccate ChatGPT, non bruciate token, e tutta l’attenzione è sul vostro MCP.

Cosa fare con ACP e checkout

In modo analogo potete aggiungere un helper callCreateCheckoutSession che colpisca l’endpoint ACP. Qui è importante usare la modalità di test/sandbox del PSP per non generare ordini reali. Una chiamata tipica è un semplice POST con JSON:

async function callCreateCheckoutSession(productId: string) {
  const started = Date.now();
  const res = await fetch("http://localhost:3000/api/checkout/create", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ productId, test: true }),
  });
  const latencyMs = Date.now() - started;
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return latencyMs;
}

Poi potete, in runVirtualUser, usare il pattern: 3 volte ricerca → 1 volta checkout, per simulare l’imbuto «più ricerche che acquisti».

5. Strumenti più seri: k6 (ma in modo semplice)

Uno script Node è ottimo come «ingresso minimo», ma talvolta è comodo usare uno strumento specializzato come k6, in cui gli scenari si scrivono in JavaScript mentre il runtime è in Go (quindi veloce).

Esempio di piccolo script k6 per il MCP:

// loadtest-mcp.js
import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  stages: [
    { duration: "30s", target: 30 },
    { duration: "2m", target: 30 },
  ],
};

export default function () {
  const payload = JSON.stringify({
    jsonrpc: "2.0",
    id: `search-${Math.random()}`,
    method: "tools/call",
    params: {
      toolName: "giftgenius.search_gifts",
      arguments: { occasion: "birthday", budget: 50 },
    },
  });

  const res = http.post(__ENV.MCP_URL, payload, {
    headers: { "Content-Type": "application/json" },
  });

  check(res, { "status is 200": (r) => r.status === 200 });
  sleep(1);
}

Comando di esecuzione:

MCP_URL=http://localhost:3000/api/mcp k6 run loadtest-mcp.js

k6 calcolerà automaticamente p95/p99 e il tasso di errore, e produrrà report gradevoli — poi potete esportarli in Grafana e altri sistemi.

È importante che anche con strumenti simili l’obiettivo resti lo stesso: non reggere un milione di RPS, ma assicurarvi che a 5–10× del picco atteso il sistema non collassi e che p95 resti entro gli SLO.

6. Cosa osservare durante (e dopo) il run di carico

Abbiamo già discusso metriche e SLO; ora caliamoli nel contesto del carico.

Primo: la latenza. Per gli strumenti MCP come search_gifts vi eravate dati obiettivi del tipo «p95 < 2–3 secondi». Durante lo smoke‑load osservate: p95/p99 non sono cresciuti di 2–3 volte? Importante confrontare con il baseline: se prima della modifica del codice p95 era 400 ms e dopo è 1500 ms, anche se formalmente siete ancora negli SLO, è già motivo di riflessione.

Secondo: il tasso di errore. Sotto carico emergono spesso elementi inattesi: pool di connessioni al DB esaurito, inaspettati 429 da API esterne, timeout nella chiamata al PSP. A carico normale il tasso di errore dovrebbe essere vicino allo zero; nello smoke‑load sono ammissibili singoli fallimenti, ma certamente non il 5–10%.

Terzo: metriche di risorsa. CPU, memoria, a volte — numero di file descriptor e connessioni aperte. Dipendono dall’infrastruttura, ma l’idea chiave è semplice: non volete vedere la CPU al 100% con 30 VU e un GC che consuma metà del tempo.

Quarto: i webhooks. Se avete uno scenario commerce, il punto finale dell’ordine spesso dipende dalla corretta elaborazione del webhook del PSP. È importante osservare non solo la velocità della richiesta in ACP, ma anche la latenza «webhook arrivato → elaborazione completata».

E infine, i log. Log strutturati con trace_id/checkout_session_id consentono, dopo un run di carico, di prendere un paio di richieste più lente o fallite e seguirne la catena: MCP → API esterna → ACP → webhook. È particolarmente utile se sotto carico vedete code strane in p99.

7. Qualità dei dati del feed: dalla struttura al significato

Abbiamo visto come sotto carico si comportano latenza, errori e risorse. Ma anche se per tutti questi SLO rientrate negli obiettivi, l’esperienza utente può comunque «sgretolarsi» a causa di dati scadenti.

Passiamo al secondo grande tema: i dati. In un’App commerce come GiftGenius il product feed (feed dei prodotti) non è «qualcosa su disco», ma letteralmente carburante per LLM e agenti. Se nel feed c’è spazzatura, il modello non «inventerà» per voi prezzo e disponibilità.

È utile pensare alla qualità del feed su tre livelli.

Livello strutturale

Questa è la validità di base dei dati:

  • Il JSON si parsifica correttamente.
  • Sono presenti tutti i campi obbligatori: id, name, price, currency, imageUrl, availability, ecc.
  • I tipi dei valori corrispondono alle aspettative: il prezzo è un numero, availability è un enum, categories è un array di stringhe.
  • Nessun duplicato di id.

Parte di questo l’avete già coperta con test di contratto, quando avete descritto lo schema JSON/Zod per il feed. Ora bisogna applicare questi schemi a volumi reali di dati.

Esempio di uno schema Zod semplice per un elemento del feed di GiftGenius:

import { z } from "zod";

export const giftItemSchema = z.object({
  id: z.string().min(1),
  name: z.string().min(3),
  description: z.string().optional(),
  price: z.number().positive(),
  currency: z.enum(["USD", "EUR", "GBP"]),
  imageUrl: z.string().url(),
  inStock: z.boolean(),
  tags: z.array(z.string()).default([]),
});

E lo schema dell’intero feed è semplicemente z.array(giftItemSchema).

Livello business (semantica)

Un prodotto può essere strutturalmente valido, ma dal punto di vista del business — assurdo:

  • Prezzo 0 o 0.01 per un prodotto costoso.
  • Valuta non coerente con il mercato (USD per prodotti venduti solo in EUR).
  • inStock = true, ma la data dell’ultimo aggiornamento è di sei mesi fa.
  • Categorie tra 1000 varianti senza unificazione.

Per questo livello è utile aggiungere verifiche extra e «regole di buon senso». Ad esempio:

const businessRules = (item: GiftItem) => {
  const problems: string[] = [];

  if (item.price > 10000) {
    problems.push("prezzo sospettosamente alto");
  }
  if (!item.inStock && item.tags.includes("bestseller")) {
    problems.push("bestseller, ma non disponibile");
  }
  return problems;
};

Queste verifiche possono essere eseguite come parte di un job notturno o durante la generazione di un nuovo feed.

Livello LLM

Il modello è molto capace, ma ha i suoi «difetti»:

  • Descrizioni piene di HTML, tag superflui e testo tecnico.
  • Lingue mescolate (mezzo feed in russo, mezzo in inglese) senza indicazione della locale.
  • Nomi «SEO» molto lunghi in stile «Compra il miglior super mega regalo subito a poco prezzo».

A questo livello è importante portare i dati a un formato adatto al modello:

  • Rimuovere i tag HTML o convertirli in testo semplice.
  • Normalizzare la lingua delle descrizioni (o almeno indicare chiaramente la locale).
  • Accorciare nomi eccessivamente lunghi e informazioni duplicate.

Questi compiti si possono automatizzare in parte (ad esempio, con script di pre‑processing) e in parte — concordarli con il team che popola il feed.

8. Pratica: validatore del feed per GiftGenius

Aggiungiamo al progetto uno script semplice validateFeed.ts, che legge il JSON del feed, lo valida con Zod e calcola metriche base di qualità.

// scripts/validateFeed.ts
import { readFile } from "fs/promises";
import { giftItemSchema } from "../src/schema/giftItem";

async function main() {
  const raw = await readFile("data/gift-feed.json", "utf-8");
  const data = JSON.parse(raw);

  const items = giftItemSchema.array().parse(data);
  console.log(`Totale prodotti: ${items.length}`);

  const missingImages = items.filter(i => !i.imageUrl).length;
  console.log(`Senza immagini: ${missingImages}`);
}

main().catch((e) => {
  console.error("Feed validation failed:", e);
  process.exit(1);
});

Qui usiamo lo stesso contratto del server MCP, cioè i test di contratto e la verifica del feed usano lo stesso schema — questo riduce molto la probabilità di divergenze.

Poi si possono aggiungere verifiche di business e metriche come:

  • quota di prodotti senza descrizione;
  • quota di prodotti con prezzo sospettosamente basso/alto;
  • numero di duplicati di id o di combinazioni ripetute name + price.

Questi numeri si possono inviare a un sistema di metriche (Prometheus, Datadog, ecc.) e impostare SLO separati per la qualità dei dati — proprio come fate per il codice.

9. Come carico e feed sono collegati tra loro

A volte sembra che «prestazioni» e «qualità dei dati» siano due temi poco correlati. In pratica sono piuttosto intrecciati.

Esempi di legami:

  • Sotto carico una parte delle richieste inizia a percorrere rami «rari» della logica, quasi mai incontrati prima. Ad esempio, prodotti con tipi di sconti particolari o shipping non standard. Se il feed è sporco in questi punti, potete ottenere sia errori sia una seria degradazione delle prestazioni (molte validazioni, eccezioni, logica di fallback).
  • Se il feed è molto rumoroso (descrizioni enormi con HTML, tag inutili), al server MCP tocca prelevare e serializzare più dati: questo incide direttamente sul tempo di elaborazione del tool call e sulla dimensione della risposta.
  • Nella parte commerce un feed scadente può portare a molti tentativi di checkout «vuoti», quando l’utente sceglie un prodotto improvvisamente out of stock. Questo impatta sia l’UX sia le metriche ACP (crescita degli intent non riusciti).

È utile guardarlo come una matrice:

Problema del feed Sintomo sotto carico Dove guardare
Prezzi/valute inconsistenti Errori in ACP, pagamenti rifiutati Log ACP + SLO del checkout
Duplicati di prodotti Risultati di raccomandazioni anomali, chiamate superflue Log MCP, metriche UX
Mancano immagini/descrizioni Il modello produce raccomandazioni «piatte» Log dell'app + feedback UX
HTML/rumore nelle descrizioni Serializzazioni lente, payload grandi Latenza MCP

Il run di carico qui funge da torcia: aiuta a illuminare le parti del feed che nella vita normale venivano toccate raramente, ma con traffico attivo iniziano a «sparare».

10. Inserire tutto questo nel processo di release di GiftGenius

Dal punto di vista del processo, quanto descritto non dovrebbe essere «una volta prima del primo prod». Nel piano didattico dei moduli 16 («Produzione, rete e scalabilità») e 17 («Osservabilità e qualità») questo approccio è previsto proprio come parte della checklist di release: prima del rilascio non solo eseguite unit/contract/E2E, ma anche un breve smoke‑load più la verifica del feed.

Una pipeline minima ragionevole prima di distribuire una nuova versione:

  1. Unit + contract + test di integrazione verdi.
  2. Breve smoke‑load contro MCP/ACP su staging, se è cambiato codice critico (logica di ricerca, lavoro con il DB, checkout).
  3. Il validatore del feed termina senza errori, le metriche base del feed (numero di record rotti, quota senza immagini, ecc.) sono entro i limiti accettabili.
  4. Dashboard e alert aggiornati in base ai nuovi endpoint e agli SLO.
  5. Piano di rollback pronto in caso di failure: o disattivare la feature con un flag, o fare il rollback del build.

Così il vostro GiftGenius smette di essere «una demo per il DevDay» e diventa un servizio pronto a vivere nello Store e a gestire picchi di traffico.

11. Errori tipici nei test di carico e nella verifica del feed

Errore n. 1: test di carico «tramite ChatGPT», non sul proprio backend.
A volte si cerca di «testare tutto come in realtà» e si lanciano script che passano proprio dall’UI di ChatGPT. Il risultato è sbattere contro i limiti di OpenAI, bruciare token e ottenere risultati molto rumorosi. Nel frattempo i problemi di MCP/ACP si potevano cogliere a costi cento volte inferiori, sparando direttamente su /mcp e /api/checkout.

Errore n. 2: focalizzarsi solo sul tempo medio di risposta.
«La nostra latenza media è 500 ms, tutto ottimo» — e che p95 sia 5 secondi, lo si dimentica. Abbiamo già discusso nel tema SLO che è la coda della distribuzione (p95/p99) a determinare la vera UX. Sotto carico la media spesso resta decente, ma la coda cresce di due‑tre volte.

Errore n. 3: tentare un’«enterprise load» invece di uno smoke‑load pratico.
Sviluppare per mesi un ambiente complesso che imita decine di migliaia di utenti, per una ChatGPT App come GiftGenius, è quasi sempre superfluo. Molto più utile avere uno smoke‑load semplice ma eseguito regolarmente a 50–100 VU con metriche chiare.

Errore n. 4: scenario di carico poco realistico.
Lo script invia sempre la stessa richiesta, senza variazioni di utente, lingua, tipo di prodotto, e non tocca ACP e webhooks. In pratica testate un happy path caldo, mentre gli «angoli» reali del sistema restano nell’ombra. Meglio modellare almeno un flusso semplificato ma plausibile: budget diversi, interessi diversi, una parte degli utenti arriva al checkout, un’altra no.

Errore n. 5: verificare il feed «a occhio» o solo in produzione.
Il feed viene assemblato, caricato in prod, si vedono raccomandazioni strane del modello e ci si gratta la testa. Un semplice script con Zod/JSON Schema avrebbe potuto mostrare in un minuto che il 10% dei prodotti è senza immagini, il 5% ha prezzo 0, e il 3% usa la valuta XXX. L’assenza di validazione automatica del feed è una delle fonti più comuni di imbarazzo nelle app commerce.

Errore n. 6: sperare che la LLM «capisca tutto» con un feed scadente.
Sì, il modello sa fare molto, ma non inventerà un prezzo corretto o la disponibilità. Se lo stesso prodotto compare nel feed con prezzi diversi o con «disponibile»/«non disponibile» contemporaneamente, l’agente può produrre sia allucinazioni sia un’esperienza incoerente per l’utente. La responsabilità della pulizia dei dati è vostra, non del modello.

Errore n. 7: assenza di collegamento tra metriche del feed e SLO complessivi.
Potete avere MCP e ACP perfettamente rapidi, ma se il 30% dei prodotti nel feed è «rotto», l’esperienza utente sarà comunque pessima. Spesso i team tracciano solo SLO tecnici (latenza, tasso di errore) e ignorano gli SLO sulla qualità dei dati (percentuale minima di SKU validi, massimo di duplicati, ecc.). Il risultato: «sulle cifre tutto bene», ma sulla percezione — no.

Errore n. 8: lanciare test di carico direttamente in produzione senza preparazione.
A volte qualcuno il venerdì sera decide «di far girare al volo k6 sul MCP di produzione», senza avvisare nessuno. Nel migliore dei casi altererete le metriche reali e confonderete l’on‑call con un picco di traffico; nel peggiore — incapperete nei rate limit delle API esterne o del PSP. Eseguite sempre i primi scenari su staging e, se serve un test in prod, fatelo consapevolmente, con finestre e notifiche.

1
Sondaggio/quiz
Osservabilità e qualità, livello 17, lezione 4
Non disponibile
Osservabilità e qualità
Osservabilità e qualità
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION