MÓDULO 3.3

💾 Memória avançada e context engineering

Do "existe memória" visto na Trilha 1 para o mecanismo real: compactação rolling de histórico, recall semântico via embeddings, poisoning defensivo e um teste que prova que sobrescrita funciona.

5
Tópicos
55
Minutos
Avançado
Nível
Prático
Tipo

💡 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.

1

📄 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.

1

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.

2

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.

3

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.

4

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.

2

🗜️ 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.

3

🔍 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.

4

☠️ 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.

5

🧪 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.

1

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.

2

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.

3

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.

4

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.

5

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

Histórico, cache, memória são 3 coisas — escrever em uma esperando efeito em outra é fonte clássica de bug.
Compactação precisa preservar o pin — rolling summary só funciona se decisões críticas ficam intactas; resumo cego perde o que importa.
Recall é filtro duro + vetor + rerank — filtro por user/tag/TTL vem antes do vector search. 90% dos bugs de recall moram nos filtros, não no embedding.
Poisoning é o bug mais difícil de achar — valide schema, marque origem, mantenha TTL curto, audite amostras semanalmente.
Sobrescrita só é sobrescrita se existe teste — inspecione o prompt final, não a resposta do modelo. Coloque no CI para sempre.
Memória sem TTL é passivo — cresce sem limite, degrada o recall, vira superfície de poisoning. Default curto, extensão justificada.

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.