Avaliação e Teste de LLM
Guia abrangente para avaliar e testar aplicações de Grandes Modelos de Linguagem, incluindo teste de prompts, validação de saída, detecção de alucinações, criação de benchmarks, teste A/B e métricas de qualidade.
Referência Rápida
Quando usar esta skill:
- Testar saídas de aplicações LLM
- Validar qualidade e consistência de prompts
- Detectar alucinações e erros factuais
- Criar benchmarks de avaliação
- Testar A/B de prompts ou modelos
- Implementar avaliação contínua (CI/CD)
- Medir qualidade de recuperação (para RAG)
- Depurar comportamento inesperado de LLM
Métricas abordadas:
- Tradicionais: BLEU, ROUGE, BERTScore, Perplexidade
- LLM como Juiz: Avaliação GPT-4, pontuação baseada em rubrica
- Específicas da tarefa: Correspondência exata, F1, precisão, recall
- Qualidade: Toxicidade, viés, coerência, relevância
Parte 1: Fundamentos da Avaliação
O Desafio da Avaliação de LLM
Por que avaliar LLMs é difícil:
- Qualidade subjetiva - Saída "boa" varia conforme o caso de uso
- Sem verdade absoluta única - Múltiplas respostas válidas
- Dependente do contexto - Mesma saída pode ser boa/ruim em cenários diferentes
- Rotulagem cara - Avaliação humana não escala
- Fragilidade adversária - Pequenas mudanças no prompt = grandes mudanças na saída
Solução: Avaliação em múltiplas camadas
Camada 1: Métricas Automatizadas (rápido, escalável)
↓
Camada 2: LLM como Juiz (flexível, sutil)
↓
Camada 3: Revisão Humana (padrão ouro, caro)
Estrutura do Dataset de Avaliação
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class EvalExample:
"""Exemplo de avaliação único."""
input: str # Entrada do usuário / prompt
expected_output: Optional[str] # Padrão ouro (se existir)
context: Optional[str] # Contexto adicional (para RAG)
metadata: dict # Categoria, dificuldade, etc.
@dataclass
class EvalResult:
"""Resultado da avaliação para um exemplo."""
example_id: str
actual_output: str
scores: dict # {'nome_da_metrica': pontuacao}
passed: bool
failure_reason: Optional[str]
# Exemplo de dataset
eval_dataset = [
EvalExample(
input="Qual é a capital da França?",
expected_output="Paris",
context=None,
metadata={'categoria': 'factual', 'dificuldade': 'facil'}
),
EvalExample(
input="Explique o emaranhamento quântico",
expected_output=None, # Sem resposta única
context=None,
metadata={'categoria': 'explicacao', 'dificuldade': 'dificil'}
)
]
Parte 2: Métricas Tradicionais
Métrica 1: Correspondência Exata (Mais Simples)
def exact_match(predicted: str, expected: str, case_sensitive: bool = False) -> float:
"""
Métrica binária: 1.0 se corresponde, 0.0 caso contrário.
Uso para: Classificação, respostas curtas, saída estruturada
Limitações: Muito rigorosa para tarefas de geração
"""
if not case_sensitive:
predicted = predicted.lower().strip()
expected = expected.lower().strip()
return 1.0 if predicted == expected else 0.0
# Exemplo
score = exact_match("Paris", "paris") # 1.0
score = exact_match("A capital é Paris", "Paris") # 0.0
Métrica 2: ROUGE (Orientado a Recall)
from rouge_score import rouge_scorer
def compute_rouge(predicted: str, expected: str) -> dict:
"""
Métricas ROUGE para sobreposição de texto.
ROUGE-1: Sobreposição de unigramas
ROUGE-2: Sobreposição de bigramas
ROUGE-L: Subsequência comum mais longa
Uso para: Sumarização, tradução
Limitações: Não captura semântica
"""
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
scores = scorer.score(expected, predicted)
return {
'rouge1': scores['rouge1'].fmeasure,
'rouge2': scores['rouge2'].fmeasure,
'rougeL': scores['rougeL'].fmeasure
}
# Exemplo
scores = compute_rouge(
predicted="Paris é a capital da França",
expected="A capital da França é Paris"
)
# {'rouge1': 0.82, 'rouge2': 0.67, 'rougeL': 0.82}
Métrica 3: BERTScore (Similaridade Semântica)
from bert_score import score as bert_score
def compute_bertscore(predicted: List[str], expected: List[str]) -> dict:
"""
Similaridade semântica usando embeddings BERT.
Melhor que ROUGE para:
- Paráfrases
- Equivalência semântica
- Qualidade de geração
Retorna: Precisão, Recall, F1
"""
P, R, F1 = bert_score(predicted, expected, lang="pt", verbose=False)
return {
'precision': P.mean().item(),
'recall': R.mean().item(),
'f1': F1.mean().item()
}
# Exemplo
scores = compute_bertscore(
predicted=["A capital da França é Paris"],
expected=["Paris é a cidade capital da França"]
)
# {'precision': 0.94, 'recall': 0.91, 'f1': 0.92}
Métrica 4: Perplexidade (Confiança do Modelo)
import torch
from transformers import GPT2LMHeadModel, GPT2Tokenizer
def compute_perplexity(text: str, model_name: str = "gpt2") -> float:
"""
Perplexidade: Quão "surpreso" o modelo está com este texto?
Mais baixo = Mais provável/fluente
Uso para: Fluência, naturalidade
Limitações: Não mede correção
"""
model = GPT2LMHeadModel.from_pretrained(model_name)
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs, labels=inputs["input_ids"])
loss = outputs.loss
perplexity = torch.exp(loss).item()
return perplexity
# Exemplo
ppl = compute_perplexity("Paris é a capital da França") # Baixo (fluente)
ppl2 = compute_perplexity("Capital França a é de Paris") # Alto (estranho)
Parte 3: Avaliação LLM como Juiz
Padrão 1: Pontuação Baseada em Rubrica
from openai import OpenAI
client = OpenAI()
PROMPT_AVALIACAO = """
Você é um avaliador especialista. Atribua uma pontuação de 1 a 5 para a resposta do assistente em cada critério:
**Critérios:**
1. **Precisão**: A informação é factualmente correta?
2. **Completude**: Ela responde totalmente à pergunta?
3. **Clareza**: É fácil de entender?
4. **Concisão**: É adequadamente breve?
**Resposta a avaliar:**
{response}
**Resposta esperada (referência):**
{expected}
Forneça as pontuações em formato JSON:
{{
"precisao": <1-5>,
"completude": <1-5>,
"clareza": <1-5>,
"concisao": <1-5>,
"justificativa": "Explicação breve"
}}
"""
def llm_judge_score(response: str, expected: str) -> dict:
"""
Use GPT-4 como juiz com pontuação por rubrica.
Prós: Flexível, sutil, escala bem
Contras: Custa $, viés potencial, mais lento
"""
prompt = PROMPT_AVALIACAO.format(response=response, expected=expected)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
import json
scores = json.loads(completion.choices[0].message.content)
return scores
# Exemplo
scores = llm_judge_score(
response="Paris é a capital da França, localizada na parte centro-norte do país.",
expected="Paris"
)
# {'precisao': 5, 'completude': 5, 'clareza': 5, 'concisao': 3, 'justificativa': '...'}
Padrão 2: Avaliação Binária Aprovado/Reprovado
PROMPT_APROVACAO = """
Avalie se a resposta do assistente é aceitável.
**Pergunta:** {question}
**Resposta:** {response}
**Critério:** {criteria}
Retorne APENAS "APROVADO" ou "REPROVADO" seguido de uma justificativa de uma frase.
"""
def binary_eval(question: str, response: str, criteria: str) -> tuple[bool, str]:
"""
Avaliação simples de aprovado/reprovado.
Uso para: Testes unitários, testes de regressão, CI/CD
"""
prompt = PROMPT_APROVACAO.format(
question=question,
response=response,
criteria=criteria
)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.0 # Determinístico
)
result = completion.choices[0].message.content
passed = result.startswith("APROVADO")
reason = result.split(":", 1)[1].strip() if ":" in result else result
return passed, reason
# Exemplo
passed, reason = binary_eval(
question="Qual é a capital da França?",
response="A capital é Paris",
criteria="A resposta deve mencionar Paris"
)
# (True, "A resposta identifica corretamente Paris como a capital")
Padrão 3: Comparação Pareada (Teste A/B)
PROMPT_PAR = """
Compare duas respostas para a mesma pergunta. Qual é melhor?
**Pergunta:** {question}
**Resposta A:**
{response_a}
**Resposta B:**
{response_b}
**Critério:** {criteria}
Retorne APENAS: "A", "B" ou "EMPATE", seguido de uma explicação de uma frase.
"""
def pairwise_comparison(
question: str,
response_a: str,
response_b: str,
criteria: str = "Qualidade geral, precisão e utilidade"
) -> tuple[str, str]:
"""
Teste A/B de duas respostas.
Uso para: Engenharia de prompts, comparação de modelos
"""
prompt = PROMPT_PAR.format(
question=question,
response_a=response_a,
response_b=response_b,
criteria=criteria
)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
result = completion.choices[0].message.content
winner = result.split()[0] # "A", "B", ou "EMPATE"
reason = result.split(":", 1)[1].strip() if ":" in result else result
return winner, reason
# Exemplo
winner, reason = pairwise_comparison(
question="Explique computação quântica",
response_a="Computadores quânticos usam qubits em vez de bits...",
response_b="Computação quântica é complexa. Ela usa mecânica quântica."
)
# ("A", "A resposta A fornece mais detalhes e explicação")
Parte 4: Detecção de Alucinações
Método 1: Verificação de Fundamentação
def check_grounding(response: str, context: str) -> dict:
"""
Verifique se a resposta está fundamentada no contexto fornecido.
Crítico para sistemas RAG.
"""
PROMPT_FUNDAMENTACAO = """
Contexto: {context}
Resposta: {response}
A resposta é totalmente suportada pelo contexto? Responda com:
- "FUNDAMENTADA": Todas as afirmações são suportadas
- "PARCIALMENTE_FUNDAMENTADA": Algumas afirmações não suportadas
- "NAO_FUNDAMENTADA": Contém afirmações não suportadas
Liste quaisquer afirmações não suportadas.
"""
prompt = PROMPT_FUNDAMENTACAO.format(context=context, response=response)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
result = completion.choices[0].message.content
status = result.split("\n")[0]
unsupported = [line for line in result.split("\n")[1:] if line.strip()]
return {
'grounding_status': status,
'unsupported_claims': unsupported,
'is_hallucination': status != "FUNDAMENTADA"
}
Método 2: Verificação de Factualidade (Verificação Externa)
def check_factuality(claim: str, use_search: bool = True) -> dict:
"""
Verifique afirmações factuais usando fontes externas.
Opções:
1. Pesquisa na web + verificação
2. Consulta a base de conhecimento
3. Referência cruzada com fonte confiável
"""
if use_search:
# Use pesquisa na web para verificar
from tavily import TavilyClient
tavily = TavilyClient(api_key="sua-chave")
# Pesquise por evidências
results = tavily.search(claim, max_results=3)
# Peça ao LLM para verificar com base nos resultados da pesquisa
PROMPT_VERIFICACAO = """
Afirmação: {claim}
Resultados da pesquisa:
{results}
A afirmação é suportada por essas fontes? Responda: VERDADEIRO, FALSO ou INCERTO.
Explicação:
"""
prompt = PROMPT_VERIFICACAO.format(
claim=claim,
results="\n\n".join([r['content'] for r in results])
)
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
result = completion.choices[0].message.content
is_factual = result.startswith("VERDADEIRO")
return {
'claim': claim,
'factual': is_factual,
'evidence': results,
'explanation': result
}
Método 3: Verificação de Autoconsistência
def self_consistency_check(question: str, num_samples: int = 5) -> dict:
"""
Gere múltiplas respostas, verifique consistência.
Se o modelo estiver confiante, as respostas devem ser consistentes.
Inconsistência sugere risco de alucinação.
"""
responses = []
for _ in range(num_samples):
completion = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": question}],
temperature=0.7 # Alguma aleatoriedade
)
responses.append(completion.choices[0].message.content)
# Calcular similaridade pareada
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
vectorizer = TfidfVectorizer()
vectors = vectorizer.fit_transform(responses)
similarities = cosine_similarity(vectors)
# Similaridade pareada média
avg_similarity = similarities.sum() / (len(responses) * (len(responses) - 1))
return {
'responses': responses,
'avg_similarity': avg_similarity,
'is_consistent': avg_similarity > 0.7, # Limiar
'confidence': 'alta' if avg_similarity > 0.85 else 'media' if avg_similarity > 0.7 else 'baixa'
}
Parte 5: Avaliação Específica para RAG
Métricas de Qualidade de Recuperação
def evaluate_retrieval(query: str, retrieved_docs: List[dict], relevant_doc_ids: List[str]) -> dict:
"""
Avalie a qualidade da recuperação usando métricas de RI.
Precisão: Qual % dos documentos recuperados são relevantes?
Recall: Qual % dos documentos relevantes foram recuperados?
MRR: Mean Reciprocal Rank
NDCG: Ganho Cumulativo Descontado Normalizado
"""
retrieved_ids = [doc['id']