Paramètres

Langue

Pourquoi votre AI Agent n'arrête pas de perdre sa mémoire

L
LemonData
·5 mars 2026·740 vues
Pourquoi votre AI Agent n'arrête pas de perdre sa mémoire

Votre agent AI vient de passer 30 minutes à discuter avec un utilisateur. Ils ont discuté des exigences du projet, partagé des préférences, pris des décisions. Puis l'utilisateur tape /new pour démarrer une nouvelle session.

L'agent tente de consolider cette conversation dans sa mémoire à long terme. L'appel LLM échoue. Rate limit. Timeout. Ou alors le modèle renvoie du texte au lieu d'appeler le tool requis.

La mémoire a disparu. Trente minutes de contexte, évaporées.

Cela arrive plus souvent qu'on ne le pense. Nous l'avons suivi sur nos instances LemonClaw : la consolidation de la mémoire affichait un taux d'échec d'environ 15 % sur n'importe quel modèle unique. Pour une fonctionnalité censée être une infrastructure invisible, c'est inacceptable.

Si vous construisez l'interface produit globale plutôt que seulement le sous-système de mémoire, consultez également le guide du chatbot à clé unique et le guide LemonClaw auto-hébergé. La durabilité de la mémoire n'a d'importance que lorsque l'agent vit réellement au sein d'une application utilisable.

Comment les autres frameworks gèrent cela (ils ne le font pas)

La plupart des frameworks d'agents AI traitent la consolidation de la mémoire comme un simple appel LLM. Si ça marche, tant mieux. Sinon, la mémoire est perdue.

Le framework prédécesseur de LemonClaw utilise le même modèle pour la consolidation que pour la conversation. Un appel Claude Sonnet qui coûte 0,003 $ et prend plus de 8 secondes, juste pour résumer un chat que l'utilisateur ne verra jamais. Lorsque cet appel échoue (rate limit, timeout, erreur de modèle), le framework logue un avertissement et passe à autre chose. Le contexte de l'utilisateur est perdu.

nanobot, un autre framework populaire, possède la même architecture. Un seul modèle, une seule tentative, pas de fallback. La fonction de consolidation n'a même pas de timeout. Un upstream lent (les erreurs Cloudflare 524 sont courantes) bloque toute la session jusqu'à ce que la connexion soit interrompue.

Aucun de ces frameworks ne sépare la consolidation du modèle principal. Aucun ne possède de logique de fallback pour les opérations de mémoire. Aucun ne fait la distinction entre "l'appel API a échoué" et "l'appel API a réussi mais le modèle n'a pas fait ce que nous avons demandé".

Ce ne sont pas des cas marginaux. Avec des taux d'échec de 15 % sur un modèle unique, un framework exécutant 100 consolidations par jour perd la mémoire sur 15 d'entre elles. Sur une semaine, cela représente 105 conversations où l'agent oublie tout.

Le problème est plus profond qu'une simple logique de retry

La solution évidente est le retry avec backoff exponentiel. Nous l'avions. Cela gère bien les erreurs HTTP transitoires :

# Boucle de retry : backoff 1s → 2s → 4s
for attempt in range(3):
    try:
        response = await acompletion(**kwargs)
        return await self._collect_stream(response)
    except (RateLimitError, APIConnectionError) as e:
        await asyncio.sleep(RETRY_DELAYS[attempt])

Cela capture les 429 et les micro-coupures réseau. Mais deux modes d'échec passent entre les mailles du filet :

Mode d'échec 1 : le modèle ne parvient pas à effectuer le tool calling. Certains modèles, en particulier les plus petits fonctionnant sur des moteurs d'inférence rapides, échouent occasionnellement à générer des appels de fonction valides sur des prompts complexes. L'API renvoie un 200 avec une ServiceUnavailableError enveloppée dans une MidStreamFallbackError. Votre logique de retry voit une exception, réessaie le même modèle, et obtient la même erreur.

Mode d'échec 2 : le modèle "réussit" mais n'appelle pas le tool. Le LLM renvoie une réponse parfaitement valide. HTTP 200. Aucune erreur. Mais au lieu d'appeler save_memory avec des données structurées, il écrit un résumé en texte brut. Votre moteur de retry considère cela comme un succès. La fonction de consolidation vérifie les appels de tool, n'en trouve aucun, et abandonne.

Le second mode d'échec est le plus insidieux. La couche de transport pense que tout a fonctionné. La couche business sait que non. Aucun volume de retries au niveau HTTP ne corrigera un modèle qui ne comprend pas votre schéma de tool.

Architecture de Fallback à Double Couche

Nous avons résolu ce problème avec deux boucles de fallback indépendantes opérant à différents niveaux :

L'utilisateur envoie /new
    │
    ▼
consolidate() ─── Fallback de la Couche Business
    │               "Le modèle a-t-il appelé save_memory ?"
    │               Non → essayer le modèle suivant dans la chaîne
    │
    ▼
_chat_with_retry() ─── Fallback de la Couche Transport
    │                    Erreurs HTTP → backoff exponentiel
    │                    Retries épuisés → parcourir la chaîne de fallback
    │
    ▼
Chaîne de fallback MODEL_MAP :
    llama-3.3-70b  ─$0.59/M─→  qwen3-32b  ─$0.29/M─→  llama-4-scout  ─$0.11/M─→  gpt-4.1-mini  ─→  claude-haiku
    (394 TPS)                   (662 TPS)                (594 TPS)                  (fiable)        (dernier recours)

La couche 1 gère les échecs de transport. La couche 2 gère les échecs de logique business. La chaîne de fallback est partagée entre les deux couches, définie une seule fois dans un catalogue central.

C'est une approche fondamentalement différente du simple retry sur le même modèle. Lorsqu'un modèle échoue à appeler un tool, le réessayer avec le même prompt aide rarement. Passer à un modèle différent avec des poids différents et un comportement de tool calling différent est bien plus efficace.

Le Catalogue de Modèles : Une Source Unique de Vérité

Chaque modèle de notre catalogue possède un champ optionnel fallback pointant vers le modèle suivant à essayer :

@dataclass(frozen=True)
class ModelEntry:
    id: str
    label: str
    tier: str
    description: str
    fallback: str | None = None
    hidden: bool = False  # Masqué de la liste /model côté utilisateur

MODEL_CATALOG = [
    # Modèles visibles par l'utilisateur (16 modèles au choix)
    ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
               "Recommended", fallback="claude-sonnet-4-5"),
    ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
               "Stable tool calling", fallback="claude-haiku-4-5"),

    # Modèles de consolidation masqués (usage interne uniquement)
    ModelEntry("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", "economy",
               "394 TPS", fallback="qwen3-32b", hidden=True),
    ModelEntry("qwen3-32b", "Qwen3 32B (Groq)", "economy",
               "662 TPS", fallback="llama-4-scout-17b-16e-instruct", hidden=True),
    # ...
]

Le flag hidden=True maintient les modèles internes hors de la commande /model destinée aux utilisateurs, tout en participant aux chaînes de fallback. Les utilisateurs voient 16 modèles. Le système en utilise 19. Les trois modèles cachés existent uniquement pour les tâches d'arrière-plan comme la consolidation de la mémoire, où la vitesse et le coût importent plus que la qualité conversationnelle.

Ce catalogue est la source unique de vérité pour tout le routage des modèles. Ajouter un nouveau modèle à la chaîne de fallback revient à ajouter une ligne. Pas de fichiers de config à synchroniser, pas de variables d'environnement à mettre à jour, pas de scripts de déploiement à modifier.

Couche Transport : Fallback en Chaîne avec Détection de Cycle

Le moteur de retry parcourt la chaîne de fallback en utilisant un set visited pour éviter les boucles infinies :

async def _chat_with_retry(self, kwargs, original_model):
    # Phase 1 : Backoff exponentiel sur le modèle primaire
    for attempt in range(3):
        try:
            response = await acompletion(**kwargs)
            return await self._collect_stream(response)
        except (RateLimitError, APIConnectionError, APIError) as e:
            await asyncio.sleep(RETRY_DELAYS[attempt])
        except AuthenticationError:
            return LLMResponse(content="API key invalid.", finish_reason="error")

    # Phase 2 : Parcourir la chaîne de fallback
    visited = {original_model}
    current = original_model
    while True:
        entry = MODEL_MAP.get(current)
        if not entry or not entry.fallback or entry.fallback in visited:
            break
        current = entry.fallback
        visited.add(current)

        # Résoudre la gateway correcte pour ce modèle
        gw = self._resolve_gateway_for_model(current)
        resolved = self._resolve_model(current, gateway=gw)
        fb_kwargs = {**kwargs, "model": resolved}

        # Fixer api_base pour le protocole du modèle cible
        if gw and gw.default_api_base:
            fb_kwargs["api_base"] = gw.default_api_base

        try:
            response = await acompletion(**fb_kwargs)
            return await self._collect_stream(response)
        except Exception:
            continue  # Essayer le suivant dans la chaîne

    return LLMResponse(content="Service unavailable.", finish_reason="error")

Le set visited est critique. Sans lui, une chaîne comme A→B→A bouclerait indéfiniment. Avec lui, le moteur essaie chaque modèle exactement une fois.

La résolution de la gateway est également cruciale. Différents modèles nécessitent différents formats d'API. Les modèles Claude passent par une gateway au format Anthropic (sans suffixe /v1). Les modèles GPT passent par une gateway compatible OpenAI (avec /v1). Les modèles Groq utilisent encore un autre endpoint. Le moteur de fallback résout la gateway correcte pour chaque modèle de la chaîne, évitant les erreurs de protocole comme l'envoi de requêtes Anthropic à un endpoint OpenAI.

C'est un détail que la plupart des frameworks ignorent. Ils supposent que tous les modèles parlent le même protocole. En production, avec 19 modèles sur 4 formats d'API différents, cette hypothèse s'effondre immédiatement.

Couche Business : Vérification du Tool Call

La fonction de consolidation ajoute sa propre boucle de fallback par-dessus :

async def consolidate(self, session, provider, model, **kwargs):
    visited = set()
    current_model = model

    while current_model and current_model not in visited and len(visited) <= 3:
        visited.add(current_model)

        response = await asyncio.wait_for(
            provider.chat(messages=messages, tools=SAVE_MEMORY_TOOL, model=current_model),
            timeout=30,
        )

        if response.has_tool_calls:
            # Succès : extraire et sauvegarder la mémoire
            args = response.tool_calls[0].arguments
            self.write_long_term(args["memory_update"])
            self.append_history(args["history_entry"])
            return True

        # Le modèle n'a pas appelé le tool — essayer le suivant dans la chaîne
        entry = MODEL_MAP.get(current_model)
        next_model = entry.fallback if entry else None
        if next_model and next_model not in visited:
            current_model = next_model
            continue

        return False  # Plus de fallbacks disponibles

    return False

Cela capture le cas où _chat_with_retry renvoie une réponse réussie (HTTP 200, contenu valide) mais où le modèle n'a pas utilisé le tool. La fonction de consolidation vérifie has_tool_calls, et s'il manque, passe au modèle suivant.

Le wrapper de timeout (asyncio.wait_for) déclenche également le fallback. Si un modèle prend plus de 30 secondes (fréquent avec les erreurs Cloudflare 524 sur des upstreams lents), la fonction capture TimeoutError et essaie le modèle suivant au lieu de bloquer indéfiniment la session de l'utilisateur.

Pourquoi Groq pour la consolidation

La consolidation de la mémoire est une tâche d'arrière-plan. L'utilisateur ne voit pas le résultat. Il a juste besoin que cela fonctionne. Cela en fait un candidat parfait pour des modèles rapides et bon marché.

La plupart des frameworks utilisent le même modèle coûteux pour tout. Si vous utilisez Claude Sonnet pour la conversation, vous l'utilisez aussi pour la consolidation. C'est 3 $/M de tokens d'input et plus de 8 secondes par consolidation, pour une tâche qui produit un résultat qu'aucun humain ne lit jamais.

Nous avons entièrement découplé la consolidation du modèle de conversation. La conversation utilise le modèle choisi par l'utilisateur. La consolidation utilise une chaîne dédiée de modèles hébergés sur Groq :

Modèle Vitesse Coût Input Coût Output
llama-3.3-70b-versatile 394 TPS 0,59 $/M 0,79 $/M
qwen3-32b 662 TPS 0,29 $/M 0,59 $/M
llama-4-scout-17b-16e 594 TPS 0,11 $/M 0,34 $/M
gpt-4.1-mini (précédent) ~150 TPS 0,40 $/M 1,60 $/M

Le modèle primaire (llama-3.3-70b) consolide une session de 60 messages en environ 5 secondes. Le précédent par défaut (gpt-4.1-mini) prenait plus de 8 secondes. Le coût par consolidation est passé d'environ 0,003 $ à environ 0,001 $.

Le compromis : les modèles Groq ont un tool calling moins fiable sur des prompts complexes. C'est précisément pourquoi le fallback à double couche existe. Quand llama-3.3-70b échoue à appeler le tool, qwen3-32b prend le relais. Si cela échoue aussi, llama-4-scout essaie. Si les trois modèles Groq échouent, gpt-4.1-mini s'en charge avec une fiabilité de tool calling proche de 100 %.

En production, nous voyons le modèle primaire réussir environ 85 % du temps. La chaîne atteint gpt-4.1-mini dans moins de 2 % des consolidations. Le taux d'échec total est pratiquement nul.

Résultats en Production

Nous avons déployé cela sur deux instances LemonClaw et testé avec de vraies conversations Telegram.

Premier déploiement (fallback à une seule couche) :

Memory consolidation (archive_all): 56 messages
llama-3.3-70b-versatile → "Failed to call a function"
Falling back → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "Memory archival failed, session not cleared."

La couche transport a capturé le premier échec et a basculé. Mais qwen3-32b a renvoyé du texte sans appeler le tool. Le fallback à couche unique ne pouvait pas gérer cela. C'est exactement le scénario où tout autre framework perdrait silencieusement la mémoire.

Deuxième déploiement (fallback à double couche) :

Memory consolidation (archive_all): 60 messages
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining

Même modèle, même volume de messages. Cette fois, cela a fonctionné du premier coup. La nature intermittente de l'échec du tool calling est précisément la raison pour laquelle vous avez besoin d'une chaîne de fallback plutôt que d'un seul modèle de secours.

Lorsque le modèle primaire échoue réellement, la chaîne le rattrape :

llama-3.3-70b → tool call failed
→ consolidate() fallback → qwen3-32b
→ qwen3-32b didn't call tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout didn't call tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini called save_memory ✓
Memory consolidation done

Quatre modèles essayés, mémoire sauvegardée. L'utilisateur voit "Nouvelle session démarrée" et n'a aucune idée de ce qui s'est passé en coulisses.

Le fossé architectural

Le système de mémoire de LemonClaw par rapport aux alternatives, fonctionnalité par fonctionnalité :

Capacité Framework d'Agent AI Typique LemonClaw
Modèle de consolidation Même que la conversation (cher, lent) Chaîne de modèles indépendante, accélérée par Groq
Gestion des échecs Log d'avertissement, perte de mémoire Fallback double couche, profondeur de 5 modèles
Fallback transport Retry 3x sur le même modèle Fallback en chaîne sur différents modèles
Fallback logique business Aucun Vérification du tool call + changement de modèle
Protection timeout Aucune (Cloudflare 524 bloque la session) asyncio.wait_for(timeout=30) + fallback
Troncation de session Aucune (le contexte croît indéfiniment) Troncation des vieux messages après consolidation
Recherche d'historique Aucune Fenêtre glissante HISTORY.md, searchable via grep
Modèles internes Non supportés hidden=True pour les modèles système uniquement
Prévention de cycle Inutile (pas de chaînes) Set visited empêchant les boucles A→B→A
Résolution de gateway Format d'API unique supposé Gateway par modèle avec détection de protocole

Chaque ligne de ce tableau représente un échec de production que nous avons soit vécu nous-mêmes, soit observé dans les trackers d'issues d'autres frameworks. Le fallback double couche, le catalogue de modèles cachés, la résolution de gateway par modèle, le fallback déclenché par timeout : rien de tout cela n'existe dans nanobot ou tout autre framework d'agent open-source que nous avons examiné.

Ce que nous avons appris

"Requête réussie" n'est pas "tâche réussie". Les moteurs de retry génériques opèrent au niveau HTTP. Ils ne peuvent pas savoir qu'une réponse 200 avec un JSON valide est en réalité un échec parce que le modèle n'a pas utilisé le tool demandé. Les opérations critiques pour le business ont besoin de leurs propres critères de succès et de leur propre logique de fallback.

Les petits modèles échouent différemment des grands. Les grands modèles (GPT-4.1, Claude Sonnet) appellent presque toujours les tools quand on leur demande. Les petits modèles sur des moteurs d'inférence rapides génèrent parfois des réponses d'apparence valide qui ignorent totalement le schéma du tool. Ce n'est pas un bug que l'on peut corriger avec du prompt engineering. C'est un écart de capacité qui nécessite une atténuation architecturale.

Testez avec des données de production, pas des données synthétiques. Notre test initial avec 6 messages synthétiques passait sur tous les modèles. La session réelle de 60 messages avec historique d'appels de tools, timestamps et langues mixtes a échoué sur deux des trois modèles Groq. La complexité des données réelles expose des modes d'échec que des données de test propres ne révèleront jamais.

C'est aussi pourquoi le guide sur le rate limiting des API AI est important ici. Le système de mémoire n'a pas seulement besoin d'un "meilleur modèle". Il a besoin d'une politique de transport, d'une vérification de succès de la logique business et d'une échelle de fallback qui ne s'effondre pas lors d'une défaillance ordinaire d'un fournisseur.


LemonClaw est un framework d'agent AI open-source avec routage multi-modèle intégré, mémoire persistante et plus de 10 intégrations de plateformes de chat. L'intégralité du système de fallback à double couche décrit ici est incluse dans la version open-source. Lancez-le sur votre propre serveur : github.com/hedging8563/lemonclaw

Besoin de plus de 300 modèles AI via une seule clé API ? lemondata.cc offre un accès unifié à OpenAI, Anthropic, Google, DeepSeek, Groq, et bien d'autres.

Share: