Configurações

Idioma

Por que seu agente de IA continua perdendo a memória

L
LemonData
·5 de março de 2026·750 visualizações
Por que seu agente de IA continua perdendo a memória

Seu agente de IA acabou de ter uma conversa de 30 minutos com um usuário. Eles discutiram requisitos de projeto, compartilharam preferências, tomaram decisões. Então, o usuário digita /new para iniciar uma nova sessão.

O agente tenta consolidar essa conversa na memória de longo prazo. A chamada do LLM falha. Rate limit. Timeout. Ou o modelo retorna texto em vez de chamar a tool necessária.

A memória se foi. Trinta minutos de contexto, evaporados.

Isso acontece com mais frequência do que você imagina. Rastreamos isso em nossas instâncias do LemonClaw: a consolidação de memória teve uma taxa de falha de ~15% em qualquer modelo individual. Para um recurso que deveria ser uma infraestrutura invisível, isso é inaceitável.

Se você está construindo a interface do produto ao redor, em vez de apenas o subsistema de memória, combine esta página com o guia de chatbot de uma única chave e o guia do LemonClaw auto-hospedado. A durabilidade da memória só importa quando o agente realmente vive dentro de uma aplicação utilizável.

Como outros frameworks lidam com isso (Eles não lidam)

A maioria dos frameworks de agentes de IA trata a consolidação de memória como uma simples chamada de LLM. Se funcionar, ótimo. Se não, a memória é perdida.

O framework predecessor do LemonClaw usa o mesmo modelo para consolidação e para a conversa. Uma chamada do Claude Sonnet que custa $0.003 e leva mais de 8 segundos, apenas para resumir um chat que o usuário nunca verá. Quando essa chamada falha (rate limit, timeout, erro do modelo), o framework registra um aviso e segue em frente. O contexto do usuário desaparece.

O nanobot, outro framework popular, tem a mesma arquitetura. Um modelo, uma tentativa, sem fallback. A função de consolidação nem sequer tem um timeout. Um upstream lento (erros Cloudflare 524 são comuns) bloqueia toda a sessão até que a conexão caia.

Nenhum dos frameworks separa a consolidação do modelo principal. Nenhum possui lógica de fallback para operações de memória. Nenhum distingue entre "a chamada de API falhou" e "a chamada de API teve sucesso, mas o modelo não fez o que pedimos".

Estes não são casos isolados. Com taxas de falha de 15% em qualquer modelo individual, um framework que executa 100 consolidações por dia perde a memória em 15 delas. Ao longo de uma semana, são 105 conversas onde o agente esquece tudo.

O problema é mais profundo do que a lógica de retry

A correção óbvia é o retry com exponential backoff. Nós tínhamos isso. Ele lida bem com erros HTTP transitórios:

# Loop de retry: backoff de 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])

Isso captura 429s e oscilações de rede. Mas dois modos de falha passam despercebidos:

Modo de falha 1: o modelo não consegue realizar o tool calling. Alguns modelos, especialmente os menores rodando em motores de inferência rápidos, ocasionalmente falham em gerar chamadas de função válidas em prompts complexos. A API retorna um 200 com um ServiceUnavailableError envolto em um MidStreamFallbackError. Sua lógica de retry vê uma exceção, tenta o mesmo modelo novamente e recebe o mesmo erro.

Modo de falha 2: o modelo "tem sucesso", mas não chama a tool. O LLM retorna uma resposta perfeitamente válida. HTTP 200. Sem erros. Mas, em vez de chamar save_memory com dados estruturados, ele escreve um resumo em texto simples. Seu mecanismo de retry considera isso um sucesso. A função de consolidação verifica as chamadas de tool, não encontra nenhuma e desiste.

O segundo modo de falha é o mais insidioso. A camada de transporte acha que tudo funcionou. A camada de negócio sabe que não. Nenhuma quantidade de retries em nível HTTP corrigirá um modelo que não entende o seu esquema de tool.

Arquitetura de Fallback de Camada Dupla

Resolvemos isso com dois loops de fallback independentes operando em níveis diferentes:

Usuário envia /new
    │
    ▼
consolidate() ─── Fallback da Camada de Negócio
    │               "O modelo chamou save_memory?"
    │               Não → tenta o próximo modelo na cadeia
    │
    ▼
_chat_with_retry() ─── Fallback da Camada de Transporte
    │                    Erros HTTP → exponential backoff
    │                    Todos os retries esgotados → percorre a cadeia de fallback
    │
    ▼
Cadeia 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)                  (confiável)       (último recurso)

A Camada 1 lida com falhas de transporte. A Camada 2 lida com falhas de lógica de negócio. A cadeia de fallback é compartilhada entre ambas as camadas, definida uma única vez em um catálogo central.

Esta é uma abordagem fundamentalmente diferente de "tentar novamente o mesmo modelo". Quando um modelo falha ao chamar uma tool, repeti-lo com o mesmo prompt raramente ajuda. Mudar para um modelo diferente, com pesos diferentes e comportamento de tool calling diferente, resolve o problema.

O Catálogo de Modelos: Uma Única Fonte de Verdade

Cada modelo em nosso catálogo possui um campo opcional fallback apontando para o próximo modelo a ser tentado:

@dataclass(frozen=True)
class ModelEntry:
    id: str
    label: str
    tier: str
    description: str
    fallback: str | None = None
    hidden: bool = False  # Oculto da lista /model voltada ao usuário

MODEL_CATALOG = [
    # Modelos visíveis ao usuário (16 modelos que os usuários podem alternar)
    ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
               "Recomendado", fallback="claude-sonnet-4-5"),
    ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
               "Tool calling estável", fallback="claude-haiku-4-5"),

    # Modelos de consolidação ocultos (apenas uso interno)
    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),
    # ...
]

A flag hidden=True mantém os modelos internos fora do comando /model voltado ao usuário, enquanto eles ainda participam das cadeias de fallback. Os usuários veem 16 modelos que podem alternar. O sistema usa 19. Os três modelos ocultos existem exclusivamente para tarefas de segundo plano, como a consolidação de memória, onde velocidade e custo importam mais do que a qualidade conversacional.

Este catálogo é a única fonte de verdade para todo o roteamento de modelos. Adicionar um novo modelo à cadeia de fallback significa adicionar uma única linha. Sem arquivos de configuração para sincronizar, sem variáveis de ambiente para atualizar, sem scripts de deploy para modificar.

Camada de Transporte: Fallback em Cadeia com Detecção de Ciclo

O mecanismo de retry percorre a cadeia de fallback usando um conjunto de visitados para evitar loops infinitos:

async def _chat_with_retry(self, kwargs, original_model):
    # Fase 1: Exponential backoff no modelo primário
    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 inválida.", finish_reason="error")

    # Fase 2: Percorrer a cadeia 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)

        # Resolver o gateway correto para este modelo
        gw = self._resolve_gateway_for_model(current)
        resolved = self._resolve_model(current, gateway=gw)
        fb_kwargs = {**kwargs, "model": resolved}

        # Corrigir api_base para o protocolo do modelo de destino
        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  # Tentar o próximo na cadeia

    return LLMResponse(content="Serviço indisponível.", finish_reason="error")

O conjunto visited é crítico. Sem ele, uma cadeia como A→B→A entraria em loop eterno. Com ele, o mecanismo tenta cada modelo exatamente uma vez.

A resolução de gateway também é importante. Modelos diferentes precisam de formatos de API diferentes. Modelos Claude roteiam através de um gateway no formato Anthropic (sem o sufixo /v1). Modelos GPT roteiam através de um gateway compatível com OpenAI (com /v1). Modelos Groq usam ainda outro endpoint. O mecanismo de fallback resolve o gateway correto para cada modelo na cadeia, evitando incompatibilidades de protocolo, como enviar requisições Anthropic para um endpoint OpenAI.

Este é um detalhe que a maioria dos frameworks ignora completamente. Eles assumem que todos os modelos falam o mesmo protocolo. Em produção, com 19 modelos em 4 formatos de API diferentes, essa suposição quebra imediatamente.

Camada de Negócio: Verificação de Tool Call

A função de consolidação adiciona seu próprio loop de fallback por cima:

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:
            # Sucesso: extrair e salvar memória
            args = response.tool_calls[0].arguments
            self.write_long_term(args["memory_update"])
            self.append_history(args["history_entry"])
            return True

        # O modelo não chamou a tool — tentar o próximo na cadeia
        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  # Sem mais fallbacks

    return False

Isso captura o caso em que _chat_with_retry retorna uma resposta bem-sucedida (HTTP 200, conteúdo válido), mas o modelo não usou a tool. A função de consolidação verifica has_tool_calls e, se estiver ausente, passa para o próximo modelo na cadeia.

O wrapper de timeout (asyncio.wait_for) também aciona o fallback. Se um modelo levar mais de 30 segundos (comum com erros Cloudflare 524 em upstreams lentos), a função captura o TimeoutError e tenta o próximo modelo, em vez de bloquear a sessão do usuário indefinidamente.

Por que usar Groq para consolidação

A consolidação de memória é uma tarefa de segundo plano. O usuário não vê a saída. Ele apenas precisa que funcione. Isso a torna uma candidata perfeita para modelos rápidos e baratos.

A maioria dos frameworks usa o mesmo modelo caro para tudo. Se você está rodando Claude Sonnet para a conversa, também está rodando Claude Sonnet para a consolidação de memória. Isso custa $3/M de tokens de entrada e leva mais de 8 segundos por consolidação, para uma tarefa que produz uma saída que nenhum humano jamais lerá.

Nós desacoplamos totalmente a consolidação do modelo de conversa. A conversa usa qualquer modelo que o usuário selecionou. A consolidação usa uma cadeia dedicada de modelos hospedados no Groq:

Modelo Velocidade Custo de Entrada Custo de Saída
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 (anterior) ~150 TPS $0.40/M $1.60/M

O modelo primário (llama-3.3-70b) consolida uma sessão de 60 mensagens em ~5 segundos. O padrão anterior (gpt-4.1-mini) levava mais de 8 segundos. O custo por consolidação caiu de ~$0.003 para ~$0.001.

O tradeoff: modelos Groq têm tool calling menos confiável em prompts complexos. É exatamente por isso que o fallback de camada dupla existe. Quando o llama-3.3-70b falha ao chamar a tool, o qwen3-32b assume. Se ele também falhar, o llama-4-scout tenta. Se todos os três modelos Groq falharem, o gpt-4.1-mini resolve com quase 100% de confiabilidade no tool calling.

Em produção, vemos o modelo primário ter sucesso em ~85% das vezes. A cadeia chega ao gpt-4.1-mini em menos de 2% das consolidações. Taxa de falha total: efetivamente zero.

Resultados em Produção

Implementamos isso em duas instâncias do LemonClaw e testamos com conversas reais do Telegram.

Primeira implementação (apenas fallback de camada única):

Consolidação de memória (archive_all): 56 mensagens
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."

A camada de transporte capturou a primeira falha e realizou o fallback. Mas o qwen3-32b retornou texto sem chamar a tool. O fallback de camada única não conseguiu lidar com isso. Este é exatamente o cenário em que qualquer outro framework perderia a memória silenciosamente.

Segunda implementação (fallback de camada dupla):

Consolidação de memória (archive_all): 60 mensagens
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining

Mesmo modelo, mesmo volume de mensagens. Desta vez funcionou na primeira tentativa. A natureza intermitente da falha de tool calling é exatamente o motivo pelo qual você precisa de uma cadeia de fallback, em vez de um único modelo de backup.

Quando o modelo primário falha, a cadeia o captura:

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

Quatro modelos tentados, memória salva. O usuário vê "Nova sessão iniciada." e não tem ideia de que nada disso aconteceu.

A lacuna na arquitetura

Sistema de memória do LemonClaw vs. as alternativas, recurso por recurso:

Capacidade Framework de Agente de IA Típico LemonClaw
Modelo de consolidação Mesmo da conversa (caro, lento) Cadeia de modelos independente, acelerada por Groq
Tratamento de falhas Registra aviso, perde memória Fallback de camada dupla, 5 modelos de profundidade
Fallback de transporte Tenta o mesmo modelo 3x Fallback encadeado entre diferentes modelos
Fallback de lógica de negócio Nenhum Verificação de tool call + troca de modelo
Proteção de timeout Nenhuma (Cloudflare 524 bloqueia a sessão) asyncio.wait_for(timeout=30) + fallback
Truncamento de sessão Nenhum (contexto cresce para sempre) Trunca mensagens antigas após consolidação
Busca no histórico Nenhuma Janela deslizante HISTORY.md, pesquisável via grep
Modelos internos Não suportado hidden=True para modelos apenas do sistema
Prevenção de ciclos Não necessário (sem cadeias) Conjunto visited evita loops A→B→A
Resolução de gateway Assume formato de API único Gateway por modelo com detecção de protocolo

Cada linha nesta tabela representa uma falha de produção que experimentamos ou observamos nos rastreadores de problemas de outros frameworks. O fallback de camada dupla, o catálogo de modelos ocultos, a resolução de gateway por modelo, o fallback acionado por timeout: nada disso existe no nanobot ou em qualquer outro framework de agentes de código aberto que examinamos.

O que aprendemos

"Requisição bem-sucedida" não é "tarefa bem-sucedida". Mecanismos de retry genéricos operam no nível HTTP. Eles não podem saber que uma resposta 200 com JSON válido é, na verdade, uma falha porque o modelo não usou a tool que você solicitou. Operações críticas para o negócio precisam de seus próprios critérios de sucesso e sua própria lógica de fallback.

Modelos pequenos falham de forma diferente dos modelos grandes. Modelos grandes (GPT-4.1, Claude Sonnet) quase sempre chamam as tools quando solicitados. Modelos pequenos em motores de inferência rápidos às vezes geram respostas que parecem válidas, mas ignoram completamente o esquema da tool. Isso não é um bug que você possa corrigir com engenharia de prompt. É uma lacuna de capacidade que exige mitigação arquitetural.

Teste com dados de produção, não com dados sintéticos. Nosso teste inicial com 6 mensagens sintéticas passou em todos os modelos. A sessão real de 60 mensagens com histórico de chamadas de tool, timestamps e idiomas mistos falhou em dois dos três modelos Groq. A complexidade dos dados reais expõe modos de falha que dados de teste limpos nunca mostrarão.

É também por isso que o guia de rate limiting de API de IA é importante aqui. O sistema de memória não precisa apenas de um "modelo melhor". Ele precisa de uma política de transporte, uma verificação de sucesso da lógica de negócio e uma escada de fallback que não desmorone sob uma falha comum do provedor.


LemonClaw é um framework de agentes de IA de código aberto com roteamento multi-modelo integrado, memória persistente e integrações com mais de 10 plataformas de chat. Todo o sistema de fallback de camada dupla descrito aqui está incluído na versão de código aberto. Execute-o em seu próprio servidor: github.com/hedging8563/lemonclaw

Precisa de mais de 300 modelos de IA através de uma única chave de API? lemondata.cc oferece acesso unificado a OpenAI, Anthropic, Google, DeepSeek, Groq e muito mais.

Share: