Prompt engineering pour les agents de coding
TL;DR
Un bon prompt d'agent de coding a trois composants : ce qu'on veut (tâche précise), comment le valider (condition de sortie vérifiable), et ce qu'on ne veut pas (contraintes explicites). La plupart des résultats décevants viennent d'instructions qui manquent l'un des trois.
La différence entre un résultat décevant et un résultat utilisable tient rarement au modèle. Elle tient presque toujours à la façon dont la tâche a été formulée. Un agent de coding est très bon pour exécuter des instructions précises — et particulièrement mauvais pour deviner ce qu’on voulait dire quand les instructions étaient vagues.
Ce guide documente les patterns qui produisent des résultats fiables, extraits de 4 mois d’usage quotidien sur des projets Symfony et TypeScript en production.
Quelle est la structure d’un bon prompt d’agent ?
Trois composants, dans cet ordre :
1. La tâche — ce qu’on veut Précis, délimité, avec un verbe d’action. Pas d’objectif flou.
2. La condition de sortie — comment savoir que c’est fait Ce que l’agent peut vérifier de façon autonome. Idéalement : les tests passent, le build compile, un fichier existe avec tel contenu.
3. Les contraintes — ce qu’on ne veut pas Ce que l’agent ne doit pas faire, même si ça lui semble logique.
# Structure type
[Verbe d'action] [objet précis] [dans quel fichier / avec quelle interface]
Condition de sortie :
- [critère 1 vérifiable de façon autonome]
- [critère 2]
Contraintes :
- [ce que l'agent ne touche pas]
- [convention à respecter]
En pratique :
Extrais la logique de calcul des remises de OrderService.php
vers un nouveau DiscountCalculator.php dans le même namespace.
Condition de sortie :
- php bin/phpunit --filter OrderServiceTest passe sans modification des tests
- DiscountCalculator a ses propres tests dans DiscountCalculatorTest.php
- L'interface publique de OrderService est inchangée
Contraintes :
- Ne pas modifier OrderRepository.php
- Utiliser AbstractServiceTest pour les tests (voir tests/AbstractServiceTest.php)
- DiscountCalculator est final
Comment formuler la tâche ?
Partir du résultat attendu, pas du processus
L’agent choisit comment faire. Ton rôle est de définir quoi. La description du processus (“d’abord fais X, ensuite fais Y”) est souvent contreproductive — elle contraint l’agent sur la méthode sans garantir le résultat.
❌ "D'abord lis OrderService.php, ensuite identifie la logique de remises,
puis crée DiscountCalculator.php, enfin mets à jour les tests"
✅ "Extrais la logique de remises de OrderService dans DiscountCalculator.
Les tests existants doivent passer."
Nommer les fichiers explicitement
L’agent peut chercher les fichiers, mais lui donner les chemins exacts réduit les erreurs de navigation et le context window consommé.
❌ "Refactorise le service de paiement"
✅ "Refactorise src/Service/PaymentService.php pour extraire
la logique de retry dans src/Service/PaymentRetryService.php"
Préciser l’interface ou le contrat
Surtout pour les refactorings : ce qui ne doit pas changer.
✅ "L'interface publique de PaymentService reste identique —
les signatures des méthodes publiques ne changent pas"
Comment définir une condition de sortie ?
La condition de sortie est ce qui distingue un prompt qui produit du code d’un prompt qui produit du code validé. Sans elle, l’agent s’arrête quand le code lui semble logiquement correct — pas quand il fonctionne.
Préférer les conditions que l’agent peut vérifier lui-même
✅ "php bin/phpunit --filter OrderServiceTest passe"
✅ "npx tsc --noEmit retourne 0 erreur"
✅ "npx vitest run --reporter=verbose passe"
✅ "Le fichier src/Service/DiscountCalculator.php existe"
❌ "Le code est propre" (non vérifiable)
❌ "Tout fonctionne" (trop vague)
Plusieurs conditions pour les tâches complexes
Condition de sortie :
- Les tests existants passent : php bin/phpunit --filter OrderTest
- Les nouveaux tests passent : php bin/phpunit --filter DiscountTest
- Aucune erreur TypeScript : npx tsc --noEmit
- Aucun import circulaire : pas de DeprecationWarning dans les logs
Avec ces conditions, l’agent itère jusqu’à ce qu’elles soient toutes satisfaites — sans intervention intermédiaire.
Comment formuler les contraintes ?
Les contraintes sont la partie la plus souvent omise et la plus impactante. Un agent sans contraintes optimise pour “une solution qui ressemble à ce qui est demandé” — pas pour “une solution qui respecte les conventions du projet”.
Les trois types de contraintes utiles
Périmètre — ce que l’agent ne touche pas :
- Ne pas modifier OrderRepository.php
- Ne pas changer les signatures des méthodes publiques existantes
- Travailler uniquement dans src/Service/ et tests/Service/
Conventions — comment l’agent doit faire :
- Les classes de service sont final
- Injection via constructeur uniquement, pas de setter injection
- Utiliser les fixtures existantes dans tests/fixtures/ plutôt que d'en créer
Références — où trouver les patterns à suivre :
- Suivre le pattern de PaymentService.php pour la structure
- Les tests doivent ressembler à OrderServiceTest.php
- Voir AGENTS.md pour les conventions du projet
Ce dernier type est souvent le plus efficace : plutôt que de décrire la convention, on pointe vers un exemple existant. L’agent l’extrait lui-même.
Quels patterns éviter ?
Le prompt trop court
❌ "Écris les tests pour UserService"
Quel framework ? Quelle couverture ? Quels cas edge ? L’agent va inventer des réponses à toutes ces questions — et probablement pas celles qu’on aurait choisies.
Le prompt trop long sans structure
Un bloc de texte de 30 lignes sans structure. L’agent va traiter les premières lignes avec attention et les dernières comme un contexte flou. Les contraintes importantes doivent être en liste, pas noyées dans un paragraphe.
Les instructions contradictoires
❌ "Refactorise PaymentService pour le simplifier,
mais ne change pas son comportement actuel,
et améliore les performances si possible"
“Simplifier”, “ne pas changer le comportement” et “améliorer les performances” peuvent être contradictoires. Quand il y a contradiction, l’agent choisit — pas toujours dans le bon sens.
Demander un avis et une implémentation dans le même prompt
❌ "Est-ce que tu penses qu'on devrait utiliser un Value Object pour Price ?
Si oui, implémente-le."
Séparer en deux prompts : d’abord l’analyse avec sa réponse, puis la décision humaine, puis l’implémentation. Mélanger les deux produit une implémentation qui reflète l’opinion de l’agent, pas la tienne.
Comment industrialiser avec les custom commands ?
Les prompts qu’on réutilise deviennent des custom commands. C’est le moyen de transformer le prompt engineering en capital partagé plutôt qu’en effort individuel répété.
Une custom command qui devient un standard d’équipe mérite le même circuit qu’une merge request : relecture par quelqu’un d’autre, nommage cohérent avec les commands existantes, une ligne de doc qui explique quand l’utiliser. C’est exactement ce qui transforme un prompt bien écrit en capital partagé — sans review, c’est juste un fichier de plus dans .opencode/commands/ que personne ne sait s’il faut encore utiliser.
Structure d’une custom command bien faite :
# .opencode/commands/write-tests.md
Écris les tests unitaires PHPUnit pour `$ARGUMENTS`.
Analyse d'abord le fichier pour identifier :
- Les méthodes publiques à tester
- Les cas nominaux
- Les cas d'erreur (exceptions, valeurs limites)
- Les dépendances à mocker
Ensuite crée le fichier de test correspondant dans tests/
en suivant le pattern de AbstractServiceTest.php.
Condition de sortie :
- php bin/phpunit --filter [NomDeLaClasse]Test passe
- Couverture minimale : tous les cas d'erreur sont testés
Contraintes :
- Utiliser PHPUnit MockObject, pas de librairie de mock externe
- Suivre la convention de nommage des tests existants
Utilisation :
> /write-tests src/Service/DiscountCalculator.php
Le $ARGUMENTS est remplacé par ce qui suit la commande. Un prompt soigné une fois, réutilisé indéfiniment.
Ressources
Questions fréquentes
- Faut-il reformuler différemment selon le modèle utilisé ?
- Légèrement. Claude Sonnet suit très bien les instructions structurées avec des listes de contraintes. À l'époque où je routais une partie des tâches mécaniques vers MiniMax 2.5 (période Kilo Gateway, abandonnée depuis), ce modèle était plus robuste sur les instructions courtes et directes. Gemini Flash préfère les exemples concrets aux descriptions abstraites. En pratique, une instruction bien formulée fonctionne sur tous les modèles — les ajustements fins n'ont d'intérêt qu'une fois les bases solides.
- Quelle longueur idéale pour un prompt d'agent ?
- Entre 3 et 15 lignes pour la plupart des tâches. En dessous de 3 lignes on manque souvent la condition de sortie ou les contraintes. Au-delà de 20 lignes l'agent commence à perdre le fil des exigences. Si une instruction nécessite 30 lignes, c'est souvent le signe qu'il faut décomposer en plusieurs sessions.
- Comment gérer les cas où l'agent fait quelque chose d'inattendu ?
- Deux réponses selon la situation. Si c'est une déviation mineure (style, nommage) : corriger dans le prompt suivant avec "ne fais pas X, fais Y". Si c'est une déviation architecturale : ne pas continuer sur cette session, ouvrir une nouvelle session avec un prompt qui reformule la contrainte de façon plus explicite. Tenter de corriger un agent dans une session qui a dévié est rarement efficace.
- Les custom commands remplacent-ils le prompt engineering ?
- Non — elles encapsulent des prompts bien formulés. Une custom command est un template de prompt réutilisable. Le travail de prompt engineering se fait une fois pour créer la commande, puis on réutilise. C'est pour ça que soigner les prompts des custom commands est plus rentable que soigner les prompts à la volée.
- Comment transmettre du contexte implicite que l'agent ne peut pas voir ?
- Via le fichier AGENTS.md pour les conventions permanentes du projet, et via des commentaires dans le code pour le contexte spécifique à un fichier. Un commentaire `// TODO: à refactoriser — voir #142` dans le code est du contexte que l'agent lit et intègre naturellement. Ne pas hésiter à annoter le code pour guider l'agent.