Paramètres

Langue

Pourquoi votre AI Agent perd sans cesse la mémoire (et comment nous avons résolu le problème)

L
LemonData
·28 février 2026·31 vues
#agents IA#mémoire#solution de repli#architecture#LemonClaw
Pourquoi votre AI Agent perd sans cesse la mémoire (et comment nous avons résolu le problème)

Pourquoi votre agent AI perd sans cesse sa mémoire (et comment nous avons corrigé cela)

Votre agent AI vient de terminer une conversation de 30 minutes avec un utilisateur. Ils ont discuté des exigences du projet, partagé des préférences, pris des décisions. Ensuite, 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 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.

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 cela fonctionne, tant mieux. Sinon, la mémoire est perdue.

OpenClaw, le framework d'agent open-source le plus populaire, 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 enregistre 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 modèle, une 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 n'a 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 n'importe quel modèle, un framework effectuant 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 exponential backoff. Nous l'avions. Cela gère bien les erreurs HTTP transitoires :

# Retry loop: 1s → 2s → 4s backoff
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 erreurs 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 peut pas effectuer de tool calling. Certains modèles, particulièrement les plus petits tournant sur des moteurs d'inference rapides, échouent occasionnellement à générer des appels de fonction valides sur des prompts complexes. L'API renvoie un 200 avec une ServiceUnavailableError encapsulée dans une MidStreamFallbackError. Votre logique de retry voit une exception, réessaie le même modèle, 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 métier sait que ce n'est pas le cas. Aucun nombre 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 métier (Business Layer)
    │               "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 de transport (Transport Layer)
    │                    Erreurs HTTP → exponential backoff
    │                    Tous les 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 métier. 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 "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, oui.

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

Chaque modèle de notre catalogue possède un champ fallback optionnel pointant vers le prochain modèle à 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",
               "Recommandé", fallback="claude-sonnet-4-5"),
    ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
               "Tool calling stable", 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 permet de garder les modèles internes hors de la commande /model destinée à l'utilisateur, tout en participant aux chaînes de fallback. Les utilisateurs voient 16 modèles qu'ils peuvent alterner. Le système en utilise 19. Les trois modèles cachés existent uniquement pour des tâches de fond 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 seule ligne. Pas de fichiers de config à synchroniser, pas de variables d'environnement à mettre à jour, pas de scripts de déploiement à modifier.

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

Le moteur de retry parcourt la chaîne de fallback en utilisant un ensemble d'éléments visités pour éviter les boucles infinies :

async def _chat_with_retry(self, kwargs, original_model):
    # Phase 1 : Exponential backoff 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="Clé API invalide.", 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 l'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 indisponible.", finish_reason="error")

L'ensemble 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 importante. 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 complètement. Ils supposent que tous les modèles parlent le même protocole. En production, avec 19 modèles répartis sur 4 formats d'API différents, cette supposition s'effondre immédiatement.

Couche Métier : Vérification de l'Appel de Tool

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 que 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 de la chaîne.

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

Pourquoi Groq pour la Consolidation

La consolidation de la mémoire est une tâche de fond. 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 utilisez également Claude Sonnet pour la consolidation de la mémoire. C'est 3 $/M de tokens en entrée et plus de 8 secondes par consolidation, pour une tâche qui produit un résultat qu'aucun humain ne lira 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 Entrée Coût Sortie
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 principal (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 pour cela que le fallback à double couche existe. Lorsque 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 gère la tâche avec une fiabilité de tool calling proche de 100 %.

En production, nous constatons que le modèle primaire réussit environ 85 % du temps. La chaîne atteint gpt-4.1-mini dans moins de 2 % des consolidations. Taux d'échec total : 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 à couche unique uniquement) :

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 de transport a capturé le premier échec et a basculé. Mais qwen3-32b a renvoyé du texte sans appeler le tool. Le fallback à couche unique n'a pas pu gérer cela. C'est exactement le scénario où tous les autres frameworks perdraient silencieusement la mémoire.

Second 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, 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 tout ce qui s'est passé en arrière-plan.

L'écart 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 l'avertissement, perd la mémoire Fallback double couche, jusqu'à 5 modèles
Fallback Transport Retry 3x sur le même modèle Fallback chaîné sur différents modèles
Fallback Logique Métier Aucun Vérification 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) Tronquer les vieux messages après consolidation
Recherche 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 Non nécessaire (pas de chaînes) Ensemble visited pour éviter les boucles A→B→A
Résolution 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'incidents d'autres frameworks. Le fallback à double couche, le catalogue de modèles masqués, la résolution de gateway par modèle, le fallback déclenché par timeout : rien de tout cela n'existe dans OpenClaw, 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 que vous avez demandé. Les opérations critiques pour le métier nécessitent leurs propres critères de succès et leur propre logique de fallback.

Les petits modèles échouent différemment des grands modèles. Les grands modèles (GPT-4.1, Claude Sonnet) appellent presque toujours les tools lorsqu'on le leur demande. Les petits modèles sur des moteurs d'inference 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 vous pouvez 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 chaque modèle. La session réelle de 60 messages avec historique d'appels de tools, horodatages 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 les données de test propres ne révéleront jamais.


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 inclus 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 fournit un accès unifié à OpenAI, Anthropic, Google, DeepSeek, Groq, et plus encore.

Share: