O que é Bancada de Evals?
Bancada de Evals (Eval Harness) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Operação e qualidade. Rodar casos de teste automaticamente a cada mudança de prompt, modelo ou código.
O time de dados da Joana construiu um sistema que transforma perguntas em consultas ao banco. E eles já fizeram a parte difícil. Têm um dataset. Cento e vinte perguntas reais, cada uma com a consulta correta.
O problema é que rodar o eval é um ritual de meia hora. Abrir um notebook, apontar para a versão certa do prompt, esperar, copiar os resultados numa planilha e comparar com a planilha da semana passada.
Então, na prática, o eval roda quando alguém lembra. E na semana em que três pessoas mudaram o prompt, ninguém rodou. O acerto caiu. E eles tiveram que vasculhar as três mudanças na mão, para achar qual causou.
Ter os casos não basta. Eles precisam rodar sozinhos, do mesmo jeito sempre, e o resultado precisa chegar a quem fez a mudança, antes do merge. Nesta aula, você vai ver como transformar um dataset numa checagem automática que roda a cada mudança.
Um eval harness tem quatro partes fixas.
Carregar os casos.
Rodar o sistema de verdade em cada um.
Dar nota a cada saída com a regra certa.
E comparar o resultado com uma linha de base guardada.
Aí você liga no seu pipeline. Toda mudança num prompt, num modelo, numa ferramenta ou no código do pipeline dispara o eval e anexa o relatório ao pull request. E um detalhe importa muito. O eval harness roda o sistema como ele roda em produção. Mesmo código, mesmo prompt versionado. Não uma cópia simplificada.
Aqui está o fluxo. Alguém abre um pull request.
O eval harness carrega a versão atual do dataset.
Cada caso roda pelo pipeline de verdade, em paralelo, com algumas repetições quando a saída varia.
Cada saída recebe nota. Por comparação exata, por uma regra no código ou por um juiz.
As notas viram taxas por categoria, mais custo e latência.
O resultado é comparado com a linha de base da branch principal.
E o relatório vai para o pull request. Uma regressão acima do limite bloqueia o merge.
Na segunda, o pull request da Joana muda só um exemplo do prompt, o de juntar tabelas. O pipeline dispara o eval harness, roda os cento e vinte casos pelo sistema de verdade e posta isto no pull request.
Agregação se mantém. Filtro de data se mantém. Junção cai quase dezessete pontos. Regressão.
O merge fica bloqueado. O relatório lista os onze casos de junção que passaram de certo para errado. A Joana abre o primeiro, vê a consulta gerada e percebe que o exemplo novo inverteu qual tabela era a principal. Sem o eval harness, essa queda só apareceria na planilha de sexta, misturada com outras duas mudanças. Com ele, a mudança culpada é óbvia, porque só um pull request chegou a entrar.
E repare como esses casos recebem nota. Não comparando o texto da consulta. Duas consultas diferentes podem devolver a mesma tabela. O eval harness roda a consulta gerada e a esperada num banco de teste e compara as linhas. Uma consulta escrita de outro jeito que devolve as linhas certas passa. Uma com uma junção a mais que duplica linhas falha. Comparar texto teria recusado a primeira, e às vezes aceitado a segunda.
Uma pergunta para você. Lembra da semana em que três pessoas mudaram o prompt e o acerto caiu? O dataset já existia. O que o eval harness teria feito de diferente?
Cada pull request teria rodado os cento e vinte casos no sistema de verdade. E o primeiro que quebrasse as junções teria sido bloqueado. Ter casos no repositório não protege. O que falhou foi o ritual do notebook. O eval harness existe para o eval rodar sem ninguém precisar lembrar. E ele fatia o problema para você, então ninguém precisa vasculhar três mudanças na mão.
Qual nota usar? Comece pelo que o código decide sozinho. A rota de um classificador: comparação exata.
Uma consulta ao banco: rode e compare as linhas.
Um agente: confira o caminho. Ele chamou a consulta de pedido. Ele não chamou o estorno sem aprovação.
E texto livre, só quando não há outro jeito: um modelo como juiz, com rubrica, conferido contra uma pessoa numa amostra. O juiz é o último recurso. Custa uma chamada a mais por caso, e pode concordar com uma resposta ruim. Se uma regra no código cobre 80% dos casos, deixe o juiz para os outros 20%. Não o contrário.
O eval harness costuma rodar em duas velocidades. A cada commit, um subconjunto de vinte casos canário roda em poucos minutos. Ele pega o erro óbvio.
Antes do merge, e toda noite, o conjunto inteiro roda, com repetições. Esse pega a categoria rara.
E o limite não é questão de gosto. Rode a mesma versão duas vezes, sem mudança nenhuma, e veja quanto o resultado se mexe. Se a variação natural é de um ponto, um limite zero recusa pull requests inocentes. Se é de quatro pontos, um limite de dois deixa passar uma regressão de verdade. O limite fica um pouco acima do ruído que você mediu.
Agora, os erros comuns. Uma cópia simplificada. O eval harness chama o modelo direto, sem a busca, sem o roteador, sem as ferramentas. O eval passa, e a produção falha. O ponto de entrada tem que ser a mesma função que a produção chama.
Um notebook fazendo papel de pipeline. Funciona uma vez. Na semana dos três pull requests, ninguém abre.
Um dataset congelado. O eval harness só é tão bom quanto os casos. Todo bug de produção vira um caso novo. Senão você está medindo um mundo velho.
E um limite zero num sistema com muita variação. O mesmo caso passa e falha. Coloque repetições e médias antes de apertar o limite.
O eval harness também precisa ser avaliado. O juiz concorda com uma pessoa? Quão estável é o resultado entre rodadas idênticas? E quantos bugs de produção do último trimestre têm um caso no dataset?
E fique de olho num sinal. Se o eval da noite cai sem mudança nenhuma no código, suspeite do provedor, com um modelo atualizado, ou dos seus dados de teste. Esse alerta é tão útil quanto o do pull request. A linha de base também envelhece. Guarde os traces de eval separados dos de produção, marcados com a versão testada.
Na página, o eval harness vem em Python e TypeScript, e os dois fazem a mesma coisa. Carregam os casos de um arquivo. Rodam cada um algumas vezes pelo sistema de verdade, com um limite de concorrência, para não estourar o limite do provedor. E transformam as notas em taxas por categoria.
Depois comparam cada categoria com a linha de base da branch principal. Se alguma cair mais que o limite, o processo sai com erro, e o pipeline falha.
É um comando que o seu pipeline consegue rodar. Notas com regras e juízes, e um relatório mais caprichado no pull request, vêm nas próximas iterações. E ferramentas prontas podem poupar a estrutura. O que você nunca terceiriza é o dataset, a regra de nota e o gatilho no pull request.
Então, a ideia para levar. Um dataset só protege quando roda sozinho. Rode o sistema de verdade, dê nota com a regra mais simples que funciona, compare por categoria contra uma linha de base e coloque a resposta no pull request, antes do merge. Aí três mudanças na mesma semana deixam de ser um mistério.
Na próxima aula, vamos olhar o combustível de tudo isso: o golden dataset, e como mantê-lo vivo. Até lá.
Eval Harness
Bancada de Evals (Eval Harness) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Operação e qualidade. Rodar casos de teste automaticamente a cada mudança de prompt, modelo ou código.
Rodar os casos de teste automaticamente a cada mudança de prompt, modelo ou código, e comparar com a versão anterior. Serve para qualquer sistema com IA que muda com frequência e precisa provar que não piorou.
A esteira de testes de uma fábrica: toda peça nova passa pelo mesmo gabarito antes de sair.
O time de dados de Joana tem um sistema de perguntas em linguagem natural para SQL. Eles já têm um dataset: 120 perguntas reais com a consulta correta. O problema é que rodar o eval é um ritual de meia hora: abrir um notebook, apontar para a versão certa do prompt, esperar, copiar a tabela de resultados para uma planilha e comparar com a planilha da semana passada.
Na prática, o eval roda quando alguém lembra. Na semana em que três pessoas mudaram o prompt, ninguém rodou. A acurácia caiu e foi preciso bisseccionar à mão para descobrir qual das três mudanças causou a queda.
Ter os casos não basta. Eles precisam rodar sozinhos, do mesmo jeito, toda vez, e o resultado precisa chegar a quem fez a mudança antes do merge.
A pergunta que este padrão responde: como transformar um dataset de eval numa verificação automática que roda a cada mudança?
Construa uma bancada de evals com quatro partes fixas: carregar os casos, rodar o sistema de verdade sobre cada um, corrigir cada saída com a regra certa, e comparar o resultado com a linha de base guardada. Ligue a bancada na CI: toda mudança em prompt, modelo, ferramenta ou código do pipeline dispara o eval e anexa o relatório ao pull request.
A bancada roda o sistema como ele roda em produção (mesmo código, mesmo prompt versionado), não uma cópia simplificada. E ela guarda cada execução, para que dê para abrir um caso que falhou e ver exatamente o que o modelo devolveu.
Na segunda-feira o PR da Joana muda só o exemplo de JOIN. A CI sobe a bancada, carrega evals/sql@19, roda os 120 casos no pipeline real e cola isto no PR:
categoria baseline atual delta
agregação 94.2% 94.0% -0.2%
join 88.1% 71.3% -16.8% REGRESSÃO
filtro_data 91.0% 90.5% -0.5%
O merge bloqueia. O relatório lista os 11 casos de join que saíram de certo para errado. Joana abre o primeiro, vê a SQL gerada e percebe que o novo exemplo inverteu a tabela de fatos. Sem a bancada, essa queda só apareceria na planilha da sexta, misturada com as outras duas mudanças da semana.
A correção dos casos de SQL não compara o texto da consulta. Duas SQLs diferentes podem devolver a mesma tabela. A bancada executa a SQL gerada e a esperada no banco de teste e compara as linhas. Caso sql-041:
{ "id": "sql-041", "input": "Quantos pedidos tivemos em março por estado?", "expected_rows": "fixtures/sql-041.csv", "category": "agregação", "grader": "rule" }
Se o modelo escrever date_trunc no lugar de EXTRACT(MONTH ...) e o resultado for o mesmo, o caso passa. Se escrever um JOIN a mais e duplicar linhas, falha. Comparar string de SQL teria recusado a primeira (boa) e, às vezes, aceitado a segunda (ruim).
flowchart LR
PR(["1. Pull request"]):::io --> L["2. Carregar casos"]:::tool
DS[("Dataset versionado")]:::store --> L
L --> R(["3. Rodar o sistema real"]):::llm
R --> G{"4. Corrigir cada saída"}
G --> A["5. Agregar por categoria"]:::tool
BL[("Linha de base")]:::store --> C{"6. Comparar"}
A --> C
C --> REP(["7. Relatório no PR e bloqueio"]):::io
schema Case { id: string, input: any, expected: any, category: string, grader: "exact" | "rule" | "judge" }
schema Result { case_id: string, output: any, score: float, cost: float, latency: float }
function run_eval(system_version, dataset, repeats=3):
results = []
parallel { # 3. em paralelo
for case in dataset, r in range(repeats):
out = system(system_version).run(case.input)
score = grade(case, out) # 4
results.append(Result(case.id, out, score, out.cost, out.latency))
}
return aggregate(results, by="category") # 5
function grade(case, out):
if case.grader == "exact": return 1.0 if out == case.expected else 0.0
if case.grader == "rule": return RULES[case.category](out, case.expected)
return llm(JUDGE_RUBRIC, case, out) -> float # juiz, quando não há alternativa
function check(pr):
current = run_eval(pr.version, load("evals/dataset@latest"))
baseline = load_baseline("main")
diff = compare(current, baseline) # 6
report(pr, diff) # 7
if any(cat.drop > THRESHOLD for cat in diff): fail(pr)
Comece pelo que o código consegue decidir sozinho. Rota de um classificador: string exata. SQL: executar e comparar linhas. Agente: a trajetória das ferramentas (chamou buscar_pedido, não chamou reembolsar sem aprovação). Texto livre, quando não houver outra via: LLM como juiz, com rubrica, medido contra uma pessoa numa amostra.
O juiz é o último recurso. Ele custa uma chamada a mais por caso e pode concordar com uma resposta ruim. Se a regra em código cobre 80% dos casos, deixe o juiz nos 20% que sobram, não o contrário.
Um subconjunto de 20 casos canário roda em todo commit, em poucos minutos. Os 120 completos rodam antes do merge e de noite. O canário pega o erro óbvio; o completo pega a categoria rara.
O limite de queda (2 pontos percentuais no código de exemplo) não é um gosto. Rode a mesma versão duas vezes, sem mudar nada, e veja quanto o resultado oscila. Se a variação natural for 1 ponto, um limite de 0 recusa PRs inocentes. Se for 4 pontos, um limite de 2 vai deixar regressão passar. O limite fica um pouco acima do ruído medido.
classify ou run da produção.Frameworks prontos (Promptfoo, LangSmith, o runner de eval do seu provedor) economizam o andaime. O que você não terceiriza: o dataset, a correção e o gatilho no PR.
| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | Casos vezes repetições, mais o juiz quando houver | um trace por caso, marcado como eval |
| Custo | Proporcional ao dataset; o juiz pode dobrar | custo total do eval no relatório |
| Tempo | Limitado pelo paralelismo permitido | duração do job na CI |
| Falha: eval instável | O mesmo caso passa e falha sem mudança | variação alta entre repetições |
| Falha: correção errada | A regra ou o juiz aprova saída ruim | casos aprovados que uma pessoa reprovaria |
| Falha: dataset velho | A bancada aprova uma versão que falha em casos novos | bugs de produção sem caso correspondente |
Os números abaixo são ilustrativos. Suponha 120 casos, 3 repetições, sem juiz, concorrência 8.
| Quando | O que roda | Para que |
|---|---|---|
| Cada commit | 20 canários, 1 repetição | pegar o erro óbvio em minutos |
| Cada PR / noite | 120 casos, 3 repetições | bloquear regressão de categoria |
O canário barato não substitui o completo. Ele só evita esperar o job longo para descobrir que o prompt não parseia mais.
A bancada também precisa ser avaliada:
{ "id": "sql-041", "input": "Quantos pedidos tivemos em março por estado?", "expected_rows": "fixtures/sql-041.csv", "category": "agregação", "grader": "rule" }
Se o eval noturno cai sem mudança de código, o suspeito é o provedor (modelo atualizado) ou o banco de teste. Esse alerta é tão útil quanto o do PR: a linha de base também envelhece.
| Prós | Contras |
|---|---|
| Toda mudança vem com um número | Custa dinheiro a cada execução |
| Regressões aparecem antes do merge | O dataset precisa de manutenção |
| Troca de modelo vira decisão medida | Correção ruim dá confiança falsa |
| Discussões viram comparações | CI mais lenta |
As duas abas implementam uma bancada mínima com casos em JSONL, correção exata, execução paralela e comparação com a linha de base.
Python. Use quando o pipeline é Python e você quer rodar o eval com pytest ou um script na CI.
import asyncio
import json
from collections import defaultdict
from statistics import mean
from my_app.pipeline import classify # 3. o sistema real, não uma cópia
THRESHOLD = 0.02 # queda máxima tolerada por categoria
async def run_case(case: dict, repeats: int, sem: asyncio.Semaphore) -> list[float]:
async with sem:
outs = await asyncio.gather(*(classify(case["input"]) for _ in range(repeats)))
return [1.0 if o == case["expected"] else 0.0 for o in outs] # 4. correção exata
async def run_eval(path: str, repeats: int = 3) -> dict[str, float]:
cases = [json.loads(line) for line in open(path, encoding="utf-8")]
sem = asyncio.Semaphore(8) # respeita o rate limit
scores = await asyncio.gather(*(run_case(c, repeats, sem) for c in cases))
by_cat = defaultdict(list)
for case, s in zip(cases, scores):
by_cat[case["category"]].extend(s)
return {cat: mean(v) for cat, v in by_cat.items()} # 5
def main() -> None:
current = asyncio.run(run_eval("evals/tickets.jsonl"))
baseline = json.load(open("evals/baseline.json")) # gerado na branch principal
failed = False
for cat, base in sorted(baseline.items()):
now = current.get(cat, 0.0)
flag = "REGRESSÃO" if base - now > THRESHOLD else ""
failed |= bool(flag)
print(f"{cat:20} {base:6.1%} {now:6.1%} {now - base:+6.1%} {flag}") # 7
raise SystemExit(1 if failed else 0)
if __name__ == "__main__":
main()O que você ganhou: um comando que a CI roda e que falha o build em regressão. O que você perdeu: correções por regra e juiz, e o relatório no PR, ficam para a próxima iteração.
TypeScript. Use quando o pipeline roda em Node e o eval entra no mesmo test runner do projeto.
import { readFileSync } from "node:fs";
import { classify } from "../src/pipeline"; // 3. o sistema real
type Case = { input: string; expected: string; category: string };
const THRESHOLD = 0.02;
async function pool<T, R>(items: T[], limit: number, fn: (t: T) => Promise<R>): Promise<R[]> {
const out: R[] = new Array(items.length);
let next = 0;
const workers = Array.from({ length: limit }, async () => {
while (next < items.length) {
const i = next++;
out[i] = await fn(items[i]);
}
});
await Promise.all(workers);
return out;
}
export async function runEval(path: string, repeats = 3): Promise<Record<string, number>> {
const cases: Case[] = readFileSync(path, "utf8").trim().split("\n").map((l) => JSON.parse(l));
const scores = await pool(cases, 8, async (c) => {
const outs = await Promise.all(Array.from({ length: repeats }, () => classify(c.input)));
return outs.map((o) => (o === c.expected ? 1 : 0)); // 4
});
const byCat: Record<string, number[]> = {};
cases.forEach((c, i) => (byCat[c.category] ??= []).push(...scores[i]));
return Object.fromEntries(
Object.entries(byCat).map(([k, v]) => [k, v.reduce((a, b) => a + b, 0) / v.length]),
); // 5
}
const current = await runEval("evals/tickets.jsonl");
const baseline: Record<string, number> = JSON.parse(readFileSync("evals/baseline.json", "utf8"));
const regressions = Object.entries(baseline).filter(([cat, base]) => base - (current[cat] ?? 0) > THRESHOLD);
console.table(Object.keys(baseline).map((cat) => ({ cat, base: baseline[cat], now: current[cat] })));
process.exit(regressions.length ? 1 : 0); // 6 e 7O que você ganhou: o eval roda com o mesmo npm do projeto, com concorrência limitada. O que você perdeu: o mesmo que em Python; comece simples e acrescente correções conforme o caso.
Responda as 6 perguntas para concluir a aula. Só assistir não conclui: a checagem é o que conta para a trilha e para o certificado.
Bancada de Evals (Eval Harness) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Operação e qualidade. Rodar casos de teste automaticamente a cada mudança de prompt, modelo ou código.
Prompt Sem Eval, Prompt Preso ao Modelo e Eval no Olhômetro.
Nos cenários reais Perguntas em linguagem natural para SQL, Bot de suporte com escalonamento humano e Revisor automático de PR.