Efetividade x eficiência: ir mais rápido com IA não significa chegar antes

Passo grande parece velocidade, e é mais ou menos essa a promessa de iterar rápido com IA: mais terreno por rodada. Resolvi simular. Dois agentes saem de A para B, um trajeto de 10 unidades. O rápido dá passos quase 4x maiores: andou 50 unidades no total e o prazo dele variou de 28 a 294 passos. O de passo curto andou 10,8 e nunca passou de 26. O que segura o rápido não é o passo, é a pontaria: passo maior espalha mais o desvio, e ele gasta voltando o que ganhou. Depois testei uma terceira versão, com a mesma pontaria ruim, só encurtando o passo perto do alvo. Chegou em 8. Existe uma conta simples ligando o tamanho do passo à pontaria, e é ela que separa efetividade de eficiência. Usei a pergunta como desculpa para aprender Manim, a biblioteca Python de animação matemática que o Grant Sanderson escreveu para o canal 3Blue1Brown. Abaixo estão o vídeo de 74 segundos, o modelo, os números e o processo, inclusive o que deu errado.

A pergunta

Dois agentes vão de A até B. Cada um sabe fazer uma coisa só: olhar para B, escolher uma direção e andar em linha reta. Os dois erram a direção. O que muda entre eles é o tamanho do passo e o tamanho do erro. O eficiente é eficiente no sentido que a palavra tem quando se fala de produtividade com IA: cobre muito mais terreno por passo e, em troca, erra mais a direção. Os dois chegam. Quem chega antes, andando quanto e com que previsibilidade de prazo, não é o que a intuição sugere.

Efetivo

\(L = 0{,}47\)  ·  \(\sigma = 22^\circ\)

Também erra feio a mira, mas anda pouco por passo, então o erro não tem espaço para virar desvio. Chega em 23 passos.

Eficiente

\(L = 1{,}80\)  ·  \(\sigma = 35^\circ\)

Cobre quase 4x mais terreno por passo e erra 1,6x mais a direção. Chega perto de B em 6 passos e depois orbita.

Adaptativo

\(L_k = c\,d_k\)  ·  \(\sigma = 35^\circ\)

A mesma mira torta do eficiente. Só encolhe o passo conforme se aproxima. Chega em 8 passos na mediana.

74 segundos, 1920x1080 a 60 fps, sem áudio. Renderizado com Manim Community v0.20.1 em Python 3.14. O arquivo tem 2,8 MB.

O modelo

O agente opera em malha fechada: re-mira em B a cada passo. Isso importa, porque mirar uma vez só e andar reto é trivial. Com re-mira, um passo é isto:

$$ x_{k+1} = x_k + L \cdot R(\varepsilon_k)\,\frac{B - x_k}{\|B - x_k\|}, \qquad \varepsilon_k \sim \mathcal{N}(0, \sigma^2) $$
Um passo: mira em B, erra o ângulo por \(\varepsilon_k\), anda \(L\). \(R(\cdot)\) é a matriz de rotação. O agente re-mira a cada passo (malha fechada).

Com \(d_k\) = distância até B, dá para escrever o que acontece depois de um passo. O termo negativo aproxima, o positivo afasta:

$$ \mathbb{E}\!\left[d_{k+1}^{2}\right] = d_k^{2} - 2\,L\,d_k\,c + L^{2}, \qquad c = \mathbb{E}[\cos\varepsilon] = e^{-\sigma^{2}/2} $$
Distância esperada ao alvo depois de um passo, com \(d_k = \|B - x_k\|\). O termo \(-2Ld_kc\) aproxima; o termo \(+L^2\) afasta.

Existe uma distância em que os dois termos se anulam e o progresso esperado zera. Igualando \(2 L d_k c = L^2\):

$$ d^{*} = \frac{L}{2\,e^{-\sigma^{2}/2}} $$
Ponto fixo: onde o progresso esperado zera. Abaixo dessa distância o passo, em média, não desce mais. Passo grande com mira torta produz \(d^{*}\) grande.

Esse \(d^{*}\) é um piso: não importa quantos passos você dê, em média não se desce abaixo dele, e o agente acaba orbitando um anel de raio \(d^{*}\) em volta do alvo. Repare no que a fórmula diz: o piso é proporcional ao passo. Dobrar o passo dobra a distância mínima em que dá para parar. Com o alvo de raio \(\rho = 0{,}35\):

Piso teórico de cada perfil contra o raio do alvo.
Perfil \(L\) \(\sigma\) \(c = e^{-\sigma^2/2}\) \(d^{*} = L/2c\) Entra no alvo?
Efetivo 0,47 22° 0,9289 0,253 sim, com folga
Eficiente 1,80 35° 0,8298 1,085 não, para nunca

1,085 é três vezes o raio do alvo. O eficiente não erra por azar, erra por construção: o passo dele é grande demais para o próprio erro de mira.

Os números medidos

A fórmula é bonita, mas descreve mesmo a simulação? 3.000 execuções de cada perfil, sementes diferentes, orçamento de 400 passos:

3.000 execuções por perfil. Trajeto A até B = 10,0 unidades. Alvo com raio 0,35.
Perfil Chegou Mediana Média p90 Pior caso Caminho andado
Efetivo 100% 23 22,9 24 26 10,8
Eficiente 100% 28 37,9 84 294 50,4
Adaptativo 100% 8 8,4 10 15 14,2

O agente eficiente chega. Só que não por convergência: por sorteio. Ele orbita o anel e, de vez em quando, um passo aleatório o joga dentro do alvo. Por isso a mediana (28) é bem menor que a média (37,9), e por isso existe uma cauda que vai até 294 passos. Enquanto isso ele andou 50,4 unidades para percorrer um trajeto de 10,0. O efetivo andou 10,8.

Olhe as caudas, que é onde mora a diferença prática. O efetivo vai de 23 passos na mediana a 26 no pior caso das 3.000 execuções. O eficiente vai de 28 a 294. Os dois erram a mira; só um converte o erro em imprevisibilidade de prazo.

A distância média do eficiente ao alvo, depois de estabilizar, foi 1,085. A fórmula previa 1,085. Foi aí que decidi que o vídeo valia a pena.

O desfecho

Se o problema é o passo ser grande demais perto do alvo, a correção é óbvia depois que você vê: faça o passo encolher junto com a distância. Pondo \(L_k = \alpha\,d_k\) na recursão:

$$ L_k = \alpha\,d_k \;\Longrightarrow\; \mathbb{E}\!\left[d_{k+1}^{2}\right] = d_k^{2}\left(1 - 2\alpha c + \alpha^{2}\right) $$
Passo proporcional à distância: contrai para qualquer \(\alpha < 2c\), com taxa ótima em \(\alpha = c\). A pontaria define o passo que se pode dar.

O piso some: a distância contrai por um fator constante a cada passo. E o \(\alpha\) ótimo é exatamente \(c\), o fator de pontaria, ou seja o passo que você pode se dar ao luxo de usar é definido pela sua precisão.

O agente adaptativo do vídeo mantém a mira péssima de 35° do eficiente e só encolhe o passo perto de B. Chega em 8 passos, contra 23 do preciso e 38 do eficiente. É o argumento inteiro: eficiência não substitui pontaria, é consequência dela.

Iterar rápido demais com IA

Lá em cima a palavra eficiente entrou com um sentido específico, e é hora de cobrar a dívida. Quando se fala de produtividade com IA, o ganho anunciado é sempre o mesmo: mais terreno por passo. Você pede uma feature, não uma função. Um arquivo, não um trecho. O \(L\) subiu, e subiu muito.

O que quase nunca entra na conta é que o \(\sigma\) sobe junto, pela mesma causa. Mira, aqui, é a fração do passo que aponta para onde você realmente queria ir, e ela depende de duas coisas: quão claro estava o pedido e quanto do que voltou você de fato leu. Peça 40 linhas e você lê as 40. Peça 600 e você passa o olho no diff, aprova, segue. O passo cresceu; a conferência não acompanhou. Como \(d^{*} = L/2c\), o raio de órbita piora nos dois lugares ao mesmo tempo: o numerador cresce e o denominador encolhe.

Os mesmos cinco termos das seções anteriores, com os nomes trocados. Nenhuma linha aqui é nova.
No modelo Iterando com IA
\(L\), tamanho do passo O tamanho do pedaço que você pede de uma vez: uma função, um arquivo, uma feature inteira.
\(\sigma\), erro de mira Quão longe do que você queria o resultado sai. Especificação vaga e contexto faltando somam aqui.
\(c = e^{-\sigma^2/2}\), fator de pontaria Na prática, a fração do que voltou que você leu e conferiu de verdade.
\(d^{*} = L/2c\), raio de órbita O quase pronto: a distância do fim abaixo da qual o trabalho para de convergir e passa a circular.
\(\alpha = c\), passo ótimo O tamanho de pedido que a sua clareza atual comporta.

Agora o detalhe que faz o estudo valer mais que a intuição. Olhe a mediana das 3.000 execuções: o eficiente chega em 28 passos, o efetivo em 23. Cinco passos. No caso típico, iterar acelerado é quase indistinguível de iterar devagar, e é justamente por isso que a prática se sustenta: a semana normal parece boa. A conta está fora da mediana, na média (37,9), no p90 (84) e no pior caso (294). Uma cauda dessas não se sente numa tarefa, se sente no trimestre, quando a estimativa que valia para o caso típico deixa de valer para qualquer coisa. O efetivo foi de 23 a 26 no pior caso das 3.000 execuções; o eficiente foi de 28 a 294.

E tem o caminho andado, que é a métrica mais honesta das três: 50,4 unidades para percorrer um trajeto de 10,0. Isso não é tempo parado esperando, é trabalho feito e desfeito. O código escrito e revertido, o refactor que voltou atrás, a terceira tentativa no mesmo bug. Ele sai em volume de diff, o que o torna especialmente traiçoeiro, já que volume de diff é o que costuma ser contado como produtividade. O efetivo andou 10,8.

O conserto não é usar menos IA, é o terceiro agente. O adaptativo mantém a mira de 35° do eficiente, péssima, intocada, e ainda assim chega em 8 passos contra 23 do efetivo. Ele não melhorou a pontaria: só parou de dar passo de 1,80 quando o que faltava até o alvo já era menor que o próprio passo.

É o que \(\alpha = c\) diz, traduzido. Passo grande vale enquanto você está longe, onde errar a direção custa pouco e explorar rende: esboço, andaime, protótipo descartável, código que ninguém vai manter. Perto do alvo o passo tem que encolher: integração, o bug que sobrou, o que vai para produção. A ferramenta não troca de marcha sozinha, porque gerar 600 linhas custa o mesmo prompt perto ou longe do fim. A decisão de encolher é sua, e ela é contraintuitiva exatamente onde mais importa: quanto maior o seu \(\sigma\), menor deveria ser o seu passo, e é quando você menos sabe o que quer que mais dá vontade de pedir tudo de uma vez.

Como fiz com o Manim

Manim é uma biblioteca Python: você declara objetos geométricos (mobjects) e anima transformações neles dentro de um método construct(), e o render sai em mp4 via ffmpeg. Use a Manim Community (v0.20.1 aqui), que é o fork mantido pela comunidade. O repositório pessoal do Grant, o manimgl, é otimizado para o fluxo dele, não para terceiros.

1. Instalação

Precisa de cairo e pango, de ffmpeg, e de LaTeX se você for usar MathTex: as equações são compiladas em LaTeX de verdade e convertidas em SVG por dvisvgm.

bash
# dependências de sistema (Arch; nos demais, os pacotes equivalentes)
sudo pacman -S --needed cairo pango ffmpeg \
     texlive-basic texlive-latexextra texlive-fontsrecommended texlive-binextra

# a lib fica num venv local, fora do versionamento
python -m venv manim/.venv
manim/.venv/bin/python -m pip install manim

Em Python 3.14 não há wheels para pycairo e manimpango: o pip compila os dois do source, o que só funciona com os headers de cairo e pango instalados. Faltando, o erro fala de cairo.h, não de Python.

2. Separar o modelo da cena

A decisão que mais economizou tempo. Toda a matemática vive em sim.py, que só depende de numpy, e a cena importa dele. Assim 3.000 simulações rodam em segundos e o render nunca entra no laço de exploração.

manim/
├── sim.py            o modelo: perfis, um passo, a recursão. Só numpy.
├── efetividade.py    a cena Manim. Só encenação.
├── seed_search.py    escolhe a semente da execução que vai ao ar.
├── diagnostico.py    os números de 3.000 execuções.
├── sweep.py          varredura de parâmetros.
├── requirements.txt
├── .venv/            gitignore
├── media/            gitignore: cache de render, centenas de MB
└── out/              gitignore: frames de inspeção
sim.py
@dataclass
class Perfil:
    nome: str
    passo: float
    sigma_graus: float
    adaptativo: bool = False

    @property
    def c(self) -> float:
        """E[cos eps] para erro gaussiano: o 'fator de pontaria', em (0, 1]."""
        return math.exp(-(self.sigma ** 2) / 2)

    @property
    def raio_orbita(self) -> float:
        """d* = L / (2c). Distância abaixo da qual o agente não desce."""
        return self.passo / (2 * self.c)

3. Escolher a semente sem trapacear

O modelo é aleatório, mas o vídeo mostra uma execução só. Existe uma tentação óbvia aqui: rodar até achar a semente em que o agente eficiente se dá mal de forma espetacular. Isso seria fabricar o resultado.

O que fiz foi diferente. Defini critérios de legibilidade (o eficiente precisa liderar de forma visível no começo, o efetivo precisa chegar perto da mediana medida) e depois contei quantas sementes passavam. Foram 1.081 de 2.999, ou seja 36,0%. A execução que está no vídeo é o caso comum, não a exceção. Esse número está impresso na saída do script justamente para poder ser cobrado.

Os dois agentes usam a mesma semente. Isso é de propósito: mesma sequência de azar, estratégias diferentes. É um experimento controlado, não duas corridas separadas.

4. O truque que faz a animação fluir

A forma ingênua de animar 26 passos é chamar self.play() 26 vezes. O resultado é picotado, porque cada chamada tem começo e fim próprios, e o render fica lento. A forma certa no Manim é um ValueTracker: um número animável que serve de relógio, mais updaters que redesenham os objetos em função dele. Aí toda a corrida cabe em uma chamada só, com velocidade constante.

Cada passo é desenhado como uma seta, não como um trecho de linha. É o que o modelo diz que ele é: um vetor, com magnitude e ângulo. E tem um efeito colateral ótimo no último ato, onde as setas visivelmente encolhem conforme o agente adaptativo se aproxima do alvo.

efetividade.py
class Agente:
    """Um vetor por passo, revelados em sequência por um ValueTracker."""

    def __init__(self, pontos, cor, espessura, raio, ponta):
        self.pontos = pontos
        self.t = ValueTracker(0.0)
        self.opacidade = ValueTracker(1.0)  # escurecer sem brigar com o updater

        # Setas dos passos concluídos. A geometria NUNCA é mexida: só a opacidade.
        self.setas = VGroup(*[
            Arrow(em_3d(a), em_3d(b), **self._estilo)
            for a, b in pairwise(pontos)
        ])
        self.setas.set_opacity(0)
        self.setas.add_updater(self._atualizar_setas)

        # A seta do passo em curso é um objeto à parte, refeito a cada quadro.
        self.corrente = VMobject()
        self.corrente.add_updater(self._atualizar_corrente)

    def _atualizar_setas(self, grupo):
        concluidos = int(np.floor(self.t.get_value()))
        alfa = self.opacidade.get_value()
        for i, seta in enumerate(grupo):
            seta.set_opacity(alfa if i < concluidos else 0.0)

    def _atualizar_corrente(self, m):
        t = self.t.get_value(); k = int(np.floor(t)); fracao = t - k
        # Abaixo de 5% do passo a seta é degenerada e o Manim não a orienta.
        if k >= len(self.pontos) - 1 or fracao <= 0.05:
            m.set_opacity(0); return
        fim = self.pontos[k] + fracao * (self.pontos[k + 1] - self.pontos[k])
        m.become(Arrow(em_3d(self.pontos[k]), em_3d(fim), **self._estilo))
        m.set_opacity(self.opacidade.get_value())

# a corrida inteira, os dois agentes em lockstep, numa chamada:
self.play(
    efetivo.t.animate.set_value(24),
    eficiente.t.animate.set_value(24),
    run_time=6.2,
    rate_func=linear,   # sem ease: passo tem que ter cadência constante
)

Três detalhes que só aparecem quando você erra. rate_func=linear é obrigatório: o padrão é smooth, que acelera no meio e destrói a leitura de cadência. Os updaters precisam de clear_updaters() antes do FadeOut, senão o objeto é redesenhado enquanto tenta desaparecer. E a opacidade também precisa de ValueTracker próprio, senão o updater sobrescreve qualquer .animate.set_opacity() no quadro seguinte.

5. Paleta

Preto e branco puro, como o Manim é por padrão. Sem cor, os agentes se distinguem por espessura: setas brancas finas (2,4 px) contra cinzas grossas (3,8 px), o que também carrega o significado, já que passo grande é literalmente traço mais pesado. A ordem de adição define o empilhamento, então o branco entra depois do cinza para não sumir no emaranhado. E a razão da ponta (max_tip_length_to_length_ratio) tem que ser menor no eficiente, 0,16 contra 0,30: as setas dele são 4x mais longas e com a mesma razão a cabeça vira um borrão perto de B.

6. Render e iteração

O ciclo que funcionou: renderizar rápido e feio, extrair quadros com ffmpeg, olhar, corrigir. Nunca assisti ao vídeo durante a iteração, só olhei frames em pontos escolhidos da linha do tempo.

bash
# iteração: 480p a 15 fps, uns 20 segundos de render
.venv/bin/manim -ql --disable_caching efetividade.py Efetividade

# inspeção: quadros em pontos-chave, mais rápido que assistir
for t in 4.5 21 30 38; do
  ffmpeg -ss $t -i media/videos/efetividade/480p15/Efetividade.mp4 \
         -frames:v 1 -y out/frames/f$t.png
done

# final: 1080p a 60 fps, cerca de um minuto em 16 núcleos
.venv/bin/manim -qh efetividade.py Efetividade

# compressão para a web: 4,5 MB viram 2,8 MB, com faststart
ffmpeg -i media/videos/efetividade/1080p60/Efetividade.mp4 \
  -c:v libx264 -preset slow -crf 21 -pix_fmt yuv420p \
  -movflags +faststart -an assets/video/efetividade-eficiencia.mp4

Dois flags que valem a explicação. --disable_caching: o Manim reaproveita trechos já renderizados, o que é ótimo em produção e péssimo quando você mexe em updaters, porque ele serve um trecho velho e você caça um bug já corrigido. -movflags +faststart: move o índice do mp4 para o começo do arquivo, senão o navegador baixa quase tudo antes de tocar.

O que deu errado

A parte que os tutoriais não mostram. Em ordem cronológica:

  1. O modelo estava fácil demais para o eficiente Com o alvo em raio 0,45 ele caía dentro por sorteio em 90% das execuções, e meu seed search estava filtrando justamente os 10% em que ele falhava. Isso é escolher o resultado a dedo. Varri o espaço de parâmetros procurando um regime que salvasse a tese e não achei nenhum: no melhor ponto ele ainda chegava em 37% das vezes. A correção não foi mexer nos parâmetros, foi trocar a pergunta. Medir quantos passos leva e quanto caminho anda, em vez de chega ou não, deu números mais fortes e que valem no caso típico.
  2. Um agente sem erro nenhum, e um diagnóstico errado no meio O efetivo chegava em exatamente 20 passos nas 3.000 execuções: mediana, p90 e pior caso iguais. Parece fraude. Culpei 10,0 ÷ 0,5 dar inteiro redondo e troquei o passo para 0,47; a variância continuou zero. A causa real era outra: com erro de 5°, cada passo entrega 99,6% do avanço pretendido e o ruído nunca soma um passo inteiro. O agente era determinístico. Só que isso tinha um sintoma que nenhuma tabela mostrava: no vídeo a trajetória dele é uma reta, e um agente que não erra apaga a premissa de que os dois erram. O reflexo de trocar a semente não resolveria nada, porque num sistema determinístico nenhuma semente muda o resultado. A alavanca era o σ. Subi para 22°, escolhido renderizando os candidatos lado a lado, porque "a trajetória parece torta?" é pergunta visual e não se responde por tabela.
  3. A fonte padrão saiu serifada O Text() usa a fonte padrão do sistema via Pango, que aqui resolvia para uma serifada e brigava com a Computer Modern do MathTex. Fixei font="Noto Sans" na interface e deixei serifa só nas equações.
  4. A ponta da seta descolando do marcador Reaproveitei o mesmo objeto Arrow para o passo em curso e o concluído, chamando put_start_and_end_on() a cada quadro. O método reescala a cabeça em função do novo comprimento e o erro acumula, até a ponta não bater mais com o destino. Correção: setas concluídas nunca têm a geometria tocada, só a opacidade, e a seta em curso é um VMobject separado que faz become() de uma Arrow nova por quadro.
  5. Três coisas que só o frame mostra A chegada do efetivo sumia no emaranhado cinza (resolvido invertendo a ordem de adição e pondo um halo no ponto final). A legenda do ato da fórmula colidia com o anel tracejado, e depois de mover a fórmula para a esquerda esqueci de mover a que a substitui no Transform, que voltava para o centro e colidia de novo. O ponto A ficou fora do FadeOut final e seguia aceso atrás da frase de encerramento. Nenhuma das três aparece lendo código.

Reproduzir

A pasta manim/ do repositório deste site tem os cinco arquivos Python e um requirements.txt. O venv, o cache de render e os frames de inspeção estão no gitignore, então clonar e rodar custa uma instalação:

bash
cd manim
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt

.venv/bin/python sim.py           # os pisos teóricos de cada perfil
.venv/bin/python diagnostico.py   # os números de 3.000 execuções
.venv/bin/python seed_search.py   # a semente e a fração de casos típicos

.venv/bin/manim -qh efetividade.py Efetividade

Ressalvas

O \(d^{*}\) é o ponto fixo da recursão do valor esperado de \(d^2\), não a média da distribuição estacionária. Para o eficiente os dois coincidiram até a terceira casa, mas isso não é garantido em geral. O erro de mira também é suposto gaussiano e independente entre passos; viés sistemático só piora o quadro do agente de passo grande.

A distância só é conferida no fim de cada passo. Um passo longo pode passar raspando em B no meio do caminho e não contar. Isso é deliberado: a metáfora aqui é a de decisões discretas, em que o que importa é onde você para, não por onde você passou.

Referências

  1. Manim Community. Manim: a community-maintained Python library for creating mathematical animations. manim.community Versão 0.20.1 aqui. É o que se instala via pip.
  2. Sanderson, G. 3Blue1Brown. 3blue1brown.com Autor do Manim e do estilo visual que este vídeo imita.
  3. Robbins, H., & Monro, S. (1951). A Stochastic Approximation Method. Annals of Mathematical Statistics, 22(3), 400-407. As condições clássicas sobre o tamanho do passo; o passo adaptativo do último ato é um caso particular.
  4. Bottou, L., Curtis, F. E., & Nocedal, J. (2018). Optimization Methods for Large-Scale Machine Learning. SIAM Review, 60(2), 223-311. Formaliza por que passo constante estaciona num raio em torno do ótimo em vez de convergir: o mesmo fenômeno do d* daqui.
  5. Drucker, P. F. (1967). The Effective Executive. Harper & Row. Origem da distinção entre fazer as coisas certas e fazer as coisas do jeito certo.