LLMEvaluation / evaluator.py
Danielfonseca1212's picture
Create evaluator.py
9af42b9 verified
# evaluator.py — G-Eval style LLM Evaluation Engine
"""
Implementa avaliação LLM-as-Judge para 5 dimensões críticas de produção:
1. Faithfulness — resposta é fiel ao contexto? Sem alucinações?
2. Answer Relevance — resposta realmente responde a pergunta?
3. Completeness — cobre todos os aspectos relevantes?
4. Conciseness — sem verbosidade ou padding?
5. Hallucination — detecta afirmações não suportadas pelo contexto
Baseado na metodologia G-Eval (Liu et al., 2023) com Chain-of-Thought scoring.
"""
import json
import re
from dataclasses import dataclass, field
from typing import Optional
from openai import OpenAI
# ── DIMENSÕES ─────────────────────────────────────────────────
@dataclass
class EvalDimension:
name: str
key: str
description: str
prompt: str
weight: float = 1.0
DIMENSIONS = [
EvalDimension(
name="Faithfulness",
key="faithfulness",
description="A resposta é fiel ao contexto fornecido? Sem informações inventadas?",
weight=0.30,
prompt="""Você é um avaliador especialista em qualidade de LLMs.
Avalie a FAITHFULNESS (fidelidade) da resposta ao contexto fornecido.
Critério: A resposta contém apenas informações suportadas pelo contexto?
Penalize alucinações, afirmações sem base e extrapolações não justificadas.
Contexto:
{context}
Pergunta:
{question}
Resposta do LLM:
{answer}
Raciocine passo a passo, identifique cada afirmação e verifique se tem suporte no contexto.
Responda APENAS com JSON:
{{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["lista de problemas encontrados, vazia se nenhum"]}}"""
),
EvalDimension(
name="Answer Relevance",
key="relevance",
description="A resposta realmente responde o que foi perguntado?",
weight=0.25,
prompt="""Você é um avaliador especialista em qualidade de LLMs.
Avalie a RELEVANCE (relevância) da resposta à pergunta.
Critério: A resposta aborda diretamente a pergunta? Sem desvios ou respostas tangenciais?
Pergunta:
{question}
Resposta do LLM:
{answer}
Raciocine: a resposta responde exatamente o que foi perguntado?
Responda APENAS com JSON:
{{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["lista de problemas, vazia se nenhum"]}}"""
),
EvalDimension(
name="Completeness",
key="completeness",
description="A resposta cobre todos os aspectos relevantes da pergunta?",
weight=0.20,
prompt="""Você é um avaliador especialista em qualidade de LLMs.
Avalie a COMPLETENESS (completude) da resposta.
Critério: A resposta cobre todos os aspectos importantes da pergunta usando o contexto disponível?
Contexto:
{context}
Pergunta:
{question}
Resposta do LLM:
{answer}
Raciocine: quais aspectos importantes estão faltando?
Responda APENAS com JSON:
{{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["aspectos faltantes, vazio se completo"]}}"""
),
EvalDimension(
name="Conciseness",
key="conciseness",
description="A resposta é direta, sem verbosidade ou padding desnecessário?",
weight=0.10,
prompt="""Você é um avaliador especialista em qualidade de LLMs.
Avalie a CONCISENESS (concisão) da resposta.
Critério: A resposta é direta ao ponto? Sem repetições, padding ou verbosidade excessiva?
Pergunta:
{question}
Resposta do LLM:
{answer}
Raciocine: há partes que poderiam ser removidas sem perda de informação?
Responda APENAS com JSON:
{{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["redundâncias encontradas, vazio se conciso"]}}"""
),
EvalDimension(
name="Hallucination Detection",
key="hallucination",
description="Detecta afirmações específicas não suportadas pelo contexto.",
weight=0.15,
prompt="""Você é um detector especialista de alucinações em LLMs.
Analise a resposta e identifique ALUCINAÇÕES — afirmações específicas, números, nomes ou fatos
que NÃO estão no contexto e que o modelo parece ter inventado.
Contexto:
{context}
Pergunta:
{question}
Resposta do LLM:
{answer}
Score: 10 = zero alucinações, 1 = resposta completamente alucinada.
Responda APENAS com JSON:
{{"score": <1-10>, "reasoning": "análise em 2-3 frases", "hallucinations": ["lista de alucinações específicas encontradas, vazia se nenhuma"]}}"""
),
]
# ── RESULTADO ─────────────────────────────────────────────────
@dataclass
class DimensionResult:
key: str
name: str
score: float
reasoning: str
issues: list = field(default_factory=list)
weight: float = 1.0
@dataclass
class EvalResult:
question: str
context: str
answer: str
dimensions: list # List[DimensionResult]
overall_score: float
verdict: str # EXCELLENT / GOOD / FAIR / POOR
summary: str
model_label: str = "LLM"
@property
def verdict_color(self):
if self.overall_score >= 8.5: return "excellent"
if self.overall_score >= 7.0: return "good"
if self.overall_score >= 5.0: return "fair"
return "poor"
# ── ENGINE ────────────────────────────────────────────────────
class EvaluationEngine:
def __init__(self, openai_api_key: str):
self.client = OpenAI(api_key=openai_api_key)
self.model = "gpt-4o-mini"
def _call(self, prompt: str) -> dict:
resp = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=400,
)
raw = resp.choices[0].message.content.strip()
raw = re.sub(r'```json|```', '', raw).strip()
return json.loads(raw)
def _eval_dimension(self, dim: EvalDimension, question: str,
context: str, answer: str) -> DimensionResult:
prompt = dim.prompt.format(
question=question,
context=context or "(nenhum contexto fornecido)",
answer=answer,
)
try:
data = self._call(prompt)
score = float(data.get("score", 5))
score = max(1.0, min(10.0, score))
issues = data.get("issues", data.get("hallucinations", []))
if isinstance(issues, str):
issues = [issues] if issues else []
except Exception as e:
score = 5.0
issues = [f"Erro na avaliação: {e}"]
data = {"reasoning": "Erro ao processar"}
return DimensionResult(
key=dim.key, name=dim.name, score=score,
reasoning=data.get("reasoning", ""),
issues=[i for i in issues if i],
weight=dim.weight,
)
def _generate_summary(self, question: str, answer: str,
dims: list, overall: float) -> str:
weak = [d for d in dims if d.score < 7.0]
strong = [d for d in dims if d.score >= 8.5]
parts = []
if strong:
parts.append(f"Pontos fortes: {', '.join(d.name for d in strong)}.")
if weak:
parts.append(f"Melhorar: {', '.join(d.name for d in weak)}.")
if overall >= 8.5:
parts.append("Resposta de alta qualidade para produção.")
elif overall >= 7.0:
parts.append("Resposta adequada com espaço para melhoria.")
elif overall >= 5.0:
parts.append("Qualidade abaixo do esperado para produção.")
else:
parts.append("Resposta inadequada. Revisar o pipeline.")
return " ".join(parts)
def evaluate(self, question: str, context: str, answer: str,
model_label: str = "LLM") -> EvalResult:
"""Avalia uma resposta em todas as dimensões."""
results = []
for dim in DIMENSIONS:
r = self._eval_dimension(dim, question, context, answer)
results.append(r)
# Weighted average
total_w = sum(d.weight for d in results)
overall = sum(r.score * r.weight for r in results) / total_w
overall = round(overall, 2)
verdict = (
"EXCELLENT" if overall >= 8.5 else
"GOOD" if overall >= 7.0 else
"FAIR" if overall >= 5.0 else
"POOR"
)
summary = self._generate_summary(question, answer, results, overall)
return EvalResult(
question=question, context=context, answer=answer,
dimensions=results, overall_score=overall,
verdict=verdict, summary=summary, model_label=model_label,
)