Moderniser du code hérité complexe avec des agents IA : l’approche de Mistral

Mistral a publié des recherches sur l’utilisation d’agents IA pour moderniser des bases de code legacy complexes. Plutôt que de traiter la migration comme une réécriture unique, l’approche décompose de grands systèmes enchevêtrés en unités analysables, permettant aux agents de cartographier les dépendances, de proposer des changements et de vérifier le comportement. Cet article examine la technique, son flux de travail pratique et les points où la revue humaine reste essentielle.

Lecture audio non disponible dans ce navigateur
Moderniser du code hérité complexe avec des agents IA : l’approche de Mistral

Tags

Résumé rapide

Mistral a publié des recherches sur l’utilisation d’agents IA pour moderniser des bases de code legacy complexes. Plutôt que de traiter la migration comme une réécriture unique, l’approche décompose de grands systèmes enchevêtrés en unités analysables, permettant aux agents de cartographier les dépendances, de proposer des changements et de vérifier le comportement. Cet article examine la technique, son flux de travail pratique et les points où la revue humaine reste essentielle.

Moderniser du code legacy complexe avec des agents IA : l'approche de Mistral

Un système legacy est rarement un seul problème. C'est une pile de problèmes : une compilation qui ne réussit que sur l'ordinateur portable d'un ingénieur, des règles métier qui n'existent que dans les données de production, une suite de tests qui documente l'intention d'il y a une décennie, et un graphe de dépendances que personne n'a entièrement cartographié. L'outillage de modernisation traditionnel — codemods, réécrivains d'AST, refactorisations scriptées — est excellent au niveau mécanique et fragile au niveau sémantique. Dès qu'une règle est exprimée via un dispatch dynamique, un fichier généré ou un drapeau de configuration, la réécriture déterministe s'arrête.

Mistral a publié un article sur la modernisation de code legacy complexe avec des agents IA à l'adresse https://mistral.ai/news/legacy-code-modernization. Cette publication cadre le problème de la même manière que cet article : la modernisation est une tâche à l'échelle du dépôt, pas une tâche de complétion. Ce qui suit est un traitement d'ingénierie pratique de ce cadre — comment mettre en place une boucle d'agent qui parvient réellement à appliquer des changements dans une base de code legacy, quoi vérifier, et où l'approche casse.

Une note sur les preuves : la seule source externe utilisée ici est la publication de Mistral sur le sujet. Lorsque l'article décrit le comportement des outils, le sandboxing ou la stratégie de test, il s'agit de pratiques d'ingénierie standard et de mes propres conseils de mise en œuvre, et non d'affirmations produit. Aucun nom de modèle, version ou benchmark n'est affirmé, car la source ne les étaye pas et ils évoluent plus vite que tout article ne peut les suivre.

Ce qui rend la modernisation legacy différente

Les workflows d'agents greenfield sont indulgents. Le code compile, les tests ont du sens, et une mauvaise modification est détectée rapidement. Le travail legacy inverse ces trois hypothèses.

Le comportement n'est pas documenté. Le système fonctionne, ce qui signifie que le comportement actuel est la spécification. Toute refactorisation qui modifie la sortie observable est une régression, même si la nouvelle sortie semble plus raisonnable.

Le signal de retour est bruité. Une compilation peut prendre quarante minutes, échouer par intermittence, ou nécessiter un instantané de base de données pour pouvoir s'exécuter. Les agents optimisent en fonction du retour, donc la qualité de ce retour fixe le plafond de qualité des résultats.

La portée est illimitée par défaut. « Moderniser le module de facturation » peut toucher quatre cents fichiers ou quatre. Sans définition mécanique de ce qui est terminé, un agent continuera à élargir jusqu'à manquer de contexte.

Le rayon d'impact est asymétrique. Supprimer une fonction qui semble inutilisée peut être catastrophique lorsque le site d'appel est résolu par réflexion ou par une chaîne dans une ligne de base de données.

Toute approche sérieuse doit répondre à ces quatre contraintes avant de répondre quoi que ce soit sur le choix du modèle.

Ce que les agents apportent que l'outillage statique ne peut pas apporter

La différence significative entre un agent et un modèle de complétion de code est la boucle. Un agent lit, modifie, exécute, observe le résultat et révise. Dans le travail legacy, l'environnement exécutable — compilateur, linter, harnais de test, vérificateur de types — devient la vérité terrain qu'aucune quantité de prompting ne peut contourner.

Trois capacités importent spécifiquement ici :

  • Assemblage de contexte à l'échelle du dépôt. Les agents peuvent rechercher, lire et recouper bien plus de la base de code que ce qui tient dans un seul prompt, en construisant une carte de travail avant de modifier.
  • Vérification médiée par outils. La confiance de l'agent lui-même n'a pas d'importance ; le résultat de compilation, si. Chaque changement proposé peut être conditionné à une commande qui renvoie un code de sortie.
  • Réparation itérative. Les migrations legacy produisent de longues traînées de petites erreurs de compilation. Une boucle qui corrige les erreurs jusqu'à ce que la compilation soit verte déleste exactement le travail que les humains trouvent le plus fastidieux.

La conséquence pratique : vous cessez de demander à un agent d'écrire une migration et commencez à lui demander de converger vers une migration, sous des contraintes que vous contrôlez.

Prérequis

Avant d'installer quoi que ce soit, confirmez que les éléments suivants sont en place. Chaque élément correspond à un mode de défaillance coûteux à découvrir en pleine migration.

  • Contrôle de version avec un arbre de travail propre. Les agents produisent de gros diffs ; sans git, vous ne pouvez ni les inspecter, ni les bisecter, ni les annuler.
  • Un point d'entrée de compilation reproductible. Une seule commande qui compile le projet depuis zéro et renvoie un code de sortie non nul en cas d'échec.
  • Une commande de test exécutable. Même une suite mince suffit pour commencer. S'il n'y a pas de suite, les tests de caractérisation viennent en premier (voir les exemples d'utilisation).
  • Un environnement d'exécution isolé. Un conteneur ou une VM jetable. Ne pointez jamais un agent disposant d'un accès au système de fichiers et au shell vers une machine détenant des identifiants de production.
  • Une chaîne d'outils de langage pour le système cible — compilateur, gestionnaire de paquets et tout générateur de code dont dépend la compilation.
  • Python 3.10 ou version ultérieure pour le harnais décrit ci-dessous.
  • Un identifiant d'API pour votre fournisseur de modèle, stocké dans une variable d'environnement plutôt que dans un fichier susceptible d'être commité.
  • Un plan de budget et de limites de débit. Les boucles à l'échelle du dépôt font de nombreux appels ; un agent emballé peut consommer le quota rapidement.

Installation étape par étape

La configuration ci-dessous crée un petit espace de travail qui garde le harnais d'agent séparé du dépôt sur lequel il opère. La séparation compte : vous voulez que le harnais soit versionné et révisable, et le dépôt cible sur sa propre branche.

1. Créer l'espace de travail du harnais

mkdir legacy-agent && cd legacy-agent
python3 -m venv .venv
source .venv/bin/activate

La première commande crée un répertoire pour votre code d'orchestration, la deuxième crée un environnement Python isolé, et la troisième l'active afin que les installations ultérieures ne touchent pas à l'interpréteur système.

2. Installer le client et l'outillage de vérification

pip install --upgrade pip
pip install mistralai

Cela met à niveau pip et installe le client Python officiel de Mistral. Consultez la documentation actuelle du client de votre fournisseur pour la signature exacte d'import et de constructeur — celles-ci évoluent, et il est fortement recommandé d'épingler une version dans un requirements.txt pour tout ce que vous comptez exécuter de manière répétée.

pip install pytest pytest-cov ruff

Ceux-ci ajoutent un lanceur de tests, une mesure de couverture et un linter rapide. Le linter compte plus que d'habitude dans le travail legacy : il attrape les suppressions accidentelles et la dérive syntaxique avant que la compilation lente ne s'exécute.

3. Construire un sandbox jetable

docker run --rm -it \
  -v "$PWD/../legacy-repo:/work" \
  -w /work \
  --network none \
  python:3.12-slim bash

Cela démarre un conteneur jetable avec le dépôt cible monté sur /work et, surtout, --network none pour couper le trafic sortant. Ne montez que ce dont la compilation a besoin. Si le projet nécessite un accès réseau pour résoudre les dépendances, résolvez-les pendant la construction de l'image et exécutez la boucle d'agent hors ligne.

4. Configurer les identifiants et les chemins

export MISTRAL_API_KEY="your-key-here"
export LEGACY_REPO="$HOME/src/legacy-repo"
export AGENT_MODEL="<model available in your account>"

La clé est fournie via l'environnement afin qu'elle ne finisse jamais dans un fichier commité. AGENT_MODEL est délibérément laissé non défini à une valeur spécifique : la disponibilité et le nommage des modèles changent, donc lisez-le depuis la configuration plutôt que de le coder en dur dans le harnais.

5. Capturer une base de référence

cd "$LEGACY_REPO"
git checkout -b modernization/agent-work
./build.sh > ../baseline-build.log 2>&1; echo "exit=$?"
./test.sh  > ../baseline-test.log  2>&1; echo "exit=$?"

Ces commandes créent une branche dédiée et enregistrent les résultats actuels de compilation et de test. Sans base de référence, vous ne pouvez pas distinguer une régression introduite par l'agent d'un test déjà en échec lorsque vous avez commencé. Enregistrez les deux codes de sortie et conservez les logs.

Configuration : rendre le dépôt lisible

Les agents échouent sur du code legacy le plus souvent parce que le dépôt est illisible, pas parce que le modèle est faible. Trois artefacts de configuration corrigent la majeure partie de cela.

Le fichier de contexte

Placez un fichier nommé AGENTS.md à la racine du dépôt. Il est lu au début de chaque tâche et doit être court, factuel et ennuyeux.

# Repository context

Build:      ./build.sh          (expect exit 0)
Test:       ./test.sh           (expect exit 0)
Lint:       ruff check src/

## Rules
- Do not modify anything under tests/ or testdata/.
- Do not edit generated files (headers marked "DO NOT EDIT").
- Maximum diff size per task: 400 changed lines.
- If a symbol appears unused, report it. Do not delete it.
- Prefer adding an adapter over changing an existing public signature.

## Known hazards
- src/legacy/pricing.py resolves handlers by string name at runtime.
- The build requires JAVA_HOME to be set.
- Module `reporting` has no test coverage.

L'interdiction de supprimer des symboles apparemment inutilisés n'est pas de la paranoïa. La résolution dynamique, la réflexion et le dispatch piloté par configuration sont exactement les motifs qui rendent les systèmes legacy résistants à l'analyse statique.

La liste d'autorisation des outils

Définissez, dans un petit fichier de configuration, précisément les commandes que l'agent peut exécuter. Tout le reste doit être refusé par défaut.

# agent-policy.yaml
tools:
  read_file: true
  write_file: true
  search: true
  shell:
    allow:
      - "./build.sh"
      - "./test.sh"
      - "ruff check"
      - "pytest"
    deny:
      - "git push"
      - "rm -rf"
      - "curl"
      - "pip install"
limits:
  max_iterations: 25
  max_files_changed: 15
  max_diff_lines: 400

Les plafonds d'itérations et de diff sont le garde-fou le plus efficace. Ils transforment « l'agent a tout réécrit » en « l'agent s'est arrêté et a demandé ».

Le fichier d'ignorance

Ajoutez .agent-work/ à .gitignore afin que les inventaires temporaires, les logs et les rapports intermédiaires n'entrent jamais dans le diff en cours de revue.

Un harnais d'agent minimal

Le harnais ci-dessous est délibérément petit. Il expose quatre outils, exécute une boucle bornée et traite le résultat de compilation comme le seul signal de succès. Le code client spécifique au fournisseur est isolé derrière une fonction adaptatrice afin que vous puissiez le remplacer sans toucher à la boucle.

import json, os, subprocess, pathlib

REPO = pathlib.Path(os.environ["LEGACY_REPO"])
POLICY = json.loads(pathlib.Path("agent-policy.yaml.json").read_text())

def run(cmd: str) -> dict:
    """Execute an allowlisted command and return its output and exit code."""
    if not any(cmd.startswith(a) for a in POLICY["tools"]["shell"]["allow"]):
        return {"error": f"command not allowlisted: {cmd}"}
    p = subprocess.run(cmd, shell=True, cwd=REPO,
                       capture_output=True, text=True, timeout=900)
    return {"exit": p.returncode, "stdout": p.stdout[-4000:], "stderr": p.stderr[-2000:]}

def read_file(path: str) -> str:
    return (REPO / path).read_text(errors="replace")[:20000]

def write_file(path: str, content: str) -> str:
    target = REPO / path
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content)
    return f"wrote {len(content)} bytes to {path}"

def search(pattern: str) -> str:
    p = subprocess.run(["rg", "-n", "--max-count", "5", pattern, str(REPO)],
                       capture_output=True, text=True)
    return p.stdout[:8000]

def ask_model(messages: list) -> dict:
    """Adapter: return {'tool': name, 'args': {...}} or {'final': text}."""
    raise NotImplementedError("wire this to your provider's client")

TOOLS = {"read_file": read_file, "write_file": write_file,
         "search": search, "shell": run}

def run_agent(task: str) -> str:
    messages = [{"role": "user", "content": task}]
    for step in range(POLICY["limits"]["max_iterations"]):
        reply = ask_model(messages)
        if "final" in reply:
            return reply["final"]
        result = TOOLS[reply["tool"]](**reply["args"])
        messages.append({"role": "assistant", "content": json.dumps(reply)})
        messages.append({"role": "user", "content": json.dumps(result)})
        if reply["tool"] == "shell" and reply["args"]["cmd"] == "./build.sh" \
           and result.get("exit") == 0:
            messages.append({"role": "user",
                             "content": "Build is green. Run ./test.sh to confirm."})
    return "iteration limit reached — escalating to human review"

Deux choix de conception méritent d'être soulignés. Premièrement, la boucle n'accepte jamais la parole du modèle comme preuve de succès ; seul un code de sortie zéro de la compilation compte. Deuxièmement, atteindre la limite d'itérations est traité comme un résultat légitime qui produit un rapport, pas comme un échec à réessayer indéfiniment.

Exemples d'utilisation

Exemple 1 — Inventaire avant intervention

Ne laissez jamais un agent modifier lors de la première passe. Commencez par une tâche de cartographie en lecture seule.

En utilisant uniquement search et read_file, produis inventory.json contenant :
- chaque module de premier niveau et son nombre de fichiers
- les dix fichiers ayant le plus grand nombre de références entrantes
- les modules sans fichier de test correspondant
- tout fichier contenant la chaîne "DO NOT EDIT"
Ne modifie aucun fichier. Indique ta confiance pour chaque module.

Vérifiez vous-même le résultat avec l'outillage standard avant de lui faire confiance :

rg --files -g '*.py' | wc -l
rg -n "DO NOT EDIT" -l

La première commande compte les fichiers source, la deuxième liste les fichiers générés. Comparez les deux avec l'inventaire de l'agent ; les divergences indiquent où votre fichier de contexte doit être amélioré.

Exemple 2 — Les tests de caractérisation comme filet de sécurité

Si le module que vous comptez modifier n'a pas de tests, construisez-en un autour de son comportement actuel avant d'y toucher. Le but n'est pas la correction — c'est de figer la sortie d'aujourd'hui afin que la refactorisation de demain puisse y être comparée.

import subprocess, pytest

CASES = ["order-1001", "order-1002", "refund-partial", "currency-mixed"]

@pytest.mark.parametrize("case", CASES)
def test_current_output_is_preserved(case):
    result = subprocess.run(
        ["./legacy_cli", "--case", case],
        capture_output=True, text=True, check=True,
    )
    assert result.stdout == open(f"golden/{case}.txt").read()

Générez les fichiers golden/ à partir du système non modifié, révisez-les à la main une fois, puis figez-les. Faites écrire le harnais par l'agent ; vous approuvez les sorties golden.

Exemple 3 — Extraction incrémentale avec le patron de l'étrangleur

Décomposez la migration afin que chaque tâche puisse être annulée indépendamment.

Tâche : extraire le calcul de taxe de src/legacy/orders.py vers
src/tax/calculator.py derrière un adaptateur.

Définition de terminé :
- src/legacy/orders.py importe le nouveau module
- la fonction d'origine reste, en déléguant à l'adaptateur
- ./build.sh se termine avec le code 0
- ./test.sh se termine avec le code 0 et la couverture sur src/tax/ ne diminue pas
- aucun fichier en dehors de src/legacy/orders.py et src/tax/ n'est modifié
- le diff fait moins de 400 lignes

Si une contrainte ne peut pas être respectée, arrête-toi et explique pourquoi.

Exécutez vous-même la vérification plutôt que d'accepter le résumé de l'agent :

git diff --stat
git diff --name-only | grep -v -E '^(src/legacy/orders.py|src/tax/)' && echo "SCOPE VIOLATION"
./build.sh && ./test.sh

La commande du milieu est la plus importante. Elle échoue bruyamment si l'agent a touché des fichiers en dehors du périmètre convenu, ce qui est la forme la plus courante de dérive silencieuse.

Exemple 4 — Mise à niveau de dépendance pilotée par les retours du compilateur

Les erreurs de compilation sont le retour le moins coûteux qu'un agent puisse obtenir. Formulez la tâche comme une convergence, pas comme une rédaction.

Mets à niveau la version épinglée de <dependency> dans requirements.txt vers
la prochaine version majeure. Ne modifie pas la logique applicative.

Boucle : modifie, exécute ./build.sh, lis les erreurs, corrige uniquement ce que les erreurs
exigent. Une fois la compilation verte, exécute ./test.sh. Si un test échoue,
annule le changement qui l'a causé et signale l'échec au lieu
d'ajuster le test.

L'instruction de ne pas ajuster les tests est essentielle. Un agent qui optimise pour une suite verte réécrira volontiers l'assertion, ce qui transforme une véritable régression en compilation réussie.

Garde-fous et modes de défaillance

Les modes de défaillance sont suffisamment constants pour être anticipés :

  • Manipulation des tests. L'agent modifie le test pour qu'il corresponde à la nouvelle sortie. Atténuation : interdisez les écritures sous tests/, et examinez tout diff touchant des assertions comme une régression suspectée.
  • Dérive du périmètre. Une petite tâche devient une grande. Atténuation : plafonds stricts de fichiers et de lignes, appliqués en dehors de l'agent.
  • Fabrication confiante. L'agent décrit un comportement qu'il a déduit plutôt qu'observé. Atténuation : exigez une commande et sa sortie comme preuve pour chaque affirmation comportementale.
  • Suppression de code référencé dynamiquement. Atténuation : n'autorisez jamais la suppression dans la même tâche que l'extraction ; exigez une passe séparée, approuvée par un humain.
  • Exposition de secrets. Atténuation : sandbox hors ligne, identifiants injectés par l'environnement, et un scan de chaque diff à la recherche de chaînes ressemblant à des clés avant la fusion.

Traitez toute sortie d'agent comme une pull request d'un contributeur inconnu : utile, plausiblement correcte, et nécessitant une revue.

Mesurer si cela fonctionne

Suivez un petit ensemble de chiffres par migration, pas par tâche :

  • Taux de compilation verte après la première tentative de l'agent.
  • Taille médiane des diffs, et le nombre de violations de périmètre.
  • Couverture sur les modules touchés avant et après.
  • Taux d'annulation — changements fusionnés puis annulés ultérieurement.
  • Minutes de revue humaine par changement fusionné.

Le dernier est la métrique honnête. Un agent qui produit des diffs impressionnants que les humains doivent scruter pendant une heure chacun n'a rien amélioré.

Ce que signale la publication de Mistral

L'article de Mistral positionne la modernisation legacy comme une tâche adaptée aux agents plutôt qu'à l'autocomplétion. Ce cadrage est cohérent avec tout ce qui précède : la valeur vient d'une boucle bornée et vérifiable opérant sur un dépôt, et non d'une seule génération astucieuse. Au-delà de ce cadrage, cet article n'affirme rien sur des produits, des capacités ou des performances spécifiques, car la source ne les étaye pas et les détails sont des cibles mouvantes. Les pratiques d'ingénierie présentées ici — bases de référence, sandboxes, tests de caractérisation, plafonds de diff, revue humaine — tiennent quel que soit le modèle ou le fournisseur derrière l'adaptateur.

Conclusion

Moderniser du code legacy complexe avec des agents IA n'est pas une question de trouver un meilleur prompt. C'est une question de construire un environnement où les suppositions de l'agent sont peu coûteuses et où ses erreurs sont attrapées par un compilateur ou un test golden master avant qu'un humain ne lise le diff.

La séquence qui fonctionne est sans glamour. Établissez une compilation reproductible et une commande de test exécutable. Cartographiez le dépôt en lecture seule avant d'autoriser des modifications. Figez le comportement actuel avec des tests de caractérisation. Extrayez par petites tranches réversibles avec des plafonds de périmètre stricts. Conditionnez chaque fusion à des commandes que vous exécutez vous-même. Gardez un humain dans la boucle pour les suppressions, les changements de signature et tout ce qui touche aux tests.

Faites cela, et l'agent gère la longue traîne de convergence mécanique qui ralentit la migration legacy — pendant que vous gardez le contrôle des décisions qui la rendent dangereuse.

Sources