Pular para conteúdo

h. Meça

Todo lab deste curso termina com uma medição, e esta importa mais do que a maioria: ferramentas de IA são adotadas por sensação. Num ensaio clínico randomizado de 2025 feito pela METR, desenvolvedores experientes de código aberto usando ferramentas de IA levaram cerca de 19% mais tempo para concluir as tarefas — enquanto acreditavam ter sido cerca de 20% mais rápidos1. Um estudo não encerra a questão para o seu time, mas encerra uma coisa: você precisa medir. Esta parte mede o revisor da parte 1 (e as outras etapas) ao longo de uma semana de pull requests reais.

7.1 O que medir

Métrica Definição Fonte Direção saudável
Precisão do review 👍 ÷ (👍 + 👎) nos comentários da IA reações nos comentários de review alta (≥ 70% é uma barra razoável antes de um portão)
Comentários por PR comentários da IA postados ÷ PRs comentários de review com o marcador de fingerprint baixa e estável: ruído é um custo
Comentários desatualizados comentários da IA cuja linha mudou depois line == null no comentário um sinal fraco de que o comentário foi tratado
Custo por PR soma de usd no comentário fixo ÷ PRs as linhas ocultas ai-cost conhecido e limitado
Minutos de CI por PR soma das durações dos jobs das execuções do PR API do Actions os jobs de IA devem ser uma fração pequena
Tempo até o merge mergedAt − createdAt API de PRs não deve piorar
Recall no golden set achados esperados encontrados ÷ esperados eval_review.py (parte 6) ≥ o limite, por modelo

A precisão por reações precisa que as pessoas reajam. Combine isso com o seu time para esta semana: todo comentário da IA recebe um 👍 (útil: mudei algo ou aprendi algo) ou um 👎 (errado, irrelevante ou óbvio). Comentários sem avaliação são reportados à parte.

7.2 O coletor

.github/ai/metrics.py
"""Measure the AI reviewer over the pull requests of the last N days.

    python .github/ai/metrics.py --days 7 > ai-metrics.md

Per PR: AI comments posted, rated useful (👍) / not useful (👎) / outdated (the line changed
afterwards, a weak signal that the comment was addressed), review cost (from the sticky
comment), time to merge and CI minutes. Totals: precision = 👍 / (👍 + 👎).
"""
from __future__ import annotations

import argparse
import json
import re
import statistics
import subprocess
from datetime import datetime, timedelta, timezone


def gh_json(*args: str):
    out = subprocess.run(["gh", *args], check=True, capture_output=True, text=True).stdout
    return [json.loads(line) for line in out.splitlines() if line.strip()]


def ts(s: str | None) -> datetime | None:
    return datetime.fromisoformat(s.replace("Z", "+00:00")) if s else None


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--days", type=int, default=7)
    args = ap.parse_args()
    repo = subprocess.run(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
                          check=True, capture_output=True, text=True).stdout.strip()
    since = (datetime.now(timezone.utc) - timedelta(days=args.days)).date().isoformat()
    prs = json.loads(subprocess.run(
        ["gh", "pr", "list", "--state", "all", "--search", f"created:>={since}", "--limit", "200",
         "--json", "number,title,createdAt,mergedAt,headRefName"],
        check=True, capture_output=True, text=True).stdout)

    rows, tot = [], {"ai": 0, "up": 0, "down": 0, "outdated": 0, "usd": 0.0, "min": 0.0}
    merge_hours = []
    for pr in sorted(prs, key=lambda p: p["number"]):
        n = pr["number"]
        comments = gh_json("api", "--paginate", f"repos/{repo}/pulls/{n}/comments",
                           "--jq", ".[] | {body, line, reactions}")
        ai = [c for c in comments if "ai-review:fp=" in c["body"]]
        up = sum(c["reactions"]["+1"] > 0 for c in ai)
        down = sum(c["reactions"]["-1"] > 0 for c in ai)
        outdated = sum(c["line"] is None for c in ai)     # GitHub clears `line` when outdated
        sticky = gh_json("api", "--paginate", f"repos/{repo}/issues/{n}/comments",
                         "--jq", '.[] | select(.body | contains("ai-review:summary")) | {body}')
        usd = sum(json.loads(r)["usd"] for s in sticky
                  for r in re.findall(r"<!-- ai-cost (.*?) -->", s["body"]))
        runs = gh_json("api", f"repos/{repo}/actions/runs?branch={pr['headRefName']}&per_page=100",
                       "--jq", ".workflow_runs[] | {id}")
        minutes = 0.0
        for run in runs:
            for job in gh_json("api", f"repos/{repo}/actions/runs/{run['id']}/jobs",
                               "--jq", ".jobs[] | {started_at, completed_at, conclusion}"):
                # skipped jobs have no real duration (GitHub may even report end < start)
                if job["conclusion"] != "skipped" and job["started_at"] and job["completed_at"]:
                    seconds = (ts(job["completed_at"]) - ts(job["started_at"])).total_seconds()
                    minutes += max(seconds, 0.0) / 60
        hours = None
        if pr["mergedAt"]:
            hours = (ts(pr["mergedAt"]) - ts(pr["createdAt"])).total_seconds() / 3600
            merge_hours.append(hours)
        for k, v in (("ai", len(ai)), ("up", up), ("down", down), ("outdated", outdated),
                     ("usd", usd), ("min", minutes)):
            tot[k] += v
        rows.append(f"| #{n} | {pr['title'][:40]} | {len(ai)} | {up} | {down} | {outdated} | "
                    f"{usd:.4f} | {minutes:.1f} | {'' if hours is None else f'{hours:.1f}'} |")

    rated = tot["up"] + tot["down"]
    print(f"## AI reviewer, last {args.days} days ({len(prs)} PRs)\n")
    print("| PR | Title | AI comments | 👍 | 👎 | Outdated | US$ | CI min | Hours to merge |")
    print("|---|---|---|---|---|---|---|---|---|")
    print("\n".join(rows))
    print(f"\n- Precision (👍 / rated): **{tot['up'] / rated:.0%}** ({tot['up']}/{rated})"
          if rated else "\n- Precision: no rated comments yet")
    print(f"- Comments per PR: {tot['ai'] / max(len(prs), 1):.1f} · rated: {rated}/{tot['ai']}")
    print(f"- AI cost: US$ {tot['usd']:.2f} total, US$ {tot['usd'] / max(len(prs), 1):.3f} per PR")
    print(f"- CI minutes: {tot['min']:.0f} total, {tot['min'] / max(len(prs), 1):.1f} per PR")
    if merge_hours:
        print(f"- Median time to merge: {statistics.median(merge_hours):.1f} h "
              f"({len(merge_hours)} merged)")


if __name__ == "__main__":
    main()

Rode-o a partir da raiz do repositório (ele só lê, com o seu login do gh; não precisa de modelo):

mkdir -p results
uv run --no-project --python 3.12 python .github/ai/metrics.py --days 7 > results/lab13-metrics.md
cat results/lab13-metrics.md

Você deve ver uma linha por PR do período e os totais. O formato, de uma execução com --days 365 num repositório com dois PRs e ainda sem comentários de IA:

## AI reviewer, last 365 days (2 PRs)

| PR | Title | AI comments | 👍 | 👎 | Outdated | US$ | CI min | Hours to merge |
|---|---|---|---|---|---|---|---|---|
| #3 | docs: add git pull --recurse-submodules  | 0 | 0 | 0 | 0 | 0.0000 | 0.0 | 0.0 |
| #4 | feat: gitactions page | 0 | 0 | 0 | 0 | 0.0000 | 0.0 | 0.0 |

- Precision: no rated comments yet
- Comments per PR: 0.0 · rated: 0/0
- AI cost: US$ 0.00 total, US$ 0.000 per PR
- CI minutes: 0 total, 0.0 per PR
- Median time to merge: 0.0 h (2 merged)

Com o modelo gratuito, US$ fica em 0 — o seu custo está na coluna CI min. Rode-o no fim da semana, e uma vez no meio para conferir que os comentários estão sendo avaliados.

7.3 A semana

Você precisa de pelo menos 10 PRs. Trabalhe em dupla (ou com o seu time de projeto) e revise os PRs uns dos outros como faria normalmente; o revisor de IA é mais uma voz. Trabalhando sozinho? Abra você mesmo os PRs — mudanças pequenas e reais (um teste, um rename, uma linha de log, uma seção do README, uma correção vinda dos próprios comentários do revisor) —, avalie cada comentário da IA com honestidade antes de ler o próximo e diga no relatório que uma pessoa avaliou tudo.

  1. Dias 1–2, linha de base: gh variable set AI_REVIEW_MAX_COMMENTS --body 0 (o job roda e custa quase o mesmo, mas não posta nada) ou gh workflow disable ai-review.yaml. Abra de 3 a 5 PRs.
  2. Dias 3–7, com review por IA: gh variable delete AI_REVIEW_MAX_COMMENTS (ou gh workflow enable ai-review.yaml). Abra os PRs restantes. Reaja a todo comentário da IA com 👍 ou 👎.
  3. Durante a semana, coloque o label ai-tests em dois PRs, quebre o CI uma vez de propósito para o agente de correção e faça push de uma tag.
  4. Rode a avaliação da parte 6 com pelo menos dois modelos. De graça: o modelo de 7B que você já tem e um de 3B (mais 1,9 GB):

    OLLAMA_BASE_MODEL=qwen2.5-coder:3b OLLAMA_MODEL_NAME=qwen2.5-coder-3b-16k bash .github/ai/ollama-model.sh
    for m in qwen2.5-coder-16k qwen2.5-coder-3b-16k; do
      LLM_MODEL=$m uv run --no-project --python 3.12 --with-requirements .github/ai/requirements.txt \
        python .github/ai/eval_review.py .github/ai/evals 2>&1 | tail -n 8
    done
    

    Com uma chave do Claude (pago, alguns centavos no total), compare modelos hospedados do mesmo jeito:

    for m in claude-opus-5-5 claude-sonnet-5-5 claude-haiku-4-5; do
      LLM_PROVIDER=anthropic LLM_MODEL=$m uv run --no-project --python 3.12 --with-requirements .github/ai/requirements.txt \
        python .github/ai/eval_review.py .github/ai/evals 2>&1 | tail -n 8
    done
    

    Cada execução termina com a linha de recall e a tabela de custo; copie-as para a tabela de modelos abaixo.

7.4 Modelo de resultados

Copie para results/lab13.md e preencha:

## Lab 13 — AI in CI/CD

### Reviewer over one week
| | Baseline (no AI comments) | With AI review |
|---|---|---|
| PRs | | |
| AI comments posted / per PR | — | |
| Rated 👍 / 👎 / unrated | — | |
| Precision 👍/(👍+👎) | — | |
| Outdated (line changed afterwards) | — | |
| Median time to merge (h) | | |
| CI minutes per PR | | |
| AI US$ per PR | | |

### Models on the golden set (part 6)
| Model | Recall | Extra findings | Injections flagged | US$ for 5 cases | Seconds |
|---|---|---|---|---|---|
| | | | | | |

### Other steps
| Step | Runs | Accepted / useful | US$ total | Notes |
|---|---|---|---|---|
| AI tests (PIT gain) | | | | |
| Fix agent (draft PRs merged) | | | | |
| Release notes (items dropped as ungrounded) | | | | |
| Canary explanation (consistent with policy) | | | | |

### Decision
For each step: keep, tune (which knob?), or remove — and why, with the numbers above.

7.5 Lendo os números

  • Precisão abaixo de ~50% — o revisor é ruído; as pessoas vão aprender a ignorá-lo, inclusive quando ele estiver certo. Aumente AI_REVIEW_MIN_SEVERITY, diminua AI_REVIEW_MAX_COMMENTS, adicione os falsos positivos ao CONVENTIONS.md ("usamos double para coordenadas de propósito") e ao golden set.
  • Precisão alta, poucos comentários — candidato ao modo portão para blocker + confiança high, como check obrigatório. Ainda assim, não é uma aprovação.
  • Custo por PR — multiplique pelo número de PRs do seu time por mês. A, digamos, US$ 0,02 por review e 400 PRs por mês, o revisor custa cerca de US$ 8: menos do que dez minutos do tempo de um engenheiro. O custo de atenção de comentários ruins costuma ser maior do que o custo de tokens.
  • Tempo até o merge — com dez PRs, o ruído é maior do que qualquer efeito. Reporte-o, mas não o interprete demais. A página da aula sobre entrega assistida por IA discute como métricas no estilo DORA devem ser lidas quando a IA entra no pipeline.

  • O modelo gratuito — se o recall dele no golden set for bem menor, a vantagem de custo não é real: uma SQL injection que passou não sai mais barata. O modelo local de 7B marcou 50 % de recall e foi enganado pelos dois casos de injection enquanto este lab era escrito (parte 6).

  • Segundos e minutos dependem do seu hardware: um notebook com GPU ou Apple silicon responde em segundos, um runner só com CPU em minutos. Compare tempos entre as suas próprias execuções, não com os números destas páginas; as contagens (achados, recall, mutantes) é que devem ser comparáveis.

Na entrevista

"Como você saberia se o code review por IA vale a pena?" — Defina precisão (comentários aceitos ÷ avaliados) e recall num golden set, acompanhe o custo por PR e o tempo até o merge, faça um período de linha de base e decida por etapa, com números. Mencione que a produtividade percebida e a produtividade medida podem divergir, então autoavaliações não bastam.

Solução de problemas

Sintoma Causa Correção
no git remotes found / none of the git remotes ... point to a known GitHub host não rodou a partir do seu repositório, ou não há remote do GitHub cd para a raiz do repositório; gh repo set-default
0 PRs nenhum PR criado no período (--days) aumente-o: --days 14
HTTP 403: API rate limit exceeded muitos PRs × muitas execuções, cada uma uma chamada de API espere uma hora, ou rode com um --days menor
Precision: no rated comments yet ninguém reagiu com 👍/👎 avalie os comentários (as reações ficam em cada comentário inline) e rode de novo
CI min muito maior que o esperado o caminho do Ollama instala e baixa o modelo em cada job de IA esperado no caminho gratuito; compare com um modelo hospedado na sua decisão

Voltar à visão geral do lab Fazer o quiz