Blog

Congelar o conjunto de avaliação: por que o seu eval envelhece e como renovar

Você montou um conjunto de avaliação para o seu agente, ele acertava 78% dos casos, e seis meses depois acerta 94%. A leitura óbvia é que o sistema melhorou. A leitura provável é que o conjunto envelheceu: cada bug corrigido virou um caso do eval, cada caso adicionado foi um que o sistema já errava e passou a acertar, e o que sobrou é um retrato do passado. O eval continua rodando verde enquanto o cliente reclama de coisas que ele não mede. Esse é o problema central: um conjunto de avaliação precisa ser estável para você comparar versões ao longo do tempo, e precisa mudar para continuar representando o tráfego real, e essas duas exigências se contradizem. A saída não é escolher uma delas, é separar o conjunto em partes com regras diferentes de mudança, versionar cada mudança como se fosse código, e nunca comparar números que vieram de versões distintas sem dizer que são distintas. Este artigo mostra como fazer isso na prática, com o que congelar, o que renovar, como saber que chegou a hora e como medir se o eval ainda tem alguma coisa a dizer sobre o sistema.

2026-08-09 / IA Aplicada / 15 min

01

Por que o eval envelhece mesmo sem ninguém mexer nele

Um conjunto de avaliação é uma amostra de um mundo que se move. Ele fica velho por três caminhos independentes, e os três acontecem em paralelo em qualquer produto vivo. O primeiro é a mudança do tráfego: os clientes passam a perguntar coisas que não perguntavam, entram novos produtos no catálogo, uma campanha muda o perfil de quem chega, uma feature nova cria uma classe inteira de dúvida que não existia. O eval montado no trimestre passado mede um público que não é mais o seu.

O segundo caminho é o vazamento por correção, e é o mais insidioso porque é consequência direta de fazer a coisa certa. Todo caso que entra no eval porque o sistema errou é um caso que alguém vai corrigir logo em seguida. Com o tempo, o conjunto se transforma numa lista de problemas já resolvidos, e a nota sobe sem que a qualidade percebida pelo cliente mude. O terceiro é a otimização contra a régua: quando o time ajusta o prompt olhando o resultado do eval, o eval deixa de ser uma medição independente e vira um alvo, e o sistema aprende a acertar aqueles casos específicos em vez de aprender a tarefa.

Forma de envelhecerSintoma no painelO que o número passa a significar
Deriva de tráfegoNota estável enquanto reclamações sobemQualidade em um público que não é mais o seu
Vazamento por correçãoNota sobe de forma monótona, sem platôQuantos bugs antigos continuam corrigidos
Otimização contra a réguaNota alta no eval, queda em amostra novaAderência aos casos do eval, não à tarefa
Rótulo desatualizadoFalhas que a revisão humana considera corretasAderência a uma política que já mudou

A quarta linha merece destaque porque quase nunca é lembrada. As respostas esperadas do eval refletem a política do produto no dia em que foram escritas. Se a empresa mudou a regra de reembolso, o eval continua exigindo a resposta antiga e marcando como erro a resposta certa. Nesse ponto o eval não está apenas velho, ele está ativamente errado, e um time disciplinado que persegue eval verde vai empurrar o sistema de volta para o comportamento que o produto abandonou.

02

Congelar não é deixar parado: as três camadas

A confusão que trava a maioria dos times é tratar o eval como um objeto único que ou está congelado ou está mudando. Ele deve ser dividido em camadas com contratos de mudança diferentes, e cada camada responde a uma pergunta distinta. Uma camada existe para detectar regressão e nunca pode mudar. Outra existe para representar o tráfego atual e precisa mudar. A terceira existe para proteger contra otimização e você nem pode olhar.

Camadas do conjunto de avaliacao

  +-- NUCLEO CONGELADO (30-40% dos casos) ------------+
  |  Casos canonicos, invariantes de seguranca,       |
  |  comportamentos que nunca podem regredir.         |
  |  Muda so por RFC explicita. Comparavel entre       |
  |  todas as versoes do sistema.                     |
  +---------------------------------------------------+

  +-- JANELA ROTATIVA (40-50% dos casos) -------------+
  |  Amostra do trafego dos ultimos N meses.          |
  |  Renovada a cada ciclo. Comparavel apenas         |
  |  dentro da mesma versao do conjunto.              |
  +---------------------------------------------------+

  +-- HOLDOUT SELADO (20-30% dos casos) --------------+
  |  Nunca olhado durante o desenvolvimento.          |
  |  Aberto so na decisao de release.                 |
  |  Detecta otimizacao contra a regua.               |
  +---------------------------------------------------+

  nucleo verde + janela verde + holdout muito abaixo
     -> o time otimizou contra o eval, nao melhorou o sistema

O núcleo congelado é o que dá comparabilidade histórica. Ele contém os casos que definem o mínimo aceitável: a recusa que o bot precisa fazer, o dado sensível que ele não pode repetir, a escalada que ele precisa disparar, o cálculo que ele não pode errar. Esses casos não mudam porque o tráfego mudou; eles mudam quando a política do produto muda, e aí a mudança passa por revisão explícita e vira uma nova versão do núcleo, com o número antigo preservado.

A janela rotativa é o oposto: ela deve mudar, porque a função dela é responder "como o sistema se sai no que os clientes estão perguntando agora". Amostrar essa janela do tráfego real, e não de casos que alguém escreveu à mão, é o que evita o vazamento por correção. E o holdout selado é o instrumento de honestidade: se a nota do holdout acompanha as outras duas, a melhoria é real; se ela fica para trás, o ganho foi decorado.

// eval/dataset.js
// Conjunto de avaliacao em tres camadas com contratos de mudanca distintos.
// O nucleo congela, a janela rotaciona, o holdout so abre no release.

import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';

const load = (file) => JSON.parse(readFileSync(new URL(file, import.meta.url), 'utf8'));

export function loadEvalSuite({ allowHoldout = false } = {}) {
  const core = load('./core.frozen.json');       // muda so por RFC
  const window = load('./window.current.json');  // rotaciona por ciclo
  const holdout = load('./holdout.sealed.json'); // aberto so no release

  // O hash do nucleo entra em todo relatorio: dois numeros so sao
  // comparaveis se vierem do mesmo hash de nucleo.
  const coreHash = createHash('sha256')
    .update(JSON.stringify(core.cases))
    .digest('hex')
    .slice(0, 12);

  const suite = {
    coreVersion: core.version,
    coreHash,
    windowVersion: window.version,
    layers: { core: core.cases, window: window.cases },
  };

  // Barreira explicita: acessar o holdout fora do release e o erro
  // que transforma o instrumento de honestidade em mais um alvo.
  if (allowHoldout) {
    suite.layers.holdout = holdout.cases;
  }

  return suite;
}

export function assertComparable(reportA, reportB) {
  if (reportA.coreHash !== reportB.coreHash) {
    throw new Error(
      `Nucleo diferente (${reportA.coreVersion} vs ${reportB.coreVersion}): ` +
        'as notas nao sao comparaveis diretamente.',
    );
  }
}

A função de comparabilidade no final não é um detalhe de engenharia, é o mecanismo que impede o erro mais comum na renovação: trocar o conjunto e continuar plotando a série histórica como se nada tivesse acontecido. Um salto de cinco pontos no gráfico que na verdade foi troca de conjunto contamina toda decisão tomada em cima dele, e como o gráfico não avisa, ninguém percebe.

03

Como renovar a janela sem quebrar a série histórica

Renovar a janela é substituir casos antigos por uma amostra nova do tráfego. O erro que arruina a comparabilidade é fazer isso de um dia para o outro e seguir olhando o mesmo número. O procedimento que preserva a leitura tem uma etapa a mais: antes de aposentar o conjunto velho, rode as duas versões na mesma versão do sistema e registre a diferença. Essa diferença é a calibração entre os dois conjuntos, e ela separa "o sistema mudou" de "a régua mudou".

  1. Amostre o novo tráfego por estrato, não uniformemente: preserve a proporção de intenções, canais e idiomas do período, senão o conjunto novo mede um recorte enviesado.
  2. Rotule com a política vigente, não com a política do conjunto antigo, e registre a data da política junto com o caso.
  3. Rode o sistema atual, sem mudanças, nas duas versões do conjunto e anote o delta: essa é a diferença de dificuldade entre conjuntos, não de qualidade do sistema.
  4. Publique a série histórica com uma marca visível de troca de conjunto no ponto da renovação, e o delta de calibração ao lado.
  5. Guarde o conjunto antigo executável, não apenas arquivado: sem poder rodá-lo de novo, você perde a capacidade de reinterpretar decisões passadas.
  6. Promova ao núcleo os casos da janela que viraram invariantes de fato, mas só via revisão, nunca automaticamente por antiguidade.

O item três é o que a maioria pula por pressa, e é o único que custa quase nada: uma execução extra do sistema atual contra o conjunto velho, no mesmo dia da troca. Sem esse número, quando a nota cair três pontos no mês seguinte ninguém vai saber se o sistema piorou ou se o conjunto novo é mais difícil, e a discussão vira opinião. Com ele, a resposta é aritmética.

CamadaFrequência de mudançaQuem autorizaComparável com
Núcleo congeladoSó quando a política mudaRevisão explícita com registro do motivoTodo o histórico com o mesmo hash
Janela rotativaA cada ciclo definido, tipicamente mensal ou trimestralDono do eval, seguindo o procedimento de amostragemApenas a mesma versão da janela
Holdout seladoRenovado quando é abertoAberto só na decisão de releaseNada: é medida pontual, não série

04

Os gatilhos que dizem que chegou a hora

Renovar por calendário funciona, mas é grosseiro: renova cedo demais quando o produto está estável e tarde demais quando ele muda rápido. Os gatilhos abaixo são sinais mensuráveis de que o conjunto perdeu representatividade, e vale monitorá-los como se fossem alertas de produção, porque é exatamente isso que eles são.

  • Cobertura de intenção abaixo do limite: uma fatia relevante do tráfego cai em intenções que o conjunto não representa, tipicamente quando um lançamento cria uma família nova de perguntas.
  • Saturação da nota: o conjunto passou de um teto alto e parou de discriminar, ou seja, versões claramente diferentes do sistema tiram notas praticamente iguais.
  • Divergência entre eval e realidade: a nota fica estável enquanto reclamações, transbordos para humano ou reaberturas de ticket sobem.
  • Abertura do holdout revelando distância: a diferença entre a nota da janela e a do holdout cresce, sinal de otimização contra a régua.
  • Mudança de política registrada: qualquer alteração na regra de negócio invalida os rótulos afetados, e isso é gatilho imediato, não do próximo ciclo.
  • Troca de modelo ou de provedor: a mudança pode alterar o perfil de erro e revelar classes de falha que o conjunto atual não cobre.

A saturação merece um cuidado extra porque costuma ser confundida com sucesso. Quando o conjunto está saturado, ele para de responder à pergunta para a qual existe, que é distinguir uma versão da outra. Um eval em que todas as versões tiram 96% não diz que o sistema está ótimo, diz que o instrumento chegou ao fim da escala. A checagem barata é o poder discriminatório: pegue duas versões que você sabe serem diferentes, por exemplo o modelo atual e um modelo notoriamente mais fraco, e veja se o conjunto separa as duas. Se não separar, o conjunto parou de medir.

// eval/health.js
// Saude do proprio conjunto de avaliacao. Um eval que nao discrimina
// versoes diferentes parou de responder a pergunta para a qual existe.

export function evalHealth({ scores, trafficIntents, coveredIntents, weakBaseline }) {
  const covered = new Set(coveredIntents);
  const totalTraffic = trafficIntents.reduce((sum, i) => sum + i.volume, 0);
  const uncovered = trafficIntents
    .filter((i) => !covered.has(i.name))
    .reduce((sum, i) => sum + i.volume, 0);

  // Quanto do trafego real cai em intencoes que o conjunto nao representa.
  const coverageGap = totalTraffic > 0 ? uncovered / totalTraffic : 0;

  // Poder discriminatorio: distancia entre o sistema atual e uma baseline
  // que sabemos ser pior. Se encolhe, o conjunto saturou.
  const discrimination = scores.current - weakBaseline;

  // Distancia janela x holdout: cresce quando se otimiza contra a regua.
  const overfitGap = scores.window - scores.holdout;

  return {
    coverageGap,
    discrimination,
    overfitGap,
    needsRenewal: coverageGap > 0.15 || discrimination < 0.1 || overfitGap > 0.08,
    reasons: [
      coverageGap > 0.15 && 'cobertura de intencao insuficiente',
      discrimination < 0.1 && 'conjunto saturado, nao discrimina versoes',
      overfitGap > 0.08 && 'otimizacao contra a regua detectada',
    ].filter(Boolean),
  };
}

Os limiares desse código são pontos de partida, não constantes universais: eles dependem do tamanho do conjunto e da variância do seu domínio, e a forma honesta de calibrá-los é medir a variância de execuções repetidas do mesmo sistema no mesmo conjunto. Se rodar duas vezes o mesmo sistema já produz três pontos de diferença, um limiar de oito pontos para vazamento é razoável; se produz meio ponto, oito é frouxo demais e você vai demorar a enxergar o problema.

05

Governança: quem muda o quê e com qual registro

A parte técnica de renovar um eval é simples. O que costuma falhar é a governança, porque o conjunto de avaliação tem um dono difuso: todo mundo adiciona casos, ninguém remove, e nenhuma mudança fica registrada. Depois de um ano, ninguém sabe por que aquele caso está lá, se o rótulo ainda vale, nem quem decidiu que a resposta esperada é aquela. O conjunto vira um sedimento em vez de um instrumento.

O tratamento é o mesmo que se dá a qualquer artefato de produção: o conjunto vive no repositório, mudanças passam por revisão, e cada caso carrega procedência. Um caso sem origem, sem data de rótulo e sem justificativa é um caso que ninguém vai poder auditar quando ele começar a falhar por motivo legítimo.

// eval/core.frozen.json (trecho)
{
  "version": "core-2026-03",
  "policyDate": "2026-03-14",
  "cases": [
    {
      "id": "core-refund-window-001",
      "layer": "core",
      "input": "comprei ontem e quero cancelar, ainda da tempo?",
      "expected": {
        "mustState": ["prazo de 7 dias corridos"],
        "mustNotState": ["reembolso imediato garantido"],
        "mustCall": ["consultar_pedido"]
      },
      "rationale": "Prazo legal: errar aqui gera risco juridico, nao so ma experiencia.",
      "source": "politica-comercial-v4",
      "labeledAt": "2026-03-14",
      "labeledBy": "suporte-lead",
      "frozenSince": "2026-03-14"
    }
  ]
}

Repare que a expectativa não é uma string de resposta ideal. Comparar a saída do modelo com um texto exato é frágil e reprova respostas boas por diferença de redação. Descrever o que a resposta precisa afirmar, o que não pode afirmar e qual ferramenta precisa chamar é um contrato verificável que sobrevive a mudanças de estilo do modelo, e é justamente o que permite congelar o caso por muito tempo sem que ele fique falso.

  1. Dono nomeado por camada: alguém responde pelo núcleo e alguém pela janela, com autoridade para recusar adição de caso.
  2. Toda mudança de rótulo registra a data e a política de referência, para reconstruir por que a expectativa era aquela.
  3. Remoção é tão legítima quanto adição: caso obsoleto sai do conjunto e vai para o arquivo executável, não fica ocupando espaço e distorcendo a média.
  4. Relatório sempre carimba a versão do conjunto e o hash do núcleo junto com a nota, sem exceção.
  5. Nenhum resultado de holdout entra em painel de acompanhamento contínuo: o holdout só aparece na decisão de release.

06

O que o eval renovado deve devolver

Um conjunto de avaliação bem mantido não devolve um número, devolve uma leitura com três eixos que dizem coisas diferentes e não devem ser somados em uma média única. A nota do núcleo responde "alguma coisa que nunca podia quebrar quebrou?", e ela deve ser um portão binário: qualquer queda aqui bloqueia release, independentemente do resto. A nota da janela responde "como estamos no tráfego de hoje?", e ela é um indicador de tendência, comparável apenas dentro da mesma versão de janela. A nota do holdout responde "a melhoria é real?", e ela só é consultada quando há decisão a tomar.

EixoPergunta que respondeComo usarErro comum
NúcleoAlgo que nunca podia regredir regrediu?Portão binário de releaseDiluir na média geral e deixar uma regressão crítica passar
JanelaComo o sistema vai no tráfego atual?Tendência dentro da mesma versãoComparar entre versões diferentes sem calibrar
HoldoutA melhoria é real ou decorada?Consulta pontual na decisão de releaseColocar no painel diário e transformar em novo alvo

A tentação de colapsar os três em um índice único é forte porque um número é mais fácil de reportar. Mas a média esconde exatamente o que cada eixo existe para revelar: uma regressão de segurança no núcleo desaparece dentro de uma janela grande com desempenho bom, e é essa a falha que você menos pode deixar passar. Reportar três números com significados distintos custa uma linha a mais no relatório e preserva a informação inteira.

No fim, congelar e renovar não são fases opostas de um ciclo, são responsabilidades simultâneas de partes diferentes do mesmo conjunto. O que precisa ficar parado fica parado para que você possa comparar hoje com o ano passado. O que precisa se mover se move para que você continue medindo o cliente que existe agora. E a parte que ninguém pode olhar existe para que as outras duas continuem dizendo a verdade. Um eval que só congela vira folclore; um que só renova vira ruído. A disciplina está em saber qual pedaço é qual, e registrar toda vez que essa fronteira muda.

FAQ

Perguntas frequentes

Com que frequência devo renovar o conjunto de avaliação?

Renovar por calendário puro é grosseiro, porque renova cedo demais quando o produto está estável e tarde demais quando ele muda rápido. Um ciclo de referência mensal ou trimestral para a janela rotativa funciona como base, mas a decisão real deve vir de gatilhos mensuráveis: cobertura de intenção abaixo do limite quando um lançamento cria uma família nova de perguntas, saturação da nota quando versões claramente diferentes do sistema tiram resultados praticamente iguais, divergência entre a nota estável e reclamações ou transbordos que sobem, e distância crescente entre a nota da janela e a do holdout. Mudança de política de produto e troca de modelo ou provedor são gatilhos imediatos, não do próximo ciclo, porque a primeira invalida rótulos e a segunda muda o perfil de erro. O núcleo congelado segue outra regra: ele só muda quando a política muda, e via revisão explícita.

Se eu trocar o conjunto, perco a comparação com os resultados anteriores?

Perde se trocar sem calibrar, e é o erro mais comum da renovação: substituir os casos e continuar plotando a mesma série histórica como se nada tivesse acontecido, o que transforma diferença de dificuldade entre conjuntos em falsa evidência de melhoria ou piora. O procedimento que preserva a leitura acrescenta uma etapa barata: antes de aposentar o conjunto velho, rode o sistema atual sem nenhuma mudança nas duas versões do conjunto e registre o delta. Esse delta é a diferença de dificuldade, não de qualidade, e permite ler a série depois da troca. Junto com isso, marque visivelmente o ponto de troca no gráfico, guarde o conjunto antigo executável e não apenas arquivado, e carimbe a versão do conjunto e o hash do núcleo em todo relatório, para que dois números só sejam comparados diretamente quando vierem da mesma régua.

Por que manter um holdout que nunca é olhado durante o desenvolvimento?

Porque ele é o único instrumento que detecta otimização contra a régua. Quando o time ajusta o prompt olhando o resultado do eval, o conjunto deixa de ser medição independente e vira alvo, e o sistema aprende a acertar aqueles casos específicos em vez de aprender a tarefa, o que produz uma nota alta que não se traduz em qualidade percebida. O holdout selado, amostrado do mesmo tráfego mas nunca consultado durante o desenvolvimento, dá a resposta: se a nota dele acompanha as demais, a melhoria é real; se fica para trás, o ganho foi decorado. Por isso ele nunca pode entrar em painel de acompanhamento contínuo, já que um holdout olhado toda semana vira apenas mais um alvo, e por isso ele é renovado quando é aberto, na decisão de release.

Um eval só continua útil se você souber qual parte pode mudar

Conjunto de avaliação não é artefato que se monta uma vez: ele envelhece por deriva de tráfego, por vazamento de casos já corrigidos, por otimização contra a régua e por rótulo preso a uma política que mudou, e envelhece em silêncio, com a nota subindo enquanto o cliente reclama. Separar o conjunto em núcleo congelado, janela rotativa e holdout selado, calibrar toda troca rodando o sistema atual nas duas versões, carimbar versão e hash em todo relatório e monitorar a saúde do próprio eval transforma um número que ninguém confia em três leituras que decidem release. Posso montar e versionar esse conjunto no seu sistema de IA, definir o que congela e o que rotaciona, instalar o holdout e deixar o relatório com os três eixos separados, para você saber se melhorou de verdade em vez de comemorar um instrumento saturado.