Recherche

Améliorer continuellement notre harness d’agents

Stefan Heule & Jediah Katz13 min de lecture

Nous abordons la création de la harness d’agents de Cursor comme n’importe quel produit logiciel ambitieux. Une grande partie du travail est guidée par une vision : nous partons d’une idée précise de ce à quoi devrait ressembler l’expérience agent idéale.

À partir de là, nous formulons des hypothèses sur la manière de nous en rapprocher, menons des expériences pour les tester, puis itérons à partir de signaux quantitatifs et qualitatifs issus des évaluations et de l’utilisation réelle. Ce processus repose sur une instrumentation en ligne et hors ligne adaptée, afin de pouvoir déterminer si un changement améliore réellement la harness.

Lorsque nous obtenons un accès anticipé à de nouveaux modèles, toutes ces approches convergent. Nous passons des semaines à adapter notre harness aux points forts et aux particularités d’un modèle, jusqu’à ce que ce même modèle, au sein de notre harness spécialement optimisée, soit nettement plus rapide, plus intelligent et plus efficace.

Il nous arrive parfois de découvrir des améliorations majeures. Le plus souvent, toutefois, améliorer la harness consiste à empiler méthodiquement de petites optimisations qui, ensemble, rendent les agents plus performants pour créer des logiciels.

Faire évoluer la fenêtre de contexte

Au cœur de l’interaction avec les grands modèles de langage se trouve la fenêtre de contexte. Lorsqu’on demande à l’agent de créer quelque chose, la fenêtre de contexte commence par la requête système et les descriptions des outils, suivis de l’état actuel de la conversation, puis de la requête de l’utilisateur.

La manière dont nous remplissons et gérons cette fenêtre a considérablement évolué au fil de l’histoire de Cursor.

Lorsque nous avons commencé à développer notre agent de codage à la fin de 2024, les modèles étaient bien moins capables de choisir eux-mêmes leur contexte, et nous avons beaucoup investi dans l’ingénierie du contexte pour mettre en place des garde-fous — par exemple, en transmettant à l’agent les erreurs de lint et de typage après chaque modification, en reformulant ses demandes de lecture de fichiers lorsqu’il demandait trop peu de lignes, et même en limitant le nombre maximal d’outils qu’il pouvait appeler en un seul tour.

Nous fournissions également d’importantes quantités de contexte statique, toujours disponibles pour l’agent au début de chaque session. À différents moments, cela incluait l’arborescence des dossiers de la base de code, des extraits de code sémantiquement pertinents pour la requête, ainsi que des versions compressées de fichiers que l’utilisateur avait joints manuellement.

Tout cela a en grande partie disparu.

Nous incluons toujours certains éléments de contexte statique utiles (par ex. le système d’exploitation, l’état de git, les fichiers ouverts et récemment consultés). Mais nous nous sommes adaptés à la capacité croissante des modèles en levant des garde-fous et en fournissant davantage de contexte dynamique, que l’agent peut récupérer pendant qu’il travaille. Dans un article précédent, nous avons proposé une analyse approfondie de certaines des techniques qui sous-tendent le contexte dynamique, dont beaucoup ont depuis été adoptées par d’autres agents de codage. Une grande partie de notre travail se concentre désormais sur la mise à disposition de davantage de moyens permettant à l’agent de récupérer dynamiquement du contexte et d’interagir avec le monde.

Avec le contexte dynamique, le modèle peut décider quand faire entrer des informations supplémentaires dans la fenêtre de contexte, comme des conversations passées, des sessions de terminal actives ou des outils pertinents.Avec le contexte dynamique, le modèle peut décider quand faire entrer des informations supplémentaires dans la fenêtre de contexte, comme des conversations passées, des sessions de terminal actives ou des outils pertinents.

Deux façons d'évaluer les changements du harness

Le harness et le modèle déterminent ensemble la qualité de l'agent, mais il est difficile de définir précisément ce que veut dire « bon ». Pour la cerner, nous avons mis en place plusieurs niveaux de mesure.

Nous maintenons des benchmarks publics parallèlement à notre propre suite d'évaluation, CursorBench, qui nous donne une indication rapide et standardisée de la qualité et nous permet de comparer les résultats dans le temps. Mais même les meilleurs benchmarks n'approximent qu'imparfaitement l'utilisation réelle, ce qui signifie que nous passerions à côté de signaux importants si nous nous y fiions entièrement.

Nous menons donc aussi des expériences en ligne dans lesquelles nous déployons côte à côte deux variantes de harness ou plus et les soumettons à des tests A/B sur un usage réel. Nous mesurons la qualité de l'agent dans ces tests à l'aide de diverses métriques. Certaines sont simples, comme la latence, l'efficacité en tokens, le nombre d'appels d'outils et le taux de succès du cache. Elles sont utiles pour dégager des tendances, mais elles ne répondent toujours pas aux questions plus floues — et plus importantes — de savoir si l'agent a réellement bien fait son travail. Nous les mesurons de deux façons.

La première est le "Keep Rate" du code généré par l'agent. Pour un ensemble donné de changements de code proposés par l'agent, nous suivons la part de ces changements qui restent dans la base de code de l'utilisateur après des intervalles de temps fixes. Cela nous permet de comprendre à quel moment les utilisateurs doivent ajuster manuellement le résultat de l'agent, ou itérer et lui demander de corriger certains éléments, ce qui indique que la réponse initiale de l'agent était de moins bonne qualité.

Deuxièmement, nous utilisons un modèle de langage pour lire les réponses de l'utilisateur au résultat initial de l'agent afin de déterminer, sur le plan sémantique, si l'utilisateur était satisfait ou non. Le fait qu'un utilisateur passe à la fonctionnalité suivante est un signal fort que l'agent a fait son travail, tandis qu'un utilisateur qui colle une stack trace est un signal fiable que ce n'est pas le cas.

Il arrive que ces tests en ligne nous amènent à mettre de côté une idée qui semble prometteuse. Dans une expérience, nous avons essayé un modèle plus coûteux pour la synthèse du contexte et avons constaté qu'il n'apportait qu'une amélioration négligeable de la qualité de l'agent, insuffisante pour justifier le surcoût.

Suivi et correction des dégradations

À mesure que nous ajoutons des modèles et des capacités, le harness gagne en complexité, avec davantage d’états possibles, comme n’importe quel logiciel. Cela multiplie aussi les occasions de voir apparaître des bugs, dont beaucoup ne peuvent être détectés qu’à grande échelle.

Les outils de l’agent constituent l’un des principaux terrains où des bugs peuvent apparaître, et les erreurs d’appel d’outil peuvent être extrêmement préjudiciables à une session dans Cursor. Bien que l’agent puisse souvent se corriger de lui-même, les erreurs restent dans le contexte, gaspillent des tokens et provoquent une « érosion du contexte », où l’accumulation d’erreurs dégrade la qualité des décisions ultérieures du modèle.

Il arrive aussi qu’après un échec d’appel d’outil, l’agent puisse se retrouver bloqué ou dérailler complètement. Même si des métriques comme le volume d’appels d’outil et le taux d’erreur ne mesurent pas directement si l’agent a bien travaillé, elles servent d’indicateurs pouvant signaler un problème plus large.

Toute erreur inconnue représente un bug dans le harness, et nous la traitons comme telle. Mais de nombreuses erreurs sont « attendues », par exemple lorsque le modèle propose occasionnellement une modification incorrecte ou tente de lire un fichier qui n’existe pas. Nous classons ces erreurs attendues par cause. InvalidArguments et UnexpectedEnvironment correspondent aux erreurs du modèle et aux contradictions dans la fenêtre de contexte, tandis que ProviderError recense les pannes côté fournisseur pour des outils comme GenerateImage ou WebSearch.

Nous avons plusieurs autres classifications, comme UserAborted et Timeout, qui couvrent ensemble la plupart des erreurs attendues.

Lors d’un sprint ciblé plus tôt cette année, nous avons porté tous les appels d’outil à au moins deux, et souvent trois, “9” de fiabilité.Lors d’un sprint ciblé plus tôt cette année, nous avons porté tous les appels d’outil à au moins deux, et souvent trois, “9” de fiabilité.

Nous définissons des alertes à partir de ces métriques pour détecter les régressions significatives qui atteignent la production. Comme les erreurs inconnues sont toujours des bugs, nous déclenchons une alerte dès que le taux d’erreurs inconnues pour un outil dépasse un seuil fixe. Mais il peut être difficile de déterminer si les erreurs attendues révèlent un bug dans le harness ou correspondent à un comportement attendu.

Par exemple, un timeout sur une recherche grep peut venir d’un problème de performance de l’outil, ou simplement du fait que la base de code est énorme et que le modèle a formulé une requête inefficace. Pour y faire face, nous avons des alertes de détection d’anomalies qui se déclenchent lorsque les erreurs attendues dépassent significativement la référence. Nous calculons des références par outil et par modèle, car différents modèles peuvent rater les appels d’outil à des fréquences différentes.

Nous exécutons également chaque semaine une Automatisation dotée d’une compétence qui apprend au modèle à parcourir nos journaux, à faire remonter les issues nouvelles ou récemment en forte hausse, et à créer ou mettre à jour des tickets dans un backlog avec une investigation. Nous nous appuyons fortement sur les Agents Cloud pour lancer des correctifs sur de nombreux problèmes à la fois, et pouvons même les déclencher directement depuis Linear.

Ce processus fait partie de la manière dont nous concrétisons une « usine logicielle » automatisée pour notre harness d’agents. Au cours d’un sprint ciblé plus tôt cette année, nous avons réduit d’un ordre de grandeur les erreurs inattendues d’appel d’outil.

Personnalisation du harness pour différents modèles

Toutes nos abstractions de harness sont indépendantes du modèle et peuvent être largement personnalisées pour chaque modèle que nous prenons en charge. Par exemple, les modèles d'OpenAI sont entraînés à modifier des fichiers à l'aide d'un format basé sur des patchs, tandis que les modèles d'Anthropic sont entraînés au remplacement de chaînes de caractères. L'un comme l'autre peut utiliser l'un ou l'autre outil, mais lui donner un outil qui ne lui est pas familier consomme des token de raisonnement supplémentaires et entraîne davantage d'erreurs. Dans notre harness, nous fournissons donc à chaque modèle le format d'outil avec lequel il a été entraîné.

Cette personnalisation va très loin et inclut des requêtes personnalisées pour différents fournisseurs, et même pour différentes versions de modèles. Les modèles d'OpenAI ont tendance à suivre les instructions de manière plus littérale et plus précise, tandis que Claude est un peu plus intuitif et plus tolérant face aux instructions imprécises.

Lorsque nous obtenons un accès anticipé à un nouveau modèle avant son lancement, nous partons du harness du modèle existant le plus proche et commençons à itérer. Nous exécutons des évaluations hors ligne pour repérer les points de confusion du modèle, demandons à des membres de notre équipe de l'utiliser et de faire remonter les problèmes, puis ajustons le harness en conséquence. Nous itérons ainsi jusqu'à obtenir une combinaison modèle-harness que nous estimons prête à être livrée.

Une grande partie de ce processus d'ajustement consiste à adapter le harness aux points forts d'un nouveau modèle, mais il nous arrive aussi de rencontrer de véritables particularités de modèle que nous pouvons atténuer avec le harness. Par exemple, nous avons observé chez un modèle l'apparition de ce que nous avons fini par appeler une anxiété de contexte : à mesure que sa fenêtre de contexte se remplissait, il commençait à refuser d'effectuer le travail, en laissant entendre que la tâche semblait trop importante. Nous avons pu atténuer ce comportement grâce à des ajustements de la requête.

Faciliter le changement de modèle en cours de chat

Il est particulièrement difficile de concevoir le harness pour prendre en charge le changement de modèle en cours de conversation, car les différents modèles ont des comportements, des requêtes et des formats d’outil différents.

Lorsqu’un utilisateur change de modèle, Cursor bascule automatiquement vers le harness approprié, avec l’ensemble de requêtes et d’outils personnalisé de ce modèle. Cependant, le modèle doit toujours appliquer ces outils à un historique de conversation produit par un autre modèle, donc hors de la distribution sur laquelle il a été entraîné.

Pour y remédier, nous ajoutons des instructions personnalisées qui indiquent au modèle quand il reprend la main en cours de chat après un autre modèle. Ces instructions l’incitent également à éviter d’appeler des outils qui apparaissent dans l’historique de conversation mais ne font pas partie de son propre ensemble d’outils.

Empêcher les modèles d'appeler des outils qui ne figurent pas dans leur jeu d'outilsEmpêcher les modèles d'appeler des outils qui ne figurent pas dans leur jeu d'outils

Un deuxième défi est que les caches sont spécifiques au fournisseur et au modèle, donc changer entraîne un défaut de cache et un premier tour plus lent et plus coûteux. Nous atténuons cela en résumant la conversation au moment du changement, ce qui fournit au modèle un résumé clair qui réduit la pénalité liée au cache. Mais si l’utilisateur est déjà bien avancé dans une tâche complexe, le résumé peut omettre des détails importants, c’est pourquoi nous recommandons généralement de rester sur un seul modèle pendant toute la durée d’une conversation, sauf si vous avez une bonne raison d’en changer.

Une autre façon de contourner les difficultés liées au changement de modèle en cours de conversation consiste à utiliser un sous-agent, qui démarre avec une nouvelle fenêtre de contexte. Nous avons récemment ajouté au harness la possibilité pour les utilisateurs de demander directement l’exécution d’un sous-agent avec un modèle particulier.

Le harness et l’avenir du développement logiciel

L’avenir de l’ingierie logicielle assistée par l’IA sera multi-agents. Au lieu de confier chaque sous-tâche à un seul agent, le système apprendra à déléguer à des agents et sous-agents spécialisés : l’un pour la planification, un autre pour des modifications rapides, et un troisième pour le débogage, chacun dans le domaine où il excelle.

Pour que cela fonctionne bien, le principal défi est celui du harness. Le système doit savoir quel agent mobiliser, comment formuler la tâche en fonction des points forts de cet agent, et comment intégrer les résultats dans un workflow cohérent. La capacité à orchestrer ce type de coordination reposera sur le harness plutôt que sur un agent unique. Cela signifie que, même si l’ingénierie du harness a toujours été importante pour la réussite des agents, elle ne fera que gagner en importance à l’avenir.

Classé dans : Recherche

Auteurs: Stefan Heule & Jediah Katz