Voltar ao blog
21 de maio de 20265 min de leitura

Benchmarkando um Motor de Raciocínio: Métricas que Importam

pythonaitesting

Como saber se um motor de raciocínio está funcionando? Não é como testar um CRUD onde você verifica se o dado foi salvo e retornado corretamente. Aqui você precisa medir coisas como: o sistema extraiu os fatos certos? Consolidou informações de múltiplas fontes? Criou conexões criativas? Reduziu incerteza com novas evidências? São capacidades qualitativas que precisam ser quantificadas de forma objetiva.

Criei um benchmark com 5 cenários que testam capacidades diferentes do motor. O score total foi 14/16 (87.5%), com tempos médios entre 5 e 15 segundos por cenário. Cada cenário mede uma capacidade específica com critérios objetivos de sucesso. O resultado não é "funciona ou não funciona", é um perfil detalhado de onde o sistema é forte e onde precisa melhorar.

Guia de tópicos:

  • Por Que Benchmarkar um Motor de Raciocínio
  • Os 5 Cenários de Teste
  • Métricas Além do Score
  • Resultados e O Que Revelam
  • O Que os Pontos Perdidos Ensinam
  • Como Criar Seus Próprios Cenários
  • Exemplo Prático em Python
  • Considerações Finais

Por Que Benchmarkar um Motor de Raciocínio

Sem benchmark, você não sabe se uma mudança melhorou ou piorou o sistema. "Parece que está respondendo melhor" não é métrica. Você precisa de números: antes era 12/16, depois é 14/16. Melhorou. Ou: antes extraía 3 triplas por mensagem, agora extrai 1. Piorou.

O benchmark também revela trade-offs. Talvez uma mudança melhore extração mas piore criatividade. Sem medir ambos, você não percebe. Com benchmark, cada capacidade é medida independentemente e você vê o impacto completo de cada mudança.

Os 5 Cenários de Teste

Fato simples (max 3 pontos): envia informação factual, verifica se extraiu a tripla correta, criou/atualizou o mundo certo, e armazenou com confiança adequada. Testa o pipeline básico.

Consolidação (max 4 pontos): envia múltiplas informações sobre o mesmo tema, verifica se foram consolidadas no mesmo mundo, se triplas redundantes foram unificadas com boost, e se o centroide reflete o tema.

Criatividade cross-world (max 3 pontos): cria dois mundos com entidades em comum, verifica se pontes criativas foram geradas entre eles.

Pesquisa web factual (max 3 pontos): envia pergunta que requer informação atual, verifica se classificou como WEB, buscou, e integrou resultados.

Redução de entropia (max 3 pontos): cria crença incerta, fornece evidência que deveria resolver, verifica se entropia caiu e hipótese correta dominou.

Métricas Além do Score

Cada cenário mede mais que "passou/não passou":

avg_time: tempo médio em segundos. Indica se é rápido o suficiente para uso interativo (< 10s bom, < 15s aceitável).

worlds: número de mundos criados. Fragmentação é um sinal de problema quando mundos surgem sem justificativa — para pouca informação, um único mundo é o esperado. Múltiplos mundos são aceitáveis quando os temas são genuinamente distintos.

triples: triplas extraídas e armazenadas. Indica eficácia da extração.

entropy_start/entropy_end: entropia antes e depois. No cenário de redução, deve cair. Nos outros, estável ou caindo levemente.

Resultados e O Que Revelam

Fato simples: 2/3, 15.37s, 1 mundo, 2 triplas. Perdeu 1 ponto por extração incompleta.

Consolidação: 4/4, 9.1s, 3 mundos, 5 triplas. Score perfeito. Os 3 mundos refletem os subtemas distintos enviados — fragmentação esperada, não problemática.

Criatividade: 2/3, 11.68s, 3 mundos, 4 triplas. Ponte gerada não atingiu threshold esperado.

Pesquisa web: 3/3, 11.13s, 1 mundo, 3 triplas. Perfeito. Pipeline web funciona.

Redução de entropia: 3/3, 5.66s, 3 mundos, 7 triplas. Perfeito e o mais rápido.

O Que os Pontos Perdidos Ensinam

Os 2 pontos perdidos têm causa comum: dependência do LLM para extração. No fato simples, o LLM extraiu 2 triplas em vez de 3 (omitiu relação implícita). Na criatividade, a ponte ficou com confiança abaixo do threshold por atenuação excessiva.

Partes determinísticas (consolidação, redução de entropia) têm score máximo. Partes que dependem do LLM (extração, criatividade via extração) são o ponto fraco. A solução: melhorar prompts de extração ou adicionar mais fontes de validação.

Como Criar Seus Próprios Cenários

Um bom cenário tem: setup claro (que informação enviar), critérios objetivos (que triplas devem existir, que entropia deve ter), e independência (cada cenário roda em engine limpo).

Dicas: comece simples (fato único) e aumente complexidade. Meça tempo para detectar regressões. Compare entropia antes/depois para cenários de aprendizado. Use contagem de mundos e triplas como proxy de "o sistema entendeu?", lembrando que múltiplos mundos só são problema quando não há distinção temática que os justifique.

Exemplo Prático em Python

import time
import numpy as np

class BenchmarkScenario:
    """A single benchmark scenario with scoring."""

    def __init__(self, name: str, max_score: int):
        self.name = name
        self.max_score = max_score
        self.score = 0
        self.time_taken = 0.0
        self.metadata = {}

    def check(self, condition: bool, points: int = 1, label: str = ""):
        """Award points if condition is met."""
        if condition:
            self.score += points
            status = "✓"
        else:
            status = "✗"
        if label:
            print(f"    {status} {label}")

    def result(self) -> dict:
        return {
            "name": self.name,
            "score": self.score,
            "max": self.max_score,
            "time": round(self.time_taken, 2),
            **self.metadata
        }


def run_benchmark(engine) -> dict:
    """Run all benchmark scenarios against an engine instance."""
    scenarios = []

    # Scenario 1: Simple fact extraction
    s = BenchmarkScenario("Fato simples", max_score=3)
    t0 = time.time()
    engine.process("Python foi criado por Guido van Rossum em 1991")
    s.time_taken = time.time() - t0

    triples = engine.get_triples()
    s.check(any("python" in str(t) for t in triples), label="Triple contains 'python'")
    s.check(len(engine.worlds) >= 1, label="At least 1 world created")
    s.check(any(t.get("c", 0) > 0.5 for t in triples), label="Confidence > 0.5")
    s.metadata = {"worlds": len(engine.worlds), "triples": len(triples)}
    scenarios.append(s)

    # Scenario 2: Entropy reduction
    s = BenchmarkScenario("Redução de entropia", max_score=3)
    t0 = time.time()

    # Create uncertain belief
    engine.add_belief("plutão", "tipo", {"planeta": 0.5, "planeta_anão": 0.5})
    h_before = engine.get_entropy("plutão", "tipo")

    # Provide resolving evidence
    engine.process("Plutão é classificado como planeta anão desde 2006")
    h_after = engine.get_entropy("plutão", "tipo")
    s.time_taken = time.time() - t0

    s.check(h_after < h_before, label="Entropy decreased")
    s.check(h_after < 0.5, label="Entropy below 0.5")
    s.check(engine.get_dominant("plutão", "tipo") == "planeta_anão", label="Correct dominant")
    s.metadata = {"entropy_start": round(h_before, 3), "entropy_end": round(h_after, 3)}
    scenarios.append(s)

    # Summary
    total = sum(s.score for s in scenarios)
    max_total = sum(s.max_score for s in scenarios)

    return {
        "total": total,
        "max": max_total,
        "pct": round(total / max_total * 100, 1),
        "scenarios": [s.result() for s in scenarios]
    }


# Demo: display benchmark results format
results = {
    "total": 14, "max": 16, "pct": 87.5,
    "scenarios": [
        {"name": "Fato simples", "score": 2, "max": 3, "time": 15.37, "worlds": 1, "triples": 2},
        {"name": "Consolidação", "score": 4, "max": 4, "time": 9.10, "worlds": 3, "triples": 5},
        {"name": "Criatividade", "score": 2, "max": 3, "time": 11.68, "worlds": 3, "triples": 4},
        {"name": "Pesquisa web", "score": 3, "max": 3, "time": 11.13, "worlds": 1, "triples": 3},
        {"name": "Redução entropia", "score": 3, "max": 3, "time": 5.66, "worlds": 3, "triples": 7},
    ]
}

print(f"{'='*50}")
print(f"  BENCHMARK RESULTS: {results['total']}/{results['max']} ({results['pct']}%)")
print(f"{'='*50}\n")

for s in results["scenarios"]:
    filled = "█" * s["score"]
    empty = "░" * (s["max"] - s["score"])
    print(f"  {s['name']:22s} [{filled}{empty}] {s['score']}/{s['max']}  ({s['time']}s)")

print(f"\n{'─'*50}")
print(f"  Deterministic (consolidation, entropy): 10/10 (100%)")
print(f"  LLM-dependent (extraction, creativity):  4/6  (67%)")
print(f"\n  Insight: LLM extraction is the bottleneck.")
print(f"  Action: improve extraction prompts or add validation sources.")

Considerações Finais

Benchmarkar um motor de raciocínio é diferente de benchmarkar um CRUD, mas não é impossível. A chave é definir critérios objetivos para capacidades subjetivas: "extraiu os fatos certos" vira "número de triplas esperadas vs extraídas". "Aprendeu" vira "entropia caiu". "Conectou domínios" vira "ponte com confiança > threshold existe".

O benchmark de 87.5% não significa "quase perfeito". Significa que as partes determinísticas funcionam perfeitamente e as partes probabilísticas (LLM) têm margem de melhoria. É um mapa de onde investir esforço, não um selo de qualidade.


Links indicativos: