S'inscrire Dashboard
21 septembre 2026

Comment on empêche le slop d'atteindre la prod

Explore with AI

Tu es probablement en train de construire une usine logicielle, ou tu en loues une auprès d’un fournisseur. Tu as un système qui t’amène d’un prompt à une pull request. Il gère les tests, le linting, le formatage et les revues de code automatisées.

Mais tu n’as toujours pas de réponse à la question la plus importante : ce changement est-il prêt pour la prod ?.

graph TB
    W["Agent writes the code"] --> T["Types and tests"]
    T --> L["Linter and formatter"]
    L --> R["Code review"]
    R --> Q(["Is this okay for prod?"])
    Q --> D["Deploy"]
    style Q fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d

Toutes tes vérifications actuelles regardent le diff, mais rien dans ton usine ne connaît le système de production sur lequel ce diff est sur le point d’atterrir. Rien ne peut vraiment empêcher le slop d’atteindre la prod.

On a construit cette capacité dans Polylane et cet article détaille les aspects techniques de son implémentation.

Comment ça fonctionne

La seule question à laquelle ce système doit répondre est :

Ce changement, une fois fusionné et déployé, aurait-il un impact négatif sur la production ?

On ne s’intéresse pas vraiment aux éléments classiques qu’un agent de revue de code vérifierait, comme le style, le nommage, la couverture de tests, etc. Et on a décidé de répondre à cette question au stade de la pull request, en parallèle de tous tes tests existants.

En fin de compte, Polylane commente la pull request avec un simple message «\u00A0go\u00A0»\u00A0/\u00A0«\u00A0no-go\u00A0» accompagné des preuves de son enquête.

Le flux est plutôt simple :

  • Cette pull request touche-t-elle des fichiers qui pourraient affecter la production ?
  • Quelles ressources cloud sont potentiellement affectées ?
  • Rassembler le contexte sur l’état actuel de la production pour ces ressources
  • Évaluer plusieurs modes de défaillance potentiels que ce changement pourrait introduire
  • Prévoir comment la production pourrait évoluer une fois ces changements déployés
  • Alerter les développeurs des modes de défaillance potentiels probables
polylane bot commented 2 minutes ago ···
Caution

Merging this pull request may degrade production (high impact).

Merging this blocks every write to orders while the index builds. migrations/0114_order_search_trgm.sql:3 adds CREATE INDEX … USING gin (search_text gin_trgm_ops) without CONCURRENTLY, and a plain CREATE INDEX takes a full write lock on orders for the whole build. Checkout sustains ~38 writes/s on that table; each one queues behind the lock until the build finishes.

To make this safe: build the index with CREATE INDEX CONCURRENTLY outside the transactional migration.

orders-db · writes per second · last 48h
0 20 40
-48h -24h now
every one of these writes blocks while the index builds
Un verdict no-go : le mécanisme en première ligne, les preuves en dessous, et ce qui rendrait le changement sûr.

Tout cela repose sur le context graph qu’on construit en continu et qui connecte toutes les ressources cloud de tes différents comptes cloud.

graph TB
    P["Open pull request"] --> Q(["Meaningful code change?"])
    Q -->|"docs, tests, comments"| C["Conclude the check"]
    Q -->|"yes"| R["Find affected resources<br/>in the context graph"]
    R -->|"none"| C
    R -->|"yes"| E["Gather context"]
    E --> X["Build failure trajectories"]
    X --> F["Forecast the affected series"]
    F --> V["Verdict"]
    V --> C
    style X fill:#fef3c7,stroke:#fcd34d,color:#78350f
    style F fill:#fef3c7,stroke:#fcd34d,color:#78350f
    style V fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Assembler le contexte

Notre context graph est essentiel au fonctionnement de tout ça, il construit un registre de toutes tes ressources cloud, de tous tes dépôts, équipes, etc. Par exemple, un nœud de calcul comme une fonction Lambda est connecté à la base de données qu’il lit et à la queue qui le déclenche. On ajoute aussi les dépôts au graphe. Cela se fait en examinant les fichiers manifestes habituels du dépôt, par exemple les fichiers terraform, Cloudformation ou Wrangler. Cela permet d’établir la connexion entre les dépôts et les ressources cloud.

graph LR
    Q["SQS queue<br/>orders-events"] -->|"triggers"| A["Lambda function<br/>checkout-api"]
    R["Repository<br/>checkout-edge"] -->|"deploys_to"| A
    A -->|"connects_to"| D["RDS instance<br/>orders-db"]
    style R fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Quand une pull request est soumise au dépôt, on suit les chemins du context graph pour rassembler toutes les ressources cloud potentiellement affectées par le changement. On utilise un petit modèle pour filtrer les ressources, car un même dépôt peut déployer un grand nombre de ressources. On transmet ce contexte à l’agent, avec le diff, la description de la PR et les commits.

graph LR
    R["Repository"] -->|"deploys_to"| C["Candidate resources"]
    C --> F(["Small model filters"])
    F --> A["Agent"]
    D["Diff, description, commits"] --> A
    style A fill:#d1fae5,stroke:#6ee7b7,color:#065f46

On fait aussi passer le diff par un ensemble d’heuristiques déterministes pour orienter rapidement l’attention de l’agent vers les éléments qui ont généralement tendance à avoir un impact négatif sur la production :

  • une migration qui doit être appliquée à la main, ou dans un ordre particulier par rapport au deploy
  • CREATE INDEX sans CONCURRENTLY, ou ADD COLUMN ... NOT NULL sans valeur par défaut, qui verrouillent tous les deux la table pendant toute la durée de l’opération
  • un endpoint retiré alors que la version actuellement déployée le lit encore
  • du code qui commence à lire une variable d’environnement, un secret ou un binding que rien dans le diff ne provisionne

Ce sont des recommandations qui orientent l’agent vers les risques de déploiement probables.

Trajectoires de défaillance

Avec le contexte fourni ci-dessus, le modèle imagine différents modes de défaillance que le nouveau diff pourrait introduire en production, puis enquête sur chacun d’eux.

Son résultat est un registre de trajectoires de défaillance. Une trajectoire est une chaîne causale qui va d’un déclencheur, à travers le code modifié, jusqu’à une dégradation observable sur une métrique donnée, et chaque maillon porte une citation : un fichier et une ligne, un template de log avec son compteur, une lecture de métrique, une clé de config, une arête du graphe.

L’agent essaie à la fois de valider et d’invalider chaque trajectoire avant d’arriver à un verdict. Chacune se termine dans un des trois états suivants :

  • confirmed quand la trajectoire a été confirmée par rapport à la production.
  • plausible quand la chaîne est concrète mais qu’un ou plusieurs maillons n’ont pu être que «\u00A0devinés\u00A0», sans données de télémétrie pour les confirmer.
  • refuted quand les données de télémétrie de production apportent suffisamment de preuves que cette trajectoire est peu susceptible de se produire en production.

On adopte une approche conservatrice, et toute trajectoire confirmed conduit à une évaluation en échec.

export function deriveTrajectoryVerdict(trajectories: Trajectory[]): "pass" | "fail" {
  return trajectories.some((entry) => entry.status === "confirmed") ? "fail" : "pass";
}

graph LR
    D["The diff"] --> T1["Dropped index still<br/>used by checkout"]
    D --> T2["Retry change amplifies<br/>load on orders-db"]
    D --> T3["Removed binding<br/>breaks the worker"]
    T1 --> C1["confirmed"]
    T2 --> C2["plausible"]
    T3 --> C3["refuted"]
    C1 --> V["No-go"]
    C2 --> V
    C3 --> V
    style C1 fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
    style C2 fill:#dbeafe,stroke:#93c5fd,color:#1e3a8a
    style C3 fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style V fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d

Prévoir l’impact

On donne à l’agent un outil pour prévoir des séries temporelles à partir de données historiques, en injectant d’éventuels facteurs externes.

Disons qu’une trajectoire affirme qu’un changement réduit de moitié le timeout sur un chemin avec retries. Que ça compte ou non dépend du trafic, et le trafic n’est pas vraiment un chiffre unique, il a une forme. Une queue qui reste stable à 60\u00A0% de profondeur, ça va. La même queue à 60\u00A0% mais qui grimpe chaque semaine, c’est une autre situation, et lire la dernière heure de métriques ne te dira pas laquelle des deux tu observes.

Donc avant que l’agent ne rédige le verdict d’une trajectoire, il récupère les séries historiques des ressources affectées et les prévoit ensemble. On self-host actuellement le modèle Toto-2.0-22m.

graph LR
    A["Agent"] -->|"telemetry query"| P["Provider"]
    P -->|"aligned series"| A
    A -->|"up to 16 series"| M["Forecasting model"]
    M -->|"p10, p50, p90"| A
    style M fill:#fef3c7,stroke:#fcd34d,color:#78350f

La raison d’utiliser un modèle multivarié dédié plutôt qu’un prompt supplémentaire, c’est que les séries ne sont pas indépendantes. Le taux de requêtes, le taux d’erreur, la latence et la profondeur de la queue d’une même ressource évoluent ensemble, et les prévoir séparément fait perdre la corrélation qui donne toute sa valeur à la prévision. Chaque série d’un appel partage un même groupe d’attention.

C’est la capacité la plus expérimentale qu’on a ajoutée récemment, et on mesure encore son impact. Mais elle passe déjà le vibes-eval.

checkout-api request forecast
Hourly observations from the latest 64 complete buckets, followed by a 24-hour probabilistic forecast.
observed p10–p90 forecast
20,000 25,000 30,000 35,000 Forecast starts Sep 18 00:00 Sep 18 20:00 Sep 19 16:00 Sep 20 12:00 Sep 21 08:00 Time (UTC) Requests per hour
Figure 2
Un exemple de fonctionnement de la prévision
64 observations horaires d'une ressource affectée entrent, et 24 heures de médiane et de p10-p90 sortent. La bande s'élargit avec l'horizon, et l'agent lit la bande plutôt que la seule médiane : un p10 qui reste au-dessus du seuil dont dépend une trajectoire donne une réponse différente d'un p10 qui le franchit.

La contrainte de latence

Exécuter l’évaluation d’impact en production dans le flux de la pull request implique que ça doit être rapide. Personne ne veut d’une étape qui ajoute 15 minutes à son pipeline CI. Par exemple, sur nos propres dépôts, cette évaluation est une étape de CI obligatoire, si elle est lente, tout notre SDLC s’enlise.

On a essentiellement un budget de 2 à 3 minutes pour produire une évaluation d’impact précise. Au-delà, ce n’est pas acceptable dans un pipeline CI/CD.

Notre premier prototype échouait complètement à respecter cette contrainte. Notre médiane était de près de 7 minutes, et il était courant de voir des runs prendre jusqu’à 20 minutes.

0 s 60 s 120 s 180 s 240 s 300 s Prélude webhook, enregistrements, diff 7.4 s Sandbox cloner le HEAD 16.5 s Tour de revue 242.4 s Livraison check run, commentaire 3 s Non comptabilisé 38.6 s
Figure 3
Latence médiane de chaque étape de l'évaluation d'impact en production
Médianes en production, 15 septembre 2026, sur 325 runs. Chaque phase a sa propre médiane, donc le bloc non comptabilisé à la fin correspond à la mise en queue entre les phases, plus l'arithmétique liée à l'addition des médianes.

L’objectif est devenu de trouver comment réduire le nombre d’étapes du modèle pour faire passer tout le flux sous les 3 minutes. On exécutait généralement 30 à 40 étapes de modèle séquentielles, et dans les pires scénarios jusqu’à près de 600 étapes, chacune consommant 17 secondes de notre budget, et l’immense majorité de leur résultat était des tokens de raisonnement.

On a apporté pas mal de changements ces 3 dernières semaines, avec des impacts variés sur la performance. Voici les plus significatifs.

Fixer un budget d’étapes et le communiquer à l’agent

On a introduit un budget d’étapes, plafonné à 30 étapes par tour d’agent, et on communique clairement au modèle combien d’étapes il a consommées à chaque étape. Mettre le compteur dans le prompt permet au modèle de s’organiser en conséquence, ce qui s’avère compter plus que le nombre lui-même.

graph TB
    A["Turn starts, 30 steps"] --> B["Model step"]
    B --> T["Tool call"]
    T --> C(["Steps left?"])
    C -->|"1"| E["Record the verdict now"]
    C -->|"more"| R["Next step, carrying<br/>the count in the prompt"]
    R --> B
    style R fill:#fef3c7,stroke:#fcd34d,color:#78350f
    style E fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Démarrer de nouvelles revues plutôt que les intégrer à l’existante

Une pull request typique continue de recevoir de nouveaux commits après son ouverture. Dans le premier prototype, on intégrait tout le nouveau diff dans le même thread de revue, sous forme de message utilisateur d’orientation. Cette orientation embrouillait beaucoup le modèle et le poussait à relire des fichiers déjà examinés et à relancer des requêtes de télémétrie déjà effectuées.

Maintenant, un nouveau commit annule le run d’évaluation précédent et démarre un thread tout neuf. Ça peut sembler contre-intuitif, mais ça a finalement réduit la latence des revues complètes.

Réutiliser le travail précédent

Quand un nouveau commit arrive sur la pull request après qu’une revue a déjà été complétée, on avait l’habitude, un peu naïvement, de redémarrer une nouvelle évaluation d’impact depuis zéro.

On a introduit la capacité de réutiliser l’évaluation précédente, et de déclencher une nouvelle évaluation sur le diff plus petit entre les deux commits consécutifs.

On a déployé ces changements tout au long du mois de septembre et amélioré progressivement la latence, qui reste désormais confortablement dans le budget.

0 s 150 s 300 s 450 s 600 s 27 Aug 31 Aug 4 Sep 8 Sep 12 Sep 16 Sep 94 s
Figure 4
Latence médiane d'un tour d'évaluation d'impact

Est-ce que ça change vraiment quelque chose ?

À quoi ça sert de brûler tous ces tokens si on ne voit aucun résultat significatif ? Notre métrique de succès est le nombre d’incidents qu’on évite chaque semaine pour chacun de nos clients. Un incident évité est une pull request où :

  • on signale un risque potentiel pour la production
  • un ingénieur pushe un ou plusieurs commits
  • une nouvelle évaluation conclut que le risque potentiel est atténué
  • la pull request est fusionnée
2.3
Incidents de production évités, par client, chaque semaine

C’est déjà assez significatif et on s’attend à ce que ça continue de croître. On itère en continu et on fait tourner des evals pour améliorer la performance et la qualité de notre agent.

Personne ne devrait être on-call. Polylane surveille ton infra, enquête et répare ce qui casse.

S'inscrire

Continuer la lecture