Pendant des mois, notre rotation d'astreinte avait un récidiviste : l'agent qui ne voulait tout simplement pas s'arrêter. Il replanifiait, requêtait à nouveau, se reposait la même sous-question en la formulant légèrement différemment, accumulant de la latence jusqu'à ce qu'un timeout finisse par le tuer. La trace atterrissait dans nos tableaux de bord comme un mauvais ECG, une ligne plate de tentatives suivie d'une falaise.
Nous avons d'abord blâmé la qualité des prompts. Ensuite la fiabilité des outils. Puis le modèle lui-même. Tout cela était réel, mais rien de tout cela n'était la cause profonde. La cause profonde était architecturale : notre planificateur n'avait aucun mécanisme raisonné pour cesser d'essayer.
Le problème de boucle
Une boucle de planification survient quand les croyances d'un agent sur le monde ne convergent pas. Il émet une action, observe un résultat qu'il juge ambigu, met à jour son état interne d'une façon qui réactive la même branche de raisonnement, et recommence. Vu de l'extérieur, ça ressemble à du patinage. Vu de l'intérieur, si l'on peut dire, ça ressemble à de la diligence.
Les mesures correctives habituelles (nombre d'étapes maximal, timeouts, déduplication des appels d'outils) traitent le symptôme. Elles coupent la boucle après qu'elle tourne déjà. Ce que nous voulions, c'était un mécanisme qui empêche la boucle de se former en premier lieu, ou du moins qui remonte une réponse dégradée propre quand la confiance n'est véritablement pas récupérable.
Le budget de confiance
Le budget de confiance est une valeur de première classe dans le contexte d'exécution de notre planificateur. Pensez-y comme une monnaie : chaque étape de raisonnement dépense du budget, et chaque information nouvelle et non redondante en reconstitue. Quand le solde atteint zéro, le planificateur ne retente pas. Il escalade vers un repli élégant.
L'asymétrie dépense/gain est le choix de conception central. Les observations redondantes, les réponses d'outils dont le contenu est sémantiquement équivalent à quelque chose déjà dans le contexte, ne rapportent aucun budget. Les observations nouvelles à forte information en rapportent proportionnellement à la réduction d'incertitude qu'elles apportent. Les étapes qui ne font que reformater ou réexaminer le contexte existant dépensent au taux normal sans rien rapporter.
Esquisse d'implémentation
Le primitif lui-même est petit. Une trentaine de lignes de Python logées dans notre package planificateur, câblées dans le contexte d'exécution qui traverse déjà chaque étape. Deux unités d'état, trois méthodes, un point de décision dans Planner.step.
from dataclasses import dataclass, field @dataclass class ConfidenceBudget: initial: float = 1.0 balance: float = field(default_factory=lambda: 1.0) step_cost: float = 0.08 # flat spend per planning step novelty_gain: float = 0.12 # max earn per novel observation def spend(self) -> None: self.balance = max(0.0, self.balance - self.step_cost) def earn(self, novelty_score: float) -> None: # novelty_score: 0.0 = fully redundant, 1.0 = fully novel gain = novelty_score * self.novelty_gain self.balance = min(self.initial, self.balance + gain) def is_depleted(self) -> bool: return self.balance <= 0.05 # 5% threshold, graceful exit class Planner: def step(self, ctx: PlanContext) -> PlanResult: self.budget.spend() if self.budget.is_depleted(): return self.best_effort_reply(ctx) observation = self.execute_action(ctx) novelty = self.novelty_scorer.score(observation, ctx.seen) self.budget.earn(novelty) return PlanResult(observation=observation, budget=self.budget)
Le novelty scorer mérite son propre billet. Nous utilisons une comparaison d'embeddings légère sur une fenêtre glissante d'observations récentes, avec un seuil de similarité calibré sur notre surface d'outils. Les valeurs exactes ci-dessus (0.08 de dépense, 0.12 de gain) sont empiriques et seront différentes pour votre charge de travail. Le ratio compte plus que les valeurs absolues.
Ce que « repli élégant » signifie concrètement
C'est la partie qui nous a pris le plus de temps à mettre au point. Un planificateur qui plante simplement à l'épuisement du budget ne vaut pas mieux qu'un timeout. Le repli doit être une vraie réponse, incomplète, prudente, clairement partielle, mais utile. Nous avons retenu trois modes.
- Partiel confiant. Le planificateur sait quelque chose. Il rapporte ce qu'il a trouvé, nomme explicitement ce qu'il n'a pas pu vérifier, et s'arrête. C'est le repli le plus fréquent et souvent suffisant pour l'utilisateur.
- Incertitude structurée. Le planificateur a des hypothèses concurrentes sans moyen de les trancher. Il les expose toutes avec une pondération de confiance approximative. Les systèmes en aval peuvent utiliser ce signal plutôt que de se retrouver face à une impasse.
- Abandon ferme. Le planificateur n'a véritablement rien. Il retourne une erreur structurée avec la trace partielle, qui escalade soit vers un humain, soit vers une stratégie de planification différente. Nous maintenons ce cas rare, sous 2 % des invocations en production.
Résultats
| métrique | avant | après | signal |
|---|---|---|---|
| latence p50 | 1,4 s | 1,2 s | −14 % |
| latence p99 | 42 s | 26 s | −38 % |
| taux de timeout | 3,1 % | 0,28 % | −91 % |
| part des réponses partielles | 0 % | 6,4 % | nouveau |
| taux d'abandon ferme | s.o. | 1,8 % | cible < 2 % |
L'amélioration de la latence p99 nous a surpris. Nous pensions que la p50 en serait la principale bénéficiaire. En pratique, la p99 s'est le plus améliorée parce que les boucles incontrôlées étaient concentrées dans la queue. Une fois qu'elles se résolvaient rapidement en replis, toute la queue de la distribution de latence s'est comprimée.
Le chiffre de satisfaction sur les réponses partielles mérite d'être contextualisé : nous le comparions au comportement précédent où un planificateur épuisé retournait une erreur générique. Une réponse partielle bien formulée s'avère considérablement plus utile qu'un « quelque chose a mal tourné ». Les utilisateurs peuvent agir sur une information partielle. Ils ne peuvent pas agir sur un 500.
Ce que nous ferions différemment
Nous avons initialisé les budgets de manière uniforme. Avec le recul, la complexité de la requête devrait informer le solde de départ. Une simple recherche reçoit un budget modeste ; une tâche de recherche ouverte devrait démarrer avec plus de marge avant que la courbe dépense-nouveauté ne s'enclenche. Nous construisons cela maintenant et nous attendons à ce que ça pousse les scores de satisfaction de quelques points supplémentaires.
Nous n'avons pas non plus pris en compte les courbes de nouveauté propres à chaque outil. Certains outils sont intrinsèquement plus bruités : leurs réponses varient en surface tout en encodant la même information. Le novelty scorer pénalisait ces outils injustement jusqu'à ce que nous ajoutions une calibration par outil. Si vous implémentez ceci, prévoyez du temps pour ce réglage.
La leçon plus profonde est que le comportement de terminaison est une décision produit, pas seulement une décision d'ingénierie. La façon dont votre agent échoue, ou refuse d'échouer élégamment, fait partie de l'expérience utilisateur. Construisez-le explicitement.
Si vous souhaitez que nous examinions comment votre agent se termine, le formulaire de contact est la voie la plus rapide. Nous offrons des revues gratuites de 30 minutes pour les systèmes en production.