en
· 3 min de leitura

Testando sistemas não determinísticos

Você não pode fazer assert em uma string exata de um modelo. Pode fazer assert em propriedades, distribuições e invariantes, e deve.

aiengineering

O primeiro teste que escrevi para uma feature baseada em modelo fazia assert de que a saída era igual a uma string. Passou uma vez. A segunda execução produziu um sinônimo e o teste falhou, e a terceira produziu o original de novo. Eu tinha escrito um teste flaky de propósito sem perceber. Testar sistemas com um modelo dentro exige abrir mão da igualdade exata e adotar um kit diferente, um que engenheiros de sistemas distribuídos e algoritmos randomizados já conhecem. O modelo não é intestável. Ele é testável do jeito que um load balancer é testável: por propriedades, ao longo de muitas execuções, com tolerâncias.

Separe o que é determinístico

A maior parte do código em volta de um modelo é código comum. Montagem de prompt, retrieval, parsing, validação, roteamento, fallbacks. Tudo isso recebe testes unitários comuns com o modelo substituído por um stub que devolve saídas fixas, incluindo saídas malformadas. É aí que a maioria dos bugs mora, e é aí que testes são baratos e exatos. Eu coloco o modelo atrás de uma interface justamente para que essa camada possa ser testada sem ele. Um time que testa "a feature de IA" de ponta a ponta e nada mais tem uma suíte lenta, flaky, e nenhuma ideia de qual camada quebrou.

Testes de propriedade para o modelo em si

Para a chamada ao modelo, faço assert em propriedades da saída, não no conteúdo. A saída parseia conforme o schema. Os campos obrigatórios estão presentes. A classificação é um dos rótulos permitidos. O resumo é mais curto que a entrada. A data extraída aparece no texto de origem. A resposta cita um trecho que existe no conjunto recuperado. Cada propriedade é uma classe real de bug que já vi em produção, e cada uma pode ser verificada sem saber a redação exata. Esses testes rodam contra o modelo real em um conjunto pequeno e fixo, e falham fazendo barulho quando o provedor muda algo.

Evals como testes estatísticos

Qualidade é uma distribuição, então testo como tal. Um conjunto de evals com algumas centenas de entradas reais e saídas esperadas ou uma regra de pontuação, rodado contra a implementação atual, produz uma nota. O teste é que a nota não cai abaixo de um limiar, e que não cai de forma significativa em relação à última execução. Uma entrada única virando de certa para errada é ruído. Vinte virando é regressão. Rodo isso a cada mudança de prompt e a cada troca de modelo, no CI, com os resultados armazenados para que a tendência fique visível. É o único teste que diz se a feature piorou.

Seeds, temperatura e replay

Onde o provedor permite, defino temperatura zero e seed fixa para os testes, o que remove a maior parte da variância sem remover toda. Não dependo disso. Do que dependo é de traces gravados: requests reais de produção com suas saídas reais, reproduzidos contra uma nova versão e comparados. A diferença é revisada por uma pessoa ou pontuada por um modelo. Não é um teste de passa ou falha no sentido tradicional, é um relatório de mudança, e já pegou mais surpresas do que qualquer assert.

O que um build verde significa

Quando um build passa, significa que o código determinístico está correto, a saída do modelo satisfaz as propriedades estruturais, e a nota de qualidade no conjunto de evals não regrediu. Não significa que a feature está certa para toda entrada. Nada significa isso. O que a suíte de testes te dá é a capacidade de mudar o prompt, o modelo ou o retrieval numa terça à tarde e saber em dez minutos se você quebrou algo mensurável. Para um sistema não determinístico, isso é o jogo inteiro.