Avaliação de Memória: Medindo Como a Memória de IA se Degrada ao Longo da Vida de um Projeto
A maioria dos benchmarks de memória de IA avalia recall e para por aí. Isso esconde o modo de falha real: fatos desatualizados envenenando silenciosamente a janela de contexto. Aqui está um framework de avaliação baseado em ciclo de vida que testa recall, revisão e esquecimento controlado em todos os pontos de mudança pelos quais um projeto de longa duração passa.
A maioria dos times com quem trabalhei prende uma camada de memória nos seus assistentes de IA — um vector store, um job de sumarização, uma tabela de "fatos" — e então avalia uma vez, em um único ponto no tempo. O recall parece bom, a latência é aceitável, e sobem para produção.
Seis meses depois o assistente diz, com confiança, a uma nova contratada que o serviço de auth ainda roda em Node, que payments-v1 é a fonte da verdade e que a Alice é dona do domínio de billing. Nada disso é verdade agora. Era verdade. A camada de memória nunca percebeu que deixou de ser.
Essa é a lacuna que quero fechar. Um sistema de memória útil não é avaliado pelo que ele lembra; é avaliado por se a sua representação do projeto acompanha a realidade à medida que o projeto muda. Se você está construindo um copilot de longa duração — para uma base de código, um time, uma conta de cliente — você precisa tratar memória da mesma forma que trata um cache: correção inclui invalidação.
No fim deste post, você terá um framework concreto para avaliar memória ao longo do ciclo de vida de um projeto, as quatro métricas que importam e os modos de falha em torno dos quais você deve projetar.
Snapshots Não Capturam um Alvo em Movimento
A avaliação padrão de memória parece mais ou menos assim: popular o store com N fatos, fazer M perguntas, medir recall e precisão contra um conjunto de referência. Isso é um snapshot. Não diz nada sobre o que acontece quando o fato 17 se torna obsoleto na semana 12.
Projetos reais produzem um fluxo contínuo de eventos invalidadores:
- Um serviço é reescrito em uma linguagem diferente. Toda resposta específica de código ancorada na stack antiga está agora errada.
- Um time se reorganiza. "Pergunta para a Alice sobre billing" era verdade; agora isso roteia para uma pessoa que saiu da empresa.
- Uma decisão é revertida. Você escolheu Kafka, depois migrou para SQS, depois voltou. A memória que reflete qualquer um desses estados isoladamente é enganosa.
- Um requisito é abandonado. O assistente continua citando uma restrição com que ninguém mais se importa.
- Uma fronteira arquitetural muda. O módulo que a memória "conhece" não existe mais.
Cada um desses é uma mutação da verdade fundamental. Um benchmark de snapshot não os vê. Pior, o modo de falha padrão é silencioso: o sistema continua respondendo com confiança usando contexto desatualizado porque nada no pipeline de retrieval sinaliza um fato como expirado. As pontuações de relevância parecem normais. Os embeddings ainda batem. A resposta simplesmente está errada.
A parte difícil não é o retrieval. A parte difícil é que "a resposta certa" é um alvo móvel e a maioria dos harnesses de avaliação o congela. O LongMemEval (ICLR 2025) é um dos poucos benchmarks públicos que trata atualizações de conhecimento como uma capacidade de primeira classe — vale a leitura mesmo que você não adote o benchmark diretamente.
Memória como uma Read Replica do Projeto
O modelo mental que uso é emprestado da replicação de bancos de dados: memória é uma read replica do projeto. O que você mede é lag de replicação, drift e divergência — não apenas throughput.
Em vez de um benchmark de ponto único, eu rodo uma avaliação de ciclo de vida. O harness de avaliação guia o sistema através de uma sequência de pontos de mudança e sonda em cada um. A cada passo ele faz quatro perguntas:
- Preservação — Ele manteve fatos que ainda são válidos?
- Revisão — Ele atualizou fatos que mudaram?
- Esquecimento — Ele descartou fatos que estão obsoletos?
- Não-contaminação — Respostas antigas estão vazando para as novas?
Essas quatro colapsam em quatro métricas que você pode acompanhar ao longo do tempo:
- Frescor — % de fatos recuperados cuja última validação cai dentro da janela aceitável de staleness para seu tipo.
- Consistência — % de fatos recuperados que não contradizem a verdade fundamental atual.
- Cobertura — % de fatos da verdade fundamental atual que são recuperáveis.
- Taxa de esquecimento controlado — % de fatos invalidados que foram removidos ou substituídos dentro de N pontos de mudança a partir do evento invalidador.
Note o que está faltando: recall bruto. Recall é um componente de cobertura, mas por si só recompensa o acúmulo. Um store que nunca esquece nada terá recall perfeito e consistência catastrófica.
Construindo o Harness de Ciclo de Vida
Modelando pontos de mudança
O primeiro artefato concreto é uma timeline — uma lista ordenada de eventos que mutam a verdade fundamental. Em testes, eu sintetizo; em produção, eu derivo a partir de fontes que já tenho (histórico de git, ADRs, organogramas, fechamentos de ticket).
sealed interface ChangeEvent {
val id: String
val timestamp: Instant
data class FactAdded(
override val id: String,
override val timestamp: Instant,
val fact: Fact,
) : ChangeEvent
data class FactRevised(
override val id: String,
override val timestamp: Instant,
val previous: Fact,
val current: Fact,
) : ChangeEvent
data class FactInvalidated(
override val id: String,
override val timestamp: Instant,
val fact: Fact,
val reason: InvalidationReason,
) : ChangeEvent
}
data class Fact(
val subject: String,
val predicate: String,
val value: String,
val domain: FactDomain, // TECH, OWNERSHIP, DECISION, REQUIREMENT
)Domínios importam porque a tolerância a staleness é específica do domínio. Um fato de ownership fica desatualizado em dias após uma reorganização. Uma decisão arquitetural pode ser válida por trimestres. Um requisito pode ser válido por anos. Tratá-los de forma uniforme é a raiz da maioria dos bugs de esquecimento-ávido-demais que vi.
Rodando o harness
O avaliador reproduz a timeline, e após cada evento roda um conjunto de probes contra o sistema de memória. Uma probe é uma pergunta com uma resposta esperada ciente do tempo — a resposta é uma função de "o que é verdadeiro no timestamp T".
class LifecycleEvaluator(
private val memory: MemoryService,
private val probes: List<Probe>,
private val groundTruth: GroundTruth,
) {
fun evaluate(timeline: List<ChangeEvent>): List<StageResult> {
val results = mutableListOf<StageResult>()
timeline.forEach { event ->
memory.apply(event)
val metrics = probes
.map { probe -> score(probe, event.timestamp) }
.aggregate()
results += StageResult(event, metrics)
}
return results
}
private fun score(probe: Probe, at: Instant): ProbeScore {
val expected = groundTruth.resolve(probe, at)
val actual = memory.answer(probe.question, at)
return ProbeScore(
freshness = freshness(actual.facts, at),
consistency = consistency(actual.facts, expected),
coverage = coverage(actual.facts, expected),
contaminated = actual.facts.any { it.invalidatedBefore(at) },
)
}
}A escolha importante aqui é que memory.answer retorna os fatos usados para produzir a resposta, não apenas o texto da resposta. Pontuar no nível do fato é o que permite distinguir "a resposta está certa pelos motivos errados" de correção genuína. Já vi sistemas pontuarem bem em similaridade de texto de resposta enquanto recuperavam evidências completamente desatualizadas — estavam a uma pergunta reformulada de quebrar.
Forçando o sistema a esquecer
Esquecimento controlado é a métrica em que a maioria das implementações falha. A stack padrão de retrieval — embed, nearest-neighbor, return — não tem conceito nativo de invalidação. Você precisa de um sinal explícito.
Três mecanismos, em ordem crescente de invasividade:
- TTL por domínio. Fatos de ownership expiram em 30 dias a menos que revalidados. Barato, ruidoso, mas pega a cauda longa.
- Links de supersessão. Quando um evento
FactRevisedchega, escreva uma tombstone apontando o fato antigo para o novo. O retrieval filtra fatos com tombstone a menos que a consulta peça explicitamente por histórico. - Propagação de invalidação. Um evento
FactInvalidatedse espalha para fatos dependentes. Matarpayments-v1 é fonte da verdadetambém deve invalidarpayments-v1 usa Postgres 13.
Na prática, rodo os três. TTL lida com o drift que você não modelou, supersessão lida com as mudanças que você modelou, propagação lida com as cascatas. O harness de avaliação sonda especificamente cada modo de falha — um TTL perdido, uma supersessão perdida, uma cascata perdida — então você pode dizer qual camada está vazando.
A arquitetura publicada do Mem0 (abril de 2025) vai na direção oposta — extração single-pass, ADD-only, sem delete nativo. Funciona para fatos estilo chat que mudam devagar; para memória de projeto de longa duração é exatamente o design que falha no teste de ciclo de vida.
Conectando ao Spring
Para serviços que já expõem memória através de uma API de retrieval baseada em Spring, o avaliador encaixa como mais um cliente. O harness se torna um job agendado que reproduz uma timeline sintética contra uma instância shadow de memória e publica métricas na mesma stack de observabilidade de tudo mais. Não há razão para construir um dashboard paralelo.
@Component
class MemoryHealthJob(
private val evaluator: LifecycleEvaluator,
private val timelineSource: TimelineSource,
private val meter: MeterRegistry,
) {
@Scheduled(cron = "0 0 * * * *")
fun run() {
val timeline = timelineSource.latest()
val results = evaluator.evaluate(timeline)
results.last().metrics.let {
meter.gauge("memory.freshness", it.freshness)
meter.gauge("memory.consistency", it.consistency)
meter.gauge("memory.coverage", it.coverage)
meter.gauge("memory.contamination", it.contaminationRate)
}
}
}Frescor e consistência são os dois em que faço alertas. Cobertura tende a se mover lentamente e contaminação é um indicador antecedente para consistência — se a contaminação está subindo, a consistência vai seguir.
Armadilhas e Casos Extremos
Esquecimento excessivo. Na primeira vez que você conecta TTLs agressivos, a cobertura despenca. Fatos de ownership expiram antes que o job de revalidação alcance; o assistente esquece quem é dono de qualquer coisa. O conserto é um estado de soft-expiry: fatos que passaram do TTL mas ainda não foram invalidados são recuperáveis com uma penalidade de confiança, não excluídos diretamente.
Drift de embedding se fazendo passar por drift de memória. Se você muda o modelo de embedding, tudo parece desatualizado. O avaliador precisa distinguir "o fato está desatualizado" de "a camada de retrieval mudou". Versione os embeddings e inclua a versão do embedding no registro do fato.
Contradição silenciosa. Dois fatos podem ser individualmente plausíveis e conjuntamente inconsistentes — o store afirma que o serviço X possui o domínio A e que o serviço Y possui o domínio A. Retrieval ponto a ponto nunca vê o conflito. O avaliador deve rodar checagens de consistência sobre o conjunto recuperado, não apenas por fato.
Vazamento de probe. Se você cria probes uma vez e nunca atualiza, o avaliador começa a testar o projeto de ontem. Probes devem ser versionadas junto com a base de código e invalidadas nos mesmos sinais que os fatos que testam.
O caso "ainda verdadeiro, só que menos verdadeiro". Alguns fatos se degradam gradualmente — uma característica de performance, um tamanho de time, um padrão de tráfego. Eles nunca são explicitamente invalidados. Esquecimento controlado não ajuda aqui; você precisa de revalidação contínua contra observações frescas. Trate isso como um pipeline separado e não tente encaixá-lo no modelo de eventos.
Não-determinismo na pontuação. Se a consistência é pontuada por um juiz LLM, o drift dele próprio vai aparecer nas suas métricas. Fixe a versão do modelo juiz e refaça o baseline deliberadamente, não por acidente.
Conclusões Práticas
- Avalie memória como um ciclo de vida, não como um snapshot. Cada ponto de mudança é um caso de teste.
- Acompanhe frescor, consistência, cobertura e esquecimento controlado. Recall bruto sozinho recompensa o acúmulo.
- Pontue no nível do fato, não no nível do texto de resposta. Resposta-certa-com-evidência-errada é um bug, não aprovação.
- Segmente por domínio de fato. Ownership, decisões, requisitos e fatos técnicos têm tolerâncias a staleness diferentes.
- Combine TTLs, links de supersessão e propagação de invalidação. Cada um pega uma classe diferente de drift.
- Rode o avaliador como um job agendado contra um store shadow e conecte suas métricas à sua stack de observabilidade existente.
- Versione suas probes. Um conjunto de probes congelado se transforma silenciosamente em um teste de regressão para o passado.
Quando Investir em Avaliação de Ciclo de Vida
A camada de memória é um cache sobre a verdade fundamental do seu projeto. Como qualquer cache, ela é útil apenas na medida da sua história de invalidação. Avaliá-la uma vez, em um único ponto no tempo, diz quão bom foi o snapshot que ela tirou; não diz nada sobre quão rápido ela vai apodrecer.
Use este framework quando o projeto for de longa duração, a verdade fundamental for mutável e respostas erradas forem piores que "não sei" — que é a maioria dos contextos reais de engenharia. Pule-o quando a memória for de escopo de sessão ou quando o domínio subjacente realmente não mudar. O overhead não é grátis: um harness de ciclo de vida, uma fonte de timeline e políticas de staleness por domínio são investimento real de engenharia. Pague por eles onde o custo da confiança desatualizada for maior que o custo do harness.
A mudança de mentalidade é pequena mas estruturante. Pare de perguntar "ele lembra?" e comece a perguntar "o que ele lembra ainda bate com a realidade, e quão rápido ele percebe quando não bate?"
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
DBOS vs Temporal: Quando o Postgres É Suficiente para Execução Durável de Workflows
O DBOS reutiliza o Postgres como camada de durabilidade para workflows, enquanto o Temporal roda um cluster dedicado. A escolha certa depende do tamanho do time, da forma do workload e de onde você quer que seu orçamento operacional vá. Este é um critério prático para escolher entre eles.
JetBrains Tracy: Observabilidade Pragmática de IA para Kotlin
JetBrains Tracy é uma biblioteca Kotlin que conecta tracing ciente de LLM na sua aplicação em cima do OpenTelemetry. Este post percorre como eu integrei no serviço Spring Boot, as decisões de design que importam, e os modos de falha que times encontram quando chamadas de LLM se tornam o caminho mais quente do sistema.
Virtual Threads Depois do JEP 491: O Gargalo Se Moveu
O JEP 491 removeu o problema de pinning do `synchronized` que mantinha as virtual threads fora da produção. A pergunta interessante agora não é se habilitar ou não — é qual gargalo aparece em seguida. Um guia de campo para serviços Spring Boot / Kotlin rodando em JDK 24+.