Memoirs: Ensinando Agentes a Lembrar (Sem Perder a Cabeça na Nuvem)

1 A Amnésia de Mil Tokens
Todo agente conversacional com que já trabalhei — Claude, Cursor, vários loops de CLI — compartilha a mesma falha silenciosa. Eles acordam sem nenhuma memória de ontem. As janelas de contexto ficaram enormes, mas eles ainda começam de uma página em branco a cada nova sessão. Se você tenta contornar isso mantendo uma thread contínua, acaba com algo pior: uma monstruosidade arquitetural que despeja todo o histórico do chat na janela de contexto a cada turno. Você paga o custo de inferência de 40K tokens, e o agente ainda enterra o fato mais importante em algum lugar no meio — invisível, ignorado. Esse é o efeito Lost in the Middle. É caro e nem sequer funciona.
Existem soluções corporativas para isso. Pesadas. Bancos de dados vetoriais hospedados na nuvem com idas e voltas pela rede medidas em segundos num dia bom. Mas eu não vou enviar minhas notas de arquitetura, minhas credenciais meio censuradas e minhas convenções pessoais para alguma API externa que não controlo. Eu queria algo local-first. Privado por construção. Algo que de fato entendesse minhas variáveis de ambiente, minhas escolhas de framework, meu estilo — e mantivesse esse conhecimento por perto.
Foi isso que se tornou o memoirs. Não um wrapper de RAG. Isso teria sido fácil demais, e entediante demais. Eu queria um sistema de memória de verdade — um que esquece organicamente e consolida o que realmente importa.
2 A Anatomia Subjacente: 6 Camadas sobre o SQLite
Construí a camada de persistência sobre algo que a maioria das pessoas subestima: o SQLite. Não só um arquivo .db plano. Uma instância de SQLite turbinada com sqlite-vec para busca vetorial densa e FTS5 nativo para busca lexical por índice invertido com pontuação BM25.
A arquitetura se assentou em seis camadas, porque um sistema de memória não é uma caixa de armazenamento — é uma estação de tratamento de água:
- Logs brutos: O tubo de entrada. Recebe tudo — histórico completo de conversa, diffs ruidosos, saída bruta.
- Extração: Um pequeno LLM curador rodando localmente (atualmente Qwen 2.5 3B — rápido o suficiente para não se notar) que filtra sinal do ruído: heurísticas, credenciais, preferências, marcadores de estilo.
- Grafo: O tecido conjuntivo. As memórias são interligadas usando princípios no estilo Zettelkasten, encontrando nós semânticos compartilhados entre conceitos e sessões.
- Indexação Dupla: Escritas atômicas paralelas no sqlite-vec e no FTS5. Ambos os índices permanecem sincronizados.
- Motor de Memória: A camada de curadoria. As memórias ganham e perdem pontuação ao longo do tempo, com suporte bi-temporal completo — você pode consultar o que o sistema acreditava em qualquer momento passado.
- Superfície: A camada de exposição. HTTP + REST, e mais importante, um servidor MCP de 22 endpoints que permite a qualquer agente consultar o sistema de forma limpa sem saber nada sobre as entranhas.
3 RRF e BM25: Vetores Não Resolvem Tudo
No começo, bati numa parede. Os embeddings do sentence-transformers falhavam de um jeito muito específico: a recuperação exata de identificadores. Peça ao memoirs para recuperar um nome de ferramenta específico ou uma string de dependência versionada e ele voltava com ruído semanticamente adjacente. Os espaços vetoriais são brilhantes com significado. Eles sabem que “carro” está perto de “veículo”. Mas quando uma memória contém “o sistema antigo usava psql-driver-v9”, o embedding não dá nenhum peso especial a essa string exata. Para um agente tentando reconstruir uma decisão, isso é um erro crítico.
A solução foi combinar algo novo com algo muito antigo: Reciprocal Rank Fusion (RRF). Rodamos as duas buscas em paralelo — uma consulta semântica densa pelo sqlite-vec, e uma consulta de índice invertido por correspondência exata pelo FTS5 com pontuação BM25. Se o BM25 encontra a string exatamente e a busca semântica entende o contexto ao redor, o RRF estabiliza as pontuações e empurra o resultado certo firmemente para a posição um. O resultado: a latência de recuperação p50 caiu para cerca de 3,9 ms. Num banco de dados SQLite local. Isso supera a maioria dos armazenamentos vetoriais na nuvem por uma boa margem.
4 Ebbinghaus, Curvas de Esquecimento e Sono Assíncrono
Um sistema não tem memória de longo prazo se não consegue esquecer. Os agentes têm tendência a se afogar em conhecimento obsoleto. “Ontem eu estava brigando com a API do Docker” seguido imediatamente de “Hoje abandonamos o Docker por completo e estamos em chamadas OCI puras.” Se o motor de memória traz à tona ambos os fatos com confiança igual, o agente vai tomar a decisão errada. Toda vez.
Voltei a um caderno antigo e implementei funções de decaimento baseadas na curva de esquecimento de Hermann Ebbinghaus: $R(t) = e^{-\Delta t_h / (S \cdot 24)}$. As memórias novas perdem peso assintoticamente com o tempo. Mas a pontuação de Força delas — a qualidade do sinal original — é multiplicada cada vez que o sistema recupera e confirma positivamente aquela memória. Decisões arquiteturais centrais e preferências profundas decaem muito mais devagar do que um relato de guerra de depuração de uma única sessão de madrugada.
A computação que isso exige — consolidação, resolução de conflitos semânticos, compactação de grafo — é cara demais para rodar em tempo real. Então recorri a um mecanismo que os humanos usam constantemente, mas que o software quase nunca usa: o sono.
Escrevi um daemon (sleep_consolidation.py) que vigia os vales de CPU. Ele só roda quando o desenvolvedor e os agentes se afastaram — literalmente as janelas de ociosidade em que o lock está livre e a ociosidade do sistema esteve acima de N minutos. Nessa janela assíncrona cega, o modelo Qwen ou Phi local acorda, revisa os perfis armazenados recentemente, caça contradições entre versões de memória e consolida ou arquiva as obsoletas. O grafo é compactado silenciosamente em segundo plano antes da próxima sessão começar.
No fim do ciclo de benchmarking, alcançar pontuações fortes de MRR em benchmarks de recuperação de memória como o LoCoMo — com um overhead de RAM de apenas 231 MB, comparado ao que o LlamaIndex ou o Mem0 consomem — confirmou a hipótese. Os melhores recursos de agentes não são construídos enviando milhões de tokens para provedores remotos. Eles são construídos entendendo índices básicos a fundo e impondo uma disciplina de recursos implacável na sua própria máquina.