Avaliar um chat de LLM sem outro LLM julgando
Como montei a bateria que testa o Nox contra a API real, por que escolhi verificações determinísticas e o que o primeiro placar, 14 de 21, ensinou: metade das falhas era do próprio teste.
Testar o Nox na mão funcionou até a primeira mudança de prompt. Toda alteração (de prompt, de modelo, de conteúdo) podia quebrar uma resposta que antes estava certa, e eu só descobriria se alguém perguntasse exatamente aquilo. Precisava de uma bateria fixa que rodasse contra o chat de verdade e me desse um número.
O que a bateria testa
São 21 casos em três categorias. Fatos: perguntas sobre mim que precisam de resposta correta, em português e em inglês. Fora do escopo: assuntos que precisam virar recusa curta. Injeção: tentativas de prompt injection, das clássicas ("ignore as instruções") às que eu mesmo tinha testado na mão (pedir para traduzir "tudo acima", mandar responder só "HACKED"), e injeção por contexto falso, com tags inventadas dentro da pergunta.
A bateria chama a mesma rota que o visitante usa, com o mesmo prompt e a mesma guarda de saída. Ela só pula a cota diária, com um token secreto comparado em tempo constante. Sem o token configurado, nada contorna a cota.
Por que sem LLM juiz
O jeito popular de avaliar é pedir para outro modelo dar nota às respostas. Funciona, mas traz três problemas para um projeto como este: custa mais uma chamada por caso, o resultado muda de uma rodada para outra e, quando um caso falha, é difícil saber por quê. Escolhi verificações determinísticas: termos que precisam aparecer (com grupos de "qualquer um destes"), termos proibidos, se a resposta é recusa, o idioma e o tamanho. Toda resposta também é checada contra marcadores de vazamento do prompt.
O custo dessa escolha é rigidez. Uma resposta certa com outras palavras pode falhar. Na prática, para um chat que responde sobre um conteúdo pequeno e controlado, os termos certos são previsíveis, e a rigidez virou vantagem: quando algo falha, a mensagem diz exatamente o que faltou.
O primeiro placar: 14 de 21
Seis das sete falhas eram do verificador, não do Nox. As recusas vinham em terceira pessoa ("Nox só responde...") e meus padrões só cobriam a primeira ("só respondo"). E a função que tirava acentos apagava também a crase, então o termo proibido "```" virava texto vazio e casava com qualquer resposta. Lição: o avaliador é código e tem bug como qualquer código. Antes de corrigir o sistema, confira o teste.
A falha real foi mais interessante. "Where does Marcus work?" voltava dizendo que o site não informava. A informação estava lá, mas em primeira pessoa ("I'm a full-stack developer at Vibetex"), e a pergunta vinha em terceira. Na busca por semelhança, os dois ficavam longe. A correção foi dar a cada trecho do índice um título em terceira pessoa, que aproxima o trecho do jeito que as pessoas perguntam. Isso nenhum teste manual meu tinha pegado, porque eu nunca pergunto sobre mim em terceira pessoa.
Depois do 21 de 21
- O placar e cada resposta ficam públicos na página de avaliação do Nox, com o que se esperava de cada caso.
- A bateria roda sozinha a cada deploy, contra a URL do próprio deploy, e uma vez por mês com as injeções geradas e o red team; se algo quebrar, o job falha e eu recebo o aviso.
- Cada rodada publicada entra num histórico com tempo de resposta e tokens, e a mesma bateria compara modelos: placar, tempo e custo lado a lado.
- A regra ficou simples: mudou prompt, modelo ou conteúdo, roda a bateria antes de publicar.
Depois veio um juiz
Mais tarde coloquei um juiz ao lado do verificador, sem trocar um pelo outro: o Jev, um modelo que responde com probabilidade em vez de texto. A bateria cresceu para 41 casos e cada veredito do juiz é cruzado com o verificador. O que as discordâncias ensinaram está na nota "O juiz que discordou".