Activité pratique

Durée3h45

Consignes globales

Le fil conducteur de cette activité est un petit projet uv, comme ceux des séances 2 et 3. Vous n’allez pas y écrire un gros programme : le but n’est pas le programme, c’est ce que vous observez en le faisant écrire par des IA.

À partir de cette séance, vous serez autorisé(e)s à utiliser des agents IA pour vous aider à coder au semestre 5 (hors exceptions mentionnées explicitement). Cependant, il est nécessaire de bien comprendre comment ils fonctionnent avant de les utiliser, c’est pourquoi nous allons passer cette séance à observer leurs comportements. N’oubliez pas de consulter les fiches de la séance, notamment celle sur la responsabilité.

Contenu de l’activité

Repérer l’IA dans votre éditeur

Avant de se servir d’une IA, il faut savoir où elle est. Cette partie est un inventaire : vous allez déclencher les quatre formes d’assistance une par une et relever ce qu’elles coûtent.

Préparer le projet

À faire

Dans votre dossier IMT/S5/info/env/session4, initialisez un projet uv. Ouvrez ensuite ce répertoire en tant que dossier courant dans VS Code.

​
cd IMT\S5\info\env\session4
uv init --no-package
cd IMT/S5/info/env/session4
uv init --no-package
cd IMT/S5/info/env/session4
uv init --no-package
cd IMT/S5/info/env/session4
uv init --no-package

Vérifier son compte

À faire

Connectez-vous à votre compte GitHub depuis l’icône de compte, en bas de la barre latérale de VS Code. Vérifiez que l’activation de GitHub Education est bien passée : le menu de choix du modèle, dans la fenêtre de discussion, doit proposer plusieurs entrées.

Si votre demande GitHub Education n’est pas encore validée, installez l’extension Continue depuis la marketplace : son offre gratuite suffit pour toute l’activité. La partie sur le choix du modèle sera simplement moins démonstrative, faute de pouvoir changer de modèle.

Les quatre intégrations IA

À faire

Dans un fichier inventaire.py, déclenchez successivement les quatre formes d’assistance décrites dans la fiche L’IA dans l’IDE, rappelées ci-dessous. Pour chacune des quatre formes, regardez où elle se déclenche dans l’interface, ce qu’elle a modifié sur votre disque, et si on vous a demandé une validation avant.

  1. La complétion : tapez def moyenne(valeurs): puis passez à la ligne et attendez. Regardez le texte grisé sans l’accepter.
  2. La discussion : ouvrez la fenêtre de chat et demandez à quoi sert uv.lock.
  3. L’édition en place : sélectionnez votre fonction, appelez Ctrl+I (Cmd+I sur macOS), et demandez d’ajouter des annotations de type.
  4. Le mode agent : basculez la fenêtre de discussion en mode agent et demandez de créer un fichier bonjour.py qui affiche l’heure.

Le compteur

À faire

Ouvrez le compteur de requêtes depuis l’icône Copilot de la barre d’état, en bas de la fenêtre. Notez sa valeur : vous la regarderez de nouveau à la fin de la partie sur le choix du modèle, puis à la fin de la partie sur le mode agent.

Le choix du modèle

On va ici chercher à comparer le code produit par deux modèles, afin d’évaluer leurs différences.

L’énoncé

On veut ranger des notes d’étudiant(e)s dans des tranches via la fonction suivante :

def repartition (notes: list[float], bornes: list[float]) -> dict[str, list[float]]

  • bornes est une liste de nombres, fournie dans un ordre quelconque.
  • Chaque tranche va d’une borne à la suivante, fermée à gauche, ouverte à droite : $[b0, b1[$, $[b1, b2[$, …
  • La dernière tranche est fermée des deux côtés : $[b(n-1), b(n)]$.
  • Les notes hors de l’intervalle total sont rangées à part, sous la clé “hors bornes”.
  • Le dictionnaire associe à chaque clé la liste des notes correspondantes. Toutes les tranches y figurent, même vides, ainsi que la clé “hors bornes”.
  • Le dictionnaire est ordonné par borne croissante, et ses clés sont des chaînes décrivant les tranches, du type "[0, 10[", "[10, 20]" et "hors bornes".

Le juge

À faire

Créez test_repartition.py à la racine du projet, avec le contenu ci-dessous. C’est votre juge : il servira à départager les deux versions de la fonction, et il ne doit pas entrer dans le contexte de l’assistant avant la fin de la comparaison (voir ci-dessous).

import unittest
from repartition import repartition # Adapter ici


class RepartitionTests (unittest.TestCase):

    def test_ordre_des_bornes (self) -> None:
        # Les bornes arrivent en désordre : le résultat doit être trié.
        resultat = repartition([5], [20, 0, 10])
        self.assertEqual(list(resultat), ["[0, 10[", "[10, 20]", "hors bornes"])

    def test_tranche_ouverte_a_droite (self) -> None:
        # 10 appartient à la tranche du haut, pas à celle du bas.
        resultat = repartition([9.99, 10], [0, 10, 20])
        self.assertEqual(resultat["[0, 10["], [9.99])
        self.assertEqual(resultat["[10, 20]"], [10])

    def test_derniere_tranche_fermee (self) -> None:
        # 20 est dans la dernière tranche, pas hors bornes.
        resultat = repartition([20], [0, 10, 20])
        self.assertEqual(resultat["[10, 20]"], [20])

    def test_hors_bornes (self) -> None:
        resultat = repartition([-1, 21], [0, 10, 20])
        self.assertEqual(resultat["hors bornes"], [-1, 21])


if __name__ == "__main__":
    unittest.main()

Si l’assistant lit les tests, il écrit du code pour les faire passer, et la comparaison ne mesure plus sa capacité à suivre l’énoncé. Or, comme expliqué dans la fiche Le contexte, plusieurs mécanismes peuvent y faire entrer le juge sans que vous le demandiez :

  • Le fichier actif dans l’éditeur est joint automatiquement à vos demandes, et les autres onglets ouverts peuvent l’être aussi.
  • Les recherches dans le projet (#codebase, @workspace, et celles que le mode agent lance de lui-même) peuvent tomber dessus.
À faire

Une fois test_repartition.py enregistré :

  1. Fermez son onglet, et ne le rouvrez pas avant d’avoir obtenu les deux versions de la fonction.
  2. Excluez-le de l’espace de travail, en créant un fichier .vscode/settings.json avec le contenu ci-dessous. Le juge disparaît de l’explorateur et des recherches de l’assistant, mais il est toujours sur le disque : les tests s’exécutent normalement depuis le terminal.
  3. Restez en mode discussion pour toute cette partie, sans utiliser #codebase ni @workspace.
{
    "files.exclude": {
        "**/test_repartition.py": true
    }
}

Plus simplement, vous pouvez également déplacer le fichier dans un dossier hors du projet le temps d’utiliser vos agents, par exemple sur votre bureau. Pensez également à fermer l’onglet.

Générer la fonction avec deux modèles différents

À faire

Dans la fenêtre de discussion, choisissez le modèle le plus rapide proposé par le menu. Demandez-lui d’écrire la fonction, en donnant l’énoncé ci-dessus. Enregistrez le résultat dans repartition_rapide.py.

Puis recommencez dans un nouveau chat, avec exactement la même demande, en choisissant cette fois le modèle de raisonnement le plus performant du menu. Enregistrez dans repartition_raisonnement.py.

Vous pouvez maintenant rouvrir le juge : retirez l’exclusion de .vscode/settings.json, ou remettez le fichier dans votre projet si vous l’aviez déplacé.

À faire

Faites passer les tests aux deux versions, l’une après l’autre, en adaptant l’import en tête de test_repartition.py (par exemple from repartition_rapide import repartition). Les tests se lancent avec la commande ci-dessous.

Regardez pour chaque version le nombre de tests qui passent, et lesquels échouent le cas échéant.

​
uv run python -m unittest test_repartition.py
uv run python -m unittest test_repartition.py
uv run python -m unittest test_repartition.py
uv run python -m unittest test_repartition.py

Regardez de nouveau le compteur de requêtes. Combien cette partie a-t-elle consommé ?

Le contexte

La complétion imite votre style

Vous allez maintenant vérifier, en le provoquant, que la complétion reproduit ce qu’elle lit juste au-dessus du curseur.

Poser un style
À faire

Créez stats.py et recopiez-y les deux fonctions ci-dessous à la main, sans accepter aucune suggestion. Leur style est délibérément marqué : annotations de type, docstring en français au format NumPy, espace avant les deux-points, garde explicite sur la liste vide, et une seule sortie en fin de fonction.

def moyenne (valeurs : list[float]) -> float :
    """
    Calcule la moyenne arithmétique d'une série de valeurs.

    Parameters
    ----------
    valeurs : list[float]
        La série à résumer. Ne doit pas être vide.

    Returns
    -------
    float
        La moyenne des valeurs.
    """
    if len(valeurs) == 0 :
        raise ValueError("La série est vide.")
    resultat = sum(valeurs) / len(valeurs)
    return resultat


def etendue (valeurs : list[float]) -> float :
    """
    Calcule l'étendue d'une série de valeurs, c'est-à-dire l'écart entre
    la plus grande et la plus petite.

    Parameters
    ----------
    valeurs : list[float]
        La série à résumer. Ne doit pas être vide.

    Returns
    -------
    float
        L'étendue des valeurs.
    """
    if len(valeurs) == 0 :
        raise ValueError("La série est vide.")
    resultat = max(valeurs) - min(valeurs)
    return resultat
Observer la contagion
À faire

À la suite, tapez uniquement la ligne suivante, puis passez à la ligne et attendez sans rien taper :

def variance (valeurs : list[float]) -> float :

Regardez la proposition sans l’accepter. Puis acceptez-la et lisez-la en entier.

Regardez ce qui a été imité et ce qui ne l’a pas été : l’espace avant les deux-points ? la docstring NumPy en français ? la garde sur la liste vide ? la variable resultat et la sortie unique ?

À faire

Maintenant l’expérience inverse. Créez un fichier vide stats_bis.py, et tapez-y la même ligne def variance (valeurs : list[float]) -> float :.

Comparez la proposition à la précédente. Qu’est-ce qui change, et pourquoi ?

Réutiliser ou réécrire

C’est l’expérience la plus importante de la séance. Vous allez poser la même demande à deux endroits : dans l’éditeur, et dans un onglet de navigateur. Puis vous comparerez le code obtenu.

La demande

On veut ajouter à stats.py une fonction resume(valeurs) qui renvoie un dictionnaire contenant la moyenne, l’étendue et le nombre de valeurs.

Cette fonction n’a aucune raison de recalculer quoi que ce soit : moyenne et etendue existent déjà, juste au-dessus.

Chat externe Vs. Assistant intégré
À faire

Ouvrez un assistant dans votre navigateur (ChatGPT, Le Chat, Claude, celui que vous utilisez). Demandez-lui la fonction resume, en décrivant ce qu’elle doit renvoyer, sans lui donner votre fichier.

Collez sa réponse dans resume_externe.py.

À présent, comparons avec le chat intégré à VS Code pour voir l’importance du contexte.

À faire

Dans VS Code, ouvrez la fenêtre de discussion, joignez explicitement votre fichier avec #file:stats.py, et faites la même demande, mot pour mot.

Collez sa réponse dans resume_integre.py, puis comparez les deux fichiers.

Rendre le contexte durable

Répéter « réutilise les fonctions existantes » à chaque demande est fastidieux. On peut l’écrire une fois pour toutes.

À faire

Créez .github/copilot-instructions.md à la racine du projet, avec quelques consignes correspondant au style de stats.py : annotations de type obligatoires, docstrings NumPy en français, réutilisation des fonctions existantes, pas de nouvelle dépendance.

Puis demandez, dans une nouvelle discussion et sans joindre de fichier cette fois, une fonction mediane(valeurs). Le style est-il respecté sans que vous l’ayez redemandé ?

Découvrir le mode agent

Dans la première partie, vous avez déclenché le mode agent sur une tâche triviale. Vous allez maintenant lui confier une vraie tâche sur votre projet, qui demande de lire du code existant, de créer plusieurs fichiers et d’exécuter une commande. Le but n’est pas tant le résultat que ce que l’agent fait pour y arriver, à comparer avec la boucle décrite dans la fiche L’IA dans l’IDE.

Confier une tâche à l’agent

À faire

Notez la valeur du compteur de requêtes. Puis, dans une nouvelle discussion, passez en mode agent et formulez la demande ci-dessous.

Crée un script main.py qui lit des notes dans un fichier notes.txt (une note par ligne), puis affiche leur moyenne et leur étendue en réutilisant les fonctions de stats.py. Crée aussi un fichier notes.txt d’exemple, puis exécute main.py pour vérifier que tout fonctionne.

Observer ce qu’il fait

À faire

Pendant que l’agent travaille, ne cliquez sur rien sans avoir lu, et relevez les éléments suivants.

  1. Les fichiers qu’il lit, et comment il les trouve : les avez-vous désignés, ou les a-t-il cherchés lui-même ?
  2. Les fichiers qu’il crée ou modifie. En a-t-il touché un que vous n’aviez pas mentionné ?
  3. Les commandes qu’il veut exécuter, et le moment où il vous demande votre accord. Lisez chaque commande avant d’accepter. Utilise-t-elle uv run, ou appelle-t-elle python directement ?
  4. Le nombre de tours : combien de fois a-t-il agi, lu un résultat, puis agi de nouveau ?

Relire avant de conserver

À la fin, VS Code liste les fichiers modifiés par l’agent, et propose de conserver ou d’annuler les modifications de chacun. Tant que vous n’avez pas tranché, rien n’est définitivement validé.

À faire

Relisez le diff de chaque fichier avant de conserver quoi que ce soit. En particulier, considérez les points suivants.

  1. main.py appelle-t-il moyenne et etendue, ou recalcule-t-il de son côté ?
  2. Le style de stats.py et les consignes de .github/copilot-instructions.md sont-ils respectés ?
  3. Que se passe-t-il si notes.txt contient une ligne vide, ou une ligne abc ? Ajoutez-en une, et lancez vous-même le script avec uv run python main.py.

Demander une correction

À faire

Dans la même discussion, demandez à l’agent d’ignorer les lignes vides, et de lever une ValueError indiquant le numéro de la ligne en cas de ligne invalide. Observez de nouveau ce qu’il fait, et relisez le diff.

Regardez enfin le compteur de requêtes. Combien cette partie a-t-elle consommé, comparée à une simple question dans la fenêtre de discussion ?

Pour aller plus loin

Le harnais : ce que l’agent peut vérifier lui-même

Un agent seul ne peut que produire du texte plausible. Ce qui le rend réellement utile, c’est le harnais : l’ensemble des outils qu’on lui donne pour vérifier son propre travail : exécuter les tests, lire une erreur, relancer. Sans harnais, il devine. Avec, il itère.

Vous allez mesurer la différence sur le même énoncé.

Sans harnais

À faire

Dans un dossier sans_harnais/, en mode discussion (pas agent), demandez une fonction parse_duree(texte) qui transforme une durée écrite en chaîne vers un nombre de secondes, en acceptant les formats "90", "1m30", "1h05m", "2h", et en levant ValueError sur une entrée invalide.

Collez le résultat dans sans_harnais/duree.py. Puis écrivez vous-même, dans sans_harnais/test_duree.py, des tests unittest couvrant ces quatre formats et un cas invalide (pour ce dernier, utilisez self.assertRaises(ValueError)). Lancez-les depuis le dossier sans_harnais/ :

​
cd sans_harnais
uv run python -m unittest test_duree.py
cd sans_harnais
uv run python -m unittest test_duree.py
cd sans_harnais
uv run python -m unittest test_duree.py
cd sans_harnais
uv run python -m unittest test_duree.py

Combien passent du premier coup ?

Avec harnais

À faire

Dans un dossier avec_harnais/, écrivez d’abord le fichier de tests test_duree.py, seul, avec unittest (vous pouvez reprendre celui de sans_harnais/). Puis passez la fenêtre de discussion en mode agent et formulez la demande ainsi :

Écris parse_duree dans avec_harnais/duree.py pour que avec_harnais/test_duree.py passe entièrement. Après chaque modification, lance les tests avec uv run python -m unittest test_duree.py depuis le dossier avec_harnais/, et corrige jusqu’à ce que tous les tests passent. Ne modifie pas le fichier de tests.

Laissez l’agent travailler, et regardez ce qu’il fait : les commandes qu’il lance, les sorties qu’il lit, les corrections qu’il enchaîne. Combien de tours lui a-t-il fallu ? Sur quoi s’est-il repris ?

À faire

Comparez les deux situations : qu’est-ce qui a changé entre les deux, du point de vue du modèle ? La dernière consigne (« ne modifie pas le fichier de tests ») vous paraît-elle nécessaire ? Que se passerait-il sans elle ?

Regardez une dernière fois le compteur de requêtes. Quelle partie de l’activité a le plus consommé ? Est-ce celle qui vous a le plus servi ?

Pour aller encore plus loin

  • Une étude d’Anthropic sur l’effet de l’assistance sur l’apprentissage de la programmation, et sur les usages qui consolident ou érodent les compétences (en anglais). À lire avant de prendre vos habitudes pour le projet.
  • Ce qui distingue un enchaînement d’appels bien conçu d’un agent autonome, et pourquoi la solution la plus simple qui marche est presque toujours la bonne (en anglais). Le prolongement direct de la partie sur le harnais.
  • Une série d’articles de Simon Willison sur une vulnérabilité structurelle des agents : un agent qui lit un fichier, une page web ou un ticket peut y trouver des instructions et les suivre (en anglais).