Avaliações self-hosted para agentes de IA
Monte um ciclo de avaliações no seu repositório com casos reais, checagens determinísticas e juiz LLM. Acompanhe a taxa de aprovação a cada commit.
O que são avaliações self-hosted para agentes de IA
As avaliações self-hosted para agentes de IA são quatro elementos que mantém no seu próprio repositório: um ficheiro com casos guardados, um script que executa o agente nesses casos, um conjunto de verificações que avalia cada resposta e uma tabela de resultados que pode consultar. Nada disso exige um fornecedor. Todo o ciclo pode ser implementado com algumas centenas de linhas de Python e um ficheiro SQLite.
O agente funcionou na demonstração porque escolheu os cinco inputs manualmente. Falhou na segunda semana porque uma linha do prompt, um modelo ou a descrição de uma ferramenta mudou, e nenhuma medição abrangia essas alterações. Um ciclo de avaliação transforma «agora parece pior» em «a taxa de aprovação passou de 58 em 60 para 51 em 60 no commit 4f1c9ab».
O ciclo tem quatro passos, e este guia dedica uma secção a cada passo: recolher traces reais, transformar os mais interessantes em casos, avaliar cada caso em cada alteração e guardar a taxa de aprovação junto do commit que a produziu. O mesmo ciclo funciona independentemente daquilo em que executa o agente, e as frameworks self-hosted de agentes que vale a pena executar diferem sobretudo na quantidade do trace que disponibilizam automaticamente.
Por que o agente falha na segunda semana
Um agente é composto por um prompt, um modelo, um conjunto de definições de ferramentas e todo o contexto recuperado em tempo de execução. Qualquer um dos quatro pode mudar sem alterar o código da aplicação, por isso uma revisão de código normal não encontra nada de errado.
A causa mais comum é uma alteração no prompt. Você adiciona uma frase para impedir uma resposta rude. Essa frase altera o comportamento para entradas que ninguém voltou a testar, e os traces mostram isso claramente: o trace da semana passada para a mesma pergunta contém uma chamada de ferramenta create_refund, o desta semana não contém nenhuma, e a resposta é uma desculpa educada. Nenhum erro foi gerado, por isso nenhum alerta foi acionado.
A segunda causa é o modelo. Registre a string exata do modelo enviada em cada execução, claude-haiku-4-5-20251001 em vez de usar uma abreviação que só você conhece, porque uma taxa de aprovação que cai no dia em que você trocou de modelo só pode ser diagnosticada quando o modelo está registado na linha correspondente.
A terceira causa são as ferramentas. Reformular a descrição de uma ferramenta altera o momento em que o modelo decide chamá-la. Se as suas ferramentas forem disponibilizadas por servidores MCP executados num VPS, o schema estará noutro processo e poderá mudar sem que exista qualquer diferença no seu repositório. A quarta causa é a recuperação: a mesma pergunta consulta um índice reconstruído durante a noite, e a resposta passa a seguir o novo documento.
Crie o conjunto de referência a partir dos traces que já recolhe
Não invente casos de avaliação. Extraia-os do tráfego. Se já utiliza tracing self-hosted do Langfuse para o seu agente, cada pedido é armazenado com a entrada, as chamadas de ferramentas e a saída. Esse é exatamente o material bruto de que um caso precisa.
Exporte uma janela de observações root através da API pública. A API utiliza autenticação básica, com a sua chave pública como nome de utilizador e a sua chave secreta como palavra-passe.
export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
"$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
| jq '.data[0]'Leia um registo antes de escrever qualquer lógica de análise. As linhas são devolvidas em data, mas os nomes dos campos que contêm a pergunta e a resposta dependem da forma como o seu agente instrumenta os spans. Mapeie o que realmente encontrar, em vez do que esperava. Depois, escreva manualmente os casos, com um objeto JSON por linha, em evals/cases.jsonl:
{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}Cinco regras mantêm o conjunto útil para execução:
- 40 a 80 casos são suficientes para começar. Abaixo de 20, um caso instável altera a taxa de aprovação em 5 pontos, e um número que varia sem motivo deixa de ser tido em conta.
- Cada erro encontrado em produção que corrigir torna-se um caso no próprio dia. Este hábito faz o conjunto crescer na direção correta.
- Um comportamento por caso. Um caso que verifica simultaneamente o valor do reembolso e o tom não permite concluir nada quando falha.
- O
idnunca muda, porque o ID é o que permite comparar a execução de hoje com a do mês passado. - Remova os dados sensíveis antes de fazer o commit. Este ficheiro vai para o git, por isso remova os nomes de clientes e quaisquer números de encomenda que não sejam seus.
Faça a avaliação com verificações determinísticas primeiro, porque são gratuitas
Tudo o que tem uma resposta certa deve usar uma asserção simples. Não há chamada ao modelo, custo ou ambiguidade. As verificações determinísticas detetam regressões estruturais, que são as que afetam os sistemas à volta do seu agente: o JSON não é analisado, a ferramenta nunca foi chamada, a frase proibida voltou a aparecer ou a resposta não cita nenhuma fonte.
Apenas uma função conhece o seu agente. Todo o resto do harness é genérico.
import json, os, urllib.request
def run_agent(case):
req = urllib.request.Request(
os.environ["AGENT_URL"],
data=json.dumps({"input": case["input"]}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.load(resp)
def deterministic(case, result):
text = result.get("output", "")
called = [c["name"] for c in result.get("tool_calls", [])]
failures = []
for tool in case.get("must_call", []):
if tool not in called:
failures.append(f"tool not called: {tool}")
for phrase in case.get("must_not_include", []):
if phrase.lower() in text.lower():
failures.append(f"forbidden phrase: {phrase}")
if len(called) > case.get("max_tool_calls", 12):
failures.append(f"too many tool calls: {len(called)}")
return failuresMantenha o orçamento de ferramentas nessa lista. Um agente que resolve um caso com 3 chamadas hoje e 11 amanhã regrediu, mesmo quando a resposta final está correta, porque paga por cada chamada que faz.
LLM como avaliador e as quatro formas de falhar
Tudo o que passa pelas asserções precisa de um avaliador que leia. Um avaliador LLM é uma segunda chamada ao modelo: recebe a pergunta, a resposta do agente e um critério, e depois devolve um veredito. É a única forma prática de avaliar se “a resposta responde ao que o utilizador perguntou”.
Quatro regras tornam um avaliador utilizável:
- Veredito binário, nunca uma pontuação de 1 a 10. Uma escala devolve 7 e 8 para quase tudo, por isso o número nunca muda e não se aprende nada com ele.
- Um critério por chamada. Pergunte pelo valor do reembolso ou pelo tom, mas não pelos dois ao mesmo tempo.
- Dê ao avaliador a resposta esperada sempre que o caso tiver uma. Avaliar com base numa referência é muito mais fácil do que avaliar de forma abstrata.
- Force o formato da saída e faça o parsing de forma estrita.
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def judge_prompt(case, output):
return (
"You grade one answer against one criterion.\n"
"Reply with JSON only, in this exact shape:\n"
'{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
f"Criterion: {case['rubric']}\n"
f"Question: {case['input']}\n"
f"Answer: {output}\n"
"Length is not a criterion. Judge only the criterion above."
)
def judge(case, output, model):
msg = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt(case, output)}],
)
return json.loads(msg.content[0].text)Agora, os modos de falha. Cada um tem um teste que pode executar esta tarde, e executá-los é importante, porque um avaliador não verificado produz números que parecem precisos, mas não significam nada.
Viés de comprimento. As respostas mais longas passam com maior frequência. Teste-o: escolha dez respostas que o avaliador reprovou, acrescente a cada uma dois parágrafos de texto confiante que não adicionem nenhum facto novo e avalie-as novamente. Qualquer veredito que mude para aprovado indica viés de comprimento, e o rubric é o elemento que deve ser corrigido.
Preferência pelo próprio modelo. Muitas vezes, um avaliador avalia com mais benevolência a saída da sua própria família de modelos do que a saída de outra. Teste-o: avalie as mesmas 30 respostas com avaliadores de duas famílias diferentes e compare os vereditos caso a caso. Quando discordarem, leia o caso pessoalmente.
Viés de posição. Se usar o avaliador para comparar duas respostas, A e B, troque a ordem e execute novamente. Um veredito que mude com a troca significa que a comparação por pares ainda não é segura para esse rubric.
Deriva do rubric. Critérios vagos produzem avaliadores que tendem a aprovar. “A resposta é útil?” aprova quase tudo. “A resposta indica o valor do reembolso em dólares?” aprova apenas o que pretendia avaliar. Reescreva cada critério até este indicar claramente o facto que está a ser verificado.
Uma proteção abrange os quatro casos. Mantenha 30 casos que classificou manualmente e compare sempre o avaliador com as suas classificações quando alterar o modelo ou o prompt do avaliador. Se este discordar de si em mais de um caso em dez, corrija o rubric antes de confiar em qualquer taxa de aprovação que produza. O avaliador é código, por isso deve ser versionado e revisto como código.
Classifique com um modelo barato e escale para um modelo de fronteira
Avaliar todos os casos com o modelo mais caro em cada commit faz com que a fatura das avaliações cresça mais do que o agente que está a ser testado. Ordene os avaliadores por preço e pare assim que a resposta estiver clara.
The data behind this chart
[
{
"label": "Haiku 4.5, Batch API",
"usd_per_1000_judge_calls": "0.90"
},
{
"label": "Haiku 4.5",
"usd_per_1000_judge_calls": "1.80"
},
{
"label": "Sonnet 5",
"usd_per_1000_judge_calls": "3.60"
},
{
"label": "Opus 5",
"usd_per_1000_judge_calls": "9.00"
}
]Estes valores assumem cerca de 1,200 tokens de entrada e 120 tokens de saída por chamada do avaliador, um tamanho realista para uma pergunta, uma resposta e um critério. Avaliar 1,000 casos custa 1.80 dólares americanos no Claude Haiku 4.5 e 9.00 no Claude Opus 5. A diferença parece pequena até ser multiplicada. Um conjunto de 60 casos, avaliado em cada commit, com 40 commits por semana, representa 2,400 chamadas do avaliador por semana antes de alguém executar o trabalho noturno.
Dois descontos aplicam-se diretamente ao trabalho de avaliação e são cumulativos. As execuções de avaliação não são interativas, por isso a Batch API reduz para metade os preços de entrada e de saída em troca de entrega assíncrona. Essa é a primeira linha do gráfico. A rubrica e as instruções são idênticas byte a byte em todas as chamadas, por isso o armazenamento em cache de prompts é adequado: uma leitura da cache custa um décimo do preço base de entrada, e uma gravação na cache com validade de cinco minutos custa 1.25 vezes o preço base de entrada. Assim, a cache compensa-se depois de uma única reutilização. Estes são os preços de tabela da Anthropic em agosto de 2026, e o Sonnet 5 tem preços promocionais de lançamento até 31 agosto 2026. Por isso, a terceira barra sobe depois dessa data.
A sequência, por ordem:
- Verificações determinísticas em todos os casos. Não há qualquer custo de API.
- Um avaliador baseado num modelo pequeno nos casos que passaram essas verificações.
- Um avaliador baseado num modelo de fronteira apenas quando o avaliador pequeno indica falha ou indica aprovação com baixa confiança.
- Revisão humana de uma pequena amostra, uma vez por semana.
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"
def grade(case, result):
hard = deterministic(case, result)
if hard:
return False, "deterministic", "; ".join(hard)
first = judge(case, result["output"], CHEAP)
if first["verdict"] == "pass" and first["confidence"] == "high":
return True, CHEAP, first["reason"]
second = judge(case, result["output"], STRICT)
return second["verdict"] == "pass", STRICT, second["reason"]Isto troca alguma precisão da avaliação por redução de custos. Meça essa troca em vez de a presumir. Uma vez por mês, avalie também o conjunto completo com o avaliador rigoroso e compare as duas colunas. Se houver divergências em mais do que alguns casos, a sua rubrica é demasiado vaga para o modelo pequeno. É a rubrica que deve corrigir. Controlar os gastos do próprio agente é uma tarefa separada, abordada em controlo de custos de um agente de IA num VPS.
Acompanhe a taxa de aprovação ao longo do tempo num sistema que controla
Uma taxa de aprovação que não pode ser associada a um commit é apenas uma impressão. Armazene uma linha por caso e por execução, com o commit e o modelo nessa linha.
CREATE TABLE IF NOT EXISTS results (
run_id TEXT NOT NULL,
ran_at TEXT NOT NULL,
git_sha TEXT NOT NULL,
agent_model TEXT NOT NULL,
case_id TEXT NOT NULL,
passed INTEGER NOT NULL,
graded_by TEXT NOT NULL,
reason TEXT
);SELECT run_id, git_sha, agent_model,
count(*) AS cases,
round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;Carregue o esquema com sqlite3 evals/results.db < evals/schema.sql e leia a tendência com sqlite3 -box evals/results.db < evals/passrate.sql. Um ano de execuções diárias em 60 casos corresponde a cerca de 22,000 linhas, pelo que o armazenamento nunca se torna um projeto por si só. Executar SQLite em produção numa VPS aborda as definições que começam a ser importantes se este ficheiro for partilhado entre máquinas.
O runner apresenta a mesma informação para uma pessoa:
run 2026-08-05T09:14:22Z sha 4f1c9ab model claude-sonnet-5 58/60 pass (96.7%)
FAIL refund-double-charge deterministic: tool not called: create_refund
FAIL pto-policy-question judge(opus): reply gives no dollar amountExecute a suite nas alterações que podem quebrar um agente, ou seja, alterações de prompts, modelos e ferramentas, e não em todos os commits do repositório. Um hook pre-push cobre o subconjunto rápido:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-pushAs execuções completas são mais lentas e devem ser agendadas. Um serviço e temporizador systemd na VPS executado todas as noites corre o conjunto completo com o prompt implementado. Isto deteta alterações provenientes do exterior do repositório, como uma ferramenta alojada cujo comportamento mudou.
Revisão humana por amostragem, não exaustiva
O judge é calibrado com base em classificações humanas, por isso alguém tem de as produzir. Analise uma amostra todas as semanas: todos os casos em que o judge falhou, mais dez aprovações escolhidas aleatoriamente. As aprovações aleatórias são a metade importante, porque um judge que começou silenciosamente a aprovar respostas incorretas parece perfeito em qualquer dashboard criado a partir das suas próprias decisões.
Quinze casos a três minutos cada correspondem a 45 minutos por semana. Isto permite corrigir o rubric nos pontos em que você e o judge discordam e adicionar novos casos para tipos de falha que ninguém tinha previsto. Escreva a decisão humana na mesma tabela, com graded_by definido como human, para que a concordância entre o judge e a pessoa passe a ser uma consulta, e não algo guardado apenas na memória.
O que falha no próprio harness de avaliação
anthropic.RateLimitError na primeira execução completa. Sessenta casos executados em paralelo excedem o limite de pedidos ou de tokens do seu plano. Limite a concorrência a quatro workers e transfira a execução noturna para a Batch API.
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) do juiz. O modelo respondeu em prosa ou colocou o JSON dentro de um bloco de código. Tente novamente uma vez e registe o caso como erro. Nunca deixe uma falha de análise contar como aprovação, porque uma suite que transforma erros em aprovações aproxima-se dos 100% enquanto o agente piora.
Casos instáveis. A mesma entrada é aprovada numa execução e falha na seguinte porque o agente faz amostragem da saída. Execute o caso instável três vezes e registe a fração, em vez de o eliminar. Um caso aprovado em duas de três execuções revela um problema real de robustez, que um cliente encontrará.
Degradação do conjunto de referência. Alguém edita uma resposta esperada para fazer a suite ficar verde. Reveja as diferenças em evals/cases.jsonl com o mesmo cuidado que aplica às diferenças do agente, porque esse ficheiro é a sua definição escrita de correto.
Uma suite que nunca falha. Uma taxa de aprovação fixada em 100% durante um mês significa que o conjunto deixou de acompanhar o produto. Recolha dez traces recentes, encontre os casos que o agente tratou mal e adicione-os. Depois provoque uma falha de propósito e confirme que a execução fica vermelha. Esta é a verificação de que o mutation testing se aplica a uma suite de testes e a única forma de saber se o seu conjunto ainda deteta problemas.
FAQ
De quantos casos precisa um conjunto de avaliação de um agente de IA?
Comece com 40 a 80 e aumente o conjunto a partir de falhas reais. Abaixo de cerca de 20 casos, um resultado instável altera a taxa de aprovação em 5 pontos, pelo que essa taxa deixa de ter valor informativo. Acima de algumas centenas, cada execução custa dinheiro e tempo reais, enquanto cada caso adicional acrescenta pouca cobertura. A medida importante não é a contagem: é a percentagem dos tipos de falha conhecidos em produção que aparecem pelo menos uma vez no conjunto.
Posso confiar num avaliador LLM para classificar o meu agente?
Só depois de o medir com base nas suas próprias classificações. Mantenha 30 casos classificados manualmente e compare o avaliador com esses casos sempre que alterar o modelo ou o prompt do avaliador. Os avaliadores apresentam viés de comprimento, em que as respostas mais extensas passam com maior frequência, e preferência pelo próprio modelo, em que as respostas da sua própria família de modelos são classificadas de forma mais favorável. Ambos podem ser testados: aumente artificialmente uma resposta que falhou e volte a classificá-la, ou classifique as mesmas respostas com um avaliador de outra família. Se o avaliador discordar das suas classificações em mais de 1 caso em 10, a rubrica é demasiado vaga para ser usada.
Que modelo deve classificar as avaliações?
Comece pelo mais barato e escale quando necessário. As asserções determinísticas não têm custo, pelo que são executadas primeiro em todos os casos. Um modelo pequeno trata das aprovações claras. Apenas as falhas e os veredictos com baixa confiança são enviados para um modelo de fronteira. Aos preços de tabela de August 2026, classificar 1.000 casos custa cerca de 1.80 dólares americanos com Claude Haiku 4.5 e cerca de 9.00 com Claude Opus 5. Como as execuções de avaliação são assíncronas, a Batch API reduz para metade qualquer um dos valores.
As avaliações substituem a monitorização em produção?
Não, porque respondem a perguntas diferentes. Um conjunto de avaliações indica se uma alteração que está prestes a ser lançada melhora ou piora um conjunto fixo de casos. O tracing e a monitorização indicam o que os utilizadores reais estão a enviar neste momento, incluindo entradas que nenhum caso cobre. Ambos alimentam o outro: os traces fornecem casos novos, e o conjunto de avaliações determina se a correção funcionou realmente.