Como Saber se Seu Agente de IA Está Melhorando
Você construiu um agente. Ele está rodando. Mas ele está ficando melhor ou pior? A maioria das pessoas não sabe responder. Contam com "parece que sim" ou "os usuários não reclamaram". Isso não escala.
Este guia é sobre métricas de avaliação para agentes de IA — do básico que qualquer um pode medir em 10 minutos até sistemas avançados de LLM-as-judge, A/B testing e detecção de regressão. Tudo com código real, thresholds que funcionam na prática, e um dashboard que você pode rodar hoje.
Sumário executivo — Avaliar agentes de IA exige métricas de resultado (tempo de resolução, custo, satisfação), processo (passos, ferramentas usadas, taxa de conclusão) e qualidade (acurácia, consistência, alucinações). Comece com Task Resolution Time e Task Success Rate. Depois evolua para LLM-as-judge, A/B testing e detecção de regressão. Use Python + JSON para logs estruturados, defina thresholds por tarefa e visualize tudo em um dashboard simples. O guia inclui código real, thresholds práticos e um exemplo completo de dashboard.
TL;DR rápido — Se você só medir uma coisa, meça o tempo de resolução de tarefa (Task Resolution Time). É a métrica que mais correlaciona com valor percebido pelo usuário. Mas não pare por aí.
Este guia cobre:
- As 4 categorias de métricas que todo agente precisa
- Código para coleta automática de métricas no Hermes Agent
- LLM-as-judge: como usar um modelo para avaliar outro
- A/B testing de agentes: como comparar versões
- Detecção de regressão: como saber quando uma mudança quebrou algo
- Dashboard simples com Python + SQLite
- Anti-patterns que destroem a confiabilidade das suas métricas
O Problema: "Parece que Está Funcionando"
Agentes de IA são sistemas estocásticos. Eles não são determinísticos como um API REST que sempre retorna o mesmo JSON para o mesmo input. O mesmo prompt pode gerar respostas diferentes. A mesma tarefa pode levar 3 chamadas de ferramenta em uma execução e 12 na outra.
Isso torna a avaliação inherentemente mais difícil que software tradicional. Você não pode simplesmente rodar assert response == expected.
Os 3 erros clássicos
Erro 1 — Medir só o que é fácil
"Meu agente responde em 2 segundos." Ótimo. Mas a resposta está certa? Ele chamou as ferramentas certas? O usuário precisou fazer 3 follow-ups para corrigir? Velocidade ≠ qualidade.
Erro 2 — Avaliar com o mesmo modelo que gerou
Você pede ao GPT-4 para avaliar se a resposta do GPT-4 está boa. O viés de confirmação é real — mas menos absoluto do que parece. Pesquisas recentes (2024-2025) mostram que modelos modernos (Claude 3.5 Sonnet, GPT-4o, Gemini 1.5 Pro) são surpreendentemente bons auto-avaliadores quando usam rubricas estruturadas (como as do nosso
LLMJudge). O problema não é o modelo ser o mesmo; é a falta de critérios explícitos. Mesmo assim, múltiplos modelos diferentes + mediana continua sendo a prática mais segura.
Erro 3 — Ignorar regressão
Você melhorou o prompt de verão. O agente ficou melhor em 80% dos casos. Mas piorou nos outros 20% — e um deles era o caso mais importante do seu cliente enterprise. Você não sabia porque não estava medindo.
Regra prática: Se você não consegue apontar uma regressão específica nas últimas 2 semanas, você não está medindo o suficiente.
As 4 Categorias de Métricas
Toda métrica de agente cai em uma destas 4 categorias. Medir só uma é insuficiente. Medir todas 4 com pesos equilibrados é o mínimo para confiar no seu sistema.
1. Eficiência — Quão rápido e enxuto
Métricas de velocidade e consumo de recursos. Fáceis de medir, difíceis de interpretar isoladamente.
| Métrica | O que mede | Como calcular | Threshold saudável |
|---|---|---|---|
| Task Resolution Time | Tempo desde o input do usuário até a resposta final | timestamp_fim - timestamp_inicio |
< 30s para tarefas simples; < 5min para complexas |
| Tool Calls per Task | Quantidade de chamadas de ferramenta | Contar function_call na sessão |
< 5 para tarefas simples; < 15 para complexas |
| Tokens Consumidos | Total de tokens (input + output) | Soma do uso de API | Depende do modelo; normalize por tarefa |
| Latency Modelo | Tempo de resposta do LLM (sem I/O) | Média de t_resposta - t_prompt |
< 3s para modelos cloud; < 15s para locais |
| Latency Ferramenta | Tempo de execução de tool calls (I/O externo) | Média de t_fim_tool - t_inicio_tool |
< 5s para APIs; < 30s para buscas complexas |
Por que importa: Um agente que resolve em 10s com 2 tool calls é mais confiável que um que resolve em 30s com 8 calls. Mais steps = mais oportunidades de erro.
Distinga latências: "Latency per Step < 2s" é irrealista se o step inclui uma chamada API externa. Separe latência do modelo (puro LLM) da latência de ferramenta (I/O de rede). O modelo você controla trocando de provider; a ferramenta você controla otimizando queries ou usando cache.
2. Qualidade — Quão boa é a resposta
Métricas subjetivas que requerem avaliação humana ou LLM-as-judge. São as mais importantes e as mais difíceis.
| Métrica | O que mede | Como calcular | Threshold saudável |
|---|---|---|---|
| Task Completion Rate | A tarefa foi completamente resolvida? | Binário: sim/não (human label) | > 90% |
| Response Accuracy | A informação está correta? | Comparar com ground truth ou human review | > 85% |
| Relevance Score | A resposta atende ao que foi perguntado? | 1-5 scale (human ou LLM) | > 4.0 |
| Clarity Score | A resposta é clara e bem estruturada? | 1-5 scale (human ou LLM) | > 4.0 |
3. Confiabilidade — Quão previsível
Métricas de estabilidade e robustez. Um agente que funciona 95% das vezes é muito mais útil que um que funciona 99% mas explode nos 1% críticos.
| Métrica | O que mede | Como calcular | Threshold saudável |
|---|---|---|---|
| Success Rate | % de tarefas concluídas sem erro | sucessos / total |
> 95% |
| Human Intervention Rate | % de tarefas que precisaram de humano | intervenções / total |
< 10% |
| Recoverable Error Rate | % de erros que o agente resolve sozinho | auto-recuperados / total_erros |
> 70% |
| Hallucination Rate | % de respostas com informação fabricada | Detectado por human ou fact-checker | < 5% |
| Loop Rate | % de tarefas que entram em loop infinito | Timeout ou max_steps atingido | < 1% |
| Consistency Score | Mesma tarefa, mesma resposta? | N=5 runs, coeficiente de variação < 15% | CV < 15% |
Nota sobre métricas de confiabilidade:
- Human Intervention Rate é a métrica mais honesta para produção. Se seu agente precisa de um humano a cada 3 tarefas, ele não é autônomo — é assistido. O threshold de < 10% é ambicioso; muitos agentes em produção operam em 20-30% no primeiro mês.
- Recoverable Error Rate exige classificação automática. Use o LLM-as-judge para categorizar erros em "recuperável" (timeout, API rate limit, input malformado) vs "irrecuperável" (hallucination crítica, comando destrutivo, violação de segurança). O código do
RegressionDetectorabaixo já faz isso.- Hallucination Rate < 5% é mais realista que < 2%. Detecção automática via LLM-as-judge captura ~60-70% das alucinações; o restante requer fact-checking humano periódico.
- Consistency Score: rode a mesma tarefa 5 vezes (N=5) e calcule o coeficiente de variação (desvio padrão / média). CV < 15% = consistente; CV > 30% = instável, requer investigação.
4. Custo — Quanto custa
Métricas financeiras. Fáceis de medir, mas requerem contexto de ROI para serem úteis.
| Métrica | O que mede | Como calcular | Threshold saudável |
|---|---|---|---|
| Cost per Task | Custo médio de uma tarefa | custo_total / num_tarefas |
Depende do caso; normalize |
| Cost per Token | Eficiência de uso de tokens | custo_total / tokens_total |
Compare com baseline |
| Monthly Cost per User | Custo operacional | custo_mês / usuários_ativos |
< orçamento alocado |
| ROI | Tempo economizado vs custo | (tempo_economizado * valor_hora) - custo |
> 3x (cada R$ 1 gera R$ 3+ de valor) |
Coleta Automática de Métricas no Hermes Agent
O Hermes Agent tem acesso a todas as ferramentas necessárias para coletar métricas automaticamente. Vamos construir um sistema de logging que captura tudo — sem modificar o core do Hermes.
Estrutura de dados
Cada execução de tarefa gera um registro estruturado em JSON:
{
"task_id": "uuid-v4",
"timestamp_start": "2026-06-15T10:23:45Z",
"timestamp_end": "2026-06-15T10:24:12Z",
"task_description": "Auditar repo em busca de código morto",
"model": "claude-sonnet-4",
"tools_used": ["terminal", "file", "search_files"],
"tool_calls_count": 4,
"tokens_input": 1240,
"tokens_output": 890,
"cost_usd": 0.023,
"status": "completed",
"error": null,
"user_rating": null,
"llm_judge_score": null,
"metadata": {
"session_id": "sess-abc123",
"profile": "default",
"skills_triggered": ["repo-audit"]
}
}
Código: Logger de Métricas
# metrics_logger.py
# Logger leve que se integra ao Hermes Agent via wrapper de sessão
import json
import uuid
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
@dataclass
class TaskMetrics:
task_id: str
timestamp_start: str
timestamp_end: Optional[str] = None
task_description: str = ""
model: str = ""
tools_used: List[str] = None
tool_calls_count: int = 0
tokens_input: int = 0
tokens_output: int = 0
cost_usd: float = 0.0
status: str = "running" # running, completed, failed, timeout
error: Optional[str] = None
user_rating: Optional[int] = None # 1-5
llm_judge_score: Optional[float] = None # 0-1
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.tools_used is None:
self.tools_used = []
if self.metadata is None:
self.metadata = {}
@property
def duration_seconds(self) -> float:
if self.timestamp_end:
start = datetime.fromisoformat(self.timestamp_start.replace('Z', '+00:00'))
end = datetime.fromisoformat(self.timestamp_end.replace('Z', '+00:00'))
return (end - start).total_seconds()
return 0.0
def to_dict(self) -> Dict:
d = asdict(self)
d['duration_seconds'] = self.duration_seconds
return d
class MetricsLogger:
"""Logger que persiste métricas em SQLite + JSONL para análise."""
def __init__(self, db_path: str = "~/.hermes/metrics.db"):
self.db_path = Path(db_path).expanduser()
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.jsonl_path = self.db_path.with_suffix('.jsonl')
self._init_db()
def _init_db(self):
import sqlite3
conn = sqlite3.connect(str(self.db_path))
conn.execute('''
CREATE TABLE IF NOT EXISTS task_metrics (
task_id TEXT PRIMARY KEY,
timestamp_start TEXT,
timestamp_end TEXT,
task_description TEXT,
model TEXT,
tools_used TEXT,
tool_calls_count INTEGER,
tokens_input INTEGER,
tokens_output INTEGER,
cost_usd REAL,
status TEXT,
error TEXT,
user_rating INTEGER,
llm_judge_score REAL,
duration_seconds REAL,
metadata TEXT
)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_status ON task_metrics(status)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_model ON task_metrics(model)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_timestamp ON task_metrics(timestamp_start)
''')
conn.commit()
conn.close()
def log_task(self, metrics: TaskMetrics):
# Persiste em SQLite
import sqlite3
conn = sqlite3.connect(str(self.db_path))
conn.execute('''
INSERT OR REPLACE INTO task_metrics VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
''', (
metrics.task_id,
metrics.timestamp_start,
metrics.timestamp_end,
metrics.task_description,
metrics.model,
json.dumps(metrics.tools_used),
metrics.tool_calls_count,
metrics.tokens_input,
metrics.tokens_output,
metrics.cost_usd,
metrics.status,
metrics.error,
metrics.user_rating,
metrics.llm_judge_score,
metrics.duration_seconds,
json.dumps(metrics.metadata)
))
conn.commit()
conn.close()
# Também append em JSONL para análise externa fácil
with open(self.jsonl_path, 'a') as f:
f.write(json.dumps(metrics.to_dict(), default=str) + '\n')
def get_metrics(self, days: int = 7) -> List[Dict]:
import sqlite3
conn = sqlite3.connect(str(self.db_path))
cursor = conn.execute('''
SELECT * FROM task_metrics
WHERE timestamp_start >= datetime('now', '-{} days')
ORDER BY timestamp_start DESC
'''.format(days))
rows = cursor.fetchall()
conn.close()
columns = [d[0] for d in cursor.description]
return [dict(zip(columns, row)) for row in rows]
def get_metrics_safe(self, days: int = 7) -> List[Dict]:
"""Versão com queries parametrizadas (produção)."""
import sqlite3
conn = sqlite3.connect(str(self.db_path))
cursor = conn.execute('''
SELECT * FROM task_metrics
WHERE timestamp_start >= datetime('now', '-' || ? || ' days')
ORDER BY timestamp_start DESC
''', (days,))
rows = cursor.fetchall()
conn.close()
columns = [d[0] for d in cursor.description]
return [dict(zip(columns, row)) for row in rows]
Wrapper para sessões do Hermes
# hermes_metrics_wrapper.py
# Wrapper que intercepta execuções do Hermes e loga métricas
import asyncio
from metrics_logger import MetricsLogger, TaskMetrics
from datetime import datetime, timezone
logger = MetricsLogger()
class HermesMetricsWrapper:
"""Envolve uma sessão do Hermes e coleta métricas automaticamente."""
def __init__(self, hermes_session):
self.session = hermes_session
self.current_task: TaskMetrics = None
async def run_task(self, task_description: str) -> str:
# Inicia métricas
self.current_task = TaskMetrics(
task_id=str(uuid.uuid4()),
timestamp_start=datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
task_description=task_description,
model=self.session.config.get('model', 'unknown')
)
try:
# Executa a tarefa real
result = await self.session.run(task_description)
# Coleta métricas pós-execução
self.current_task.timestamp_end = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
self.current_task.status = "completed"
self.current_task.tool_calls_count = len(self.session.tool_calls)
self.current_task.tools_used = list(set(self.session.tool_calls))
self.current_task.tokens_input = self.session.token_usage.get('input', 0)
self.current_task.tokens_output = self.session.token_usage.get('output', 0)
self.current_task.cost_usd = self.session.cost_usd
logger.log_task(self.current_task)
return result
except Exception as e:
self.current_task.timestamp_end = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
self.current_task.status = "failed"
self.current_task.error = str(e)
logger.log_task(self.current_task)
raise
LLM-as-Judge: Avaliação Automática
A ideia é simples: use um modelo de linguagem para avaliar a qualidade das respostas do seu agente. A implementação é onde mora o diabo.
O problema do viés
Se você usar o mesmo modelo que gerou a resposta para avaliá-la, o viés de confirmação existe mas é mitigável. Pesquisas recentes mostram que modelos modernos (Claude 3.5 Sonnet, GPT-4o, Gemini 1.5 Pro) são bons auto-avaliadores quando usam rubricas estruturadas — como as do nosso LLMJudge abaixo. O problema histórico não era o modelo ser o mesmo; era a falta de critérios explícitos. Mesmo assim, múltiplos modelos diferentes + mediana continua sendo a prática mais segura.
Solção: Use um modelo diferente e mais barato para avaliar. Ou melhor: use múltiplos modelos e tire a mediana.
Código: LLM-as-Judge com múltiplos avaliadores
# llm_judge.py
# Sistema de avaliação com múltiplos modelos para reduzir viés
import asyncio
import json
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class JudgeResult:
model: str
score: float # 0-1
reasoning: str
dimensions: Dict[str, float] # accuracy, relevance, clarity, completeness
class LLMJudge:
"""Avalia respostas de agentes usando múltiplos modelos como juízes."""
# Modelos avaliadores — mistura de baratos e fortes
JUDGE_MODELS = [
"claude-haiku-3", # Rápido e barato
"gpt-4o-mini", # Rápido e barato
"claude-sonnet-4", # Forte, mais caro
]
# Rubrica de avaliação — critérios explícitos
RUBRIC = """
Você é um avaliador imparcial de respostas de agentes de IA.
Avalie a resposta abaixo em 4 dimensões, cada uma de 0 a 1:
1. **Accuracy (0-1)**: A informação está factualmente correta?
1.0 = 100% correto, 0.0 = completamente errado
2. **Relevance (0-1)**: A resposta atende diretamente à pergunta?
1.0 = responde exatamente o que foi perguntado, 0.0 = irrelevante
3. **Clarity (0-1)**: A resposta é clara, bem estruturada e fácil de entender?
1.0 = excelente, 0.0 = confusa ou ilegível
4. **Completeness (0-1)**: A resposta cobre todos os aspectos importantes da pergunta?
1.0 = completa, 0.0 = omitiu aspectos críticos
Regras:
- Seja CRÍTICO. Notas acima de 0.9 devem ser raras.
- Compare mentalmente com a melhor resposta possível.
- Se houver erro factual, accuracy = 0 independente das outras dimensões.
Responda APENAS em JSON válido:
{
"accuracy": 0.0-1.0,
"relevance": 0.0-1.0,
"clarity": 0.0-1.0,
"completeness": 0.0-1.0,
"overall": 0.0-1.0,
"reasoning": "explicação breve da nota"
}
"""
def __init__(self, api_client):
self.api = api_client
async def evaluate(
self,
task_description: str,
agent_response: str,
ground_truth: Optional[str] = None
) -> List[JudgeResult]:
"""Avalia uma resposta usando múltiplos modelos."""
# Monta o prompt de avaliação
eval_prompt = self._build_prompt(task_description, agent_response, ground_truth)
# Avalia com cada modelo em paralelo
tasks = [
self._evaluate_with_model(model, eval_prompt)
for model in self.JUDGE_MODELS
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Filtra erros
valid_results = [r for r in results if isinstance(r, JudgeResult)]
return valid_results
def _build_prompt(
self,
task: str,
response: str,
ground_truth: Optional[str]
) -> str:
parts = [
self.RUBRIC,
f"\nTAREFA: {task}",
f"\nRESPOSTA DO AGENTE:\n{response}",
]
if ground_truth:
parts.append(f"\nRESPOSTA ESPERADA (ground truth):\n{ground_truth}")
return "\n".join(parts)
async def _evaluate_with_model(self, model: str, prompt: str) -> JudgeResult:
"""Chama um modelo avaliador e parseia o resultado."""
try:
response = await self.api.complete(prompt, model=model, temperature=0.0)
# Extrai JSON da resposta
json_str = self._extract_json(response)
data = json.loads(json_str)
return JudgeResult(
model=model,
score=data.get('overall', 0.0),
reasoning=data.get('reasoning', ''),
dimensions={
'accuracy': data.get('accuracy', 0.0),
'relevance': data.get('relevance', 0.0),
'clarity': data.get('clarity', 0.0),
'completeness': data.get('completeness', 0.0),
}
)
except Exception as e:
# Fallback: retorna score baixo com erro documentado
return JudgeResult(
model=model,
score=0.0,
reasoning=f"Erro na avaliação: {str(e)}",
dimensions={}
)
def _extract_json(self, text: str) -> str:
"""Extrai JSON de texto que pode ter markdown ou explicações."""
# Tenta encontrar JSON entre ```json ... ```
if '```json' in text:
start = text.find('```json') + 7
end = text.find('```', start)
return text[start:end].strip()
# Tenta encontrar JSON entre ``` ... ```
if '```' in text:
start = text.find('```') + 3
end = text.find('```', start)
return text[start:end].strip()
# Assume que o texto inteiro é JSON
return text.strip()
def aggregate_scores(self, results: List[JudgeResult]) -> Dict:
"""Agrega scores de múltiplos juízes — usa mediana para resistir a outliers."""
if not results:
return {"overall": 0.0, "dimensions": {}, "confidence": 0.0}
import statistics
overall_scores = [r.score for r in results]
dimensions = {}
# Agrega cada dimensão
for dim in ['accuracy', 'relevance', 'clarity', 'completeness']:
scores = [r.dimensions.get(dim, 0.0) for r in results if dim in r.dimensions]
if scores:
dimensions[dim] = {
"median": statistics.median(scores),
"mean": statistics.mean(scores),
"stdev": statistics.stdev(scores) if len(scores) > 1 else 0.0
}
# Confiança = inverso da variância entre juízes
if len(overall_scores) > 1:
variance = statistics.variance(overall_scores)
confidence = max(0.0, 1.0 - variance) # 1 = total concordância
else:
confidence = 0.5
return {
"overall": {
"median": statistics.median(overall_scores),
"mean": statistics.mean(overall_scores),
"min": min(overall_scores),
"max": max(overall_scores)
},
"dimensions": dimensions,
"confidence": confidence,
"judge_count": len(results),
"raw_results": [
{"model": r.model, "score": r.score, "reasoning": r.reasoning}
for r in results
]
}
Por que múltiplos juízes?
| Juízes | Vantagem | Quando usar |
|---|---|---|
| 1 (mesmo modelo) | Barato, rápido | Nunca — viés enorme |
| 1 (modelo diferente) | Razoável | Prototipagem rápida |
| 2 (modelos diferentes) | Detecta divergência | MVP de avaliação |
| 3+ (mix barato + forte) | Mediana robusta, confiança calculada | Produção |
Regra prática: Se 3 juízes discordam amplamente (variância > 0.3), a tarefa é ambígua ou a resposta está na fronteira da qualidade. Flag para revisão humana.
A/B Testing de Agentes
Como comparar duas versões do seu agente? Não é só "rodar 10 tarefas e ver qual foi melhor". Você precisa de significância estatística.
Código: Framework de A/B Testing
# ab_testing.py
# Framework para comparar duas versões de agente com significância estatística
import random
from typing import List, Dict, Callable, Any
from dataclasses import dataclass
from statistics import mean, stdev
import math
@dataclass
class ABTestResult:
variant_a: str
variant_b: str
metric: str
n_a: int
n_b: int
mean_a: float
mean_b: float
diff_pct: float
p_value: float
significant: bool
winner: str
class ABTestFramework:
"""Compara duas variantes de agente com teste t de Student."""
def __init__(self, confidence: float = 0.95):
self.confidence = confidence
self.alpha = 1 - confidence
def run_test(
self,
variant_a: Callable,
variant_b: Callable,
test_cases: List[Dict],
metric_fn: Callable[[Any], float],
randomize: bool = True
) -> ABTestResult:
"""
Executa A/B test entre duas variantes.
Args:
variant_a: Função que executa a versão A do agente
variant_b: Função que executa a versão B do agente
test_cases: Lista de inputs de teste
metric_fn: Função que extrai a métrica do resultado (ex: lambda r: r['duration'])
randomize: Se True, distribui casos aleatoriamente entre A e B
"""
results_a = []
results_b = []
for case in test_cases:
if randomize and random.random() < 0.5:
# A primeiro
r_a = variant_a(case)
r_b = variant_b(case)
else:
# B primeiro
r_b = variant_b(case)
r_a = variant_a(case)
results_a.append(metric_fn(r_a))
results_b.append(metric_fn(r_b))
return self._analyze(results_a, results_b)
def _analyze(self, results_a: List[float], results_b: List[float]) -> ABTestResult:
"""Análise estatística dos resultados."""
n_a, n_b = len(results_a), len(results_b)
mean_a, mean_b = mean(results_a), mean(results_b)
# Teste t de Student (variâncias independentes)
var_a = stdev(results_a) ** 2 if n_a > 1 else 0
var_b = stdev(results_b) ** 2 if n_b > 1 else 0
# Standard error
se = math.sqrt(var_a / n_a + var_b / n_b)
# t-statistic
if se == 0:
t_stat = 0
else:
t_stat = (mean_a - mean_b) / se
# Graus de liberdade (Welch-Satterthwaite)
if var_a == 0 and var_b == 0:
df = n_a + n_b - 2
else:
numerator = (var_a / n_a + var_b / n_b) ** 2
denominator = (var_a / n_a) ** 2 / (n_a - 1) + (var_b / n_b) ** 2 / (n_b - 1)
df = numerator / denominator if denominator > 0 else n_a + n_b - 2
# p-value (aproximação para two-tailed)
from math import erf, sqrt
p_value = 2 * (1 - 0.5 * (1 + erf(abs(t_stat) / sqrt(2))))
diff_pct = ((mean_b - mean_a) / mean_a * 100) if mean_a != 0 else 0
winner = "tie"
if p_value < self.alpha:
winner = "B" if mean_b > mean_a else "A"
return ABTestResult(
variant_a="A",
variant_b="B",
metric="custom",
n_a=n_a,
n_b=n_b,
mean_a=mean_a,
mean_b=mean_b,
diff_pct=diff_pct,
p_value=p_value,
significant=p_value < self.alpha,
winner=winner
)
def recommend_sample_size(
self,
baseline_mean: float,
expected_improvement_pct: float,
baseline_std: float
) -> int:
"""Calcula tamanho de amostra necessário para detectar uma melhoria."""
# Efeito mínimo detectável
mde = baseline_mean * (expected_improvement_pct / 100)
# Cohen's d
cohens_d = mde / baseline_std if baseline_std > 0 else 0.5
# Para 80% power, 95% confidence, two-tailed
# Fórmula simplificada: n = 16 / d^2 por grupo
if cohens_d > 0:
n_per_group = int(math.ceil(16 / (cohens_d ** 2)))
else:
n_per_group = 100 # fallback
return n_per_group
Exemplo de uso
# Exemplo: comparar dois prompts de agente SEO
from ab_testing import ABTestFramework
# Versão A: prompt atual
agent_a = lambda case: run_seo_agent(case['url'], prompt_version="v1")
# Versão B: novo prompt com instruções mais específicas
agent_b = lambda case: run_seo_agent(case['url'], prompt_version="v2")
# Métrica: tempo de resolução
metric = lambda result: result['duration_seconds']
# Casos de teste
test_cases = [
{"url": "https://example1.com", "expected_pages": 50},
{"url": "https://example2.com", "expected_pages": 200},
{"url": "https://example3.com", "expected_pages": 10},
# ... 20+ casos
]
# Recomendação de tamanho de amostra
framework = ABTestFramework(confidence=0.95)
n_needed = framework.recommend_sample_size(
baseline_mean=45.0, # 45s médio atual
expected_improvement_pct=20, # Espero 20% de melhoria
baseline_std=12.0 # Desvio padrão de 12s
)
print(f"Precisa de {n_needed} casos por grupo ({n_needed * 2} total)")
# Executa o teste
result = framework.run_test(agent_a, agent_b, test_cases, metric)
print(f"Variante A: {result.mean_a:.1f}s (n={result.n_a})")
print(f"Variante B: {result.mean_b:.1f}s (n={result.n_b})")
print(f"Diferença: {result.diff_pct:+.1f}%")
print(f"p-value: {result.p_value:.4f}")
print(f"Significativo: {result.significant}")
print(f"Vencedor: {result.winner}")
Regras para A/B testing de agentes
- Nunca use menos de 30 casos por grupo — a variância de agentes é alta
- Randomize a ordem — se A sempre roda primeiro, efeitos de cache/warmup viciam
- Use métricas primárias e secundárias — defina uma métrica principal antes de rodar o teste
- Não peek — não pare o teste no meio porque "parece bom". Isso invalida a significância.
- Documente tudo — versão do modelo, temperatura, seed, versão do prompt. Tudo afeta.
Detecção de Regressão
A mudança que melhora 80% dos casos mas quebra 20% é pior que não mudar nada. Você precisa detectar regressões automaticamente.
Código: Sistema de Regressão Detection
# regression_detector.py
# Detecta quando uma nova versão do agente piora em casos específicos
import json
from typing import List, Dict, Tuple
from dataclasses import dataclass
from datetime import datetime, timedelta
@dataclass
class RegressionAlert:
severity: str # critical, warning, info
task_type: str
metric: str
baseline_value: float
current_value: float
change_pct: float
affected_cases: List[str]
recommendation: str
class RegressionDetector:
"""Compara métricas atuais com baseline histórico e detecta regressões."""
# Thresholds de regressão por métrica
THRESHOLDS = {
"success_rate": {"critical": -5.0, "warning": -2.0}, # queda de %
"duration_seconds": {"critical": 50.0, "warning": 20.0}, # aumento de %
"tool_calls_count": {"critical": 100.0, "warning": 50.0}, # aumento de %
"cost_usd": {"critical": 100.0, "warning": 50.0}, # aumento de %
"llm_judge_score": {"critical": -10.0, "warning": -5.0}, # queda de %
}
def __init__(self, metrics_logger, baseline_days: int = 14):
self.logger = metrics_logger
self.baseline_days = baseline_days
def detect(self, current_days: int = 1) -> List[RegressionAlert]:
"""Detecta regressões comparando período atual com baseline."""
# Busca métricas
baseline = self.logger.get_metrics(days=self.baseline_days)
current = self.logger.get_metrics(days=current_days)
alerts = []
# Agrupa por tipo de tarefa
baseline_by_type = self._group_by_task_type(baseline)
current_by_type = self._group_by_task_type(current)
for task_type in set(baseline_by_type.keys()) | set(current_by_type.keys()):
b_metrics = baseline_by_type.get(task_type, [])
c_metrics = current_by_type.get(task_type, [])
if len(b_metrics) < 10 or len(c_metrics) < 5:
continue # Dados insuficientes
# Compara cada métrica
for metric_name in ["success_rate", "duration_seconds", "tool_calls_count", "cost_usd", "llm_judge_score"]:
alert = self._compare_metric(task_type, metric_name, b_metrics, c_metrics)
if alert:
alerts.append(alert)
return sorted(alerts, key=lambda a: a.severity)
def _group_by_task_type(self, metrics: List[Dict]) -> Dict[str, List[Dict]]:
"""Agrupa métricas por tipo de tarefa (extraído da descrição)."""
groups = {}
for m in metrics:
# Extrai tipo da descrição (primeiras 3 palavras)
desc = m.get('task_description', '')
task_type = ' '.join(desc.split()[:3]).lower() if desc else 'unknown'
if task_type not in groups:
groups[task_type] = []
groups[task_type].append(m)
return groups
def _compare_metric(
self,
task_type: str,
metric_name: str,
baseline: List[Dict],
current: List[Dict]
) -> Optional[RegressionAlert]:
"""Compara uma métrica específica e retorna alerta se houver regressão."""
# Calcula valores agregados
b_value = self._aggregate(baseline, metric_name)
c_value = self._aggregate(current, metric_name)
if b_value == 0:
return None
change_pct = ((c_value - b_value) / b_value) * 100
# Determina direção do problema
thresholds = self.THRESHOLDS.get(metric_name, {})
# Para métricas onde MAIOR é pior (duration, cost, tool_calls)
is_bigger_worse = metric_name in ["duration_seconds", "tool_calls_count", "cost_usd"]
if is_bigger_worse and change_pct > 0:
# Aumento = regressão
if change_pct >= thresholds.get("critical", 50.0):
severity = "critical"
elif change_pct >= thresholds.get("warning", 20.0):
severity = "warning"
else:
return None
elif not is_bigger_worse and change_pct < 0:
# Queda = regressão (success_rate, judge_score)
if abs(change_pct) >= abs(thresholds.get("critical", -5.0)):
severity = "critical"
elif abs(change_pct) >= abs(thresholds.get("warning", -2.0)):
severity = "warning"
else:
return None
else:
return None # Melhoria, não regressão
# Identifica casos afetados
affected = [m['task_id'] for m in current if self._is_outlier(m, metric_name, b_value)]
return RegressionAlert(
severity=severity,
task_type=task_type,
metric=metric_name,
baseline_value=b_value,
current_value=c_value,
change_pct=change_pct,
affected_cases=affected[:10], # Top 10
recommendation=self._recommendation(metric_name, change_pct)
)
def _aggregate(self, metrics: List[Dict], metric_name: str) -> float:
"""Agrega métrica — média para a maioria, taxa para success_rate."""
if metric_name == "success_rate":
successes = sum(1 for m in metrics if m.get('status') == 'completed')
return (successes / len(metrics)) * 100 if metrics else 0
values = [m.get(metric_name, 0) for m in metrics if m.get(metric_name) is not None]
return sum(values) / len(values) if values else 0
def _is_outlier(self, metric: Dict, metric_name: str, baseline: float) -> bool:
"""Verifica se um caso específico é outlier negativo."""
value = metric.get(metric_name, 0)
if value is None:
return False
is_bigger_worse = metric_name in ["duration_seconds", "tool_calls_count", "cost_usd"]
if is_bigger_worse:
return value > baseline * 1.5 # 50% pior que baseline
else:
return value < baseline * 0.8 # 20% pior que baseline
def _recommendation(self, metric_name: str, change_pct: float) -> str:
"""Gera recomendação baseada na métrica e magnitude."""
recommendations = {
"duration_seconds": "Verifique se há loops ou chamadas de ferramenta desnecessárias. Reveja o prompt.",
"tool_calls_count": "O agente está chamando mais ferramentas que o necessário. Adicione restrições no prompt.",
"cost_usd": "Custo aumentou significativamente. Considere usar modelo mais barato ou reduzir tokens.",
"success_rate": "Taxa de sucesso caiu. Verifique casos de erro e adicione handling.",
"llm_judge_score": "Qualidade percebida caiu. Revise o prompt e ground truth."
}
return recommendations.get(metric_name, "Investigue a causa raiz da regressão.")
Dashboard Simples
Métricas sem visualização são números mortos. Vamos construir um dashboard leve com Python puro.
Código: Dashboard em Python
# dashboard.py
# Dashboard CLI para métricas de agente — roda no terminal
import sqlite3
from pathlib import Path
from datetime import datetime, timedelta
from typing import Dict, List
class AgentDashboard:
"""Dashboard CLI para acompanhar métricas do agente."""
def __init__(self, db_path: str = "~/.hermes/metrics.db"):
self.db_path = Path(db_path).expanduser()
def render(self, days: int = 7):
"""Renderiza dashboard no terminal."""
conn = sqlite3.connect(str(self.db_path))
# Header
print("\n" + "=" * 70)
print(f" AGENT METRICS DASHBOARD — Últimos {days} dias")
print("=" * 70)
# KPIs principais
kpis = self._get_kpis(conn, days)
print("\n 📊 KPIs PRINCIPAIS")
print(" " + "-" * 66)
print(f" Tarefas executadas: {kpis['total_tasks']:>6}")
print(f" Taxa de sucesso: {kpis['success_rate']:>5.1f}%")
print(f" Tempo médio: {kpis['avg_duration']:>5.1f}s")
print(f" Custo médio/tarefa: ${kpis['avg_cost']:>5.3f}")
print(f" Chamadas de tool/task: {kpis['avg_tools']:>5.1f}")
print(f" Score LLM Judge: {kpis['avg_judge_score']:>5.2f}")
# Tendência (comparar com período anterior)
trend = self._get_trend(conn, days)
print("\n 📈 TENDÊNCIA (vs período anterior)")
print(" " + "-" * 66)
for metric, change in trend.items():
arrow = "↑" if change > 0 else "↓" if change < 0 else "→"
color = "🟢" if (metric in ['success_rate', 'llm_judge_score'] and change > 0) or \
(metric not in ['success_rate', 'llm_judge_score'] and change < 0) else \
"🔴" if (metric in ['success_rate', 'llm_judge_score'] and change < 0) or \
(metric not in ['success_rate', 'llm_judge_score'] and change > 0) else "⚪"
print(f" {color} {metric:<20} {arrow} {abs(change):>5.1f}%")
# Top tarefas mais lentas
print("\n 🐌 TOP 5 TAREFAS MAIS LENTAS")
print(" " + "-" * 66)
slow = self._get_slowest_tasks(conn, days)
for i, task in enumerate(slow, 1):
desc = task['task_description'][:40] + "..." if len(task['task_description']) > 40 else task['task_description']
print(f" {i}. {desc:<45} {task['duration_seconds']:>6.1f}s")
# Top erros
print("\n ❌ TOP 5 ERROS MAIS FREQUENTES")
print(" " + "-" * 66)
errors = self._get_top_errors(conn, days)
for i, err in enumerate(errors, 1):
print(f" {i}. {err['error'][:50]:<50} ({err['count']}x)")
# Modelos mais usados
print("\n 🤖 MODELOS MAIS USADOS")
print(" " + "-" * 66)
models = self._get_model_usage(conn, days)
for model in models:
bar = "█" * int(model['pct'] / 5)
print(f" {model['model']:<30} {bar:<20} {model['pct']:>5.1f}%")
print("\n" + "=" * 70 + "\n")
conn.close()
def _get_kpis(self, conn, days: int) -> Dict:
"""Calcula KPIs agregados."""
cursor = conn.execute('''
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as success_rate,
AVG(duration_seconds) as avg_duration,
AVG(cost_usd) as avg_cost,
AVG(tool_calls_count) as avg_tools,
AVG(llm_judge_score) as avg_judge
FROM task_metrics
WHERE timestamp_start >= datetime('now', '-' || ? || ' days')
''', (days,))
row = cursor.fetchone()
return {
'total_tasks': row[0] or 0,
'success_rate': row[1] or 0,
'avg_duration': row[2] or 0,
'avg_cost': row[3] or 0,
'avg_tools': row[4] or 0,
'avg_judge_score': (row[5] or 0) * 100 # converter para 0-100
}
def _get_trend(self, conn, days: int) -> Dict[str, float]:
"""Calcula tendência comparando período atual vs período anterior de mesma duração."""
from datetime import datetime, timedelta
now = datetime.now()
current_start = now - timedelta(days=days)
previous_start = now - timedelta(days=days * 2)
# Período atual: [now-days, now]
cursor = conn.execute('''
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as success_rate,
AVG(duration_seconds) as avg_duration,
AVG(cost_usd) as avg_cost,
AVG(tool_calls) as avg_tools,
AVG(llm_judge_score) as avg_judge
FROM task_metrics
WHERE timestamp_start >= ? AND timestamp_start <= ?
''', (current_start.isoformat(), now.isoformat()))
row_curr = cursor.fetchone()
# Período anterior: [now-2*days, now-days]
cursor = conn.execute('''
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as success_rate,
AVG(duration_seconds) as avg_duration,
AVG(cost_usd) as avg_cost,
AVG(tool_calls) as avg_tools,
AVG(llm_judge_score) as avg_judge
FROM task_metrics
WHERE timestamp_start >= ? AND timestamp_start < ?
''', (previous_start.isoformat(), current_start.isoformat()))
row_prev = cursor.fetchone()
current = {
'success_rate': row_curr[1] or 0,
'avg_duration': row_curr[2] or 0,
'avg_cost': (row_curr[3] or 0) * 100,
'avg_tools': row_curr[4] or 0,
'avg_judge_score': (row_curr[5] or 0) * 100,
}
previous = {
'success_rate': row_prev[1] or 0,
'avg_duration': row_prev[2] or 0,
'avg_cost': (row_prev[3] or 0) * 100,
'avg_tools': row_prev[4] or 0,
'avg_judge_score': (row_prev[5] or 0) * 100,
}
trend = {}
for metric in ['success_rate', 'avg_duration', 'avg_cost', 'avg_tools', 'avg_judge_score']:
curr = current.get(metric, 0)
prev = previous.get(metric, 0)
if prev and prev != 0:
trend[metric] = ((curr - prev) / prev) * 100
else:
trend[metric] = 0
return trend
def _get_slowest_tasks(self, conn, days: int) -> List[Dict]:
cursor = conn.execute('''
SELECT task_description, duration_seconds
FROM task_metrics
WHERE timestamp_start >= datetime('now', '-' || ? || ' days')
AND status = 'completed'
ORDER BY duration_seconds DESC
LIMIT 5
''', (days,))
return [
{'task_description': row[0], 'duration_seconds': row[1]}
for row in cursor.fetchall()
]
def _get_top_errors(self, conn, days: int) -> List[Dict]:
cursor = conn.execute('''
SELECT error, COUNT(*) as count
FROM task_metrics
WHERE timestamp_start >= datetime('now', '-' || ? || ' days')
AND error IS NOT NULL
GROUP BY error
ORDER BY count DESC
LIMIT 5
''', (days,))
return [
{'error': row[0], 'count': row[1]}
for row in cursor.fetchall()
]
def _get_model_usage(self, conn, days: int) -> List[Dict]:
cursor = conn.execute('''
SELECT model, COUNT(*) as count
FROM task_metrics
WHERE timestamp_start >= datetime('now', '-' || ? || ' days')
GROUP BY model
ORDER BY count DESC
''', (days,))
rows = cursor.fetchall()
total = sum(r[1] for r in rows)
return [
{'model': row[0], 'count': row[1], 'pct': (row[1] / total * 100) if total else 0}
for row in rows
]
# Uso
if __name__ == "__main__":
dashboard = AgentDashboard()
dashboard.render(days=7)
Exemplo de output
======================================================================
AGENT METRICS DASHBOARD — Últimos 7 dias
======================================================================
📊 KPIs PRINCIPAIS
------------------------------------------------------------------
Tarefas executadas: 847
Taxa de sucesso: 94.2%
Tempo médio: 18.3s
Custo médio/tarefa: $0.042
Chamadas de tool/task: 3.2
Score LLM Judge: 0.78
📈 TENDÊNCIA (vs período anterior)
------------------------------------------------------------------
🟢 success_rate ↑ 2.1%
🔴 avg_duration ↑ 15.3%
🔴 avg_cost ↑ 8.7%
🟢 avg_tools ↓ 12.0%
🟢 avg_judge_score ↑ 5.2%
🐌 TOP 5 TAREFAS MAIS LENTAS
------------------------------------------------------------------
1. Auditar repo completo em busca de vuln... 245.6s
2. Gerar relatório SEO para site de 500 pá... 189.2s
3. Migrar componentes React para Vue... 156.3s
4. Analisar concorrentes para campanha... 134.1s
5. Criar pipeline de CI/CD completo... 98.7s
❌ TOP 5 ERROS MAIS FREQUENTES
------------------------------------------------------------------
1. Timeout: tool call exceeded 30s limit (23x)
2. API rate limit exceeded (12x)
3. Invalid JSON response from tool (8x)
4. Model context window exceeded (5x)
5. Permission denied on file access (3x)
🤖 MODELOS MAIS USADOS
------------------------------------------------------------------
claude-sonnet-4 ████████████████ 45.2%
gpt-4o-mini ██████████ 28.7%
claude-haiku-3 ██████ 18.3%
gpt-4o ██ 7.8%
======================================================================
Anti-Patterns que Destroem Métricas
1. Métricas de vaidade
"Meu agente processou 10.000 tarefas este mês." Quantas deram certo? Quantas o usuário teve que refazer? Quantas geraram valor real?
Correção: Sempre pareie volume com qualidade. Tarefas/processadas é inútil sem tarefas/sucesso.
2. Avaliação pontual
Você avalia o agente uma vez, diz "tá bom", e nunca mais avalia. O modelo muda. O prompt muda. Os dados mudam. O agente pode estar piorando silenciosamente.
Correção: Avaliação contínua. Rode o LLM-as-judge em uma amostra de 5% das tarefas automaticamente.
3. Ground truth desatualizado
Seu benchmark de avaliação tem 50 casos de teste. Você não atualiza há 3 meses. O agente agora lida com cenários que não existiam no benchmark.
Correção: Adicione 2-3 casos novos por semana. Remova casos que não são mais relevantes.
4. Métricas desconectadas de negócio
Você mede tokens/segundo. Seu CEO pergunta "quanto dinheiro isso economizou?" Você não sabe responder.
Correção: Sempre conecte métricas técnicas a métricas de negócio. Tokens → tempo → dinheiro.
5. Alert fatigue
Seu sistema de regressão dispara 50 alertas por dia. Você começa a ignorar todos.
Correção: Use thresholds dinâmicos. Só alerte quando a mudança é estatisticamente significante E afeta casos críticos.
Conclusão
Avaliar agentes de IA é mais difícil que avaliar software tradicional — mas não é impossível. O segredo é medir as 4 categorias (eficiência, qualidade, confiabilidade, custo) com múltiplas técnicas (automáticas + humanas + LLM-as-judge) e comparar ao longo do tempo (baseline, A/B, regressão).
O que você precisa lembrar:
- Task Resolution Time é a métrica mais correlacionada com valor percebido
- LLM-as-judge com múltiplos modelos reduz viés e dá confiança calculada
- A/B testing precisa de significância estatística — não adivinhação
- Regressão detection é mais importante que otimização contínua
- Dashboard semanal é o mínimo para times que levam agentes a sério
Próximos passos:
- Implemente o
MetricsLoggerno seu agente hoje — são 50 linhas de código - Escolha 5 casos de teste representativos e meça baseline
- Configure o dashboard para rodar toda segunda-feira de manhã
- Na próxima mudança de prompt, rode A/B test em vez de "testar manualmente"
- Leia como construir agentes efetivos — métricas sem boa arquitetura são números vazios
- Revise segurança de agentes em produção — métricas de confiabilidade e detecção de regressão são sua primeira linha de defesa contra falhas de segurança. Um agente que está degradando em success_rate ou aumentando em Human Intervention Rate pode estar exposto a vetores de ataque que você não detectou ainda.
Publicado em andrecosta.ia.br.