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.

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 :

"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 :

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.

✉ Me contacter Voir mes services IA →