Une méthode de travail fiable pour programmer avec l'IA
Écrire d'abord les critères d'acceptation et les tests, puis laisser l'IA travailler, et juger qu'elle a fini d'après le résultat des tests plutôt que d'après ce qu'elle dit. Dérouler cette méthode sur une vraie petite tâche, puis voir comment relire le code écrit par l'IA, et quand ne pas utiliser l'IA.
- Environ 40 minutes
- Niveau : Intermédiaire
- Testé : 2026-09-14 deepseek-flash, pytest 9.1
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
Quand on programme avec l'IA, l'échec le plus courant n'est pas qu'elle n'arrive pas à écrire le code, mais qu'elle l'écrit, annonce « c'est terminé », que vous la croyez, et que le problème n'apparaît qu'après la mise en production.
La cause se situe généralement aux deux bouts : au départ, on n'a pas dit clairement à quoi ressemble « terminé » ; à la fin, on n'a pas vérifié objectivement que c'était vraiment terminé. La partie du milieu, où l'IA écrit le code, est au contraire celle qui pose le moins de problèmes.
Cette leçon présente une méthode de travail simple qui renforce les deux bouts. Elle n'est liée à aucun outil et fonctionne aussi bien avec la complétion, la conversation ou un agent.
Écrire d'abord les « critères de fin »
Avant de laisser l'IA travailler, répondez à une question : une fois la modification faite, comment saurai-je qu'elle est juste ?
La meilleure réponse est un ensemble de tests. Les tests font de « terminé » quelque chose qu'on peut exécuter et qui donne un résultat net : tout passe, c'est terminé ; un seul échoue, ce n'est pas terminé. Pas besoin d'écouter ce que dit l'IA, ni de juger au feeling.
Démonstration sur une petite tâche. En appelant l'API, l'assistant de questions-réponses sur httpx peut recevoir un 429 (débit limité), et l'en-tête de réponse Retry-After indique quand réessayer. Il s'écrit de deux façons : un nombre de secondes (120), ou une date HTTP (Wed, 21 Oct 2026 07:28:00 GMT). Il nous faut une fonction qui le convertisse en « encore tant de secondes à attendre ».
On écrit d'abord les tests, avec tous les cas qui me viennent à l'esprit :
from datetime import datetime, timezone
from retry_after import parse_retry_after
NOW = datetime(2026, 10, 21, 7, 0, 0, tzinfo=timezone.utc)
def test_seconds():
assert parse_retry_after("120", NOW) == 120.0
def test_seconds_with_spaces():
assert parse_retry_after(" 30 ", NOW) == 30.0
def test_http_date_in_future():
assert parse_retry_after("Wed, 21 Oct 2026 07:28:00 GMT", NOW) == 28 * 60.0
def test_http_date_in_past_means_no_wait():
assert parse_retry_after("Wed, 21 Oct 2026 06:00:00 GMT", NOW) == 0.0
def test_negative_seconds_is_invalid():
assert parse_retry_after("-5", NOW) is None
def test_decimal_seconds_is_invalid():
# 标准规定秒数是非负整数,"1.5" 不合法
assert parse_retry_after("1.5", NOW) is None
def test_garbage_is_invalid():
assert parse_retry_after("soon", NOW) is None
(Les 10 tests complets sont dans code/07-ai-coding/test_retry_after.py.)
Écrire les tests aide déjà à clarifier le besoin. En arrivant à « et si la date est déjà passée ? », il faut décider de renvoyer 0 ou un nombre négatif ; à « 1.5 seconde est-il valide ? », il faut aller vérifier dans la norme. Si vous ne tranchez pas ces questions d'avance, l'IA en décidera arbitrairement pour vous.
En plus des tests, on écrit une courte description de la tâche (TASK.md) avec les exigences que les tests ne peuvent pas exprimer : n'utiliser que la bibliothèque standard, ne pas modifier le fichier de tests, ne pas écrire de cas particulier pour les valeurs précises des tests. Ce dernier point est important : sans lui, une IA « maligne » pourrait écrire du code comme « si l'entrée vaut 120, renvoyer 120.0 », conçu exprès pour tromper les tests.
Laisser l'IA travailler, vérifier avec les tests
code/07-ai-coding/ai_coding_loop.py fait de cette méthode un petit programme : il donne la description et les tests au modèle, lui fait écrire retry_after.py, lance les tests ; en cas d'échec, il lui renvoie la sortie des tests pour correction, au plus 3 tours :
for round_ in range(1, 4):
reply = client.chat.completions.create(model=MODEL, messages=messages).choices[0].message.content
TARGET.write_text(extract_code(reply))
code, output = run_tests()
summary = output.strip().splitlines()[-1] if output.strip() else ""
print(f"第 {round_} 轮:退出码 {code},{summary}")
if code == 0:
print("测试全部通过。生成的代码:\n")
print(TARGET.read_text())
break
messages += [{"role": "assistant", "content": reply},
{"role": "user", "content": f"测试没有通过,输出如下。修改代码,再给出完整的 retry_after.py:\n\n{output}"}]
else:
print("3 轮都没有通过,停下来交给人看。最后一次的测试输出:\n" + output)
Ce qui décide si c'est « terminé », c'est le code de sortie de pytest : 0 signifie que tout passe, autre chose signifie des échecs. Pas le « j'ai terminé » du modèle.
C'est en miniature ce que font les agents de programmation comme Claude Code ou Codex : écrire du code, lancer les tests, regarder le résultat, corriger. La différence : ils décident eux-mêmes quand et quels tests lancer. Donnez-leur donc un fichier de règles contenant les commandes de test (leçon précédente), et ils pourront vérifier eux-mêmes.
Les vrais résultats
Je l'ai exécuté de nombreuses fois, dans deux situations.
Quand le modèle voit la description complète et les tests, les 4 exécutions ont toutes réussi dès le premier tour.
Quand le modèle n'a qu'une phrase de besoin, sans voir les tests (avec le paramètre --vague, les tests restant entre mes mains pour l'acceptation), 10 exécutions sur 11 ont réussi dès le premier tour ; une fois, un test a échoué au premier tour, et après avoir reçu la sortie des tests, il a réussi au deuxième :
第 1 轮:退出码 1,1 failed, 9 passed in 0.01s
第 2 轮:退出码 0,10 passed in 0.00s
(À ce moment-là, mon script n'affichait pas encore le nom des tests échoués, je ne sais donc pas lequel c'était. Le script actuel affiche les lignes qui commencent par FAILED.)
Voici le code produit par la dernière exécution, sans un caractère changé :
from datetime import timezone
from email.utils import parsedate_to_datetime
def parse_retry_after(value, now):
if not isinstance(value, str):
return None
value = value.strip()
if not value:
return None
if value.isascii() and value.isdigit():
try:
return float(int(value))
except (ValueError, OverflowError):
return None
try:
retry_time = parsedate_to_datetime(value)
except (TypeError, ValueError, OverflowError):
return None
if retry_time is None:
return None
if retry_time.tzinfo is None:
retry_time = retry_time.replace(tzinfo=timezone.utc)
return max(0.0, (retry_time - now).total_seconds())
Un détail mérite l'attention : value.isascii() and value.isdigit(). isdigit() seul ne suffit pas : il renvoie aussi True pour des chiffres arabes, des chiffres en exposant et d'autres caractères non ASCII. Le isascii() ajouté par le modèle bouche justement une faille que mes tests ne couvraient pas. Inversement, s'il ne l'avait pas écrit, mes 10 tests ne l'auraient pas remarqué non plus.
Honnêtement, cette tâche n'est pas difficile pour les modèles actuels : le format de Retry-After est clairement décrit dans la norme HTTP, le modèle le connaît bien, et la bibliothèque standard fournit déjà parsedate_to_datetime pour analyser les dates HTTP.
Mais c'est justement là tout le sens de cette méthode : vous ne savez pas d'avance si cette fois-ci il va se tromper. Une fois sur 11, avec une seule phrase de besoin, il a raté un détail. Sans tests, vous auriez reçu ce code défectueux, et le modèle vous aurait dit « c'est terminé ». Avec les tests, cet échec a été détecté et corrigé automatiquement, sans même que vous ayez à regarder où était l'erreur.
Avancer par petites étapes
L'exemple ci-dessus ne concerne qu'une fonction. Les vraies tâches sont souvent bien plus grosses : « ajouter la connexion des utilisateurs à RepoBot ». Confiée à l'IA, une telle tâche aboutit le plus souvent à ceci : elle modifie d'un coup une douzaine de fichiers, des centaines de lignes, que vous ne pouvez pas relire, et vous n'avez plus qu'à tout accepter ou tout rejeter.
Mieux vaut découper les grosses tâches en petites étapes, qui remplissent chacune trois conditions :
- Une seule chose à la fois. « Ajouter une table des utilisateurs » est une étape, « écrire l'interface de connexion » en est une autre, « ajouter un formulaire de connexion au frontend » encore une autre.
- Une modification assez petite pour être relue en quelques minutes. Si le diff est si gros que vous n'avez pas envie de le lire, l'étape est trop grosse.
- Sa propre méthode d'acceptation. Idéalement des tests, au minimum un résultat que vous pouvez vérifier à la main.
À chaque étape terminée, un commit git. En cas de problème, on revient au dernier bon état, au lieu de rester désemparé devant un tas de modifications mélangées.
Avant de commencer, on peut aussi demander d'abord à l'IA un plan, sans agir (le mode plan ou lecture seule évoqué à la leçon précédente et à la leçon 1). Son plan doit dire clairement quels fichiers il compte modifier, ce que fait chaque étape, comment vérifier. Vous lisez le plan, et si la direction vous semble bonne, vous le laissez modifier. Si la direction est mauvaise, vous le découvrez au stade du plan, pour quelques minutes perdues seulement.
Relire les modifications de l'IA
Des tests qui passent ne signifient pas que tout va bien. Les tests ne vérifient que les cas auxquels vous avez pensé. Avant de fusionner, relisez les modifications de l'IA, en regardant particulièrement ces points :
- A-t-elle modifié les tests ? Pour faire passer les tests, l'IA peut modifier les tests eux-mêmes, ou supprimer ceux qui échouent. C'est ce qui doit le plus vous alerter.
- Y a-t-il des cas particuliers pour les tests ? Si le code contient exactement les mêmes valeurs précises que les cas de test, méfiez-vous.
- A-t-elle modifié autre chose au passage ? Vous lui demandez de corriger un bug, et elle « optimise » au passage trois fonctions sans rapport. Ces modifications ne sont couvertes par aucun test et sortent de ce que vous attendiez.
- Cas limites et gestion des erreurs. Valeurs vides, entrées trop longues, pannes réseau, concurrence. Le
parse_retry_afterci-dessus traite avectry/exceptl'échec de l'analyse de la date ; c'est typiquement ce qu'il faut confirmer en relisant. - Problèmes de sécurité. SQL construit par concaténation, commandes exécutées, fichiers téléversés par les utilisateurs, clés affichées ou journalisées. Les problèmes vus à la leçon 8 du module 05 et à la leçon 5 du module 06 apparaissent tout autant dans le code écrit par l'IA.
- A-t-elle introduit de nouvelles dépendances ? L'IA aime installer « au passage » un paquet pour résoudre un problème. Chaque nouvelle dépendance est une charge de maintenance durable, et un risque de sécurité potentiel.
- Le comprenez-vous ? Si vous ne comprenez pas un bout de code, ne le fusionnez pas. Quand il posera problème, il faudra que vous sachiez le réparer.
Quand ne pas utiliser l'IA
- Vous ne savez pas vous-même clairement ce que vous voulez. L'IA décidera volontiers à votre place, mais ses décisions ne seront pas forcément les bonnes. Clarifiez d'abord, écrivez les critères d'acceptation, puis lancez-vous.
- Vous ne pouvez pas vérifier le résultat. Dans un domaine que vous ne connaissez pas, ou pour du code sans tests que vous ne pouvez pas non plus vérifier à la main, vous ne saurez pas s'il est juste, si convaincant que soit le code de l'IA.
- La modification coûte cher et est irréversible. Migrations de base de données, suppressions de données, configuration de production. L'IA peut vous aider à les écrire, mais vous devez relire vous-même et valider d'abord dans un environnement de test.
- Vous voulez apprendre la chose. Faire écrire ses exercices par l'IA, comme le disait la première leçon de ce cours, c'est envoyer quelqu'un à la salle de sport à votre place.
Toute la méthode, bout à bout
1. 想清楚:做完是什么样子?写成测试或者可检查的验收标准
2. 拆小:一步只做一件事
3. 计划:让 AI 先说它打算怎么做,你确认方向
4. 动手:让 AI 改,改动控制在你能看完的范围
5. 验证:跑测试,看退出码,不看 AI 的自述
6. 审查:看它改了什么,重点看测试、边界、安全、依赖
7. 提交:git commit,然后开始下一步
8. 复盘:它犯过的错,写进项目的规则文件(上一课)
Cette méthode semble plus laborieuse que « laisser l'IA écrire directement ». Mais ce qui prend vraiment du temps n'a jamais été d'écrire le code : c'est de trouver les problèmes, de les localiser et de les corriger. Cette méthode avance le moment où l'on trouve les problèmes, et limite les dégâts d'un problème à une petite étape.
Exercices
- Lancez plusieurs fois
ai_coding_loop.pyetai_coding_loop.py --vague, en notant à chaque fois en combien de tours cela passe. - Ajoutez un test à
test_retry_after.py: que doit renvoyerparse_retry_after("Wed, 21 Oct 2026 07:28:00 +0800", NOW)? Vérifiez d'abord ce que la norme HTTP exige pour le format des dates, fixez votre réponse, puis voyez si le code écrit par l'IA la respecte. - Choisissez une petite fonctionnalité dans votre propre projet et déroulez entièrement la méthode de cette leçon : écrire d'abord les tests, faire implémenter par l'IA, lancer les tests, relire, commiter. Notez les problèmes trouvés à la relecture.
Auto-test
1. Pourquoi écrire les tests avant de laisser l'IA travailler ?
Les tests font de « terminé » un critère exécutable au résultat net. Dès lors, on juge si la tâche est terminée d'après le résultat des tests, et non d'après le « c'est terminé » de l'IA. Écrire les tests oblige aussi à clarifier les points flous du besoin (que faire si la date est passée, les décimales sont-elles valides), au lieu de laisser l'IA en décider arbitrairement.
2. Tous les tests passent : faut-il encore relire le code de l'IA ? Que regarder en priorité ?
Oui. Les tests ne vérifient que les cas auxquels vous avez pensé. En relisant, regardez en priorité : si elle a modifié ou supprimé des tests, si elle a écrit des cas particuliers pour les cas de test, si elle a modifié au passage des choses sans rapport, les cas limites et la gestion des erreurs, les problèmes de sécurité, l'introduction de nouvelles dépendances, et si vous comprenez ce code.
3. Pourquoi découper une grosse tâche en petites étapes, avec un commit par étape ?
Confiée d'un bloc à l'IA, une grosse tâche produit trop de modifications pour être relue sérieusement : on ne peut que tout accepter ou tout rejeter, et un problème est difficile à localiser. En petites étapes, chaque modification est assez petite pour être relue et a sa propre acceptation ; avec un commit par étape, on peut revenir au dernier bon état en cas de problème.