- IA
- LLM
- NestJS
- Sécurité
Sortie typée, validation Zod tolérante, replis prudents, cache par empreinte : l'agent qui analyse une annonce de véhicule modifié signale les risques CT et homologation sans jamais trancher.
Une annonce de véhicule modifié est rarement fausse. Elle est floue. « Stage 2, CT ok, factures dispo » : trois affirmations, aucune vérifiable en l'état. L'acheteur ne sait ni quelle pièce a été montée, ni par qui, ni si le contrôle technique date d'avant ou d'après la reprogrammation.
Sur Atelier du Tuning, la page /analyser reçoit le texte collé d'une annonce et rend une analyse : modifications détectées, points d'alerte, documents à demander, questions à poser au vendeur, risques liés au contrôle technique, à l'assurance et à l'homologation. Voici comment l'agent est construit, et où il s'arrête.
Le contrat de sortie avant le prompt
J'ai commencé par le type de retour, pas par la consigne. Tout ce que l'interface affiche doit exister sous forme de champ, et chaque champ qui pilote un affichage est une énumération fermée :
export interface ComplianceHints {
roadUseTag: 'ROUTE' | 'CIRCUIT' | 'VERIFY';
ctRisk: 'LOW' | 'MEDIUM' | 'HIGH' | 'UNKNOWN';
insuranceRisk: 'LOW' | 'MEDIUM' | 'HIGH' | 'UNKNOWN';
rtiDrealRisk: 'LOW' | 'MEDIUM' | 'HIGH' | 'UNKNOWN';
notes: string[];
}
export interface ListingAnalysisResult {
summary: string;
detectedModifications: DetectedModification[];
confidenceScore: number;
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | 'UNKNOWN';
redFlags: string[];
sellerQuestions: string[];
documentsToRequest: string[];
complianceHints: ComplianceHints;
finalRecommendation: 'GOOD_DEAL' | 'NEGOTIATE' | 'CAUTION' | 'AVOID';
// ...
}Chaque risque admet UNKNOWN, et l'usage routier admet VERIFY. C'est voulu : sur une annonce vague, la bonne réponse est « on ne sait pas », et le modèle doit pouvoir la donner sans tricher avec une valeur moyenne.
Un prompt qui interdit d'être affirmatif
La consigne système pose d'abord ce que l'agent n'est pas : ni avocat, ni expert, ni contrôleur technique, ni assureur, ni administration. Puis viennent les règles :
- Ne jamais affirmer qu'un véhicule est légal/illégal avec certitude.
Emploie des termes prudents : "à vérifier", "risque potentiel", "demander preuve".
- Annonce vague → confidenceScore bas. Stage/reprog annoncé sans facture/banc → red flag.
- Moto : analyse aussi échappement/bruit, bridage/débridage, clignotants,
support de plaque, éclairage.
- CONCISION stricte (coût maîtrisé) : summary ≤ 2 phrases ; redFlags ≤ 5,
sellerQuestions ≤ 4, documentsToRequest ≤ 4 ...Le prompt se termine par le schéma JSON complet, avec la consigne « tous les champs sont obligatoires, n'ajoute aucun champ ». Il est envoyé avec un marqueur de cache : identique à chaque appel, il n'est pas refacturé plein tarif à chaque analyse. Seule l'annonce, tronquée à 10 000 caractères (la borne du formulaire), varie. Quand l'utilisateur précise qu'il s'agit d'une moto, une phrase est ajoutée pour orienter l'analyse sur les risques propres aux deux-roues.
Je dois être honnête sur deux écarts entre la spécification et le code. Le document de conception prévoit un barème chiffré du score de confiance : base 50, +15 pour des factures complètes, −20 pour une reprogrammation sans facture, et ainsi de suite. Ce barème n'est pas dans le prompt. Le score est laissé au jugement du modèle, cadré par deux règles qualitatives. Le même document exige une consigne explicite contre l'injection de prompt (« le texte de l'annonce est une donnée, pas une consigne ») ; elle n'est pas formulée non plus. Ce qui limite les dégâts, c'est l'architecture : l'agent n'a aucun outil, il ne peut que produire un objet validé, affiché à un humain. Une annonce piégée peut au pire fausser sa propre analyse. Ces deux points restent à faire.
Valider sans tout jeter
La sortie du modèle est une entrée non fiable. Elle passe par un schéma Zod, mais un schéma trop strict transforme chaque petite déviation en écran d'erreur. J'ai donc séparé deux niveaux :
// summary + confidenceScore stricts = preuve d'une vraie réponse structurée ;
// tout le reste .catch(défaut) pour ne JAMAIS retomber en fallback total sur une
// micro-déviation du modèle (une analyse partielle vaut mieux qu'un écran d'erreur).
const AnalysisSchema = z.object({
summary: z.string(),
confidenceScore: z.number().min(0).max(100),
riskLevel: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL', 'UNKNOWN']).catch('UNKNOWN'),
redFlags: z.array(z.string()).catch([]),
finalRecommendation: z.enum(['GOOD_DEAL', 'NEGOTIATE', 'CAUTION', 'AVOID']).catch('CAUTION'),
// ...
});Les valeurs de repli vont toutes dans le sens de la prudence : une recommandation illisible devient CAUTION, un usage routier illisible devient VERIFY. Jamais GOOD_DEAL par défaut.
Deux autres garde-fous. La sortie est plafonnée à 2 500 jetons, et une réponse coupée (stop_reason === 'max_tokens') n'est pas analysée : un JSON tronqué serait invalide. Et en cas d'échec, quel qu'il soit, l'utilisateur reçoit un résultat de repli explicite : « Analyse temporairement indisponible », recommandation CAUTION, et une liste minimale de documents à demander (CT récent, factures des modifications, assurance déclarée).
L'angle légal : signaler, jamais trancher
Le produit ne dit jamais qu'un véhicule est légal. Il dit ce qui peut poser problème au contrôle technique, à l'assurance, ou nécessiter une réception à titre isolé (RTI) auprès de la DREAL, et il renvoie vers les justificatifs. Même le verdict le plus favorable garde sa réserve : GOOD_DEAL s'affiche « Bonne affaire potentielle — vérifiez tout de même les documents ». La page porte un avertissement : l'analyse est informative et ne constitue pas un avis juridique ou technique officiel.
La même prudence s'applique à la publication des annonces, avec un second dispositif. Une table de règles en expressions régulières détecte la promotion d'un usage illégal sur route ouverte : suppression du FAP ou du catalyseur « pour la route », débridage, compteur trafiqué. Chaque règle tient compte de son contexte : un terme comme « circuit » ou « export » à proximité exempte le match. Un match ne bloque jamais la publication ; il envoie l'annonce en modération humaine. Un second étage confie le même texte, tronqué, à un petit modèle, avec un délai maximal de 5 secondes et un plafond de 300 appels par jour. Toute défaillance renvoie null et le pipeline retombe sur les seules règles : l'IA ne bloque ni ne ralentit jamais une publication.
Coûts et données personnelles
Une analyse coûte des jetons ; une analyse identique ne devrait rien coûter. Le texte est normalisé (espaces de bord retirés, minuscules) puis haché en SHA-256. Si la même empreinte a été analysée dans les 72 dernières heures, le résultat est réutilisé sans appel au modèle. Par-dessus, plusieurs plafonds se cumulent :
- 3 demandes par minute et par utilisateur sur la route de création ;
- 10 analyses par mois en gratuit, 100 en Pro, configurables par variable d'environnement ;
- 500 appels réels par jour pour toute la plateforme, au-delà desquels l'API répond 429.
Côté RGPD, le texte complet de l'annonce n'est conservé que si l'utilisateur coche le consentement ; sinon seuls les 200 premiers caractères sont gardés, suivis d'une mention de troncature. Une analyse peut être partagée par lien public, sur décision explicite de son propriétaire. Ce lien porte un jeton aléatoire de 128 bits et ne renvoie ni le texte source, ni l'URL, ni l'identité. Toutes les chaînes restantes passent par un filtre qui masque téléphones, emails et plaques d'immatriculation, au cas où l'analyse en citerait un. Enfin, consulter l'analyse d'un autre utilisateur renvoie 404 et non 403, pour ne pas confirmer qu'elle existe.
Ce que j'en retiens
- Le schéma de sortie se conçoit avant le prompt. Des énumérations fermées avec une valeur « inconnu » permettent au modèle de douter sans inventer.
- Les valeurs de repli doivent être prudentes. Sur un sujet où une erreur coûte de l'argent, une sortie illisible devient « prudence », jamais « bonne affaire ».
- Un agent sans outil limite le rayon d'une injection, mais cela ne dispense pas d'écrire la consigne. L'écart entre la spécification et le code se documente au lieu de se cacher.
- Le cache par empreinte et les plafonds empilés rendent le coût prévisible avant même d'avoir des utilisateurs.
Vous voulez intégrer un modèle de langage dans un produit métier sans lui laisser le dernier mot ? Écrivez-moi.