Comment tester une fonction texte vers SQL
Un jeu d’évaluation pour le texte vers SQL est une liste de questions aux réponses connues, relancée à chaque changement de consigne ou de modèle. Il dit si un changement a amélioré ou abîmé les choses.
Les modules 68 et 85 notent des réponses en mots. Cette leçon note du SQL par les lignes qu’il renvoie.
À la fin de cette leçon
Remarque
Je peux noter des requêtes candidates sur un jeu de questions aux réponses connues.
Mots clés
| Mot | Ce que ça veut dire |
|---|---|
| Jeu d’évaluation | des questions aux réponses connues, relancées à chaque changement |
| Requête de référence | une requête vérifiée à la main qui donne les bonnes lignes |
| Concordance des résultats | les mêmes lignes que la requête de référence |
| Régression | une question qui réussissait avant et échoue maintenant |
| Barrière | une règle qui bloque un changement |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Chaque cas a une question et une référence
Un cas contient une question, une requête de référence et le fait que l’ordre compte ou non. Écrivez la référence vous-même et vérifiez ses lignes à la main sur le jeu d’essai.
Lancez la référence pour obtenir les lignes attendues. Le jeu continue ainsi de marcher quand les données grossissent, tant que les références restent justes.
Exemple détaillé : cinq cas pour la médiathèque
| Cas | Question | Lignes de référence |
|---|---|---|
| 1 | Prêts faits au mois 3 | 4 |
| 2 | Membres différents au mois 3 | 3 |
| 3 | Livres encore dehors | 5 |
| 4 | Titres empruntés par le membre 2 | Carte de la pluie, Moteurs tranquilles, Sept Bouilloires |
| 5 | Livres jamais empruntés | Au nord de midi |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Noter par le résultat, pas par le texte
COUNT(id) et COUNT(*) donnent ici le même nombre, et une jointure peut s’écrire en sous-requête. Deux textes différents peuvent être justes tous les deux.
Lancez donc chaque candidate et comparez ses lignes aux lignes de référence. Comparer le texte exact déclare fausses des réponses justes, comme le montre le Run.
Exemple détaillé : texte et résultat se contredisent
| Candidate | Même texte que la référence | Mêmes lignes que la référence |
|---|---|---|
| SELECT COUNT(id) FROM prets WHERE mois = 3 | Non | Oui |
| Une jointure écrite IN (SELECT livre_id ...) | Non | Oui |
| SELECT COUNT(*) FROM prets | Non | Non |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une régression se cache sous un même score
Une nouvelle consigne répare la question 2 et casse la question 3. Les deux versions réussissent quatre questions sur cinq : le score n’a pas bougé.
Comparez cas par cas. Une question qui réussissait et échoue maintenant est une régression, et la barrière bloque le changement jusqu’à sa correction.
Une erreur courante
Attention
Beaucoup pensent qu’un taux de réussite égal veut dire que rien n’a empiré. Une question peut être réparée pendant qu’une autre casse. Comparez chaque cas avec le dernier passage, et bloquez tout changement qui perd une réussite.
É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;' '}' '// Une réponse peut entourer la requête d’un bloc délimité : on garde l’intérieur.' 'function sqlDe(texte) {' ' const morceaux = texte.split(String.fromCharCode(96).repeat(3));' ' const dedans = (morceaux.length > 2 ? morceaux[1] : texte).trim();' ' return dedans.toLowerCase().startsWith(\"sql\") ? dedans.slice(3).trim() : dedans;' '}' > 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, sqlDe };' >> 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 la base de la médiathèque, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Une médiathèque inventée : six livres, quatre membres, neuf prêts.' 'const LIVRES = [[1, \"Le Phare des oranges\", 2019], [2, \"Port de verre\", 2021], [3, \"Carte de la pluie\", 2015],' ' [4, \"Moteurs tranquilles\", 2023], [5, \"Sept Bouilloires\", 2018], [6, \"Au nord de midi\", 2022]];' 'const MEMBRES = [[1, \"Amina\"], [2, \"Bruno\"], [3, \"Chloé\"], [4, \"Driss\"]];' '// id, livre_id, membre_id, mois, rendu (1 rendu, 0 encore dehors)' 'const PRETS = [[1, 1, 1, 3, 1], [2, 2, 1, 3, 1], [3, 3, 2, 3, 0], [4, 1, 3, 3, 1], [5, 1, 1, 4, 0],' ' [6, 4, 2, 4, 0], [7, 2, 3, 4, 1], [8, 5, 2, 5, 0], [9, 2, 4, 5, 0]];' > mediatheque.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 la base de la médiathèque, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function remplir(db, table, lignes) {' ' const marques = lignes[0].map(function () { return \"?\"; }).join(\", \");' ' const inserer = db.prepare(\"INSERT INTO \" + table + \" VALUES (\" + marques + \")\");' ' lignes.forEach(function (ligne) { inserer.run(...ligne); });' '}' 'function ouvrir(fichier) {' ' const db = new DatabaseSync(fichier || \":memory:\");' ' db.exec(\"CREATE TABLE livres (id INTEGER PRIMARY KEY, titre TEXT, annee INTEGER)\");' ' db.exec(\"CREATE TABLE membres (id INTEGER PRIMARY KEY, nom TEXT)\");' ' db.exec(\"CREATE TABLE prets (id INTEGER PRIMARY KEY, livre_id INTEGER REFERENCES livres(id), \"' ' + \"membre_id INTEGER REFERENCES membres(id), mois INTEGER, rendu INTEGER)\");' ' remplir(db, \"livres\", LIVRES);' ' remplir(db, \"membres\", MEMBRES);' ' remplir(db, \"prets\", PRETS);' ' return db;' '}' 'module.exports = { ouvrir };' >> mediatheque.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 script d’évaluation, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander, sqlDe } = require(\"./model\");' 'const { ouvrir } = require(\"./mediatheque\");' 'const db = ouvrir();' 'const cas = [' ' { ref: \"SELECT COUNT(*) FROM prets WHERE mois = 3\" },' ' { ref: \"SELECT COUNT(DISTINCT membre_id) FROM prets WHERE mois = 3\" },' ' { ref: \"SELECT COUNT(*) FROM prets WHERE rendu = 0\" },' ' { ref: \"SELECT l.titre FROM prets p JOIN livres l ON p.livre_id = l.id WHERE p.membre_id = 2\" },' ' { ref: \"SELECT titre FROM livres WHERE id NOT IN (SELECT livre_id FROM prets)\" },' '];' > evaluer.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 script d’évaluation, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const A = [\"SELECT COUNT(id) FROM prets WHERE mois = 3\",' ' \"SELECT COUNT(membre_id) FROM prets WHERE mois = 3\", cas[2].ref,' ' \"SELECT l.titre FROM livres l JOIN prets p ON p.livre_id = l.id WHERE p.membre_id = 2\",' ' \"SELECT l.titre FROM livres l LEFT JOIN prets p ON p.livre_id = l.id WHERE p.id IS NULL\"];' 'const B = [cas[0].ref, cas[1].ref, \"SELECT COUNT(*) FROM prets\",' ' \"SELECT titre FROM livres WHERE id IN (SELECT livre_id FROM prets WHERE membre_id = 2)\", cas[4].ref];' 'function lignes(sql) {' ' try { return db.prepare(sql).all().map(function (r) { return JSON.stringify(Object.values(r)); }).sort().join(\"|\"); }' ' catch (erreur) { return null; }' '}' >> evaluer.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 script d’évaluation, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function noter(sql, i) {' ' const obtenu = lignes(sql);' ' return obtenu === null ? \"erreur\" : obtenu === lignes(cas[i].ref) ? \"réussi\" : \"échoué\";' '}' 'function taux(liste, test) {' ' return Math.floor(100 * liste.filter(test).length / liste.length) + \" pour cent\";' '}' 'const a = A.map(noter);' 'const b = B.map(noter);' 'const meme = function (sql, i) { return sql === cas[i].ref; };' '// Une régression : réussie en A, pas réussie en B.' 'const perdues = [];' 'a.forEach(function (v, i) { if (v === \"réussi\" && b[i] === \"échoué\" || v === \"réussi\" && b[i] === \"erreur\") perdues.push(i + 1); });' >> evaluer.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 script d’évaluation, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' a.forEach(function (v, i) { console.log(\"question \" + (i + 1) + \" : A \" + v + \", B \" + b[i]); });' ' const reussi = function (v) { return v === \"réussi\"; };' ' console.log(\"par le résultat : A \" + taux(a, reussi) + \", B \" + taux(b, reussi));' ' console.log(\"par le texte exact : A \" + taux(A, meme) + \", B \" + taux(B, meme));' ' console.log(\"régressions dans B : \" + (perdues.length ? perdues.join(\", \") : \"aucune\"));' ' console.log(\"barrière : \" + (perdues.length ? \"bloquer, une question réussie échoue maintenant\" : \"livrer\"));' ' await direct();' '}' >> evaluer.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 script d’évaluation, partie 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function direct() {' ' const q = \"prets(id, livre_id, membre_id, mois, rendu)\nQuestion : Combien de membres différents ont emprunté un livre au mois 3 ?\";' ' const echantillon = \"SELECT COUNT(DISTINCT membre_id) FROM prets WHERE mois = 3;\";' ' const reponse = await demander([{ role: \"system\", content: \"Répondez par une seule requête SELECT pour SQLite, et rien d’autre.\" },' ' { role: \"user\", content: q }], echantillon, 120);' ' const sql = sqlDe(reponse.texte);' ' console.log(\"le modèle a écrit : \" + sql);' ' console.log(\" notée à part des totaux : \" + noter(sql, 1));' '}' 'main();' >> evaluer.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.
Noter deux versions sur cinq questions
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings evaluer.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 |
|---|---|
| question 2 : A échoué, B réussi | La nouvelle consigne a réparé la question 2 |
| question 3 : A réussi, B échoué | La nouvelle consigne a cassé la question 3 |
| par le résultat : A 80 pour cent, B 80 pour cent | Le score n’a pas bougé |
| par le texte exact : A 20 pour cent, B 60 pour cent | Le texte déclare fausses des réponses justes |
| régressions dans B : 3 | La question perdue |
| barrière : bloquer, une question réussie échoue maintenant | La décision |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.