Como Construir Agentes de IA que Realmente Lembram: Arquitetura de Memória para Apps LLM em Produção
Seu agente de IA acabou de esquecer tudo.
O usuário tá há vinte minutos explicando a arquitetura do codebase, as restrições de deploy, o estilo de código do time. Aí faz uma pergunta de acompanhamento — e o agente responde como se nunca tivessem se encontrado. A janela de contexto estourou. Tudo antes da mensagem #47 sumiu. O usuário recomeça do zero, frustrado.
Se você já construiu qualquer coisa com LLMs além de um demo de brinquedo, bateu nesse muro. Janelas de contexto não são memória. Uma janela de 128K tokens parece gigante até um scan de codebase preencher ela em segundos. Uma janela de 1M tokens parece infinita até você calcular o custo de API de encher ela a cada request. E mesmo com as maiores janelas, não tem persistência — fecha a sessão e o agente tem amnésia.
Esse é o desafio fundamental de aplicações de IA em produção em 2026: Como construir agentes que realmente lembram?
Não "lembrar os próximos 5 mensagens." Lembrar entre sessões. Lembrar as preferências do usuário de três semanas atrás. Lembrar que a migração de banco falhou terça passada e o workaround ainda tá ativo. Lembrar como um colega humano faria.
Este guia cobre os padrões de arquitetura de memória que times de produção realmente usam — de sliding windows simples até sistemas de memória hierárquica e armazenamento de conhecimento baseado em grafos. Vamos construir implementações reais, comparar os principais frameworks (Mem0, LangChain Memory, Letta) e mostrar exatamente onde cada padrão quebra.
Por que janelas de contexto não são memória
Antes de ir pras soluções, vamos ser precisos sobre o problema.
A ilusão da janela de contexto
Todo LLM tem uma janela de contexto — o máximo de tokens que pode processar num único request. Em 2026, elas cresceram bastante:
| Modelo | Janela de Contexto | Custo Aprox. (Input) |
|---|---|---|
| GPT-4.1 | 1M tokens | ~$2.00/M tokens |
| Claude Opus 4 | 200K tokens | ~$5.00/M tokens |
| Gemini 2.5 Pro | 1M tokens | ~$1.25/M tokens |
| Llama 4 Scout | 10M tokens | Self-hosted |
"Só usa uma janela maior" parece a solução óbvia. Eis por que não é:
1. O custo escala linearmente (ou pior). Enfiar 500K tokens em toda chamada de API quando você só precisa de 2K de contexto relevante é queimar dinheiro. A 1. Se o agente lida com 1.000 conversas por dia, são $1.000/dia — e 99% é contexto irrelevante.
2. Performance degrada com ruído. Pesquisas mostram consistentemente que LLMs performam pior com grandes quantidades de contexto irrelevante. O problema da "agulha no palheiro" é real: modelos têm dificuldade em encontrar e usar informação específica enterrada em janelas massivas. Mais contexto ≠ melhores respostas.
3. Latência aumenta com o tamanho do contexto. Time-to-first-token escala com o tamanho do input. 500K tokens demora visivelmente mais que 5K, destruindo a sensação de tempo real em apps de chat.
4. Sem persistência entre sessões. Janelas de contexto são efêmeras. Fecha a aba e tudo some. Não tem mecanismo pra carregar informação de uma sessão pra outra sem armazenamento externo.
5. Sem esquecimento seletivo. Humanos não lembram de tudo — lembram do que é importante. Uma janela de contexto não tem conceito de importância; é um buffer FIFO que descarta os tokens mais velhos, não importa se contêm informação crítica ou papo furado.
Como é a memória de verdade
Memória humana não é um buffer único. É um sistema em camadas:
- Memória de trabalho (contexto imediato): O que você tá pensando agora. Capacidade baixa, acesso rápido.
- Memória de curto prazo (eventos recentes): O que aconteceu nos últimos minutos/horas. Capacidade média, acesso moderado.
- Memória de longo prazo (conhecimento persistente): Fatos, habilidades e experiências acumuladas. Capacidade massiva, recuperação mais lenta.
Memória eficaz de agente de IA replica essa arquitetura. Vamos construir.
Padrão 1: Sliding window com truncamento inteligente
O padrão de memória mais simples — e frequentemente suficiente pra chatbots básicos.
Como funciona
Mantém os N mensagens mais recentes na janela de contexto. Quando a conversa excede o limite, descarta as mensagens mais velhas.
interface Message { role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; tokenCount: number; } class SlidingWindowMemory { private messages: Message[] = []; private maxTokens: number; private systemPrompt: Message; constructor(maxTokens: number, systemPrompt: string) { this.maxTokens = maxTokens; this.systemPrompt = { role: 'system', content: systemPrompt, timestamp: Date.now(), tokenCount: this.estimateTokens(systemPrompt), }; } addMessage(role: 'user' | 'assistant', content: string): void { this.messages.push({ role, content, timestamp: Date.now(), tokenCount: this.estimateTokens(content), }); this.trim(); } getContext(): Message[] { return [this.systemPrompt, ...this.messages]; } private trim(): void { let totalTokens = this.systemPrompt.tokenCount + this.messages.reduce((sum, m) => sum + m.tokenCount, 0); while (totalTokens > this.maxTokens && this.messages.length > 2) { const removed = this.messages.shift()!; totalTokens -= removed.tokenCount; } } private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } }
Quando usar
- Chatbots Q&A simples onde o histórico não importa muito
- Bots de suporte ao cliente com conversas curtas e focadas
- Protótipos e MVPs
Onde quebra
- Contexto importante do início é descartado primeiro. Se o usuário explicou os requisitos na primeira mensagem, é a primeira coisa a sumir.
- Sem persistência entre sessões. Cada sessão nova começa do zero.
- Sem inteligência sobre o que manter. Uma tangente confusa tem a mesma prioridade que especificações críticas.
Padrão 2: Sumarização de conversa
O primeiro passo rumo à memória inteligente — comprime contexto antigo em vez de descartar.
Como funciona
Quando a conversa fica longa demais, sumariza a parte mais velha e substitui pelo resumo. O agente trabalha com: [System Prompt] + [Resumo dos msgs antigos] + [Msgs recentes].
class SummarizingMemory { private messages: Message[] = []; private summary: string = ''; private maxTokens: number; private recentWindowSize: number; private llm: LLMClient; constructor(maxTokens: number, recentWindowSize: number, llm: LLMClient) { this.maxTokens = maxTokens; this.recentWindowSize = recentWindowSize; this.llm = llm; } async addMessage(role: 'user' | 'assistant', content: string): Promise<void> { this.messages.push({ role, content, timestamp: Date.now(), tokenCount: this.estimateTokens(content), }); if (this.shouldSummarize()) { await this.compactHistory(); } } async getContext(): Promise<Message[]> { const context: Message[] = []; if (this.summary) { context.push({ role: 'system', content: `Resumo da conversa anterior:\n${this.summary}`, timestamp: 0, tokenCount: this.estimateTokens(this.summary), }); } context.push(...this.messages.slice(-this.recentWindowSize)); return context; } private shouldSummarize(): boolean { const totalTokens = this.messages.reduce((sum, m) => sum + m.tokenCount, 0); return totalTokens > this.maxTokens * 0.8; } private async compactHistory(): Promise<void> { const messagesToSummarize = this.messages.slice( 0, this.messages.length - this.recentWindowSize ); if (messagesToSummarize.length === 0) return; const conversationText = messagesToSummarize .map(m => `${m.role}: ${m.content}`) .join('\n'); const existingSummary = this.summary ? `Resumo existente:\n${this.summary}\n\n` : ''; this.summary = await this.llm.complete({ prompt: `${existingSummary}Nova conversa pra incorporar:\n${conversationText}\n\nCrie um resumo abrangente que preserve:\n1. Decisões-chave e conclusões\n2. Preferências e requisitos do usuário\n3. Especificações técnicas mencionadas\n4. Itens de ação pendentes\n5. Contexto importante pra referência futura\n\nSeja conciso sem perder detalhes críticos.`, maxTokens: 500, }); this.messages = this.messages.slice(-this.recentWindowSize); } private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } }
O truque da sumarização progressiva
Em vez de um resumo flat, use sumarização hierárquica:
class HierarchicalSummarizingMemory { private detailedSummary: string = ''; // Últimas ~30 mensagens private broadSummary: string = ''; // Tudo antes disso private recentMessages: Message[] = []; // Últimas ~10 mensagens async compactHistory(): Promise<void> { // Passo 1: Fundir resumo detalhado atual no resumo amplo if (this.detailedSummary) { this.broadSummary = await this.llm.complete({ prompt: `Resumo geral existente:\n${this.broadSummary}\n\nResumo detalhado pra incorporar:\n${this.detailedSummary}\n\nCrie um resumo de alto nível preservando apenas os fatos, decisões e preferências mais importantes. Máximo 200 palavras.`, maxTokens: 300, }); } // Passo 2: Resumir overflow recente no resumo detalhado const overflow = this.recentMessages.slice(0, -10); this.detailedSummary = await this.llm.complete({ prompt: `Segmento de conversa:\n${overflow.map(m => `${m.role}: ${m.content}`).join('\n')}\n\nCrie um resumo detalhado preservando detalhes técnicos específicos, trechos de código mencionados e requisitos exatos. Máximo 500 palavras.`, maxTokens: 600, }); this.recentMessages = this.recentMessages.slice(-10); } async getContext(): Promise<string> { let ctx = ''; if (this.broadSummary) { ctx += `## Contexto Geral\n${this.broadSummary}\n\n`; } if (this.detailedSummary) { ctx += `## Contexto Recente (Detalhado)\n${this.detailedSummary}\n\n`; } ctx += `## Conversa Atual\n`; ctx += this.recentMessages.map(m => `${m.role}: ${m.content}`).join('\n'); return ctx; } }
Quando usar
- Aplicações de chat de longa duração
- Suporte ao cliente com conversas complexas e multi-turno
- Assistentes de código que precisam de contexto do projeto
Onde quebra
- Sumarização perde detalhe. O resumo diz "usuário quer uma REST API" mas esquece que pediu respostas HATEOAS-compliant com headers ETag.
- Erros compostos de sumarização. Resumir um resumo de um resumo introduz perda progressiva de informação. Após 5 ciclos de compactação, a nuance original sumiu.
- Custo da sumarização. Cada compactação requer uma chamada LLM, adicionando latência e custo.
Padrão 3: Extração de entidades e fatos
Em vez de sumarizar conversas inteiras, extrai e armazena fatos estruturados.
Como funciona
Após cada interação, extrai entidades e fatos-chave pra um armazenamento persistente. Antes de cada resposta, recupera fatos relevantes baseados na consulta atual.
interface Fact { id: string; subject: string; predicate: string; object: string; confidence: number; source: string; timestamp: number; supersedes?: string; } class EntityMemory { private facts: Map<string, Fact[]> = new Map(); private llm: LLMClient; private vectorStore: VectorStore; async extractFacts(messages: Message[]): Promise<Fact[]> { const conversation = messages .map(m => `${m.role}: ${m.content}`) .join('\n'); const response = await this.llm.complete({ prompt: `Extraia fatos-chave desta conversa como dados estruturados. Conversa: ${conversation} Retorne como JSON array: [{ "subject": "nome da entidade", "predicate": "relação ou atributo", "object": "valor ou entidade relacionada", "confidence": 0.0-1.0 }] Foque em: - Preferências e requisitos do usuário - Decisões técnicas tomadas - Especificações do projeto - Prazos e restrições - Pessoas e papéis mencionados`, responseFormat: 'json', }); return JSON.parse(response).map((f: any) => ({ ...f, id: crypto.randomUUID(), source: 'current_session', timestamp: Date.now(), })); } async storeFacts(facts: Fact[]): Promise<void> { for (const fact of facts) { const key = `${fact.subject}::${fact.predicate}`; const existing = this.facts.get(key) || []; const contradicting = existing.find( e => e.object !== fact.object && e.confidence < fact.confidence ); if (contradicting) { fact.supersedes = contradicting.id; } if (!this.facts.has(key)) { this.facts.set(key, []); } this.facts.get(key)!.push(fact); await this.vectorStore.upsert({ id: fact.id, text: `${fact.subject} ${fact.predicate} ${fact.object}`, metadata: fact, }); } } async recallRelevant(query: string, limit: number = 20): Promise<Fact[]> { const results = await this.vectorStore.search(query, limit); return results .map(r => r.metadata as Fact) .filter(f => !f.supersedes) .sort((a, b) => b.confidence - a.confidence); } async buildContext(query: string, recentMessages: Message[]): Promise<string> { const relevantFacts = await this.recallRelevant(query); let context = ''; if (relevantFacts.length > 0) { context += '## Fatos Conhecidos do Usuário/Projeto\n'; for (const fact of relevantFacts) { context += `- ${fact.subject} ${fact.predicate}: ${fact.object}\n`; } context += '\n'; } context += '## Conversa Atual\n'; context += recentMessages.map(m => `${m.role}: ${m.content}`).join('\n'); return context; } }
Quando usar
- Assistentes pessoais de IA que aprendem sobre o usuário com o tempo
- Bots de gerenciamento de projetos que rastreiam decisões ao longo de muitas reuniões
- Qualquer aplicação onde fatos específicos importam mais que fluxo conversacional
Onde quebra
- Extração não é perfeita. LLMs perdem nuances e às vezes alucinam fatos que não foram ditos.
- Contradições são difíceis. "O prazo é sexta" seguido de "na verdade, vamos empurrar pra segunda" requer que o sistema detecte e resolva conflitos.
- Fatos ficam velhos. Sem expiração explícita, fatos desatualizados poluem o contexto. O usuário mudou o framework preferido há dois meses, mas o dado antigo ainda tá lá.
Padrão 4: Arquitetura de memória hierárquica
Isso é o que sistemas de produção realmente usam. Combina múltiplos padrões num sistema em camadas que replica a memória humana.
Modelo de três camadas
┌─────────────────────────────────────────────┐
│ Camada 1: Memória de Trabalho │
│ (Janela de contexto atual, ~10 msgs) │
│ Acesso: Instantâneo | Capacidade: Baixa │
├─────────────────────────────────────────────┤
│ Camada 2: Memória de Curto Prazo │
│ (Resumos de sessão, fatos recentes) │
│ Acesso: Retrieval rápido | Capacidade: Média│
├─────────────────────────────────────────────┤
│ Camada 3: Memória de Longo Prazo │
│ (Grafo de conhecimento, perfil, histórico) │
│ Acesso: Busca semântica | Capacidade: Alta │
└─────────────────────────────────────────────┘
Implementação
class HierarchicalMemory { private workingMemory: Message[] = []; private shortTermMemory: ShortTermStore; private longTermMemory: LongTermStore; private llm: LLMClient; constructor(config: MemoryConfig) { this.shortTermMemory = new ShortTermStore(config.shortTermTTL); this.longTermMemory = new LongTermStore(config.vectorStore, config.graphDB); this.llm = config.llm; } async processMessage(message: Message): Promise<void> { // 1. Adicionar à memória de trabalho this.workingMemory.push(message); // 2. Extrair fatos pro armazenamento de curto prazo if (this.workingMemory.length % 5 === 0) { const recentFacts = await this.extractFacts( this.workingMemory.slice(-5) ); await this.shortTermMemory.store(recentFacts); } // 3. Promover fatos importantes pro longo prazo if (this.workingMemory.length % 20 === 0) { await this.consolidate(); } // 4. Compactar memória de trabalho se necessário if (this.getWorkingMemoryTokens() > 8000) { await this.compactWorkingMemory(); } } async buildContext(query: string): Promise<ContextBundle> { // Recuperar das três camadas const [shortTermResults, longTermResults] = await Promise.all([ this.shortTermMemory.search(query, 10), this.longTermMemory.search(query, 15), ]); // Deduplicar e rankear por relevância const allFacts = this.deduplicateAndRank([ ...shortTermResults, ...longTermResults, ]); return { systemContext: this.buildSystemContext(allFacts), workingMemory: this.workingMemory.slice(-10), relevantFacts: allFacts.slice(0, 20), tokenBudget: { system: 2000, facts: 3000, working: 8000, response: 4000, }, }; } private async consolidate(): Promise<void> { const shortTermFacts = await this.shortTermMemory.getAll(); const assessment = await this.llm.complete({ prompt: `Revise estes fatos e determine quais devem ser armazenados a longo prazo. Fatos: ${shortTermFacts.map(f => `- ${f.subject} ${f.predicate}: ${f.object} (confiança: ${f.confidence})`).join('\n')} Para cada fato, responda com: - KEEP: Importante para interações futuras (preferências, decisões-chave, specs do projeto) - DISCARD: Temporário ou conversacional (saudações, confirmações, estados transitórios) - MERGE: Pode ser combinado com outro fato Retorne como JSON array com objetos {id, action, mergeWith?}.`, responseFormat: 'json', }); const actions = JSON.parse(assessment); for (const action of actions) { if (action.action === 'KEEP') { const fact = shortTermFacts.find(f => f.id === action.id); if (fact) { await this.longTermMemory.store(fact); } } } await this.shortTermMemory.prunePromoted( actions.filter((a: any) => a.action === 'KEEP').map((a: any) => a.id) ); } private async compactWorkingMemory(): Promise<void> { const overflow = this.workingMemory.slice(0, -10); const summary = await this.llm.complete({ prompt: `Resuma este segmento de conversa preservando detalhes técnicos:\n${overflow.map(m => `${m.role}: ${m.content}`).join('\n')}`, maxTokens: 300, }); const facts = await this.extractFacts(overflow); await this.shortTermMemory.store(facts); this.workingMemory = [ { role: 'system', content: `[Resumo da conversa anterior: ${summary}]`, timestamp: Date.now(), tokenCount: this.estimateTokens(summary), }, ...this.workingMemory.slice(-10), ]; } }
Quando usar
- Assistentes de IA em produção com interações multi-sessão
- Copilots empresariais que precisam lembrar contexto do projeto por semanas
- Qualquer aplicação onde personalização de longo prazo é crítica
Padrão 5: Memória baseada em grafos (GraphRAG)
A vanguarda da memória de agentes. Em vez de armazenar fatos como texto plano, representa conhecimento como um grafo de relações.
Por que grafos batem vetores
Busca por similaridade vetorial (a espinha dorsal do RAG tradicional) tem uma limitação fundamental: encontra coisas que soam parecido mas perde coisas que estão estruturalmente relacionadas.
Exemplo: "Alice gerencia o time de pagamentos" e "O time de pagamentos é dono do microserviço de checkout" não são semanticamente similares. Mas num grafo, você pode percorrer: Alice → gerencia → time de pagamentos → é dono → checkout. Quando alguém pergunta "Com quem falo sobre bugs do checkout?", um grafo responde "Alice", enquanto um vector store não consegue.
class GraphMemory { private graph: GraphDatabase; // Neo4j ou outra async addKnowledge( subject: string, predicate: string, object: string, metadata: Record<string, any> ): Promise<void> { await this.graph.query(` MERGE (s:Entity {name: $subject}) MERGE (o:Entity {name: $object}) MERGE (s)-[r:${predicate.toUpperCase().replace(/\s/g, '_')}]->(o) SET r += $metadata, r.updatedAt = timestamp() `, { subject, object, metadata }); } async query(question: string): Promise<GraphResult[]> { // Passo 1: Extrair entidades da pergunta const entities = await this.extractEntities(question); // Passo 2: Encontrar subgrafo relevante ao redor dessas entidades const subgraph = await this.graph.query(` MATCH (e:Entity)-[r*1..3]-(connected:Entity) WHERE e.name IN $entities RETURN e, r, connected LIMIT 50 `, { entities }); // Passo 3: Formatar subgrafo como contexto return this.formatSubgraph(subgraph); } async traverseForContext( startEntity: string, maxDepth: number = 3 ): Promise<string> { const result = await this.graph.query(` MATCH path = (start:Entity {name: $startEntity})-[*1..${maxDepth}]-(end:Entity) RETURN path ORDER BY length(path) LIMIT 30 `, { startEntity }); return result.paths .map(p => this.pathToSentence(p)) .join('\n'); } private pathToSentence(path: GraphPath): string { return path.segments .map(s => `${s.start.name} ${s.relationship.type.toLowerCase().replace(/_/g, ' ')} ${s.end.name}`) .join(', que '); } }
Abordagem híbrida: Vetores + Grafos
Os sistemas de produção mais eficazes combinam ambos:
class HybridMemory { private vectorStore: VectorStore; // Pra similaridade semântica private graphStore: GraphMemory; // Pra relações estruturais private llm: LLMClient; async recall(query: string): Promise<MemoryResult> { const [vectorResults, graphResults] = await Promise.all([ this.vectorStore.search(query, 10), this.graphStore.query(query), ]); // Fundir e deduplicar const merged = this.mergeResults(vectorResults, graphResults); // Re-rankear com LLM const ranked = await this.llm.complete({ prompt: `Dada a consulta: "${query}" Avalie a relevância de cada item de memória (0-10): ${merged.map((m, i) => `${i}: ${m.text}`).join('\n')} Retorne como JSON: [{index, score, reason}]`, responseFormat: 'json', }); return { memories: this.applyRanking(merged, JSON.parse(ranked)), sources: { vector: vectorResults.length, graph: graphResults.length }, }; } }
Comparação de frameworks: Mem0 vs LangChain Memory vs Letta
Vamos comparar os três frameworks de memória mais populares pra aplicações LLM.
Mem0
Mem0 fornece uma camada de memória gerenciada pra aplicações de IA com uma arquitetura multi-store (KV store + vector store + camada de grafos).
import { MemoryClient } from 'mem0ai'; const memory = new MemoryClient({ apiKey: process.env.MEM0_API_KEY }); // Adicionar memórias da conversa await memory.add( "I prefer Python over JavaScript for backend work", { user_id: "alice", metadata: { category: "preferences" } } ); // Buscar memórias const results = await memory.search( "What programming language does Alice prefer?", { user_id: "alice" } ); // Retorna: [{memory: "Prefers Python over JavaScript for backend", score: 0.95}] // Pegar todas as memórias de um usuário const allMemories = await memory.getAll({ user_id: "alice" });
Pontos fortes:
- API ultra simples — add/search/get em três linhas
- Infraestrutura gerenciada (sem necessidade de configurar vector DB)
- Deduplicação automática e resolução de conflitos
- Funciona entre sessões por padrão
- Opção self-hosted disponível (mem0 OSS)
Pontos fracos:
- Controle limitado sobre representação de memória
- Algoritmo de ranking opaco
- Dependência do cloud pra versão gerenciada
- Camada de grafos é mais nova e menos testada em batalha que o vector store
LangChain Memory
LangChain fornece múltiplas implementações de memória prontas pra uso:
import { BufferWindowMemory } from 'langchain/memory'; import { ConversationSummaryMemory } from 'langchain/memory'; import { VectorStoreRetrieverMemory } from 'langchain/memory'; import { CombinedMemory } from 'langchain/memory'; // Opção 1: Buffer simples const bufferMemory = new BufferWindowMemory({ k: 10 }); // Opção 2: Sumarização const summaryMemory = new ConversationSummaryMemory({ llm: chatModel, returnMessages: true, }); // Opção 3: Retrieval baseado em vetores const vectorMemory = new VectorStoreRetrieverMemory({ vectorStoreRetriever: vectorStore.asRetriever(5), memoryKey: 'relevant_history', }); // Opção 4: Combinar múltiplos tipos de memória const combinedMemory = new CombinedMemory({ memories: [bufferMemory, summaryMemory, vectorMemory], });
Pontos fortes:
- Flexibilidade máxima — combine tipos de memória como quiser
- Integração profunda com ecossistema LangChain (agentes, chains, tools)
- Backends comunitários (Redis, PostgreSQL, MongoDB)
- Open-source e self-hosted
- Boa documentação com muitos exemplos
Pontos fracos:
- Requer mais setup e decisões de infraestrutura
- Pode ser over-engineering pra casos simples
- Tipos de memória nem sempre compõem limpo
- Depende do framework LangChain mais amplo
Letta (ex MemGPT)
Letta toma uma abordagem fundamentalmente diferente — trata gerenciamento de memória como um problema de sistema operacional.
import { Letta } from 'letta'; const client = new Letta({ apiKey: process.env.LETTA_API_KEY }); // Criar um agente com gerenciamento de memória tipo OS const agent = await client.createAgent({ name: 'project-assistant', memory: { coreMemory: { // Sempre no contexto — como seu system prompt persona: 'You are a senior software engineer...', human: '', // Preenchido automaticamente das conversas }, archivalMemory: true, // Armazenamento vetorial de longo prazo recallMemory: true, // Busca no histórico de conversas }, model: 'gpt-4.1', tools: ['archival_memory_insert', 'archival_memory_search', 'core_memory_replace', 'core_memory_append'], }); // O agente gerencia sua própria memória via tool calls const response = await agent.sendMessage( "I'm working on a Next.js project with PostgreSQL and Drizzle ORM" ); // O agente internamente chama: // core_memory_append(section="human", content="Works with Next.js, PostgreSQL, Drizzle ORM") // archival_memory_insert(content="User's current project stack: Next.js + PostgreSQL + Drizzle ORM")
Pontos fortes:
- Memória auto-gerenciada — o agente decide o que lembrar
- Arquitetura inspirada em OS (core/archival/recall)
- Persistente por padrão — memória sobrevive entre sessões
- O agente pode raciocinar explicitamente sobre o que guardar e recuperar
- Opções cloud e self-hosted
Pontos fracos:
- Requer chamadas LLM extras pra gerenciamento de memória (overhead de custo/latência)
- Arquitetura opinada que pode não servir pra todos os casos
- Ecossistema mais jovem comparado ao LangChain
- Atualizações de core memory podem ser imprevisíveis
Matriz de decisão
| Critério | Mem0 | LangChain Memory | Letta |
|---|---|---|---|
| Complexidade de setup | ⭐ Baixa | ⭐⭐⭐ Alta | ⭐⭐ Média |
| Flexibilidade | ⭐⭐ Média | ⭐⭐⭐ Alta | ⭐⭐ Média |
| Memória entre sessões | ✅ Built-in | ⚙️ Requer config | ✅ Built-in |
| Auto-gerenciada | ❌ | ❌ | ✅ |
| Self-hosted | ✅ | ✅ | ✅ |
| Prontidão pra produção | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Ideal pra | Integração rápida | Arquiteturas custom | Agentes autônomos |
Padrões e anti-padrões de produção
Padrão: Prompt engineering consciente de memória
A otimização mais impactante muitas vezes não é o sistema de memória em si, mas como você apresenta as memórias recuperadas pro modelo.
// ❌ Ruim: Dumpar todas as memórias como texto plano const badPrompt = ` Aqui tão umas coisas que você sabe: ${memories.map(m => m.text).join('\n')} Usuário: ${query} `; // ✅ Bom: Estruturado, priorizado, com sinais de frescura const goodPrompt = ` ## Seu Conhecimento Sobre Este Usuário ${highConfidenceMemories.map(m => `- ${m.text} (última confirmação: ${formatRelativeTime(m.updatedAt)})` ).join('\n')} ## Contexto Relevante do Projeto ${projectMemories.map(m => `- ${m.text}`).join('\n')} ## Potencialmente Desatualizado (verificar antes de usar) ${staleMemories.map(m => `- ${m.text} (de ${formatDate(m.createdAt)}, pode ter mudado)` ).join('\n')} ## Conversa Atual ${recentMessages.map(m => `${m.role}: ${m.content}`).join('\n')} `;
Anti-padrão: Memória sem esquecimento
Tão importante quanto lembrar é saber quando esquecer.
class MemoryManager { // Implementar decay — memórias que nunca são recuperadas enfraquecem async applyDecay(): Promise<void> { const allMemories = await this.store.getAll(); for (const memory of allMemories) { const daysSinceAccess = (Date.now() - memory.lastAccessedAt) / (1000 * 60 * 60 * 24); // Fórmula de decay: reduzir confiança ao longo do tempo se não acessada const decayFactor = Math.exp(-0.01 * daysSinceAccess); const newConfidence = memory.confidence * decayFactor; if (newConfidence < 0.2) { await this.store.archive(memory.id); // Não deletar, arquivar } else { await this.store.updateConfidence(memory.id, newConfidence); } } } // Implementar detecção de contradições async addWithContradictionCheck(newFact: Fact): Promise<void> { const existing = await this.store.search( `${newFact.subject} ${newFact.predicate}`, 5 ); const contradictions = existing.filter(e => e.subject === newFact.subject && e.predicate === newFact.predicate && e.object !== newFact.object ); if (contradictions.length > 0) { for (const old of contradictions) { await this.store.markSuperseded(old.id, newFact.id); } } await this.store.add(newFact); } }
Anti-padrão: Over-engineering pra casos simples
Nem todo app precisa de um sistema de memória hierárquica de três camadas com GraphRAG e consolidação automática. Siga essa árvore de decisão:
1. Conversa de <20 mensagens?
→ Sliding window basta. Pronto.
2. Usuário precisa voltar pra mesma conversa depois?
→ Soma sumarização de conversa. Talvez já baste.
3. O agente precisa lembrar fatos entre conversas diferentes?
→ Soma extração de entidades + vector store.
4. O agente precisa entender relações entre entidades?
→ Soma memória baseada em grafos.
5. O agente precisa gerenciar autonomamente o que lembra?
→ Considere a abordagem auto-gerenciada do Letta.
Benchmarking do seu sistema de memória
Como saber se o sistema de memória tá funcionando de verdade? Defina essas métricas:
// Teste: O agente consegue lembrar fatos de N mensagens atrás? async function testRecallAccuracy( agent: Agent, testFacts: { fact: string; queryAfterNMessages: number }[] ): Promise<number> { let correct = 0; for (const test of testFacts) { await agent.processMessage({ role: 'user', content: test.fact }); for (let i = 0; i < test.queryAfterNMessages; i++) { await agent.processMessage({ role: 'user', content: `Mensagem de preenchimento ${i}: Me fala sobre ${randomTopic()}`, }); } const response = await agent.processMessage({ role: 'user', content: `O que eu te falei sobre ${extractSubject(test.fact)}?`, }); if (responseContainsFact(response, test.fact)) { correct++; } } return correct / testFacts.length; }
Métricas-chave
| Métrica | O que Mede | Meta |
|---|---|---|
| Recall@N | Agente lembra um fato após N mensagens? | >90% em N=50 |
| Taxa de contradições | Com que frequência usa info desatualizada? | <5% |
| Latência de memória | Tempo de recuperação de memórias relevantes | <200ms |
| Eficiência de tokens | Ratio de tokens relevantes vs total no contexto | >60% |
| Recall entre sessões | Lembra fatos de sessões anteriores? | >80% |
Conclusão
Construir agentes de IA que lembram não é sobre encontrar a maior janela de contexto. É sobre projetar uma arquitetura de memória que reflita como a informação realmente precisa fluir no seu app.
O roteiro prático:
-
Começa simples. Sliding window + sumarização de conversa cobre 80% dos casos. Não faz over-engineering no dia um.
-
Soma persistência quando os usuários pedirem. Quando esperam que o agente lembre deles entre sessões, você precisa de extração de entidades e armazenamento persistente. Mem0 é o caminho mais rápido.
-
Soma estrutura quando memória flat não basta. Quando o agente precisa entender relações — organogramas, grafos de dependência, arquiteturas de sistemas — é onde memória baseada em grafos compensa.
-
Deixa o agente se gerenciar quando o problema for complexo o suficiente. Pra agentes autônomos rodando tarefas de horas, a abordagem auto-gerenciada do Letta evita a fragilidade de regras de memória hardcoded.
-
Sempre implementa esquecimento. Um sistema de memória sem decay vira um passivo. Fatos desatualizados causam mais estrago que fatos ausentes.
O tooling amadureceu enormemente no último ano. O que antes exigia infraestrutura custom agora é um pip install ou chamada API. O difícil não é mais a tecnologia — é projetar a arquitetura de memória certa pro seu caso específico.
Seus usuários não vão agradecer por um sistema de memória perfeito. Mas com certeza vão notar quando o agente esquecer.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit