Accueil / Articles

Optimiser un agent Terraform avec DSPy : Construire le vérificateur avant l’optimiseur

Les agents Terraform inventent des schémas. Créez un vérificateur déterministe, établissez une ligne de référence pour l’agent, puis utilisez DSPy et GEPA pour optimiser son fichier de compétences, en obtenant des résultats fiables.

Éditeur : REACTAPP.TOP18 min de lecture4065 mots

Sources: Your Terraform Agent Is Probably Wrong Half the Time

Un agent qui écrit du code d’infrastructure a probablement déjà créé un module de ce type à un moment donné : il semble tout à fait raisonnable, ressemble à tous les modules approuvés par des reviewers, mais échoue à l’exécution de terraform validate pour une raison que personne ne détecterait à première vue.

Cela fait également l’objet de mesures. IaC-Eval est un benchmark composé de 458 scénarios Terraform soigneusement sélectionnés, et GPT-4 ne parvient apparemment qu’à un taux de réussite de 19,36 % sur cet ensemble, contre 86,6 % pour le même modèle sur des benchmarks Python plus stricts. Une étude plus récente sur la génération d’IaC par des agents indique 14,0 % pour Qwen2.5-Coder 7B, 41,4 % pour GPT-4o et 62,4 % pour GPT-5.4.

Pourquoi le code d’infrastructure est-il bien plus difficile que Python ? Une configuration correcte doit répondre en même temps à quatre exigences : une syntaxe valide, une conformité avec le schéma de la version du fournisseur utilisée, un graphe de dépendances cohérent, et le respect de la politique interne de l’organisation. Le manque de l’une de ces exigences rend toute la configuration incorrecte. Un code Python étant 90 % correct fonctionne souvent encore, tandis qu’un Terraform étant 90 % correct ne s’applique pas.

Ces recherches montrent également des résultats qui rendent les solutions habituelles douteuses. Les techniques de prompting à faible quantité d’exemples, la réflexion en chaîne et les interactions à plusieurs tours n’ont aidé que peu. Environ 79 % des échecs liés aux politiques sont dus à des informations manquantes dans les instructions fournies, et non aux capacités du modèle. Le modèle était suffisamment performant ; il ne lui avait simplement pas été indiqué ce qu’il fallait.

C’est un problème d’optimisation. Le plan consiste donc à prendre un agent Terraform fonctionnel, à mettre en place l’évaluation nécessaire pour déterminer s’il y a amélioration, puis à exécuter l’ensemble du flux de travail DSPy sur cette évaluation : programmer, évaluer, optimiser. Tout est exécutable. Le projet associé est divisé en trois dossiers, un par étape :

agent/       the deepagents Terraform agent, its tools, prompts, and skills
eval/        the verifier, 38 tasks with Rego policies, and the benchmark
optimizer/   the DSPy program, metric, and GEPA compile

L’agent de démarrage

Gardez cette section courte, car ce n’est pas la partie intéressante et n’importe quel agent peut la remplacer. Cet agent cherche le schéma réel, écrit des fichiers qui sont vérifiés, puis valide avant de répondre.

Trois outils se trouvent dans agent/tools.py. L’agent lit le schéma réel du fournisseur avant d’écrire quoi que ce soit. Les fichiers qu’il écrit sont formatés et vérifiés syntaxiquement au fur et à mesure. Il valide ensuite toute la configuration et corrige tout ce que le compilateur signale, ne répondant qu’après cela.

Il est basé sur deepagents, le framework de LangChain pour les agents dotés d’outils et de compétences, bien que ce framework ait peu d’importance. C’est la structure qui compte :

from deepagents import create_deep_agent

agent = create_deep_agent(
    model="openrouter:openai/gpt-5.6-luna",
    tools=[provider_schema, write_terraform, validate_config],
    system_prompt=SYSTEM_PROMPT,
    skills=["./agent/skills"],     # SKILL.md — the thing we'll optimize
)

Le répertoire des compétences mérite une attention plus approfondie, car tout le reste ici vise à l’optimiser. Une compétence est un fichier Markdown contenant des instructions que l’agent charge à côté de son prompt système, dans un format appelé Agent Skills qui gagne rapidement en popularité : Claude, GitHub Copilot et deepagents lisent tous le même format SKILL.md. Ce format sépare clairement les différentes responsabilités. Le prompt système se limite aux aspects basiques (comment appeler les outils, comment formater les résultats), tandis que la compétence contient les connaissances sectorielles telles que les conventions de Terraform, les paramètres de sécurité par défaut et les particularités des fournisseurs. Étant donné que le même fichier fonctionne sur plusieurs agents, les compétences sont également portables.

La plupart des documents sur DSPy orientent l’optimiseur vers l’invite principale d’un programme. La question posée ici est un peu différente : peut-il optimiser une compétence ? Les équipes réelles consacrent la majeure partie de leurs ajustements manuels à ces compétences, car c’est là que réside l’expertise et elles sont modifiées sans cesse lors des revues. Dans ce projet, elle est écrite à la main, stockée dans git et soumise à des revues de code comme n’importe quel autre fichier. De plus, elle n’a jamais été mesurée, ce qui constitue le véritable problème, et DSPy a été conçu pour y remédier, car chaque modification de ce fichier modifie le comportement en production sans aucun test.

À quoi ressemble réellement un échec

Commencez par un artefact réel issu d’une période antérieure à toute correction. Il a été généré lors de l’exécution du benchmark de référence enregistré, où l’agent ne disposait d’aucun outil et écrivait directement le HCL dans sa réponse. La demande concernait un bucket d’état utilisant un chiffrement géré par le client :

  encryption {
    kms_key_name = var.encryption_key_name
  }

  lifecycle_rules {
    ...

Cela a l’air en ordre, mais l’exécution du validateur sur ce texte montre une autre histoire :

$ tofu validate

Error: Missing required argument
  on main.tf line 18, in resource "google_storage_bucket" "terraform_state":
The argument "default_kms_key_name" is required, but no definition was found.

Error: Unsupported argument
  on main.tf line 19, in resource "google_storage_bucket" "terraform_state":
An argument named "kms_key_name" is not expected here.

Error: Unsupported block type
  on main.tf line 22, in resource "google_storage_bucket" "terraform_state":
Blocks of type "lifecycle_rules" are not expected here. Did you mean
"lifecycle_rule"?

Trois erreurs sont apparues à partir d’une seule ressource. L’argument requis est default_kms_key_name, tandis que kms_key_name appartient à d’autres ressources Google, ce qui explique pourquoi il semble correct. Le nom d’un bloc a également été mis au pluriel pour désigner quelque chose qui n’existe pas.

Lorsque les échecs de base ont été classés par type, 80 % des erreurs de validation correspondaient à des schémas inventés : des arguments, des blocs ou des attributs qui ressemblent à de vrais éléments HCL mais qui n’existent pas dans la ressource concernée. Le fournisseur google compte environ mille types de ressources qui évoluent à chaque nouvelle version ; par conséquent, le modèle récite à partir de sa mémoire une API versionnée, et la mémoire produit des fictions plausibles.

Ces trois outils ciblent directement ce type de diagnostic. Seuls, ils ont ajouté 27,7 points (le taux de réussite est passé de 41,7 % à 69,4 % avec l’ensemble d’outils décrit ci-dessous). Les erreurs de formatage sont tombées à zéro, car write_terraform exécute fmt au lieu d’attendre que le modèle gère lui-même les espaces blancs. Les erreurs de schéma ont également chuté à zéro, car l’agent peut enfin voir ce que voit le compilateur. Ce qui restait concernait presque entièrement l’intention, à savoir si l’agent construisait bien ce qui avait été demandé, et c’est là que DSPy intervient.

Voici la même tâche exécutée par l’agent équipé d’outils tels qu’ils fonctionnent aujourd’hui :

$ uv run python -m agent.run --task eval/tasks/backend-var-interpolation.json

task      backend-var-interpolation (opentofu)
model     openai/gpt-5.6-luna  engine=deepagents
files     main.tf, variables.tf
tools     {'write_terraform': 2, 'validate_config': 1, 'validate_pass': 1}
elapsed   56602ms

Il y a eu deux écritures et une validation, et l’agent ne répond que lorsque le compilateur est satisfait. Le fichier de tâche ainsi que la cible opentofu proviennent du jeu d’évaluation, qui est là où les choses vont ensuite.

Un évaluateur avant un framework

Déterminer si tout cela est utile signifie répondre à la question « le résultat est-il bon ? » avec quelque chose de plus concret qu’une simple impression. La plupart des domaines utilisent un juge basé sur un LLM, qui oppose l’opinion d’un modèle à celle d’un autre sans référence de vérité, et ce juge doit lui-même être vérifié.

Terraform ne connaît pas ce problème, car l’ensemble d’outils déjà en cours d’exécution dans CI sert d’évaluateur :

  • tofu fmt -check détecte les problèmes de formatage.
  • tofu validate repère les erreurs de syntaxe, de schéma et de référence.
  • tflint indique les arguments obsolètes et les types de machine invalides.
  • tofu test avec mock_provider vérifie la sémantique sans aucune identifiante cloud.
  • conftest avec OPA Rego vérifie l’intention, en s’assurant que l’agent a construit ce qui a été demandé.
  • Ces cinq vérifications déterministes sont implémentées dans eval/verifier/run.py. Aucun juge n’est impliqué, l’évaluation ne coûte rien en termes d’appels API, et aucune étape ne peut générer de ressource facturable, car le projet ne contient nulle part de apply, intentionnellement.

    La plupart des gens sautent la dernière vérification, bien qu’elle compte pour le plus grand poids. validate indique que la configuration se compile correctement. Rego confirme que le bucket dispose bien de l’encryption activée, ce qui correspond à la demande et ce qui en fait une solution correcte et non simplement valide :

    package main
    import rego.v1
    
    deny contains "bucket must use a customer-managed KMS key" if {
        some name
        bucket := input.resource.google_storage_bucket[name][_]
        not bucket.encryption
    }
    

    Une équipe qui applique déjà des politiques via OPA dispose probablement de nombreuses politiques similaires dans Terraform Cloud ou dans son pipeline CI, et celles-ci peuvent être réutilisées sans modification. Il s’agit d’une approche de type « shift-left », appliquée aux agents. Terraform Cloud refuse bien un plan incorrect au moment de l’application, mais à ce stade, le code existe déjà, un réviseur l’a lu et une exécution est en attente. En exécutant le même Rego lors de la génération du code, on peut détecter les violations avant que tous ces efforts ne soient déployés.

    Tester le testeur. C’est l’étape la plus intéressante à reproduire. Une politique qui approuve tout est pire que aucune, car elle attribue la même note à chaque version d’agent et signale silencieusement qu’aucun changement n’a eu lieu. Chaque politique située dans eval/tasks/policy/ est donc accompagnée d’un fichier de test conforme et d’un fichier de test défaillant, et un script vérifie les deux cas de figure :

    $ cd eval/tasks && ./check_policies.sh
      ...
      ✓ vpc-subnet-firewall              good=0 bad=2
    
    ✅ 38/38 policies verified in both directions
    

    Trente-huit tâches ont été rédigées de cette manière. Elles couvrent les paramètres par défaut de sécurité, les connexions entre ressources, la portée des schémas des fournisseurs, la ciblage des dialectes, les contraintes (l’agent ajoute-t-il des éléments que personne ne souhaite) ainsi que la structure des fichiers. Chacune associe une instruction à une politique Rego qui en capture l’intention. Le dialecte fait l’objet d’un traitement spécial : chaque tâche précise s’il vise terraform ou opentofu, car ces deux outils ont évolué différemment (OpenTofu propose le chiffrement de l’état et l’instruction for_each des fournisseurs, tandis que Terraform propose les Stacks) ; par conséquent, un agent qui cible silencieusement le mauvais outil échoue à la validation.

    Le diagramme des cinq portes montre également une frontière de mesure que l’agent ne peut pas franchir, et il est facile de surestimer ce que cela signifie. En environnement de production, l’agent peut, et devrait probablement, disposer d’un outil OPA qui vérifie les politiques avant de répondre. Il s’agit du même principe de déplacement vers la gauche, ce qui permet de générer du code de meilleure qualité.

    La frontière a un but différent. Ici, ces mêmes politiques servent de critère pour comparer les compétences, les modèles et les exécutions d’optimisation. Si l’agent pouvait itérer en fonction de la politique même qui évalue l’expérience, toutes les variantes tendraient à réussir et la comparaison ne dirait rien. C’est le principe du jeu de test réservé, appliqué aux outils. La règle : l’agent peut voir n’importe quelle vérification sauf celle qui évalue actuellement l’expérience. Si l’agent de production sera livré avec l’outil de politique, évaluez également cette configuration, en utilisant des politiques réservées ou en comptant les itérations de correction nécessaires, plutôt que de déterminer si elle réussit.

    Adoption de DSPy

    DSPy se présente comme un cadre destiné à programmer des modèles de langage plutôt qu’à les solliciter par des prompts. Sa documentation organise le flux de travail en trois phases : la programmation, l’évaluation et l’optimisation, et cet ordre même constitue une leçon : l’optimiseur est la dernière étape à modifier, et tout ce qui précède est ce qui rend l’optimiseur fiable. L’optimiseur utilise précisément trois éléments : un programme, une métrique et des données.

    Toutes les fonctionnalités de DSPy se trouvent sous le répertoire optimizer/, et la configuration dépend de uv :

    uv add dspy
    uv sync
    

    Phase 1 : programmation

    Une signature est un contrat structuré pour une seule appelation de LM, composé d’entrées, de sorties et d’une documentation décrivant la tâche. Celle-ci se trouve dans optimizer/harness.py :

    import dspy
    
    class AuthorConfiguration(dspy.Signature):
        """Write a complete, valid Infrastructure-as-Code configuration."""
    
        request: str = dspy.InputField(
            desc="What the user wants built, in natural language."
        )
        target: str = dspy.InputField(
            desc="Which dialect to target: 'terraform' or 'opentofu'. These have "
                 "diverged — code targeting the wrong one will fail validation."
        )
        config: str = dspy.OutputField(
            desc="The complete configuration as fenced HCL blocks. Start each block "
                 "with a comment naming its file, e.g. '# main.tf'."
        )
    

    Les omissions sont justement l’essentiel. Il n’y a pas de phrase du type « vous êtes un ingénieur Terraform expert », pas d’exemples concrets, et aucune insistance sur le format. Les noms de champs et leurs descriptions transmettent le sens, tandis que l’optimiseur s’occupe de la formulation.

    Quiconque trouve cela magique peut le dissiper en une seule ligne :

    dspy.inspect_history(n=1)
    

    Il affiche la requête réelle que l’adaptateur a générée à partir de la signature. La sortie réelle correspondante est la suivante :

    System message:
    
    Your input fields are:
    1. `request` (str): What the user wants built, in natural language.
    2. `target` (str): Which dialect to target: 'terraform' or 'opentofu'. ...
    Your output fields are:
    1. `config` (str): The complete configuration as fenced HCL blocks. ...
    
    [[ ## request ## ]]
    {request}
    
    [[ ## target ## ]]
    {target}
    
    [[ ## config ## ]]
    {config}
    
    In adhering to this structure, your objective is:
            Write a complete, valid Infrastructure-as-Code configuration ...
    

    Rien n’est caché. Il s’agit d’un modèle structuré généré à partir de la signature, et c’est précisément pour cette raison qu’un optimiseur peut la réécrire en toute sécurité, et pourquoi il convient de commencer par y regarder lorsque le comportement est étrange.

    Un module applique une technique à une signature, et les deux varient indépendamment. Une signature peut fonctionner en une seule étape, avec d’abord le raisonnement, ou en tant que moteur d’un cycle d’outil :

    generate = dspy.Predict(AuthorConfiguration)             # one shot
    generate = dspy.ChainOfThought(AuthorConfiguration)      # reason first
    generate = dspy.ReAct(AuthorConfiguration, tools=[...])  # run a tool loop
    

    Cet agent utilise des outils, il est donc synchronisé avec dspy.ReAct et lui sont fournies exactement les fonctions d’outil que l’agent déployé appelle, importées directement du package de l’agent. Le code se trouve dans optimizer/harness_deepagents.py :

    from agent.tools import make_tools            # the deployed agent's tools
    
    class TerraformAuthoringAgent(dspy.Module):
        def __init__(self, seed_instruction: str):
            super().__init__()
            # Seed the SIGNATURE before constructing ReAct, so DSPy appends its
            # tool protocol to your instruction instead of replacing it.
            seeded = AuthorConfiguration.with_instructions(seed_instruction)
            lc_tools = make_tools(work_dir, binary="terraform", stats={})
            self.react = dspy.ReAct(
                seeded,
                tools=[dspy.Tool(t.func, name=t.name, desc=t.description)
                       for t in lc_tools.values()],
                max_iters=10,
            )
    
        def forward(self, request: str, target: str = "terraform"):
            return self.react(request=request, target=target)
    

    La décision de conception la plus importante dans cette configuration est la suivante : optimiser le même programme qui est déployé. Une expérience préliminaire a optimisé un générateur à usage unique, puis a transmis le résultat à un agent utilisant des outils, ce qui a rendu tous les nombres dénués de sens, car les instructions avaient été ajustées pour des échecs que l’agent réel ne produisait plus.

    L’instruction de départ est le fichier SKILL.md existant, il n’y a donc pas de point de départ frais, et la vraie question est de savoir si le texte manuscrit peut être amélioré. DSPy ne sait pas que cette chaîne de caractères représente une compétence plutôt qu’une instruction principale, car il ne voit que le texte des instructions de la signature. C’est ce qui rend une expérience basée sur des compétences réalisable : le résultat est enregistré directement sous forme de SKILL.md et fonctionne pour tout agent capable de lire des compétences.

    Phase 2 : évaluation

    Le type de données dans DSPy est dspy.Example, un dictionnaire qui indique quels champs sont des entrées. Le chargeur de optimizer/dataset.py convertit directement le jeu de données :

    dataset = [
        dspy.Example(
            task_id=t["id"],
            request=t["prompt"],
            target=t["target"],
            rego=t["rego"],                # path to this task's policy
        ).with_inputs("request", "target")
        for t in tasks
    ]
    

    Les champs qui ne sont pas marqués comme entrées sont transmis en tant que métadonnées. La métrique utilise rego pour trouver la politique correspondante, et le modèle ne la voit jamais.

    Il convient d’expliquer en détail comment les exemples sont sélectionnés, car l’ensemble de données détermine tout ce que l’optimiseur peut apprendre. Ils n’ont pas été extraits d’un benchmark. Ils imitent le format des tickets réels, par exemple une demande concernant un bucket GCS chiffré avec une clé gérée par le client, ou un compte de service autorisé à accéder à un seul bucket et à rien d’autre. La couverture englobe ainsi les ressources principales gérées par une équipe de plateforme : le stockage, les réseaux, IAM, SQL et Pub/Sub, ainsi que des familles de tâches destinées aux modes de défaillance déjà rencontrés : des ressources à schéma complexe où les modèles inventent des arguments, des tâches de contrôle vérifiant que l’agent n’ajoute pas de ressources non sollicitées, ainsi que les tâches dialectiques mentionnées précédemment. Certaines sont délibérément difficiles, car un ensemble de tests qui réussit tout dès le premier jour ne permet pas de montrer d’amélioration.

    Trente-huit exemples semblent peu, et c’est le cas. L’objectif est de couvrir chaque exemple, car chacun possède sa propre politique, ainsi que de procéder à des répartitions honnêtes. Répartissez avant d’optimiser et gardez l’ensemble de test sacré : l’ensemble d’entraînement sert à ce que l’optimiseur apprenne, la validation permet de sélectionner des candidats, et l’ensemble de test est évalué une seule fois, à la fin. La plupart des approches d’ingénierie de prompts ignorent cette discipline, et c’est en l’ignorant que les gens se convainquent qu’un changement a été utile alors qu’il ne l’a pas été. Cela s’est produit deux fois dans ce projet, et à chaque fois, une victoire sur un petit échantillon de validation s’est inversée une fois que l’échantillon s’est agrandi.

    La métrique est l’élément auquel le vérificateur se connecte, et il s’agit d’une fonction qui remplit trois fonctions (optimizer/metric.py) :

    def verify_metric(example, prediction, trace=None, pred_name=None, pred_trace=None):
        """Score with the SAME verifier that produces our benchmark numbers."""
        result = verify(
            config_dir=materialise(prediction.config),
            policy_dir=example.rego,
            target=example.target,
        )
    
        # Job 1 — bootstrapping (trace is set): a strict bool. Only outputs that
        # FULLY pass may become worked examples.
        if trace is not None:
            return result.passed
    
        # Job 2 — reflective optimization (pred_name is set): score AND feedback.
        # GEPA reads the text to understand *why* a candidate failed.
        if pred_name is not None:
            return dspy.Prediction(score=result.score, feedback=format_failures(result))
    
        # Job 3 — plain evaluation: a float.
        return result.score
    

    Restez succinct. Si la métrique réimplémente une logique de notation, le nombre optimisé et le nombre publié s’éloignent l’un de l’autre, ce qui signifie qu’aucune mesure n’est effectuée.

    C’est dans la branche de retour d’information que ce domaine se distingue. Les optimiseurs réflexifs prennent en compte du texte expliquant une erreur, et ce texte est ici libre et précis : des erreurs de compilateur réelles ainsi que des refus de politique réels, comme ceux présentés précédemment. Les champs qui dépendent d’un juge basé sur un LLM paient pour chaque évaluation et reçoivent des retours bien moins fiables, avant de devoir valider ce juge eux-mêmes.

    Mesurez une valeur de référence avant toute autre chose, en utilisant dspy.Evaluate :

    evaluate = dspy.Evaluate(devset=valset, metric=verify_metric,
                             num_threads=8, display_table=True)
    evaluate(program)
    

    La sortie d’un essai de fumée avec 3 exemples a cet aspect, et l’ensemble complet prend plus de temps :

    Average Metric: 0.59 / 3 (19.6%): 100%|██████████| 3/3 [01:16<00:00, 25.60s/it]
    INFO dspy.evaluate.evaluate: Average Metric: 0.5882 / 3 (19.6%)
    WARNING dspy.evaluate.evaluate: Skipping table display since `pandas` is not installed.
    

    Deux leçons ressortent de ce fragment. Chacun des « exemples » correspond à une épisode complet de l’agent, et non à une simple appel API, il convient donc d’anticiper les coûts en conséquence. De plus, la fonction display_table a besoin de pandas, il faut donc l’installer pour afficher les tableaux associés à chaque exemple.

    Si une habitude doit être adoptée à partir de cela, qu’elle soit de ne jamais optimiser quoi que ce soit sans disposer d’une valeur de référence. C’est ce chiffre qui donne un sens aux résultats de l’optimiseur.

    Phase 3 : optimisation

    Differentes méthodes d’optimisation s’adaptent à des éléments différents :

    • BootstrapFewShot synthétise des exemples fonctionnels (démos).
    • MIPROv2 ajuste les instructions et les démos à l’aide de la recherche bayésienne.
    • GEPA affine les instructions en analysant les traces d’échec.

    Le réglage des instructions constitue l’outil idéal pour le code soumis à des contraintes de schéma, car l’étude sur l’IaC agentiel a montré qu’il surpassait l’injection de démonstrations, précisément parce que ces dernières ne résolvent pas les hallucinations de schéma. GEPA est donc la solution de prédilection, déterminée par optimizer/compile.py :

    gepa = dspy.GEPA(
        metric=verify_metric,
        max_metric_calls=150,
        reflection_lm=dspy.LM("openrouter/openai/gpt-5.6-luna", max_tokens=16000),
        num_threads=8,
        track_stats=True,
    )
    
    compiled = gepa.compile(student=program, trainset=trainset, valset=valset)
    compiled.save("compiled_state.json", save_program=False)
    

    Les deux modèles accomplissent des tâches différentes. Le modèle de traitement génère du code Terraform, qui est celui qui est finalement déployé ; le modèle de réflexion analyse les échecs et rédige des instructions provisoires. Il s’exécute environ 40 fois par rapport aux quelque 900 exécutions du modèle de traitement, ce qui rend envisageable l’investissement dans un modèle plus performant.

    Quelques conseils pratiques acquis à la force de l’expérience :

    • Définissez num_threads. Lors d’une exécution, cette valeur a été oubliée, ce qui a entraîné 69 minutes de compilation ; les mesures ultérieures ont montré que la machine était inactive 94 % du temps, en attente des appels API effectués les uns après les autres.
    • « 150 appels métriques » signifie 150 épisodes d’agent, ce qui, avec une boucle d’outil, correspond à environ 900 appels LM.
    • Appelez toujours save() après compilation. Avec save_program=False, le résultat est un JSON simple : le texte des instructions optimisées ainsi que toutes les démonstrations intégrées. Ce même JSON permet également de se passer complètement de DSPy, car une vingtaine de lignes (optimizer/to_skill_md.py) le transforment à nouveau en le fichier SKILL.md que l’agent utilise. Lors de l’exécution, DSPy n’est jamais importé. L’optimiseur n’est qu’une étape de construction, et ce qu’il produit arrive sous forme de diff dans une demande de fusion que quelqu’un peut lire et rejeter.

    GEPA découvre également des situations réelles. Sur un modèle moins performant, l’agent continuait d’abandonner son cycle d’utilisation des outils : une seule appel à provider_schema, puis il s’arrêtait sans rien enregistrer. On ne l’avait jamais informé de cela. GEPA a analysé les traces d’échec et a intégré cette information dans l’instruction optimisée : il ne faut pas terminer simplement parce qu’un fichier a été écrit, car une écriture réussie ne prouve que le HCL se parse correctement, et validate_config est nécessaire pour détecter les erreurs de schéma de fournisseur ainsi que les références invalides. Il s’agit donc d’une réflexion basée sur des traces d’exécution réelles, produisant des directives qui auraient nécessité des heures de débogage pour être formulées.

    Le flux de travail reste efficace après optimisation

    Deux autres éléments de ce même mécanisme réutilisent la métrique déjà existante.

    Cette métrique peut également être exécutée au moment de l’inférence. Les fonctions BestOfN et Refine de DSPy enrobent n’importe quel module, le font fonctionner autant de fois que N et retournent le premier résultat dont la récompense atteint un seuil donné :

    robust = dspy.Refine(module=program, N=3, reward_fn=verify_metric, threshold=1.0)
    

    Le schéma devrait sembler familier, car la boucle de validation et de réparation de l’agent applique la même idée avec le compilateur en tant que fonction de récompense. Les données le classent comme le levier le plus efficace qui existe : dans l’étude sur l’IaC, la réparation itérative avec des retours du vérificateur a permis à Qwen 7B d’atteindre 62,9 % contre 14,0 % au départ, ce qui est meilleur que toutes les techniques de prompting testées. Quelle que soit la forme, fournissez au modèle son vérificateur en temps de exécution.

    Les remplacements de modèles deviennent une modification en une seule ligne, et ils sont désormais mesurables :

    program.set_lm(dspy.LM("openrouter/qwen/qwen3-coder-30b-a3b-instruct"))
    evaluate(program)
    

    Cela s’est avéré plus important que prévu. Un changement en cours de projet vers un modèle moins cher a fait chuter la qualité de 83,3 % à 37,5 %, près de la moitié des exécutions abandonnant le cycle de traitement sans générer aucun fichier. Rien dans un flux de travail manuel ne révèle cela, et le premier signe ne viendrait que d’un client. Douze minutes d’exécution du jeu d’évaluation l’ont mis en évidence.

    Le résultat honnête

    Pour cet agent, l’instruction compilée n’a pas réussi à surpasser celle écrite à la main.

    La comparaison a été effectuée neuf fois sur deux implémentations d’agents et deux modèles, y compris un ensemble de test réservé sur lequel les deux ont obtenu la même note de 92,9 %. Ce chiffre concerne le modèle le plus performant, tandis que les valeurs précédentes de 41,7 % à 69,4 % concernent le modèle moins cher utilisé avec le même jeu d’évaluation. À deux reprises, un résultat semblait être une victoire claire, a été consigné, avant de changer avec un échantillon plus important.

    C’est la leçon la plus précieuse à retenir, et cela ne prouve pas que DSPy échoue. Une raison spécifique et plausible peut expliquer pourquoi cela n’a pas fonctionné. Les améliorations rapportées par GEPA proviennent de workflows modulaires, de pipelines dont chaque étape utilise un prompt différent, ce qui permet à l’optimiseur de blâmer un module en cas d’échec et de le corriger. Un agent, quant à lui, utilise un seul prompt à chaque étape. La critique de Benjamin Anderson soutient que cela élimine précisément la localité sur laquelle repose GEPA.

    Les conseils pratiques sont les suivants :

    • Pour un pipeline simple, les améliorations rapportées sont probablement transférables.
    • Pour un cycle d’agents, exécutez votre propre ensemble de tests avant d’en déduire que cela fonctionne également.
    • Dans les deux cas, intégrez les retours du vérificateur directement dans l’agent lui-même ; c’est ce qui est le mieux étayé, tant dans les travaux publiés que dans ces chiffres.

    Et ce test est possible, car l’ensemble de tests existe bien.

    Où cela nous mène

    Les modèles de langage trouvent l’infrastructure en code difficile à maîtriser. Le score de 19,36 % de GPT-4 sur IaC-Eval n’est pas un problème lié aux prompts que la pensée optimiste pourra résoudre, et les astuces habituelles ne sont que peu efficaces. Néanmoins, la plupart des échecs liés aux politiques de fonctionnement proviennent d’un manque d’informations plutôt que d’un manque de capacités, il existe donc encore de la marge de manœuvre, et elle réside dans les instructions et les outils plutôt que dans les poids du modèle.

    Le flux de travail qui s’est avéré efficace est celui que la documentation de DSPy elle-même enseigne, dans l’ordre indiqué. Pour la programmation : des signatures pour le contrat, des modules reflétant ce qui est déployé, ainsi que la fonction inspect_history en cas de doute quant au contenu envoyé. Pour l’évaluation : des exemples avec des entrées explicites, une métrique simple servant également de retour à l’optimiseur, une ligne de référence avant toute modification et un ensemble de test consulté une seule fois. Pour l’optimisation, et seulement à ce stade, en utilisant la même métrique, on enregistre du texte qui peut être examiné dans une demande de fusion.

    Même si l’optimiseur n’a pas surpassé la performance obtenue manuellement, l’adoption de DSPy a obligé à définir ce qu’est une solution « correcte » sous forme de code exécutable. Cet artefact a continué de conserver sa place. Il a signalé une régression de 46 points après un changement de modèle, révélé que la compétence en question n’était chargée que 25 % du temps (problème résolu par une phrase réécrite, ce qui a permis au taux de chargement d’atteindre 100 %), et a empêché à deux reprises que des résultats dus au bruit soient publiés comme des victoires.

    Rien de tout cela n’exige strictement DSPy, et le vérificateur utilisé ici n’importe aucun de ses composants. DSPy est utile en ce qu’il oblige à l’existence et à la réutilisation d’une métrique, de sorte qu’une seule définition pilote l’ensemble des tests de régression, les comparaisons de modèles, les tentatives de reprise en temps de prédiction et l’optimiseur. Les équipes qui possèdent déjà cette discipline n’ont peut-être pas besoin de ce framework, tandis que celles qui ne l’ont pas peuvent l’acquérir par la suite. Quoi qu’il en soit, cessez de livrer des modifications de prompts qui ne peuvent pas être mesurées.

    Lectures complémentaires

  • Pourquoi les benchmarks de mémoire d’agent induisent en erreur, et comment évaluer avec vos propres données — Les classements publics de mémoire présentent des grilles de correction et des correcteurs défectueux. Séparez la récupération des réponses, ajoutez des vérifications de cohérence, de récenteté, d’abstention et d’oublie, et suivez le coût.
  • Choisir un framework d’agent en fonction de la forme des charges de travail : six axes et une voie de décision — Choisissez LangGraph, CrewAI, AutoGen, Temporal ou rien en testant six axes de charge de travail : déterminisme, durée de vie, idempotence, trace d’audit, vitesse d’équipe et coût par décision.