La promesse habituelle : "l'IA va faire vos code reviews". La réalité : un agent générique sur une PR .NET d'entreprise produit 40% de faux positifs et passe à côté des vrais problèmes. J'ai mis 4 mois à construire quelque chose d'utilisable. Voici l'architecture réelle, les métriques honnêtes, et surtout ce que le pipeline ne peut pas faire — parce que c'est là que la plupart des équipes se font piéger.
Pourquoi un pipeline multi-agents plutôt qu'un seul
La première erreur que j'ai commise est de vouloir un agent omniscient qui "review la PR". Un seul agent généraliste est mauvais partout : trop vague sur la sécurité, trop bavard sur le style, aveugle sur les breaking changes d'API. Le problème est structurel — un LLM avec un prompt de 3000 tokens qui couvre tout finit par couvrir rien.
La solution est la même qu'en architecture logicielle : séparation des responsabilités. Quatre agents spécialisés, chacun avec son contexte de projet, ses règles précises, son seuil de sévérité. Ils tournent en parallèle sur le diff de la PR et leurs résultats sont agrégés avant de remonter vers le reviewer humain.
- Agent Sécurité — OWASP Top 10, injection, exposition de secrets, autorisation manquante
- Agent Performance — N+1 EF Core,
async void, allocations inutiles, ToList() prématurés - Agent Style — règles SonarQube sa-smarter-sast-p, conventions de nommage, complexité cyclomatique
- Agent Contrats — breaking changes d'API, compatibilité des événements de domaine, changements de schéma
Architecture du pipeline Azure DevOps
Le pipeline est déclenché sur chaque événement pull_request. Les quatre agents tournent en parallèle dans des jobs séparés, chacun recevant le diff filtré sur son périmètre. Un job d'agrégation finale publie un commentaire structuré sur la PR via l'API ADO.
# azure-pipelines-pr-review.yml
trigger: none
pr:
branches:
include:
- develop
- main
variables:
DIFF_MAX_LINES: 800
AGENT_TIMEOUT_SECONDS: 120
stages:
- stage: PRReview
displayName: "Revue automatisée PR"
jobs:
- job: SecurityAgent
displayName: "Agent Sécurité (OWASP)"
pool: { vmImage: ubuntu-latest }
steps:
- checkout: self
fetchDepth: 0
- script: |
git diff origin/$(System.PullRequest.TargetBranch)...HEAD \
-- "*.cs" "*.json" "*.yaml" \
| head -n $(DIFF_MAX_LINES) > /tmp/diff_security.txt
displayName: "Extraire diff — périmètre sécurité"
- task: PythonScript@0
inputs:
scriptPath: tools/pr-review/security_agent.py
arguments: "--diff /tmp/diff_security.txt --output $(Build.ArtifactStagingDirectory)/security.json"
env:
ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)
- publish: $(Build.ArtifactStagingDirectory)/security.json
artifact: security-report
- job: PerfAgent
displayName: "Agent Performance (N+1, async)"
pool: { vmImage: ubuntu-latest }
steps:
- checkout: self
fetchDepth: 0
- script: |
git diff origin/$(System.PullRequest.TargetBranch)...HEAD \
-- "*.cs" \
| head -n $(DIFF_MAX_LINES) > /tmp/diff_perf.txt
displayName: "Extraire diff — périmètre performance"
- task: PythonScript@0
inputs:
scriptPath: tools/pr-review/perf_agent.py
arguments: "--diff /tmp/diff_perf.txt --output $(Build.ArtifactStagingDirectory)/perf.json"
env:
ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)
- publish: $(Build.ArtifactStagingDirectory)/perf.json
artifact: perf-report
- job: ContractsAgent
displayName: "Agent Contrats (breaking changes)"
pool: { vmImage: ubuntu-latest }
steps:
- checkout: self
fetchDepth: 0
- script: |
git diff origin/$(System.PullRequest.TargetBranch)...HEAD \
-- "src/**/*Controller.cs" \
"src/**/*Request.cs" \
"src/**/*Response.cs" \
"src/**/*Event.cs" \
| head -n $(DIFF_MAX_LINES) > /tmp/diff_contracts.txt
displayName: "Extraire diff — périmètre contrats"
- task: PythonScript@0
inputs:
scriptPath: tools/pr-review/contracts_agent.py
arguments: "--diff /tmp/diff_contracts.txt --output $(Build.ArtifactStagingDirectory)/contracts.json"
env:
ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)
- publish: $(Build.ArtifactStagingDirectory)/contracts.json
artifact: contracts-report
- job: Aggregator
displayName: "Agrégation et commentaire ADO"
dependsOn:
- SecurityAgent
- PerfAgent
- ContractsAgent
pool: { vmImage: ubuntu-latest }
steps:
- download: current
artifact: security-report
- download: current
artifact: perf-report
- download: current
artifact: contracts-report
- task: PythonScript@0
inputs:
scriptPath: tools/pr-review/aggregator.py
arguments: >
--security $(Pipeline.Workspace)/security-report/security.json
--perf $(Pipeline.Workspace)/perf-report/perf.json
--contracts $(Pipeline.Workspace)/contracts-report/contracts.json
--pr-id $(System.PullRequest.PullRequestId)
--org $(System.TeamFoundationCollectionUri)
--project $(System.TeamProject)
--repo $(Build.Repository.Name)
env:
ADO_PAT: $(ADO_PAT_PR_COMMENT)
Ce que l'agent Performance attrape — exemple concret
Sur une PR récente dans notre orchestrateur de commandes, l'agent a détecté ce pattern. Le reviewer humain ne l'avait pas signalé — la PR était "fonctionnellement correcte" et les tests passaient.
// Code soumis en PR — tests verts, comportement correct
public async Task<IReadOnlyList<OrderSummaryDto>> GetOrdersWithItems(
IReadOnlyList<Guid> orderIds,
CancellationToken ct)
{
var orders = await _dbContext.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync(ct);
// N+1 ici : une requête SQL par order pour charger les lignes
foreach(var order in orders)
{
order.Items = await _dbContext.OrderItems
.Where(i => i.OrderId == order.Id)
.ToListAsync(ct);
}
return orders.Select(MapToDto).ToList();
}
Sortie de l'agent Performance sur cette PR :
// agents-ia-pr-review — rapport agent Performance
// Fichier : src/Application/Queries/GetOrdersWithItemsQuery.cs
// Lignes : 18-26
// Sévérité: HIGH
PROBLÈME DÉTECTÉ — Pattern N+1 EF Core
Le diff introduit une boucle foreach sur `orders` (résultat d'une
première requête SQL) à l'intérieur de laquelle un second appel
`_dbContext.OrderItems` est effectué pour chaque élément.
Avec 50 commandes en entrée : 51 requêtes SQL émises.
Avec 500 commandes : 501 requêtes SQL émises.
CORRECTION SUGGÉRÉE :
Remplacer par un Include() dans la requête initiale :
var orders = await _dbContext.Orders
.Where(o => orderIds.Contains(o.Id))
.Include(o => o.Items)
.ToListAsync(ct);
Ou, si le volume d'Items par Order est élevé, une requête séparée
sur tous les IDs puis un GroupBy en mémoire (Split Query).
CONTEXTE : orderIds peut contenir jusqu'à N éléments selon le
contrat de l'appelant — vérifier la borne max côté validation.
Configuration de l'agent Sécurité — le prompt qui compte
Un agent sécurité générique sur du code .NET produit principalement du bruit. Ce qui change tout est le contexte de projet injecté dans le prompt système. Voici la structure que j'utilise pour l'agent basé sur Claude :
# security_agent.py — construction du prompt système
SYSTEM_PROMPT = """
Tu es un agent de revue de sécurité spécialisé .NET 9 / ASP.NET Core.
Tu analyses UNIQUEMENT le diff fourni — pas le reste du codebase.
RÈGLES D'ANALYSE :
- OWASP Top 10 : injection SQL/LINQ, XSS, IDOR, exposition de secrets,
SSRF, désérialisation non sécurisée
- Patterns spécifiques .NET : HttpContext.Request sans validation,
[FromBody] sans [Authorize] sur des endpoints sensibles,
Connection strings dans le code, secrets dans les logs,
async void (swallows exceptions — vecteur de denial of service),
Path.Combine avec entrée utilisateur non validée
CONTEXTE PROJET :
- Framework : ASP.NET Core 9 avec authentification JWT Bearer
- Pattern d'autorisation : [Authorize(Policy = "...")] — tout endpoint
qui manipule des données utilisateur DOIT avoir cet attribut
- Les IDs dans les routes sont des Guids — un accès sans vérification
que le Guid appartient à l'utilisateur courant est un IDOR
- Les secrets passent par IOptions depuis Azure Key Vault — jamais
de chaîne de connexion ou de clé API en dur
EXCLUSIONS CONNUES (ne pas signaler) :
- Les fichiers *Tests.cs et *Spec.cs — contexte de test, règles relâchées
- Les fichiers de migration EF Core (Migrations/) — SQL généré automatiquement
- Les classes *Stub.cs et *Fake.cs dans Tests.Common
FORMAT DE RÉPONSE : JSON strict selon le schéma fourni.
Si aucun problème détecté : {"issues": [], "verdict": "PASS"}
"""
Métriques réelles après 3 mois de production
Sur une équipe de 6 développeurs, ~45 PRs par mois, base de code .NET 9 Clean Architecture avec environ 180 000 lignes de code :
- Problèmes détectés par le pipeline avant revue humaine : 38 par mois en moyenne
- Répartition : 11 Performance (N+1, async void, allocations), 9 Sécurité (IDOR, missing Authorize, secrets), 12 Contrats (breaking changes API, champs supprimés), 6 Style (règles SonarQube bloquantes)
- Taux de faux positifs global : 12% (soit ~4-5 faux positifs par mois)
- Temps de revue humaine économisé : estimé à 35 minutes par PR en moyenne — le reviewer arrive avec le rapport et commence directement sur les problèmes non triviaux
- Problèmes bloquants en production évités : 3 sur la période (2 IDOR, 1 N+1 sur endpoint haute fréquence)
"Le pipeline ne remplace pas le reviewer. Il lui évite de passer 20 minutes à chercher des choses que la machine voit en 90 secondes — pour qu'il passe ces 20 minutes sur ce que la machine ne comprend pas."
Les faux positifs : causes et calibrage
12% de faux positifs, c'est viable. 40%, ça ne l'est pas — l'équipe coupe les notifications et le pipeline devient un bruit de fond. Les principales sources de faux positifs que j'ai rencontrées :
- Agent Sécurité sur les tests : avant d'ajouter l'exclusion
*Tests.cs, l'agent signalait des "connection strings en dur" dans les fixtures d'intégration — qui sont des bases SQLite in-memory. Fix : liste d'exclusions explicite dans le prompt système. - Agent Performance sur les Repositories de test : les
InMemoryRepositoryutilisés dans les tests unitaires déclenchaient des alertes N+1. Fix : filtrage du diff pour exclure le répertoiretests/avant d'alimenter l'agent. - Agent Contrats sur les renommages internes : renommer un champ privé dans un DTO interne déclenchait une alerte "breaking change potentiel". Fix : affiner le prompt pour distinguer les membres
publicdes membresinternaletprivate. - Agent Style sur les suppressions légitimes :
#pragma warning disableavec justification documentée déclenchait une alerte SonarQube. Fix : demander à l'agent de vérifier si la suppression est accompagnée d'un commentaire explicatif avant de signaler.
Le calibrage est itératif. Je maintiens un fichier tools/pr-review/known-false-positives.md où chaque faux positif confirmé est documenté avec la règle d'exclusion ajoutée en conséquence. Après 3 mois, les nouvelles entrées sont rares — la majorité des cas ont été couverts.
Ce que le pipeline ne peut pas faire — et c'est important
C'est la section que la plupart des articles sur "l'IA en code review" esquivent. Je préfère être direct.
Les décisions architecturales
Un agent peut détecter qu'une méthode a une complexité cyclomatique de 18. Il ne peut pas détecter que la classe qui la contient viole le principe de responsabilité unique d'une façon qui va rendre la prochaine feature impossible à livrer sans refactoring majeur. Cette lecture nécessite une compréhension du contexte produit sur plusieurs mois — c'est du travail de Tech Lead, pas de pipeline.
La logique métier incorrecte
Sur une de nos PRs, un calcul de remise était faux : la règle métier stipulait "remise de 10% si le client commande plus de 500€ sur le mois calendaire", et le code calculait sur les 30 derniers jours glissants. Tests verts, agent muet, bug en production. Aucun outil d'analyse statique ou d'IA ne peut attraper ça sans accès à la spécification métier — et même avec, la correspondance spec/code reste une validation humaine.
Les abstractions prématurées ou incorrectes
L'agent Sécurité ne voit pas qu'une interface IOrderProcessor est introduite dans une PR "pour la testabilité" alors qu'elle n'a qu'une seule implémentation et qu'elle complexifie inutilement le graphe de dépendances. Ce type de smell nécessite une vision globale du domaine que l'agent n'a pas.
Les implications de performance à l'échelle
L'agent Performance détecte les patterns N+1 connus. Il ne détecte pas qu'un algorithme O(n²) sur une collection qui contient 20 éléments aujourd'hui en contiendra 50 000 dans six mois. Cette anticipation nécessite une connaissance des données de production et des projections de croissance — hors de portée de tout agent.
Intégration dans le workflow quotidien
Le pipeline tourne automatiquement sur chaque PR ciblant develop ou main. Il se déclenche en moins de 2 minutes après l'ouverture de la PR. Le commentaire agrégé apparaît dans ADO avant que le reviewer désigné ait reçu sa notification de revue.
La convention que j'ai instaurée dans l'équipe : le reviewer commence par lire le rapport du pipeline, résout les problèmes signalés avec le développeur si nécessaire, puis effectue sa revue humaine sur ce qui reste. Les commentaires du pipeline sont résolus dans ADO avec deux statuts : fixed ou wontFix avec justification obligatoire. Cette justification est collectée et réinjectée dans l'affinement du prompt des agents — le pipeline apprend des décisions de l'équipe.
Vous voulez mettre en place ce pipeline sur votre projet .NET ?
Je peux auditer votre workflow de PR, configurer les agents pour votre stack
et former votre équipe à la calibration des faux positifs.