💡 Como ler este módulo
Memória é onde os bugs estranhos nascem. "Por que o agente ainda acha que moro em São Paulo se eu disse 3 vezes que mudei?", "Por que o lead_agent repetiu a mesma pergunta?". Respostas honestas estão em compactação ruim, recall ruim, poisoning, sobrescrita que falha silenciosamente. Este módulo é 80% diagnóstico e 20% prevenção.
📄 MEMORY_IMPROVEMENTS.md e documentos correlatos
O repositório do DeerFlow mantém MEMORY_IMPROVEMENTS.md como diário vivo das decisões arquiteturais sobre memória. Não é README — é um documento de evolução: "fizemos X, deu ruim em Y, trocamos por Z". Ler na ordem é entender o porquê do sistema atual ter a forma que tem. Há documentos vizinhos (docs/memory/*, RFCs sobre store backend) que completam o quadro.
Camadas distintas
Histórico de turno, cache de curta duração, memória persistente
O doc distingue três coisas que usuários confundem: histórico (mensagens cruas da conversa), cache (resumos e resultados reusáveis dentro da sessão) e memória (fatos persistentes entre sessões). Bugs acontecem quando alguém escreve numa camada esperando efeito em outra.
Store backend plugável
SQLite default, Postgres/Redis opcionais
A memória persiste em um MemoryStore abstrato. SQLite é default por simplicidade; Postgres entra quando há multi-processo; Redis quando latência de recall importa. O doc lista trade-offs e casos em que cada um vira o certo.
Políticas de TTL e expiração
Memória não é acúmulo infinito
Cada categoria de memória tem TTL default diferente. "Feedback" é semi-permanente, "project" expira quando o projeto fecha, "reference" é só enquanto o link existe. A regra: memória sem TTL é passivo.
Métricas e hit rate
Como saber se a memória está ajudando
O doc define métricas: recall hit rate (% de turns em que memória relevante foi encontrada), memory staleness (idade média do que é lido), poisoning flag rate (auto-detecção de entradas suspeitas). Use como dashboard fixo.
🗜️ Compactação e resumo de histórico
O histórico cresce a cada turno. A janela do modelo é finita. Compactação é o mecanismo que resume trechos antigos preservando o essencial e mantém a janela abaixo do limite sem derrubar a conversa. O DeerFlow implementa via rolling summarization — parecido com um scroll infinito que guarda só headlines.
🎯 Conceito Central
Compactação acontece em 3 fases, acionada quando o histórico passa de um threshold (tipicamente 60-70% da janela do modelo):
- 1.Segmentação — separa histórico em blocos coerentes (turno-a-turno ou por tópico)
- 2.Summarização — cada bloco vira um resumo curto; blocos recentes são preservados literalmente
- 3.Pinning — mensagens marcadas como críticas (instruções de sistema, decisões do usuário) nunca são compactadas
✓ Algoritmos bons
- ✓Rolling summary com janela deslizante (últimos N turnos intactos)
- ✓Summarizer rodando em modelo barato (gpt-5-mini basta)
- ✓Pinning explícito de decisões e correções
- ✓Teste de regressão comparando antes/depois do resumo
✗ Algoritmos ruins
- ✗Trim FIFO cego (perde a instrução de sistema)
- ✗Summarizar tudo em um bloco só (perde cronologia)
- ✗Usar modelo forte no resumo (caro e lento, sem ganho claro)
- ✗Silêncio — nunca mostrar ao usuário que houve compactação
💡 Sinalização é obrigatória
Quando o DeerFlow compacta, a UI mostra um marcador inline no histórico ("150 mensagens antigas resumidas"). Isso serve para duas coisas: o usuário entende por que o agente "esqueceu" o detalhe, e quem debuga consegue apontar o ponto exato em que a informação foi para o resumo.
🔍 Seleção e recall semântico
Memória só vira útil quando o agente encontra a entrada certa na hora certa. Jogar toda a memória no contexto é caro e degrada a atenção. O DeerFlow usa recall semântico: cada entrada é indexada por embedding, cada turno gera uma query-embedding, e só os top-k mais similares entram.
📊 Dados: o pipeline de recall
- Embedding model — text-embedding-3-small ou e5-mistral; escolha importa menos que a consistência (mesma família para index e query)
- Index — FAISS ou hnswlib in-process para pequenos stores; pgvector/Qdrant para multi-processo
- Top-k — tipicamente k=5 a k=10; mais que isso satura o contexto sem ganho
- Threshold de similaridade — corte em ~0.75 de cosine; abaixo disso é ruído que atrapalha
- Re-ranking — segunda passada com cross-encoder melhora precisão em 15-25% para stores >10k entradas
- Filtros duros — antes do vector search, filtre por user_id, tag, freshness — sempre
Pseudo-código do recall
def recall(query: str, user_id: str, k: int = 5) -> list[Memory]:
q_vec = embed(query)
# 1. filtros duros
candidates = store.filter(user_id=user_id, ttl_valid=True)
# 2. vector search
hits = index.search(q_vec, candidates, top=k*3)
# 3. threshold
hits = [h for h in hits if h.score >= 0.75]
# 4. re-rank (opcional)
if len(hits) > k:
hits = rerank(query, hits)[:k]
return hits
💡 Dica prática
Antes de trocar o embedding model, verifique se filtros duros estão certos. 90% dos problemas de "recall errado" é vector search puxando memória de outro user, outro tenant, ou sem TTL respeitado. O vetor é inocente.
☠️ Memory poisoning e entradas obsoletas
Memory poisoning é quando conteúdo errado ou hostil entra na memória e passa a influenciar todas as respostas seguintes. É o bug mais difícil de detectar porque o agente continua "funcionando" — só funcionando errado. Pode ser ataque (prompt injection instrui o agente a memorizar algo falso) ou acidente (bug de parsing grava uma string estranha).
✓ Fazer
- ✓Validar schema de cada entrada antes de persistir
- ✓Marcar origem (tool vs. user vs. agente) em cada registro
- ✓TTL curto default, estender só com justificativa
- ✓Audit trail — quem escreveu, quando, por quê
- ✓Revisão humana amostrada semanalmente
✗ Evitar
- ✗Gravar direto o output do modelo sem validação
- ✗Memória eterna (TTL=None) como default
- ✗Deixar tool escrever memória sem passar pelo store central
- ✗Confiar em "ele não ia mentir para si mesmo" — ele vai
- ✗Ignorar entradas com caracteres suspeitos (markers, SQL, HTML)
🚨 Sinal clássico de poisoning
O agente começa a repetir a mesma frase estranha em contextos diferentes — uma "preferência" do usuário que ninguém lembra de ter dito, ou uma "decisão de projeto" que não aparece em nenhuma conversa. Abra a memória na UI, procure entradas recentes, veja a origem. 8 em 10 vezes é uma entrada que entrou via prompt injection num documento que o agente leu.
🧪 Lab: teste que prova sobrescrita correta
"Atualizei a memória mas o agente ainda usa o valor velho." Você já ouviu essa queixa. Quase sempre é um bug sutil: a nova entrada foi gravada mas a velha continua no índice, ou o filtro por TTL não está fechando a anterior, ou o recall puxa ambas e o modelo escolhe a errada. A única cura é um teste automatizado que simula sobrescrita e verifica o prompt final.
Suba um harness em memória
Use o Embedded Client (módulo 3.4) com MemoryStore in-memory. Assim o teste é rápido e não deixa lixo em SQLite real.
Escreva memória inicial
Turno 1: "meu projeto é o Alpha, stack TypeScript". Confirme que a memória foi criada com categoria project e key current_project.
Sobrescreva
Turno 2: "na verdade mudei para o projeto Beta, stack Python". Espera-se que o agente atualize current_project. Assert: o store contém apenas a entrada Beta para essa key — a entrada Alpha deve estar expirada ou removida.
Verifique o prompt final
Turno 3: "qual meu projeto atual?". Intercepte o prompt do modelo (hook de debug). Assert: "Alpha" not in prompt e "Beta" in prompt. Não confie na resposta do modelo — ela pode acertar por sorte.
Teste de regressão permanente
Adicione o teste ao CI. Qualquer mudança futura que reintroduza a entrada Alpha quebra o build. É barato e cobre o bug mais caro que memória consegue produzir.
Esqueleto do teste (pytest)
def test_memory_overwrite():
h = Harness(memory_store=InMemoryStore())
h.chat("meu projeto é Alpha, stack TypeScript")
h.chat("na verdade mudei para Beta, stack Python")
mems = h.memory.filter(key="current_project", ttl_valid=True)
assert len(mems) == 1
assert "Beta" in mems[0].value
assert "Alpha" not in mems[0].value
prompt = h.capture_next_prompt()
h.chat("qual meu projeto atual?")
assert "Alpha" not in prompt.text
assert "Beta" in prompt.text
📝 Resumo do Módulo
Próximo Módulo:
3.4 — 🧩 Embutindo o DeerFlow em outro app
Usar o harness como biblioteca Python, via gateway HTTP ou via streaming server-side.