/effort et la réflexion étendue~/projets/quizops⚠️ L'exercice 1 fait tourner deux modèles différents. Si votre compte n'ouvre que Sonnet, la consigne prévoit le repli : deux essais Sonnet, même grille.
50 minutes, exercice optionnel compris : 20 min pour l'exercice 1, 12 pour le 2, 13 pour le 3, 5 pour le 4.
Le tag tp4-depart reconstruit l'état de sortie du TP 3 : le filtre ?categorie=, le 404 sur catégorie inconnue, GET /api/categories et les boutons du front. Il manque le classement des meilleurs scores, et le score des parties incomplètes est faux sans qu'aucun test ne le signale.
cd ~/projets/quizops
outils/depart.sh 4
.............. [100%]
14 passed in 0.20s
depart.sh efface quizops.db. Le classement a besoin de données : insérez les douze scores de référence.
$ uv run outils/graine.py
12 scores insérés
Les tests d'acceptation des fonctionnalités à venir attendent dans tests/a-venir/, ignorés par pytest tant qu'ils y restent. Deux d'entre eux servent dans ce TP.
$ ls tests/a-venir/ | wc -l
8
Confier la même tâche à deux modèles dans deux copies de travail isolées, puis choisir le gagnant sur des mesures plutôt que sur une impression.
Un worktree est un second répertoire de travail rattaché au même dépôt, sur une autre branche : les deux essais partagent l'historique et aucun fichier.
$ git worktree add ../quizops-a -b essai-a
Preparing worktree (new branch 'essai-a')
HEAD is now at <hash> Liste les catégories et propose un bouton par catégorie
$ git worktree add ../quizops-b -b essai-b
Preparing worktree (new branch 'essai-b')
HEAD is now at <hash> Liste les catégories et propose un bouton par catégorie
$ git worktree list
~/projets/quizops <hash> [tp4]
~/projets/quizops-a <hash> [essai-a]
~/projets/quizops-b <hash> [essai-b]
Le contrat du classement est écrit avant le code. Faites-le dans les deux répertoires, ~/projets/quizops-a puis ~/projets/quizops-b.
cd ~/projets/quizops-a
uv sync
uv run outils/graine.py
git mv tests/a-venir/test_classement.py tests/
uv run pytest -q | tail -3
FAILED tests/test_classement.py::test_chaque_entree_a_un_pseudo_et_un_score
FAILED tests/test_classement.py::test_le_classement_est_trie_du_meilleur_au_moins_bon
3 failed, 15 passed in 0.19s
Quatre tests sont arrivés, trois échouent : la route n'existe pas encore. Committez ce déplacement dans chaque essai avec git commit -qam "Prépare les tests du classement", il fera partie de la fusion.
Ouvrez une session dans chaque répertoire. Dans quizops-a, basculez sur Haiku ; dans quizops-b, sur Opus.
/model haiku
Set model to `Haiku 4.5` for this session only
⚠️ Si /model haiku ou /model opus renvoie une erreur d'accès, restez sur Sonnet des deux côtés : la grille de mesures et la leçon sont identiques, vous comparez alors deux tirages du même modèle.
Le prompt est le même mot pour mot dans les deux essais. C'est la condition pour que la comparaison veuille dire quelque chose.
Ajoute le classement des meilleurs scores à QuizOps.
Contexte : les scores sont déjà enregistrés dans la table scores par app/database.py. Le style du front est dans @static/app.js.
Tâche : une route GET /api/scores qui renvoie les dix meilleurs scores triés du plus haut au plus bas (pseudo, score, date), et l'affichage de cette liste sur l'écran de fin de partie.
Contraintes : aucune nouvelle dépendance ; la requête SQL dans app/database.py, la route dans app/main.py ; ne modifie pas les tests existants ; les tests de tests/test_classement.py doivent passer.
Vérification : lance uv run pytest -q et uvx ruff check . avant de conclure, et donne-moi le résultat.
Ne relisez pas encore le code. Trois commandes dans chaque répertoire, plus la ligne Messages de /context, suffisent à départager les essais.
uv run pytest -q | tail -1
git diff --stat | tail -1
uvx ruff check . | tail -1
| Mesure | Essai A | Essai B |
|---|---|---|
| Tests verts | ||
| Fichiers et lignes ajoutées | ||
Analyse ruff |
||
| Tokens de sortie |
cd ~/projets/quizops-a && git commit -qam "Ajoute le classement des dix meilleurs scores"
cd ~/projets/quizops && git merge essai-a && uv run pytest -q
$ uv run pytest -q | tail -1
18 passed in 0.17s
Les mesures ci-dessous ont été relevées sur le dépôt de référence avec ce prompt exact. Vos valeurs seront différentes : c'est la méthode qui se reproduit, pas les nombres.
| Mesure | Haiku 4.5 | Opus | Sonnet (référence) |
|---|---|---|---|
| Tests verts | 18 passed |
18 passed |
18 passed |
| Fichiers et lignes | 4 fichiers, 37 insertions | 5 fichiers, 50 insertions | 5 fichiers, 40 insertions |
Analyse ruff |
All checks passed! |
All checks passed! |
All checks passed! |
| Tokens de sortie | 4 600 | 5 422 | 3 260 |
| Durée | 49 s | 75 s | 35 s |
Les trois essais passent le contrat : le départage se joue sur la surface touchée et sur le coût. Le même travail demandé en une phrase (« Ajoute un système de scores. ») a produit 38 insertions pour 7 633 tokens de sortie, sans aucun test dans tests/ pour trancher.
✅ Quand deux essais passent le même test, la mesure qui reste est la quantité de code à relire.
18 passed ?Q: Pourquoi deux worktrees plutôt que deux branches ?
R: Deux sessions travaillent en même temps sans se marcher dessus : chaque worktree a ses fichiers, son .venv, sa base. Avec deux branches dans un seul répertoire, il faudrait arrêter un essai pour lancer l'autre.
Q: Que mesure-t-on à 18 passed des deux côtés ?
R: Le reste : le nombre de fichiers et de lignes touchés, la propreté au sens de ruff, les tokens dépensés. Le test dit si le contrat est rempli, il ne dit pas ce que coûte l'implémentation.
Faire diagnostiquer un bug silencieux à deux niveaux d'effort, et lire la différence.
Le test qui couvre le score des parties incomplètes attendait dans tests/a-venir/.
git mv tests/a-venir/test_score_partiel.py tests/
uv run pytest -q
=================================== FAILURES ===================================
________ test_cinq_bonnes_reponses_sur_dix_questions_donnent_cinquante _________
def test_cinq_bonnes_reponses_sur_dix_questions_donnent_cinquante():
questions = client.get("/api/questions").json()
reponses = reponses_justes(questions)
for reponse in reponses[5:]:
reponse["choix"] = None
partie = {"pseudo": "fanny", "reponses": reponses}
> assert client.post("/api/answers", json=partie).json()["score"] == 50
E assert 100 == 50
tests/test_score_partiel.py:17: AssertionError
=========================== short test summary info ============================
FAILED tests/test_score_partiel.py::test_cinq_bonnes_reponses_sur_dix_questions_donnent_cinquante
1 failed, 18 passed in 0.20s
❌ Cinq bonnes réponses sur dix donnent 100 %. Rien ne plante, l'API répond 200, le score est faux.
/effort règle le budget de raisonnement de la session : low, medium, high, xhigh, max.
/effort low
Voici la sortie de uv run pytest -q, collée telle quelle : <la trace>
Explique-moi pourquoi le score vaut 100 au lieu de 50, en citant la ligne responsable de app/quiz.py.
Ne modifie aucun fichier.
Notez la ligne Messages de /context juste après la réponse.
Relancez la même demande après /effort high et après avoir activé la réflexion étendue avec Option+T (Alt+T sous Windows et Linux). Ce que vous devez observer : l'indicateur de réflexion apparaît avant la réponse, Ctrl+O déplie le raisonnement, et la ligne Messages de /context a nettement plus augmenté qu'au tour précédent. Relevez la seconde valeur.
Corrige calculer_score dans app/quiz.py.
Contrainte : le score doit rapporter les bonnes réponses au nombre de questions posées, pas au nombre de questions répondues.
N'ajoute pas de garde qui masquerait le problème sans le résoudre.
Lance uv run pytest -q et donne-moi la dernière ligne.
$ uv run pytest -q | tail -1
19 passed in 0.20s
$ git diff --stat
app/quiz.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
Plus deux valeurs de tokens relevées dans /context, une par niveau d'effort.
La version fautive divisait par le nombre de réponses effectivement données :
repondues = [reponse for reponse in reponses if reponse.get("choix") is not None]
bonnes = compter_bonnes_reponses(repondues, questions_par_id)
return round(bonnes / len(repondues) * 100)
La correction divise par le nombre de questions posées et traite le cas de la liste vide :
total = len(reponses)
if total == 0:
return 0
bonnes = compter_bonnes_reponses(reponses, questions_par_id)
return round(bonnes / total * 100)
✅ Le if total == 0 n'est pas le correctif, c'est sa conséquence : une fois le dénominateur changé, une partie sans réponse divise par zéro.
Basculer de modèle en cours de session selon la nature du travail demandé, et constater l'écart de coût.
Deux demandes vous attendent : documenter cinq fonctions déjà écrites, et concevoir la fonctionnalité de statistiques dont le test d'acceptation dort dans tests/a-venir/test_statistiques.py. Deux natures de travail, deux modèles.
/clear : le bloc Session de /cost repart de zéro, sinon les tours de l'exercice 2 s'ajoutent au compte de Sonnet et faussent la comparaison.grep -c '"""' app/quiz.py et grep -c '^def ' app/quiz.py.haiku et faites étoffer les docstrings des cinq fonctions de app/quiz.py : une ligne de résumé, une section Args:, une section Returns:. Le docstring du module reste tel quel.sonnet ou opus et demandez un plan de la fonctionnalité GET /api/statistiques, à partir du seul fichier tests/a-venir/test_statistiques.py. Un plan, pas de code./cost et lisez le bloc Usage by model : une ligne par modèle employé depuis le /clear, chacune avec son propre montant.Contraintes à imposer dans votre prompt
💡 Indice : /model sans argument affiche le modèle courant et la liste des alias disponibles.
$ uv run pytest -q | tail -1
19 passed in 0.20s
Les compteurs de docstrings passent de 6 et 5 à deux lignes """ par fonction, soit 11 et 5 : chaque docstring multiligne ouvre et ferme sur sa propre ligne, et celui du module compte pour une.
Dans /cost, le bloc Usage by model compte deux lignes, une par modèle employé depuis le /clear, chacune avec ses tokens et son montant. Les chiffres varient d'un poste à l'autre ; ce qui ne varie pas, c'est qu'ils sont lisibles côte à côte.
1. L'état de départ
$ grep -c '"""' app/quiz.py
6
$ grep -c '^def ' app/quiz.py
5
2. Le tour Haiku
/model haiku
Set model to `Haiku 4.5` for this session only
Étoffe les docstrings des cinq fonctions de app/quiz.py.
Chacune : une ligne de résumé, une section Args: avec un paragraphe par paramètre, une section Returns:.
Contraintes : ne touche ni au code ni au docstring du module ; ne renomme rien ; n'ajoute aucune fonction.
Lance uv run pytest -q et donne-moi la dernière ligne.
3. Le tour Sonnet ou Opus
Lis tests/a-venir/test_statistiques.py et propose-moi le plan d'implémentation de GET /api/statistiques.
Contraintes : un plan numéroté, le ou les fichiers touchés à chaque étape, ce qui devient vérifiable à chaque étape.
N'écris aucun code et ne déplace pas le fichier de test.
4. La comparaison
/cost affiche le coût estimé de la session, son détail par modèle, puis les pourcentages de fenêtre d'usage. Sur un forfait, le montant est une estimation au tarif public et non une facture ; c'est la ligne Usage by model qui tranche la comparaison, puisqu'elle donne un chiffre par modèle.
You are currently using your subscription to power your Claude Code usage
Current session: 70% used · resets Sep 10 at 2:40pm (Europe/Paris)
Current week (all models): 12% used · resets Sep 14 at 11pm (Europe/Paris)
✅ Lisez les deux montants côte à côte. La tâche mécanique et bornée ne justifie pas le gros modèle ; la tâche de conception, si. Si vos chiffres disent le contraire, comptez le nombre de tours qu'il a fallu à Haiku pour arriver au résultat attendu : c'est là que se loge la différence.
Refermer proprement une comparaison : une session allégée, un seul répertoire de travail.
Vous laissez derrière vous deux worktrees, deux branches d'essai et une session qui porte les deux tours de l'exercice 3. Le TP 5 repart de zéro, autant lui laisser un dépôt propre.
/context avant, compactez la conversation avec /compact, relevez-le après.Contraintes à imposer
rm -rf sur les répertoires d'essai : c'est git qui les retire.git status --short est vide à la fin, ou ne contient que ce que vous voulez committer.💡 Indice : git worktree remove refuse un répertoire qui contient du travail non committé, et c'est très bien ainsi.
$ git worktree list
~/projets/quizops <hash> [tp4]
1. Avant et après compactage : /context, puis /compact, puis /context. La ligne Messages retombe, les fichiers mémoire et les outils restent chargés.
2. Le ménage, puis le contrôle
git worktree remove ../quizops-a
git worktree remove ../quizops-b
git branch -D essai-a essai-b
$ git worktree list
~/projets/quizops <hash> [tp4]
$ uv run pytest -q | tail -1
19 passed in 0.20s
✅ Un worktree oublié continue de bloquer sa branche : git branch -d refuse tant que le répertoire existe.
/effort : le budget de raisonnement se règle par session, de low à max ; Option+T active la réflexion étendue et Ctrl+O la donne à lire./model haiku pour le mécanique et le borné, sonnet ou opus pour la conception, dans la même session.Rendez-vous au TP 5 pour fiabiliser tout cela : la suite de tests s'étoffe avec des tests écrits par le formateur, puis avec les vôtres, et un sabotage vient vérifier qu'ils protègent vraiment.
➡️ Module associé : Prompt engineering et conception · TP suivant : TP 5 : Tester et Fiabiliser