Générer des cas de test sans tricher
Des données d’évaluation synthétiques sont des cas de test fabriqués par un programme. Écrire mille cas à la main est long, donc les équipes les génèrent à partir de gabarits.
Des cas fabriqués peuvent se répéter, ou donner leur propre réponse. Cette leçon fabrique des cas à partir d’une graine et retire les mauvais.
À la fin de cette leçon
Remarque
Je peux fabriquer de nouveaux cas à partir d’une graine et dire qu’ils ne sont pas des copies.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Gabarit | une phrase à trous qu’il faut remplir | Échanger ma {objet} |
| Graine | un nombre de départ qui fait se répéter un générateur | |
| Quasi-doublon | un cas presque identique à un autre | |
| Fuite | un cas qui donne sa réponse ou son texte d’entraînement | |
| Similarité | la part de mots que deux textes partagent, en pourcentage |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un gabarit et une graine fabriquent des cas
Un gabarit est une phrase à trous, comme « Échanger ma {objet} ». Une liste d’objets remplit le trou. Chaque gabarit a une étiquette : la réponse qu’une bonne fonction doit donner.
Une graine est un nombre qui lance un compteur. La même graine choisit toujours les mêmes gabarits et les mêmes objets, donc les cas se répètent.
Exemple détaillé : un générateur à graine
| Étape | Ce qui se passe |
|---|---|
| Départ | La graine est le premier nombre |
| Tirage | Multipliez par 16807, gardez le reste de la division par 2147483647 |
| Choix | Divisez ce nombre par la taille de la liste et gardez le reste |
| Remplissage | Mettez l’objet choisi dans le trou |
| Répétition | La même graine donne les mêmes cas à chaque fois |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Les quasi-doublons gonflent un jeu
Deux cas qui ne diffèrent que d’un mot n’apprennent rien de nouveau. Comptez les mots qu’ils partagent. Divisez par tous les mots différents des deux textes.
À partir de 80 %, on parle de quasi-doublon. Gardez le premier cas et rejetez le suivant.
Une erreur courante
Attention
Beaucoup pensent que plus de cas générés fait toujours un meilleur test. Si la plupart sont des copies, le jeu est plus petit qu’il n’en a l’air. Ne comptez que les cas qui ne sont pas des quasi-doublons.
Deux sortes de fuite gâchent un test
Une fuite de la réponse est un cas qui contient sa propre étiquette, comme « Merci de traiter le remboursement de ma lampe » avec l’étiquette remboursement. Une fonction peut réussir en repérant le mot.
Une fuite d’entraînement est un cas qui reprend un texte déjà vu par la fonction. Une bonne note dessus prouve la mémoire, pas la compétence.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le générateur, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const gabarits = [' ' [\"Ma {objet} est arrivée {defaut}, je veux qu’on me rende mon argent\", \"remboursement\"],' ' [\"Merci de traiter le remboursement de ma {objet}, elle était {defaut}\", \"remboursement\"],' ' [\"Où est ma {objet} ? Elle n’est pas arrivée\", \"suivi\"],' ' [\"Puis-je échanger ma {objet} contre une autre couleur\", \"echange\"],' ' [\"La {objet} est {defaut} et je voudrais être remboursée\", \"remboursement\"],' ' [\"Où en est la commande de ma {objet} s’il vous plaît\", \"suivi\"],' '];' 'const objets = [\"bouilloire\", \"lampe\", \"théière\", \"tasse\"];' 'const defauts = [\"cassée\", \"abîmée\", \"fissurée\"];' 'const entrainement = [\"Où est ma bouilloire ? Elle n’est pas arrivée\", \"Puis-je échanger ma lampe contre une autre couleur\"];' > cas.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le générateur, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'let s = 23;' 'function suivant(taille) { s = (s * 16807) % 2147483647; return s % taille; }' 'function generer(n) {' ' const sortie = [];' ' for (let i = 1; i <= n; i++) {' ' const g = gabarits[suivant(gabarits.length)];' ' let texte = g[0];' ' if (texte.indexOf(\"{objet}\") >= 0) texte = texte.replace(\"{objet}\", objets[suivant(objets.length)]);' ' if (texte.indexOf(\"{defaut}\") >= 0) texte = texte.replace(\"{defaut}\", defauts[suivant(defauts.length)]);' ' sortie.push({ id: i, texte: texte, etiquette: g[1] });' ' }' ' return sortie;' '}' >> cas.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le générateur, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function listeMots(texte) {' ' const m = texte.toLowerCase().replace(/[,?.]/g, \" \").split(\" \").filter(function (x) { return x.length > 0; });' ' return m.filter(function (x, i) { return m.indexOf(x) === i; });' '}' 'function similarite(a, b) {' ' const wa = listeMots(a);' ' const wb = listeMots(b);' ' const communs = wa.filter(function (m) { return wb.indexOf(m) >= 0; }).length;' ' return Math.floor(communs * 100 / (wa.length + wb.length - communs));' '}' 'function proche(texte, liste) {' ' return liste.some(function (t) { return similarite(texte, t) >= 80; });' '}' >> cas.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le générateur, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const gardes = [];' ' const raisons = {};' ' generer(12).forEach(function (c) {' ' let pourquoi = \"\";' ' if (listeMots(c.texte).indexOf(c.etiquette) >= 0) pourquoi = \"fuite de la réponse\";' ' else if (proche(c.texte, entrainement)) pourquoi = \"fuite d’entraînement\";' ' else if (proche(c.texte, gardes.map(function (k) { return k.texte; }))) pourquoi = \"doublon\";' ' if (pourquoi === \"\") gardes.push(c);' ' else raisons[pourquoi] = (raisons[pourquoi] || 0) + 1;' ' });' >> cas.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le générateur, partie 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' console.log(\"gardés \" + gardes.length + \" sur 12\");' ' console.log(\"rejetés : \" + JSON.stringify(raisons));' ' console.log(\"premier gardé : \" + gardes[0].texte + \" -> \" + gardes[0].etiquette);' ' const echantillon = \"Corrigez le gabarit pour que le mot de l’étiquette ne soit jamais dans le texte.\";' ' const q = \"En une phrase courte, comment empêcher un cas inventé de donner sa propre réponse ?\";' ' const reponse = await demander([{ role: \"user\", content: q }], echantillon, 60);' ' console.log(\"le modèle dit : \" + reponse.texte);' '}' 'main();' >> cas.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer le générateur
docker exec -w /home/node/project agent node cas.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| gardés 6 sur 12 | Les cas qui restent après les contrôles |
| rejetés : {...} | Combien ont été écartés, et pourquoi |
| premier gardé : ... -> ... | Un cas propre et son étiquette |
| le modèle dit : ... | Une phrase du labo. Un échantillon enregistré si aucun modèle ne répond |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.