L’Agent nu : ce qui se brise lorsque rien ne protège le modèle
Créez le même agent de voyage non protégé dans le Claude Agent SDK et LangChain Deep Agents, puis observez trois échecs pour identifier quels mécanismes de contrôle manquent.
Sources: Harness Engineering: The Naked Agent: Why Your Framework Hands You a Loop, Not a Harness — I
Un client demande à un agent de réréserver un vol. Le modèle semble très confiant, l’appel à l’outil réussit, et la réponse indique que tout a été géré. Puis quelqu’un remarque que la date indiquée est le 32 mars.
C’est à cet instant que l’illusion se brise. Ce n’était pas l’intelligence qui manquait au modèle. Ce qui a échoué, c’est tout ce qui l’entourait, car aucun composant ne savait quand intervenir. Ce qui suit montre un agent dépourvu de tout : pas de validateurs, pas de mémoire, pas de mécanisme de récupération, pas de filet de sécurité. Voir cet agent échouer révèle où se situent les erreurs des agents en production, et cela montre que l’environnement d’exécution autour d’un modèle de langage est plus important que le modèle lui-même. Ce même agent simplifié est développé à deux reprises, avec le Claude Agent SDK et LangChain Deep Agents, et il échoue ensuite de trois manières concrètes, chacune soulignant un mécanisme de contrôle que doit disposer un agent réel. Le résultat est une méthode permettant de distinguer un problème lié au modèle d’un problème lié à son environnement d’exécution.
L’incident du 32 mars
Imaginez un agent de réservation de voyages qui reçoit le message « 32 mars » d’un utilisateur réel. Le modèle a transmis directement cette chaîne de caractères à l’API des vols. Environ quarante minutes plus tard, après cinquante dollars de frais liés aux appels du modèle, une personne a dû expliquer à un client, à qui l’on avait assuré que tout était réglé, qu’il s’agissait d’une réservation inexistante.
Le modèle n’a rien fabriqué. Il a simplement transmis ce qui lui avait été donné, car le système de contrôle ne lui indiquait pas s’il devait le faire ou non. Cela résume l’affaire. L’intelligence provient du modèle et la discipline du système de contrôle ; donc, lorsque l’agent de production se comporte mal, la faute réside presque toujours dans la couche de discipline. Remplacer le modèle par un plus puissant ou affiner la requête ne fournira pas un contrôle qui n’existe pas, et la création de mécanismes de contrôle commence par comprendre le coût de leur absence. C’est pourquoi le point de départ est minimal : un agent fragile, deux frameworks, pas de système de contrôle.
Preuves que le temps d’exécution domine
Les données de benchmark confirment ce point. Des chercheurs de Princeton ont figé le modèle à GPT-4 Turbo et n’ont modifié que le code d’interface le reliant à la base de code. La proportion de problèmes sur SWE-bench résolus est passée de 3,8 % à 12,47 %, soit environ le triple, tandis que les poids du modèle sont restés inchangés. Cette étude, intitulée « SWE-agent : Agent-Computer Interfaces Enable Automated Software Engineering », a été publiée en 2024 et acceptée lors de NeurIPS la même année. C’est la conception de l’interface qui a permis cette amélioration, et non l’intelligence du modèle.
La discipline sous-jacente porte un nom plus ancien : la « mechanical sympathy », c’est-à-dire l’écriture de logiciels qui fonctionnent en harmonie avec le matériel ou la plateforme sur laquelle ils s’exécutent plutôt que contre eux. Le concept de « harness engineering » étend cette approche à un nouveau substrat, le modèle de langage, avec sa fenêtre de contexte et son budget d’attention. Un plafond est défini par le modèle, et le « harness » décide dans quelle mesure le système parvient à s’en approcher de manière constante.
Le changement de mentalité modifie ce que l’on fait après une erreur :
- Auparavant : après une réponse incorrecte, on ajuste la demande, on ajoute de nombreuses réserves, on essaie un modèle plus puissant en espérant que cela fonctionne
- Aujourd’hui : après une réponse incorrecte, on identifie quel contrôle manque, on le retire de la demande pour le placer pendant l’exécution
Les demandes servent à persuader, tandis que les mécanismes de contrôle assurent le respect des règles.
Le cycle présent dans chaque framework
Tous les frameworks d’agents reposent sur un cycle en cinq étapes. On fournit les outils au modèle, on reçoit une demande d’utilisation de cet outil, on l’exécute, on renvoie le résultat et on reçoit la prochaine action à entreprendre. Les documents d’Anthropic sur l’utilisation des outils, l’API Responses d’OpenAI ainsi que le runtime d’agent de LangChain se réduisent tous au même principe. Les SDK changent, mais la structure reste identique.
La version minimale utilise un SDK de fournisseur brut, deux outils de navigation et aucune protection de sécurité :
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
TOOLS = [
{"name": "search_flights",
"description": "Search flights between two cities for a date.",
"input_schema": {"type": "object", "properties": {
"origin": {"type": "string"}, "destination": {"type": "string"},
"date": {"type": "string", "description": "YYYY-MM-DD"}},
"required": ["origin", "destination", "date"]}},
{"name": "book_flight",
"description": "Book a specific flight.",
"input_schema": {"type": "object", "properties": {
"flight_id": {"type": "string"}, "passenger_name": {"type": "string"}},
"required": ["flight_id", "passenger_name"]}},
]
def run_naked(user_msg: str) -> str:
messages = [{"role": "user", "content": user_msg}]
while True: # ① no iteration cap
resp = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024,
tools=TOOLS, messages=messages,
)
if resp.stop_reason != "tool_use":
return resp.content[0].text
call = next(b for b in resp.content if b.type == "tool_use")
result = dispatch(call.name, call.input)
# ② direct side effect, no check
messages.extend([
# ③ whole history, every turn
{"role": "assistant", "content": resp.content},
{"role": "user", "content": [{"type": "tool_result",
"tool_use_id": call.id, "content": result}]},
])
Lisez-le en gardant à l’esprit trois points de défaillance potentiels :
- La boucle
while True:n’a pas de limite. Un plan défaillant continue d’appeler le modèle jusqu’à épuisement du budget, car aucune itération n’est comptabilisée. dispatchtransmet directement les arguments fournis par le modèle à un effet secondaire sans aucune vérification préalable, ce qui permet à une valeur qui se parse correctement mais qui ne devrait pas exister d’atteindre l’API en production.- Toute l’histoire des messages est prolongée et renvoyée à chaque tour, ce qui gonfle silencieusement le contexte jusqu’à ce que la qualité diminue.
Aucune des propriétés de sécurité souhaitables ne devrait se trouver à l’intérieur de ce cycle. Elles se situent dans du code qui n’a pas encore été écrit. L’adoption d’un framework ne résout pas non plus ces problèmes. Le Claude Agent SDK et Deep Agents offrent un cycle mieux structuré, avec un protocole d’outil adapté et une trajectoire d’exécution claire, mais par défaut ils ne fournissent ni validateurs, ni mémoire sur laquelle on puisse raisonner, ni mécanisme de récupération. Le cycle tel quel est fourni, et c’est au développeur de s’en occuper.
Voici le même agent brut dans chaque framework.
La version Claude Agent SDK
Le SDK gère le cycle à votre place. Les outils sont déclarés à l’aide du décorateur @tool, exposés sous forme de serveur MCP en mémoire, et exécutés via query(). Ce qui manque, c’est tout contrôle : les arguments ne sont vérifiés que contre le schéma, rien n’est mémorisé entre les appels, et il n’y a pas de mécanisme de récupération.
import asyncio
from claude_agent_sdk import (
query, ClaudeAgentOptions, tool,
create_sdk_mcp_server, AssistantMessage, ResultMessage,
)
@tool("search_flights", "Search flights between two cities for a date.",
{"origin": str, "destination": str, "date": str})
async def search_flights(args):
# ① no check that date exists
hits = flights_api.search(**args)
return {"content": [{"type": "text", "text": str(hits)}]}
@tool("book_flight", "Book a specific flight.",
{"flight_id": str, "passenger_name": str})
async def book_flight(args):
# ② destructive, ungated
confirmation = flights_api.book(**args)
return {"content": [{"type": "text", "text": confirmation}]}
server = create_sdk_mcp_server("travel", tools=[search_flights, book_flight])
async def main():
options = ClaudeAgentOptions(
mcp_servers={"travel": server},
allowed_tools=["mcp__travel__search_flights",
"mcp__travel__book_flight"],
)
async for msg in query(prompt="Rebook this customer for March 32nd.",
options=options):
if isinstance(msg, AssistantMessage):
for b in msg.content:
if hasattr(b, "text"):
print(b.text)
elif isinstance(msg, ResultMessage):
print("done:", msg.subtype)
# ③ no state survives this run
Trois éléments se détachent :
search_flightsaccepte tout ce que le modèle propose et qui correspond au schéma, de sorte qu’une date impossible à exister est transmise au corps de l’outil.book_flightest destructeur et n’est protégé par rien. Le décorateur l’intègre dans la boucle sans aucune vérification ni mécanisme d’approbation.- Une fois que
ResultMessageest reçu et quequery()a renvoyé des résultats, la session se termine et rien de cette exécution n’est conservé pour la prochaine appel.
Le SDK fournit une boucle propre. Il ne prend en compte ni les dates incorrectes, ni les doubles facturations, ni tout ce qui mériterait d’être enregistré.
La version Deep Agents
Deep Agents structure le cycle de manière différente. Les outils classiques de LangChain sont passés à create_deep_agent(), qui renvoie un graphe compilé à invoke. Les points communs restent les mêmes : il n’y a pas de validateur, et comme aucun checkpointer n’est fourni, le graphe oublie tout entre les appels.
from langchain.tools import tool
from deepagents import create_deep_agent
@tool
def search_flights(origin: str, destination: str, date: str) -> str:
"""Search flights between two cities for a date (YYYY-MM-DD)."""
return str(flights_api.search(origin, destination, date))
# ① no date check
@tool
def book_flight(flight_id: str, passenger_name: str) -> str:
"""Book a specific flight."""
return flights_api.book(flight_id, passenger_name)
# ② ungated side effect
agent = create_deep_agent(
# ③ the loop, no controls
model="anthropic:claude-sonnet-4-6",
tools=[search_flights, book_flight],
)
result = agent.invoke({"messages": [{"role": "user",
"content": "Rebook this customer for March 32nd."}]})
print(result["messages"][-1].content)
# Ask a follow-up in a second invoke, and it starts from zero: no thread,
# no memory.
Encore trois points :
- Le corps de
search_flightsappelle l’API avec la chaîne de caractères brutedate, et rien ne confirme que la date est valide avant l’envoi de la requête. book_flighteffectue la réservation dès que le modèle la sélectionne, sans aucune étape intermédiaire entre la proposition et l’effet secondaire.create_deep_agentrenvoie un graphe qui exécute simplement la boucle, sans rien de plus. En l’absence de checkpointer, il n’y a ni thread ni mémoire entre les appels.
Trois manières dont un agent non protégé échoue
Deux frameworks ont fourni deux boucles simples mais sans aucune discipline, telle est l’apparence d’un agent non protégé en 2026. Ce n’est pas non plus un exemple simplifié : il s’agit de la forme de dizaines de démos d’agents publiées chaque semaine sur GitHub. Sous un trafic réel, il échoue de trois manières, et chacun de ces échecs correspond à un contrôle ajouté par un système de gestion adéquat.
Entrée impossible. Avec la valeur "32 mars", le modèle fait ce à quoi il est destiné et remplit le champ date d’une chaîne bien formatée mais dénuée de sens. Le schéma ne confirme que le fait que date est une chaîne, sans pouvoir déterminer si ce jour existe réellement. book_flight est alors exécuté sur un itinéraire imaginaire, et le modèle annonce un succès.
La requête est book_flight avec un ID de vol inventé et un nom de passager, et elle renvoie "Réservé."> L’action a été déclenchée, sans que rien dans la boucle ne vérifie s’il fallait vraiment le faire.
book_flight(flight_id=”AC-PHANTOM”, passenger_name=”J. Moffatt”)
# -> “Booked.” The action fired. Nothing in the loop asked whether it should.
La défaillance est due à l’absence d’une frontière d’action claire, sans aucun validateur pour séparer ce que le modèle propose de ce qui se produit réellement. Compléter cette lacune relève des contrats de outils et des validateurs.
Contexte surchargé. La boucle brute envoie à nouveau toute l’histoire à chaque étape. Un long échange de réservations, rempli de résultats de recherche, de textes de politique et de tentatives de réessai, peut dépasser cinquante mille tokens, sans que rien ne le raccourcisse. Ce que la littérature qualifie de dégradation du contexte se produit, et l’agent oublie les étapes qu’il a effectuées correctement précédemment au cours de la même séance. Aucune erreur n’est générée. L’agent se détériore simplement, silencieusement.
Ici, les éléments manquants étaient la mémoire de travail et l’assemblage du contexte, qui relèvent respectivement de la mémoire et de l’état durable, ainsi que de l’assemblage du contexte.
Erreurs masquées. Un outil renvoie des données d’erreur, et le modèle, plus souvent que prévu, rédige un résumé poli qui passe outre l’échec. Une analyse de cas concrets d’incidents en production a décrit ces outils comme des menteurs convaincants : une base de données ne renvoyant aucune ligne est interprétée comme une réponse factuelle plutôt que comme une erreur. Choisir le bon outil n’a jamais été la partie difficile ; ce qui l’est, c’est de distinguer un outil qui fonctionne réellement d’un autre qui imite de manière convaincante le fonctionnement. Aucun mécanisme de boucle de retours n’a été observé dans ces cas, et son mise en place relève de la capacité d’observation et de la détection des écarts.
Considérez-les comme des modèles diagnostiques plutôt que comme une liste d’embarras. Dans les trois cas, le modèle a fait ce que font les modèles, et le système environnant n’a pas réussi à le détecter. Aucun de ces cas ne concerne un modèle insuffisamment intelligent.
Une boucle fonctionnelle pour l’ingénierie des harness
Une démarche en quatre étapes se répète lors de la création de tout mécanisme de contrôle :
- Introduire la fonction de gestion et l’échec qu’elle contrôle.
- La mettre en œuvre dans les deux frameworks sous forme de snippet ciblé.
- Déclencher l’échec pour prouver que le mécanisme de contrôle supporte une charge.
- Examiner comment la fonction de gestion réagit.
Les sections ci-dessus ont appliqué cette démarche trois fois sans la nommer : identifier une couture non protégée, déclencher l’échec qu’elle provoque, et désigner le mécanisme de contrôle auquel elle fait référence. Ensuite, on ferme progressivement les coutures une par une.
Essayez-le vous-même
On peut ressentir ce problème sans lire davantage. Dans un éditeur, quelques expériences rapides le montrent :
- Diriger l’agent non protégé dans l’un ou l’autre framework vers une instruction visant à réréserver le client pour le 32 mars, et observer cette date impossible se retrouver dans la fonction
book_flight - Limitez la boucle brute à cinq itérations. C’est le premier contrôle de harness réellement écrit, et il fait partie du temps de exécution plutôt que de l’interface utilisateur
- Faites en sorte que
book_flightretourne{"is_error": true, ...}et vérifiez si le résumé final prend en compte l’échec ou l’ignore - Réparez une version de
claude-agent-sdkainsi qu’une version dedeepagentsoulangchainavant de commencer, car les deux API changent de version, et interprétez les noms de modèles (claude-sonnet-4-6,anthropic:claude-sonnet-4-6) comme des références à ce qui est actuellement utilisé
Chaque framework fournit un boucle propre et bien conçu, mais aucun ne fournit de système de gestion des agents. C’est une caractéristique de la situation et non un défaut des outils ; c’est là que réside le véritable travail. Claude Code, OpenCode, Codex et Deep Agents comptent tous parmi les systèmes de gestion des agents, et un développeur qui a du mal avec eux diffère de celui qui obtient des résultats fiables en concevant les mécanismes de contrôle que le boucle omet. Le modèle a été la partie facile depuis un certain temps déjà. La fiabilité naît d’une discipline construite autour de lui, à force d’échecs, et le premier élément naturel est la frontière d’action, la première règle permettant de quitter le prompt pour fonctionner en temps réel.