Blog

Retenção de dados em sistema com IA: o que guardar, por quanto tempo e como apagar

Um sistema com IA guarda muito mais coisa do que o time imagina. A conversa está no banco da aplicação, mas a mesma frase do cliente também está no trace de observabilidade, no payload bruto do webhook, no índice vetorial, na memória de longo prazo do agente, no cache semântico, na fila de revisão humana, no conjunto de avaliação e no log de erro que alguém deixou verboso para depurar um incidente de março. Quando chega um pedido de exclusão, o time apaga a linha da conversa, responde que apagou, e o dado continua vivo em seis lugares. Retenção não é um campo de configuração, é uma propriedade que precisa ser desenhada por cópia, porque cada cópia tem um dono, um propósito e um prazo diferentes. Este artigo mostra como inventariar essas cópias, como definir prazo a partir do propósito em vez de definir por hábito, por que apagar de um índice vetorial e de um conjunto de avaliação é tecnicamente diferente de apagar de uma tabela, e como provar que a exclusão realmente aconteceu em vez de confiar que aconteceu.

2026-08-10 / IA Aplicada / 15 min

01

O dado não está em um lugar, está em oito

O primeiro erro de retenção é mental: o time pensa na conversa como um registro, quando ela é um rastro que se espalha por todo o pipeline. Cada componente que toca a mensagem cria uma cópia, e essa cópia quase sempre nasce sem prazo, porque foi criada para resolver um problema operacional imediato, não para ser gerenciada. O trace foi criado para depurar latência, o cache para economizar chamada, o eval para medir qualidade, e nenhum deles foi pensado como repositório de dado pessoal, mas todos são.

Antes de escrever qualquer política, faça o inventário. Ele não precisa de ferramenta: precisa de uma linha por lugar onde a mensagem do cliente pousa, com quem escreve, quem lê, e o que quebra se aquilo sumir amanhã. A última coluna é a mais importante, porque é ela que separa o que tem propósito real do que só está lá por inércia.

Onde a cópia vivePor que ela existePrazo típico defensávelO que quebra se apagar
Banco da conversaContinuidade do atendimento e histórico do clienteEnquanto durar a relação, mais o prazo legal aplicávelO cliente perde o próprio histórico
Payload bruto do webhookReprocessar entrega que falhou7 a 30 diasNada depois da janela de reprocessamento
Trace de observabilidadeDepurar incidente e atribuir custo15 a 30 dias com conteúdo, mais tempo só com metadadoInvestigação de incidente antigo fica cega
Índice vetorial do RAGRecuperar contexto relevanteEnquanto o documento de origem existirA resposta perde a fonte, não só o texto
Memória de longo prazo do agentePersonalizar sem reperguntaPrazo próprio, geralmente menor que o da conversaO agente volta a perguntar o que já sabia
Cache semânticoCortar custo e latênciaHoras a poucos diasSobe custo e latência, nada mais
Conjunto de avaliaçãoMedir regressão entre versõesLongo, mas só com dado anonimizadoA série histórica de qualidade perde comparabilidade
Log de erro da aplicaçãoDiagnóstico de falha15 a 30 diasNada, se o metadado estruturado ficar

Duas linhas dessa tabela costumam causar surpresa. O log de erro entra no inventário porque, na hora do incidente, alguém sempre loga o prompt inteiro para entender o que aconteceu, e esse log fica em um sistema com retenção pensada para volume, não para privacidade. E o cache semântico é o que tem o prazo mais curto de todos e quase sempre é o que ninguém lembra de limpar, porque ele é invisível quando funciona.

02

Prazo vem do propósito, não do hábito

A pergunta "por quanto tempo a gente guarda?" não tem resposta genérica, e o número que a maioria dos times escolhe vem de hábito de infraestrutura, não de análise. Quando alguém responde noventa dias sem hesitar, geralmente noventa dias é o padrão da ferramenta de log. A pergunta correta é outra: qual decisão essa cópia sustenta, e por quanto tempo essa decisão ainda pode ser tomada. Se a janela de disputa de cobrança é de sessenta dias, o trace com conteúdo não precisa passar disso. Se o webhook só reprocessa entrega falha em sete dias, o payload bruto não tem por que sobreviver ao oitavo.

Há um segundo eixo que resolve boa parte da tensão entre apagar e continuar operando: separar conteúdo de metadado. O conteúdo é a frase do cliente, o texto recuperado, a resposta gerada. O metadado é a duração, o número de tokens, o modelo usado, a intenção classificada, se houve transbordo. Quase todo painel operacional vive de metadado, e quase todo risco de privacidade vive de conteúdo. Quando as duas coisas ficam na mesma linha, o prazo do conteúdo contamina o do metadado, e o time se vê obrigado a escolher entre perder a série histórica de custo e guardar texto que já deveria ter sumido.

// retention/policy.js
// A politica vive no codigo, nao na cabeca de quem configurou o banco.
// Cada classe de dado declara proposito, prazo e o que sobra depois do prazo.

export const RETENTION = {
  conversation_content: { days: 365, afterExpiry: 'delete', purpose: 'historico do cliente' },
  conversation_metadata: { days: 1095, afterExpiry: 'keep', purpose: 'serie de volume e custo' },
  webhook_payload: { days: 7, afterExpiry: 'delete', purpose: 'reprocessar entrega falha' },
  trace_content: { days: 30, afterExpiry: 'redact', purpose: 'depurar incidente' },
  trace_metadata: { days: 400, afterExpiry: 'keep', purpose: 'latencia e custo por rota' },
  agent_memory: { days: 180, afterExpiry: 'delete', purpose: 'personalizacao sem repergunta' },
  semantic_cache: { days: 2, afterExpiry: 'delete', purpose: 'custo e latencia' },
  eval_case: { days: null, afterExpiry: 'keep', purpose: 'regressao', requiresAnonymization: true },
};

// Prazo nulo so e aceito quando o dado ja entrou anonimizado.
// A checagem roda no boot: politica invalida derruba o processo em vez
// de virar um dado pessoal guardado para sempre por descuido.
export function assertPolicyIsSound() {
  for (const [name, rule] of Object.entries(RETENTION)) {
    if (rule.days === null && !rule.requiresAnonymization) {
      throw new Error(`Retencao infinita sem anonimizacao em "${name}".`);
    }
    if (rule.days !== null && rule.days <= 0) {
      throw new Error(`Prazo invalido em "${name}".`);
    }
  }
}

export function expiresAt(kind, createdAt) {
  const rule = RETENTION[kind];
  if (!rule) throw new Error(`Classe de dado desconhecida: "${kind}".`);
  if (rule.days === null) return null;
  return new Date(createdAt.getTime() + rule.days * 86400000);
}

A função de verificação no boot parece exagero até a primeira vez que alguém adiciona uma classe nova de dado e esquece o prazo. Sem ela, o valor ausente vira retenção infinita silenciosa, que é exatamente o estado que a política existe para evitar. Com ela, o esquecimento derruba o deploy em ambiente de teste, que é o momento barato de descobrir o problema.

  • Escreva o propósito de cada classe antes do prazo: se ninguém consegue escrever o propósito em uma frase, a cópia provavelmente não deveria existir.
  • Trate "redigir" como uma ação de primeira classe ao lado de "apagar": muito trace precisa sobreviver sem o conteúdo, e apagar a linha inteira destrói a série de latência junto.
  • Prazo diferente por classe é o normal, não a exceção: forçar um prazo único para tudo sempre guarda demais em um lugar e de menos em outro.
  • Registre o prazo junto do dado, não só na configuração global, porque o dado migra de banco e a configuração fica para trás.
  • Ligue o expurgo desde o primeiro dia, mesmo com volume baixo: política que começa a rodar depois de dois anos de acúmulo é um projeto de limpeza, não uma política.

03

Apagar de índice vetorial e de eval não é apagar de tabela

Em uma tabela relacional, apagar é uma operação conhecida: uma linha some, as chaves estrangeiras avisam, e o resultado é verificável com uma consulta. Nos dois componentes que mais caracterizam um sistema com IA, o índice vetorial e o conjunto de avaliação, apagar tem armadilhas próprias, e é aí que a maioria das exclusões fica pela metade.

No índice vetorial, o embedding não é o texto, mas deriva dele, e um vizinho recuperado pode reconstruir boa parte do conteúdo original em uma resposta gerada. Isso significa que apagar o documento de origem sem apagar os vetores derivados não resolve nada: o RAG continua recuperando trechos de um documento que oficialmente não existe mais. Pior, muitos índices marcam como removido sem compactar, e o vetor continua materialmente presente até a próxima reconstrução. A regra prática é guardar, junto de cada vetor, a chave do titular e do documento de origem, para que a exclusão seja uma consulta por chave e não uma varredura por similaridade.

Um pedido de exclusao, oito destinos

  pedido de exclusao (titular X)
        |
        v
  +-- resolver identidade -------------------------+
  |  telefone, id externo, id interno, id de sessao|
  +------------------------------------------------+
        |
        +--> banco da conversa ........ DELETE por titular
        +--> payload de webhook ....... DELETE por titular
        +--> trace de observabilidade . REDIGIR conteudo, manter metadado
        +--> indice vetorial .......... DELETE por chave de titular
        |                               + reconstruir/compactar
        +--> memoria do agente ........ DELETE por titular
        +--> cache semantico .......... INVALIDAR entradas do titular
        +--> conjunto de avaliacao .... ANONIMIZAR ou remover o caso
        +--> backup ................... marcar para expurgo no ciclo
                                        (nao restaurar dado apagado)
        |
        v
  registro de exclusao: quando, quais destinos, resultado por destino

  destino sem confirmacao = exclusao incompleta, nao exclusao pendente

O conjunto de avaliação é o caso mais delicado, porque ele tem uma razão legítima para durar muito: sem casos estáveis não existe comparação entre versões do sistema. A saída não é abrir exceção para dado pessoal, é mudar o momento da anonimização. O caso entra no eval já sem identificador, com nomes, telefones e números de pedido substituídos por marcadores consistentes, e é essa versão que dura. Quando a anonimização acontece na entrada, o pedido de exclusão do titular não força escolher entre cumprir a lei e perder a régua de qualidade.

O backup merece a mesma honestidade. Backup imutável não é apagável sob demanda, e prometer o contrário é inventar uma capacidade que a arquitetura não tem. O comportamento defensável é declarar o ciclo de retenção do backup, garantir que o dado sai dele quando o ciclo vira, e assegurar que uma restauração nunca ressuscita registro já excluído, o que exige aplicar a lista de exclusões como etapa obrigatória do procedimento de restore. Sem essa etapa, todo restore desfaz silenciosamente meses de exclusões cumpridas.

04

Expurgo que roda sozinho e não trava o banco

Política escrita e não executada é pior do que nenhuma política, porque cria a sensação de que o problema está resolvido. O expurgo precisa ser um processo periódico, idempotente e observável, e precisa apagar em lotes. Um comando único de exclusão em cima de uma tabela grande trava a escrita, estoura o log de transação e acaba sendo cancelado no meio, deixando o expurgo pela metade e o time com medo de rodar de novo.

// retention/purge.js
// Expurgo em lotes: idempotente, com teto por execucao e pausa entre lotes.
// Roda todo dia; se o volume acumulado for grande, converge em varios dias
// em vez de tentar limpar tudo numa transacao unica.

import { RETENTION } from './policy.js';

const BATCH_SIZE = 500;
const MAX_BATCHES_PER_RUN = 200;

export async function purgeExpired({ db, kind, now = new Date(), logger }) {
  const rule = RETENTION[kind];
  if (!rule || rule.days === null) return { kind, deleted: 0, skipped: true };

  const cutoff = new Date(now.getTime() - rule.days * 86400000);
  let total = 0;

  for (let batch = 0; batch < MAX_BATCHES_PER_RUN; batch += 1) {
    // A acao depende da regra: apagar a linha ou apenas limpar o conteudo.
    const affected =
      rule.afterExpiry === 'redact'
        ? await db.redactContentBatch({ kind, cutoff, limit: BATCH_SIZE })
        : await db.deleteBatch({ kind, cutoff, limit: BATCH_SIZE });

    total += affected;
    if (affected < BATCH_SIZE) break; // acabou antes do teto
    await new Promise((resolve) => setTimeout(resolve, 200)); // folga para o banco
  }

  logger.info('retention.purge', { kind, action: rule.afterExpiry, cutoff, total });
  return { kind, deleted: total, skipped: false };
}

// A metrica que importa nao e quantos registros foram apagados, e sim
// qual e o registro mais velho que ainda esta vivo em cada classe.
export async function oldestSurviving({ db, kind }) {
  const rule = RETENTION[kind];
  const oldest = await db.oldestRecordAge({ kind });
  const overdue = rule.days !== null && oldest !== null && oldest > rule.days;
  return { kind, oldestDays: oldest, limitDays: rule.days, overdue };
}

A segunda função é a que transforma retenção em algo monitorável. Contar quantos registros o expurgo apagou não diz nada sobre conformidade: um dia sem exclusões pode significar que não havia nada vencido ou que o job falhou silenciosamente. A idade do registro mais velho vivo em cada classe responde diretamente a pergunta que importa, e ela é o alerta certo: se o mais velho passa do prazo declarado, alguma coisa parou, e o alerta dispara antes de alguém de fora perceber.

  1. Rode o expurgo diariamente, mesmo quando não há volume, para que a falha apareça como falha e não como ausência de dado vencido.
  2. Apague em lotes com pausa entre eles, e aceite convergir em vários dias quando houver acúmulo histórico.
  3. Emita por classe a idade do registro mais velho vivo e alerte quando ela ultrapassar o prazo declarado.
  4. Faça o expurgo idempotente: reexecutar depois de uma queda no meio precisa ser seguro e barato.
  5. Rode primeiro em modo de contagem em ambiente de produção, compare com o esperado, e só depois ligue a exclusão real.
  6. Inclua os destinos externos no mesmo job, porque índice vetorial e cache costumam ser os únicos que ninguém agendou.

05

Provar que apagou, não confiar que apagou

Quando chega um pedido de exclusão, a diferença entre um sistema maduro e um improvisado não está em conseguir apagar, está em conseguir dizer exatamente o que foi apagado, onde e quando. Isso exige tratar a exclusão como uma operação distribuída com confirmação por destino, e não como uma chamada que retorna verdadeiro. Cada destino confirma individualmente, o resultado é registrado, e a ausência de confirmação de um destino é uma exclusão incompleta, não uma exclusão pendente que alguém vai lembrar de terminar.

O registro dessa operação é uma exceção interessante à própria política: ele precisa durar mais que o dado que apagou, e por isso precisa conter apenas o identificador pseudonimizado do titular, a lista de destinos e o resultado de cada um. Um registro de exclusão que guarda o nome de quem pediu a exclusão é exatamente o tipo de contradição que aparece na primeira auditoria.

Pergunta da auditoriaResposta frágilResposta defensável
Onde esse dado está?No banco de conversasInventário por classe com dono, propósito e prazo
Vocês apagaram?Rodamos o deleteRegistro com destino, horário e confirmação por destino
E no backup?Backup é imutávelCiclo declarado e lista de exclusão aplicada no restore
E no eval e no índice vetorial?Isso é dado técnicoEval anonimizado na entrada, vetor apagado por chave de titular
Como sabem que continua funcionando?O job está agendadoIdade do registro mais velho vivo, por classe, com alerta

A coluna do meio não é caricatura: são as respostas reais que times competentes dão quando nunca precisaram formalizar retenção. Todas soam razoáveis e nenhuma é verificável, e é justamente essa a diferença. A coluna da direita não exige ferramenta cara nem projeto de seis meses; exige que o inventário exista, que o prazo esteja no código, que o expurgo rode com métrica e que a exclusão deixe rastro.

Vale fechar com o efeito colateral que quase nunca entra na conversa: retenção bem feita reduz custo e melhora qualidade. Índice vetorial menor busca mais rápido e recupera menos lixo antigo, cache limpo erra menos, trace com conteúdo curto custa menos em armazenamento e o eval anonimizado dura mais sem virar passivo. A política de retenção que o jurídico pede é a mesma que o time de engenharia deveria querer, e apresentá-la assim é o que faz ela sair do papel.

FAQ

Perguntas frequentes

Preciso apagar o dado do índice vetorial ou basta apagar o documento de origem?

Precisa apagar dos dois, e essa é uma das falhas de exclusão mais comuns em sistemas com RAG. O embedding não é o texto, mas deriva dele com informação suficiente para que um trecho recuperado reapareça dentro de uma resposta gerada, então um índice que ainda contém os vetores continua entregando conteúdo de um documento que oficialmente já não existe. Some a isso que muitos índices marcam o registro como removido sem compactar de imediato, e o vetor permanece materialmente presente até a próxima reconstrução. O desenho que torna isso viável é guardar, junto de cada vetor, a chave do titular e a chave do documento de origem, para que a exclusão seja uma consulta por chave e não uma varredura por similaridade, e agendar a compactação como parte do procedimento e não como manutenção eventual.

Como manter um conjunto de avaliação estável se preciso apagar dado de clientes?

Mudando o momento da anonimização em vez de abrir exceção para o eval. Se o caso entra no conjunto já sem identificadores, com nome, telefone, e-mail e número de pedido substituídos por marcadores consistentes que preservam a estrutura do caso, o que dura é uma versão que não é dado pessoal, e o pedido de exclusão do titular deixa de colidir com a necessidade de comparar versões do sistema ao longo do tempo. O erro é fazer o caminho inverso, guardar o caso cru e prometer anonimizar depois: a anonimização tardia sempre chega incompleta, porque identificador aparece no meio do texto livre e não só nos campos estruturados. Quando um caso específico ainda assim precisar sair, remova o caso e registre a mudança de versão do conjunto, para que a série histórica de qualidade continue legível.

Como lidar com backup, se ele é imutável por definição?

Sendo explícito sobre o que a arquitetura consegue fazer em vez de prometer exclusão imediata onde ela não existe. O comportamento defensável tem três partes: declarar o ciclo de retenção do backup, garantir que o dado sai naturalmente dele quando o ciclo vira, e tratar a lista de registros excluídos como etapa obrigatória do procedimento de restauração. Essa terceira parte é a que quase todo time esquece, e é a mais importante, porque sem ela qualquer restore ressuscita silenciosamente meses de exclusões já cumpridas e o sistema volta a um estado que já tinha sido corrigido. Documente o ciclo, teste a restauração com a lista aplicada pelo menos uma vez, e mantenha o registro dessa validação junto do procedimento.

Retenção é desenho de arquitetura, não campo de configuração

A mensagem do cliente não fica em um lugar: ela se multiplica em conversa, payload de webhook, trace, índice vetorial, memória do agente, cache, eval e log, e cada cópia nasce com um propósito diferente e quase sempre sem prazo. Fazer o inventário por cópia, derivar o prazo do propósito em vez do hábito, separar conteúdo de metadado, anonimizar o eval na entrada, apagar vetor por chave de titular, rodar expurgo em lotes com alerta pela idade do registro mais velho vivo e registrar cada exclusão por destino é o que transforma uma promessa em algo verificável. Posso mapear onde o dado do seu sistema de IA realmente vive, escrever a política no código, ligar o expurgo com métrica e deixar o fluxo de exclusão com prova por destino, para que a resposta a uma auditoria seja um registro e não uma lembrança.