Jangada AIJangada AI

Avaliação (evals)

A lib promete trocar provider/modelo sem mudar o código. As evals são a outra metade: provar que a troca não piorou a qualidade nem estourou o custo. Você roda um target sobre um conjunto de casos (Dataset), dá notas com Evaluators (heurística e/ou LLM juiz) e compara execuções (Experiment) por acerto × R$ × latência.

Funciona 100% offline (igual à observabilidade): evaluate() devolve os scores localmente; enviar ao painel é opcional (push=True).

from jangada_ai import LLM
from jangada_ai.eval import Evaluator, Dataset, evaluate

As peças

PeçaResponde
Dataset/Example"Em cima de quais casos eu meço?" (entradas + gabarito)
Evaluator"Essa saída está boa?" (heurística ou juiz LLM)
evaluate()roda o target sobre o dataset, aplica os evaluators e agrega
ExperimentResulto resultado: notas por evaluator, custo, p50, erros

1. Dataset — os casos

Um Example tem inputs (o que vai para o target), reference (gabarito, opcional) e metadata.

ds = Dataset.from_records([
    {"inputs": {"q": "Capital da França?"}, "reference": "Paris"},
    {"inputs": {"q": "2 + 2?"},            "reference": "4"},
], name="perguntas")

# ou de um JSONL: {"inputs": {...}, "reference": ...} por linha
ds = Dataset.from_jsonl("casos.jsonl", name="casos")

2. Evaluators — as notas

Heurística (Evaluator.fn)

Função pura (output, reference) → float, bool ou EvalResult. Sem rede.

exato = Evaluator.fn(
    "exato",
    lambda out, ref: out.text.strip().lower() == ref.lower(),
)

out é o que o target devolveu (tipicamente um Completion, com .text, .parsed, .cost); ref é o reference do exemplo.

Juiz LLM (Evaluator.judge)

Um LLM avalia a saída — por baixo é um parse() com schema fixo {score, reason}. Use para critérios subjetivos (utilidade, tom, equivalência semântica).

util = Evaluator.judge(
    "util",
    "A resposta responde à pergunta de forma correta e útil? score de 0 a 1.",
    judge=LLM("openai", "gpt-4o-mini"),  # juiz barato, separado do alvo
)

3. evaluate() — rodar e agregar

def alvo(ex):
    return LLM("openai", "gpt-4o-mini").complete(ex.inputs["q"])

res = evaluate(ds, target=alvo, evaluators=[exato, util], name="baseline")
print(res.summary())
# {'exato': 0.9, 'util': 0.95, 'costUsd': 0.0021, 'p50_ms': 740, 'count': 2, 'errors': 0}

O summary() traz: média por evaluator, costUsd (custo do target), evalCostUsd (custo dos juízes), p50_ms, count e errors. Cada item fica em res.runs. Um target que falha vira um run com error — não derruba o experimento.

Async: aevaluate(..., max_concurrency=8) (o target pode ser coroutine).

4. Comparar modelos (o que importa)

Mesmo dataset, modelos diferentes — decida com evidência:

def alvo(modelo):
    return lambda ex: LLM("openai", modelo).complete(ex.inputs["q"])

gpt   = evaluate(ds, target=alvo("gpt-5"),          evaluators=[exato, util], name="gpt5")
flash = evaluate(ds, target=alvo("gemini-3-flash"), evaluators=[exato, util], name="flash")
# gpt5:  {'exato': 0.94, 'util': 0.97, 'costUsd': 0.21, ...}
# flash: {'exato': 0.90, 'util': 0.95, 'costUsd': 0.02, ...}  → ~10× mais barato

5. Enviar ao painel (push=True)

Com a observabilidade configurada (mesma chave de projeto), push=True envia o experiment ao backend — aparece no dashboard nas abas Experiments (tabela comparativa) e Datasets (gráfico de score ao longo do tempo).

res = evaluate(
    ds, target=alvo("gpt-5"), evaluators=[exato, util], name="gpt5",
    push=True, target_info={"provider": "openai", "model": "gpt-5"},
)
print(res.pushed, res.push_error)  # True None

O push é best-effort (falha de rede/sem chave não levanta — marca res.pushed/res.push_error). Cada run guarda o traceId quando disponível, ligando o score de volta ao trace que o gerou.

Como funciona

  • Evaluator.fn roda local; Evaluator.judge faz um parse() no juiz com schema {score, reason}.
  • evaluate() mede latência, lê Completion.cost do target, soma o custo dos juízes em evalCostUsd e calcula média/p50/erros.
  • push reusa a config da observabilidade (POST /v1/experiments).

On this page