LLM ou modelo de decisão: como comparar os dois no mesmo conjunto de teste

comparação LLM vs classificador usando matriz de confusão no mesmo conjunto de teste
Resposta rápida

Pra comparar LLM vs classificador de forma justa, trate como experimento: separe um conjunto de casos já decididos (rótulo real conhecido e fora do treino), rode o classificador e o prompt do LLM sobre as MESMAS entradas e force saída categórica pra corrigir por exact match. Depois gere a matriz de confusão de cada um com confusion_matrix(y_true, y_pred) do scikit-learn e compare falsos positivos e falsos negativos. Use precisão ou recall conforme o erro que custa mais caro, e confirme a diferença com o teste de McNemar, sem confiar na impressão de que a resposta ‘parece melhor’

Fala aí, beleza? Se tu tem um classificador rodando e alguém do time já soltou o clássico ‘joga isso num LLM que fica melhor’, esse post é pra você 🙂

Achar que a resposta do LLM parece melhor não é comparação, é impressão

E impressão não segura a bronca quando o modelo começa a errar justo nos casos que importam…

Quem já tem um classificador tradicional (ou uma regra de decisão, tipo aquele monte de if bem pensado) e quer saber se um prompt com LLM faz melhor precisa montar um experimento de verdade

Mesmas entradas, mesma régua pros dois, e no final você conta os erros de cada um

Bora ver na prática?

O que você precisa antes de comparar

Antes de abrir o editor, se liga nessa lista:

  • Um conjunto de casos já decididos: entradas com o rótulo real conhecido e que NÃO foram usadas no treino do classificador (se foram, ele já viu a prova antes e a comparação fica zoada)
  • O classificador tradicional já treinado, pronto pra prever
  • O prompt do LLM com saída categórica: ele responde uma classe, não um parágrafo
  • Python com scikit-learn, pra usar confusion_matrix e classification_report
  • Um critério de sucesso escrito antes de rodar qualquer coisa

Esse último parece detalhe, mas é o que segura o experimento de pé

A documentação da Anthropic sobre critérios de sucesso e avaliações orienta trocar algo vago como ‘bom desempenho’ por algo específico e mensurável, tipo ‘classificação de sentimento precisa’

Sem isso, qualquer resultado vira argumento pra qualquer lado haha

Passo a passo: comparando LLM e modelo de decisão no mesmo conjunto de teste

Pra deixar concreto, vou usar um exemplo simples: detectar se um texto de cliente é uma reclamação (classe 1) ou outro (classe 0)

Troca pela sua tarefa e o método é o mesmo 😀

1. Defina o critério de sucesso e qual erro custa mais

O PORQUÊ primeiro: todo classificador erra de dois jeitos

Falso positivo é dizer que é reclamação quando não é

Falso negativo é deixar uma reclamação passar batido como se fosse outra coisa

Qual dos dois dói mais na sua operação? Essa resposta decide a métrica que você vai olhar:

  • Precisão = tp / (tp + fp): é a métrica pra quando o falso positivo é o erro caro
  • Recall = tp / (tp + fn): é a métrica pra quando o falso negativo é o erro caro

E se os dois importam? Aí entra o F-beta, uma média harmônica ponderada de precisão e recall que vai de 0 (pior) a 1 (melhor), onde o beta define quanto o recall pesa em relação à precisão

Escreve isso num arquivo, versionado junto com o código:

# criterio.py: escrito ANTES de rodar qualquer modelo
CRITERIO = {
    'tarefa': 'detectar reclamacao (1) vs outro (0)',
    'erro_mais_caro': 'falso negativo',  # reclamacao que passa batido
    'metrica_principal': 'recall da classe 1',
    'metrica_de_apoio': 'precisao da classe 1',
}

O erro comum deste passo: decidir a métrica DEPOIS de ver o resultado

Aí você escolhe sem perceber a métrica em que o seu favorito ganha, e o experimento vira só uma confirmação do que você já achava

2. Monte o conjunto de teste espelhando a tarefa real

A documentação da Anthropic recomenda que a avaliação espelhe a distribuição real da tarefa e inclua casos de borda

Traduzindo: se na vida real a maioria dos textos não é reclamação, o seu conjunto de teste também não pode ser metade metade

E os casos de borda são onde a briga acontece de verdade

O próprio exemplo de sentimento da Anthropic traz dois que valem ouro:

casos = [
    # sarcasmo: elogio na superficie, reclamacao no fundo
    {'texto': 'I just love it when my flight gets delayed for 5 hours. #bestdayever', 'real': 1},
    # misto: a regra escrita no passo 1 decide como isso conta
    {'texto': "The movie's plot was terrible, but the acting was phenomenal.", 'real': 1},
    # ... os casos comuns da sua operacao, na proporcao real
]

Se liga no caso misto: na doc da Anthropic ele é rotulado como ‘misto’, mas no meu exemplo binário eu precisei decidir pra qual lado ele vai

Essa decisão TEM que estar escrita, senão cada pessoa rotula de um jeito

O erro comum deste passo: montar o conjunto só com casos fáceis

Com casos fáceis os dois acertam tudo, o empate parece real e você não aprende nada sobre onde cada um quebra

3. Force saída categórica no prompt do LLM

Aqui mora metade dos problemas de quem compara LLM vs classificador

O classificador devolve 0 ou 1, sem drama

O LLM, se você deixar, devolve um textão explicando que ‘depende do contexto’ xD

A Anthropic cita alguns formatos de correção automática (múltipla escolha, comparação de string, corrigido por código ou corrigido por LLM) e indica o exact match pra respostas categóricas como sentimento

Então o prompt precisa amarrar a saída:

Classifique o texto do cliente abaixo

Responda APENAS com uma palavra, sem pontuação e sem explicação:
RECLAMACAO ou OUTRO

Texto: {texto}

E do lado do código, tu converte a resposta com exact match e marca o que vier fora do formato:

MAPA = {'RECLAMACAO': 1, 'OUTRO': 0}

def converter(resposta_llm):
    rotulo = resposta_llm.strip().upper()
    return MAPA.get(rotulo, -1)  # -1 = saida invalida, conta como erro

O erro comum deste passo: aceitar texto livre e corrigir no olho

Correção no olho é exatamente a impressão que a gente quer tirar da jogada

E saída inválida não some da conta: ela é um erro do LLM e tem que aparecer no resultado

4. Rode os dois sobre as mesmas entradas

Regra de ouro: o classificador e o LLM veem EXATAMENTE os mesmos casos, na mesma ordem, e tu guarda tudo lado a lado

textos = [c['texto'] for c in casos]
y_true = [c['real'] for c in casos]

# classificador: o seu pipeline ja treinado
y_pred_clf = list(modelo.predict(textos))

# LLM: classificar_com_llm e a sua funcao que chama a API
# e devolve o texto da resposta
RODADAS = 3  # exemplo, ajuste ao seu orcamento
rodadas_llm = []
for _ in range(RODADAS):
    rodadas_llm.append([converter(classificar_com_llm(t)) for t in textos])

from collections import Counter

# voto da maioria entre as rodadas: uma previsao final por caso,
# usando TODAS as rodadas e nao so a primeira
y_pred_llm = [
    Counter(r[i] for r in rodadas_llm).most_common(1)[0][0]
    for i in range(len(textos))
]

# casos em que o LLM mudou de ideia entre as rodadas
divergentes = [
    i for i in range(len(textos))
    if len({r[i] for r in rodadas_llm}) > 1
]
print(f'Casos instaveis no LLM: {len(divergentes)}')

Por que rodar mais de uma vez?

Porque, segundo a Anthropic (numa citação reproduzida pelo blog da memx), mesmo com temperature 0 o resultado não é totalmente determinístico: entradas idênticas podem gerar saídas diferentes entre chamadas da API

O classificador tradicional não tem esse problema, então ignorar a variação do LLM deixaria a comparação torta

Por isso o y_pred_llm sai do voto da maioria entre as rodadas: a variação entra na conta, em vez de tu apostar tudo numa rodada só que pode ter sido sorte (ou azar)

Os casos instáveis são informação, não ruído: anota quais são e dá uma olhada neles depois

O erro comum deste passo: rodar o LLM numa amostra diferente da do classificador

Tipo pegar só uma parte dos casos pra economizar chamada de API

Aí você está comparando dois modelos em duas provas diferentes, e o resultado não diz nada

5. Gere e leia a matriz de confusão de cada modelo

Agora sim a parte divertida 😀

A função do scikit-learn é a confusion_matrix, que recebe os rótulos reais (y_true) e os previstos (y_pred):

from sklearn.metrics import confusion_matrix

for nome, y_pred in [('classificador', y_pred_clf), ('LLM', y_pred_llm)]:
    cm = confusion_matrix(y_true, y_pred, labels=[0, 1])
    tn, fp, fn, tp = cm.ravel()
    print(nome, '| FP:', fp, '| FN:', fn)
    print(cm)

O labels=[0, 1] fixa a ordem das classes, e as saídas inválidas (-1) do LLM ficam fora da matriz, então conta elas à parte como erro

E como ler isso?

Na documentação do scikit-learn, cada célula da matriz conta as observações que são da classe da linha e foram previstas como a classe da coluna

Ou seja: linha é o real, coluna é o previsto

Previsto 0 (outro) Previsto 1 (reclamação)
Real 0 (outro) C[0,0]: verdadeiro negativo C[0,1]: falso positivo
Real 1 (reclamação) C[1,0]: falso negativo C[1,1]: verdadeiro positivo

Coloca as duas matrizes uma do lado da outra e olha primeiro a célula do erro que você definiu como caro lá no passo 1

O erro comum deste passo: inverter linha e coluna

Já vi muita gente ler falso positivo onde era falso negativo e tirar a conclusão exatamente ao contrário

Tome cuidado: linha é o que o caso É, coluna é o que o modelo DISSE

6. Rode o classification_report pra ver precisão e recall por classe

A matriz mostra os erros crus, o classification_report do scikit-learn transforma isso em métrica:

from sklearn.metrics import classification_report

print('CLASSIFICADOR')
print(classification_report(y_true, y_pred_clf))

print('LLM')
print(classification_report(y_true, y_pred_llm))

Ele gera um relatório em texto com precisão, recall, F1 e suporte por classe, além de duas médias:

  • Média macro: média simples por rótulo, então a classe pequena pesa igual à grande
  • Média ponderada: pondera pelo suporte de cada rótulo, então a classe grande domina

Se a classe que importa é a minoritária (reclamação, fraude, urgência…), a macro e a linha dessa classe contam MUITO mais que a ponderada

O erro comum deste passo: olhar só a acurácia geral com classes desbalanceadas

Se quase tudo é ‘outro’, um modelo que chuta ‘outro’ pra tudo tem acurácia linda e recall zero na classe que você queria pegar…

7. Teste se a diferença é real com McNemar

Beleza, o LLM errou um pouco menos que o classificador (ou o contrário)

Mas será que essa diferença é real ou foi sorte da amostra?

É aqui que entra o teste de McNemar, que compara dois classificadores sobre o MESMO conjunto de teste usando dados pareados: acerto ou erro de cada modelo em cada item

Segundo esse mesmo artigo, quem recomendou essa abordagem pra comparar classificadores foi Thomas Dietterich, em 1998, pra casos em que treinar várias cópias do modelo é caro ou inviável (e com LLM no meio, rodar tudo de novo várias vezes custa dinheiro mesmo)

Primeiro tu monta a tabela 2×2:

os_dois_erram = so_clf_erra = so_llm_erra = os_dois_acertam = 0

for real, p_clf, p_llm in zip(y_true, y_pred_clf, y_pred_llm):
    clf_ok = p_clf == real
    llm_ok = p_llm == real
    if not clf_ok and not llm_ok:
        os_dois_erram += 1
    elif not clf_ok and llm_ok:
        so_clf_erra += 1
    elif clf_ok and not llm_ok:
        so_llm_erra += 1
    else:
        os_dois_acertam += 1

print('So o classificador erra:', so_clf_erra)
print('So o LLM erra:', so_llm_erra)
LLM acerta LLM erra
Classificador acerta os dois acertam só o LLM erra
Classificador erra só o classificador erra os dois erram

A sacada do McNemar: ele olha só os casos em que os dois discordam

A hipótese nula é que os dois têm a mesma taxa de erro, e a comparação é feita entre as células ‘só A erra’ e ‘só B erra’

Os casos em que os dois acertam ou os dois erram não dizem nada sobre quem é melhor, faz sentido né?

O cálculo do p-valor em si fica com a biblioteca de estatística que tu já usa, o importante aqui é montar a tabela pareada certinho

O erro comum deste passo: declarar vencedor por uma diferença pequena sem teste nenhum

Alguns erros a menos num conjunto pequeno podem ser puro acaso, e trocar um sistema inteiro por causa de acaso é como fazer deploy na sexta às 18h: até pode dar certo, mas não é estratégia haha

Por que a impressão engana: o que vimos ao colocar modelos lado a lado

Esse papo de mesma entrada e mesma régua não é só teoria de estatística

No vídeo abaixo eu coloquei Kimi K3, GPT-5.6 Sol e Claude Fable 5 pra brigar em tarefas de código, e o que mais me ajudou a não cair na impressão foi justamente o protocolo

Os três rodaram no mesmo nível de esforço (high), em modo autônomo sem pedir confirmação, e receberam exatamente o mesmo prompt em cada projeto

Foram dois projetos iguais pros três, criados em pasta nova e do zero: um e-commerce completo e um jogo 3D estilo mundo aberto

No e-commerce eu pedi cada etapa num prompt separado, e no jogo mandei um prompt único, grande e detalhado (combate, economia, itens, interface e meta de desempenho)

E pra não ter viés de harness, cada modelo rodou na ferramenta de código da própria empresa: Kimi com Kimi Code, GPT com Codex e Claude com Claude Code

Os critérios do e-commerce também estavam definidos antes: qualidade do design e do scaffolding inicial, tempo de criação, fluxo completo de compra (home, produto, carrinho, checkout, confirmação) e existência de uma área admin pra gerir pedidos, produtos e categorias

É o mesmo espírito do passo 1 desse tutorial: régua escrita antes do jogo começar

E aí vem a parte que conversa com a impressão enganando

Antes dos testes eu fiz uma análise da ficha técnica dos três, e depois cruzei com o que vi na prática

No papel, se os três entregassem qualidade parecida, a escolha óbvia seria ir no que custa menos

Só que na prática o Kimi não acompanhou a entrega dos outros dois, então o argumento do preço sozinho não fechou a conta

Entre Sol e Fable, o Sol saiu na frente até o ponto que eu cobri, com o Fable ficando atrás, e o jogo 3D ainda estava em andamento no vídeo, então não é veredito fechado

Uma coisa que eu defendo lá e vale aqui também: compara custo-benefício, não só nota de qualidade

Um modelo nota 8 bem mais barato pode valer mais a pena que um nota 10 caro, dependendo do caso

No seu experimento de classificação, isso vira: se o LLM empata com o classificador nos erros que importam, a diferença de custo por chamada entra na decisão

Se liga num limite importante: aquele teste é de geração de código, não de classificação, então ele não diz nada sobre quem ganha no seu problema de LLM vs classificador

O que ele mostra é o método: mesma tarefa, mesma régua, e olhar o que foi entregue em vez do que pareceu bonito

Se a sua dúvida é escolher modelo pra programar, tem um guia de teste lado a lado no seu projeto que segue essa mesma lógica

Quando o LLM ganha e quando o classificador ganha

Resposta curta: depende da tarefa, e não existe vencedor universal

A literatura faz exatamente o que esse tutorial ensina, comparar LLMs e classificadores tradicionais por matriz de confusão no mesmo conjunto de teste, e os resultados variam de caso pra caso

Alguns exemplos:

  • Detecção de linguagem ofensiva: num estudo publicado no arXiv (2510.03683), o LLaMA 3 8B com fine-tuning QLoRA teve menos falsos positivos e falsos negativos que RoBERTa e ModernBERT
  • Predição clínica: o ClinicalBench compara LLMs com ML tradicional nas bases MIMIC-III e MIMIC-IV, e o próprio título pergunta se os LLMs conseguem superar o ML tradicional nessa tarefa
  • Triagem de risco de suicídio: um estudo comparou Claude e GPT com um SVM usando matriz de confusão

Repara que nem os pesquisadores partem do princípio de que o LLM ganha

Eles perguntam, montam o teste e contam os erros

É exatamente por isso que tu precisa rodar o seu próprio teste: o resultado de um paper em linguagem ofensiva não te diz nada sobre o seu classificador de tickets, de fraude ou de lead

E vale lembrar o outro lado: tem tarefa em que qualquer modelo entrega o mesmo resultado, então antes de montar o experimento todo vale saber quando não vale comparar modelos 🙂

Conclusão: decida pelos erros, não pela impressão

Recapitulando o método:

  1. Escreve o critério de sucesso e qual erro custa mais ANTES de rodar
  2. Monta um conjunto de casos já decididos, fora do treino, com a distribuição real e casos de borda
  3. Força saída categórica no LLM e corrige por exact match
  4. Roda classificador e LLM nas mesmas entradas, registrando a variação do LLM entre rodadas
  5. Lê a matriz de confusão de cada um (linha é o real, coluna é o previsto)
  6. Olha precisão ou recall conforme o erro caro, com média macro quando as classes são desbalanceadas
  7. Confirma a diferença com McNemar antes de declarar vencedor

Com isso, a conversa no time deixa de ser ‘o LLM parece mais esperto’ e vira ‘o LLM deixou passar menos reclamações, e a diferença se sustenta no teste’

Muito mais fácil de defender, né? 😀

Próximo passo: separa hoje um lote de casos já decididos da sua própria operação, e dá uma olhada no guia Classification da Claude Platform pra montar o prompt com saída categórica

E aí, no seu caso, quem tu acha que ganha: o classificador velho de guerra ou o LLM? Faça o teste!

Até o próximo post!

Perguntas frequentes

Qual teste estatístico usar para comparar LLM e classificador no mesmo conjunto de teste?

O teste de McNemar é feito pra isso: ele compara dois classificadores sobre o MESMO conjunto, usando dados pareados de acerto ou erro em cada item

Ele olha só os casos em que os dois discordam, numa tabela 2×2 com as células ‘os dois erram’, ‘só A erra’, ‘só B erra’ e ‘os dois acertam’

A comparação central é entre ‘só A erra’ e ‘só B erra’, e a hipótese nula é que os dois modelos têm a mesma taxa de erro

Por que rodar o LLM várias vezes sobre o mesmo conjunto de teste, se a temperature já está em 0?

Porque mesmo com temperature 0 a Anthropic deixa claro que o resultado não é totalmente determinístico: entradas idênticas podem gerar saídas diferentes entre chamadas da API

Ou seja, uma rodada só pode te dar uma foto de sorte (ou de azar) do LLM naquele dia

Pra comparar de forma justa com o classificador, que é 100% estável, você precisa rodar o LLM sobre o conjunto de teste levando essa variação em conta

Como ler a matriz de confusão quando comparo LLM e classificador tradicional?

Pela convenção do scikit-learn, cada célula da matriz conta as observações da classe da linha que foram previstas como a classe da coluna

Na classificação binária isso dá verdadeiros negativos em C[0,0], falsos negativos em C[1,0], verdadeiros positivos em C[1,1] e falsos positivos em C[0,1]

A função confusion_matrix(y_true, y_pred) recebe os rótulos reais e os previstos, então você monta essa mesma matriz pro classificador e pro LLM e compara lado a lado

LLM sempre ganha do classificador tradicional em tarefas de classificação?

Não existe vencedor universal, depende da tarefa

Num estudo sobre detecção de linguagem ofensiva (arXiv 2510.03683), o LLaMA 3 8B com fine-tuning QLoRA teve menos falsos positivos e falsos negativos que RoBERTa e ModernBERT

Já no ClinicalBench, que compara LLMs com ML tradicional em predição clínica no MIMIC-III e MIMIC-IV, a própria pergunta do título do estudo é se os LLMs conseguem superar o ML tradicional, o que mostra que o resultado não é dado

O que o classification_report do scikit-learn mostra que a matriz de confusão não mostra direto?

O classification_report gera um relatório em texto com precisão, recall, F1 e suporte por classe

Ele ainda traz a média macro, que é a média simples por rótulo, e a média ponderada, que pesa pelo suporte de cada rótulo

Isso ajuda a comparar LLM e classificador quando as classes são desbalanceadas, porque a média ponderada não deixa a classe rara sumir no meio da conta

Quem recomendou o teste de McNemar pra comparar modelos de classificação?

Segundo o artigo do Machine Learning Mastery sobre o teste de McNemar, foi Thomas Dietterich, em 1998, que recomendou o teste pra casos em que treinar várias cópias do modelo é caro ou inviável

É exatamente a situação de comparar um LLM via prompt com um classificador treinado: rodar dezenas de versões de cada um pra fazer estatística tradicional custa tempo e dinheiro

O McNemar resolve isso usando só uma previsão pareada de cada um sobre o mesmo conjunto de teste (no caso do LLM, o voto da maioria entre as rodadas)




Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 30min

Blog | Mais populares