Medi se arquivo de spec faz o modelo acertar mais. Não faz.
Em julho defendi que a spec estruturada muda o resultado. Medi em outubro: 360 gerações, 3 modelos locais, 3 formas do mesmo pedido. O que paga é a informação escrita, não o formato do arquivo.
Com pressa? Peça o TL;DR ao Claude — ele lê a página e resume.
Em 18 de 30 células, o braço com spec estruturada em markdown e o braço com exatamente a mesma informação em parágrafo corrido produziram o mesmo resultado. Na tarefa slugificar com o devstral:24b a temperatura 0, os dois voltaram com código byte a byte idêntico.
Em julho eu escrevi que trocar o prompt em prosa por um arquivo de spec — contrato em tabela, casos de borda, fora de escopo, critério de aceite — fazia o agente acertar mais. Fechei aquele post admitindo que o ganho era hipótese minha, com as células da tabela marcadas para preencher depois.
Agora estão preenchidas. E derrubam a hipótese.
O que muda o resultado é a informação estar escrita. Entre um pedido de uma frase e a mesma tarefa especificada, os casos de borda saltam de 46% para 90%. Entre a spec estruturada e a mesma informação em texto corrido, a diferença desaparece dentro do ruído.
Bancada · outubro de 2025
- Placa
- RTX 5090 · 32 GB
- Modelos
- qwen3-coder:30b · devstral:24b · qwen2.5-coder:32b
- Quantização
- Q4 via Ollama
- Acesso
- API crua, um turno, sem histórico
- Tarefas
- 10 funções puras pequenas em JS
- Braços
- prosa · prosa longa · spec
- Gerações
- 360 (90 + 270)
- VRAM ocupada
- 20 · 17 · 24 GB, na ordem dos modelos
- Tempo das 2 corridas
- 15,2 min somados de chamada
O braço do meio é o experimento
Comparar “uma frase” com “spec de sessenta linhas” não responde nada. Mais informação ajuda — isso é obviedade, não medição. Por isso são três braços, e o do meio é o que dá sentido ao resto:
- prosa — pedido de uma frase, só caminho feliz, nenhum caso de borda mencionado.
- prosa longa — exatamente a mesma informação da spec, em texto corrido, sem estrutura.
- spec — a mesma informação em markdown estruturado: contrato em tabela, casos de borda, fora de escopo, critério de aceite.
Assim prosa contra prosa longa mede informação, e prosa longa contra spec mede forma — que é a afirmação que eu precisava sustentar e não sustentei.
O resto do desenho existe para tirar as saídas fáceis. Os testes de cada tarefa são separados em caminho feliz e casos de borda, e cada caso de borda corresponde 1:1 a um item que a spec declara e a prosa curta omite. A instrução de formato de saída é idêntica nos três braços — se o formato pedido variasse junto com o conteúdo, eu estaria medindo formato. Cada tarefa traz uma implementação de referência que precisa passar em 100% dos próprios testes antes de a corrida começar; se a referência falha, o errado é o teste. O código gerado roda em processo isolado com timeout de 10 s, porque código de modelo não merece confiança: entra em loop, chama process.exit, estoura a memória.
O harness inteiro está no repositório: scripts/bancada-spec.mjs roda a corrida, scripts/bancada-spec/tarefas.mjs guarda as dez tarefas com os três prompts e os testes de cada uma, scripts/bancada-spec/executor.mjs roda um candidato em processo próprio.
pnpm bancada --modelos qwen3-coder:30b,devstral:24b,qwen2.5-coder:32b --amostras 3 --temperatura 0.7 referencias validadas: 10 tarefas, todas em 100% bancada: 3 modelos × 10 tarefas × 3 braços, temperatura 0.7
--retomar aproveita o que já rodou, porque 270 gerações não cabem numa sessão sem alguma coisa cair no meio.
Corrida 1: temperatura 0, 90 gerações
Um pedido por célula, sem aleatoriedade. É a corrida que responde “com o mesmo prompt, o que sai”.
| Braço | Caminho feliz | Casos de borda | % borda | Tarefas 100% |
|---|---|---|---|---|
| prosa | 78/116 | 93/200 | 47% | 0/30 |
| prosa longa | 116/120 | 185/207 | 89% | 18/30 |
| spec | 110/120 | 183/207 | 88% | 14/30 |
A primeira coluna é a que eu esperava e não é notícia: o caminho feliz o modelo acerta quase sempre, em qualquer braço. Todo mundo entende “formata um número em reais”.
A coluna de borda é onde a informação aparece — 47% contra 89%. É o mesmo fenômeno do post de julho, agora com número: o modelo não erra por burrice, erra porque fechou sozinho a lacuna que o pedido deixou.
E aí a linha de baixo. A spec estruturada ficou um ponto abaixo da mesma informação em parágrafo. Não é vitória de ninguém.
Corrida 2: 270 gerações a temperatura 0,7
Uma corrida determinística não distingue diferença real de sorte. Repeti três vezes a 0,7, que é onde essas ferramentas costumam rodar de fato.
| Braço | Casos de borda | % borda | Por repetição |
|---|---|---|---|
| prosa | 277/607 | 46% | 46%, 48%, 43% |
| prosa longa | 554/614 | 90% | 88,0%, 91,3%, 91,3% |
| spec | 538/621 | 87% | 87,4%, 86,5%, 86,0% |
A dispersão importa mais que a média. A prosa curta oscila 5 pontos entre repetições; a spec, pouco mais de 1. As três repetições da spec ficaram abaixo da pior repetição da prosa longa, o que quer dizer que os 3,6 pontos de diferença não são um artefato de uma rodada azarada.
Por modelo, com a mesma corrida:
| Modelo | prosa | prosa longa | spec |
|---|---|---|---|
| qwen3-coder:30b | 48% | 95% | 95% |
| devstral:24b | 39% | 92% | 85% |
| qwen2.5-coder:32b | 49% | 84% | 81% |
O modelo mais forte empata os dois braços em 95%. Nos outros dois, a prosa longa fica na frente por 7 e por 3 pontos. Nenhum modelo colocou a spec na frente.
O dado que decide a interpretação
A média esconde o que aconteceu. Na corrida determinística, célula por célula — 3 modelos × 10 tarefas —, spec e prosa longa deram resultado idêntico em 18 das 30 células. Nas 12 restantes, a prosa longa ganhou 8 e a spec ganhou 4.
Fui olhar uma das idênticas na mão. Tarefa slugificar, devstral:24b, temperatura 0: os dois braços produziram código byte a byte idêntico. Dois prompts de tamanho e diagramação completamente diferentes, com o mesmo conteúdo, colapsando na mesma saída.
formatarBRL: zero em 11 testes, nos três modelos, por um caractere
O achado mais interessante da corrida é qualitativo e não aparece em nenhuma média.
Na tarefa formatarBRL, o braço prosa tirou 0 de 11 nos três modelos. Zero absoluto, mesmo no caminho feliz. Causa única, idêntica nos três:
Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(1234.56);
// 'R$ 1.234,56' — visualmente correto, e reprova em qualquer comparação de string
// o caractere entre "R$" e "1" não é espaço:
// código 160 (U+00A0, espaço inquebrável), não 32
O modelo escreveu a solução idiomática, a que qualquer revisor humano aprovaria em uma passada. E ela falha em assert.equal contra 'R$ 1.234,56' com espaço comum, porque Intl devolve espaço inquebrável.
Com a informação escrita — “prefixo R$ seguido de um único espaço comum, não use espaço não separável” —, dois dos três modelos passam a tratar isso: usam Intl e substituem o caractere, ou formatam na mão. O qwen2.5-coder:32b continua chamando Intl sem tratamento e falha nos três braços, inclusive com a spec na frente dele.
Esse é exatamente o erra plausível do post de julho, no nível do caractere. Não é código absurdo, que eu pegaria em dez segundos. É a linha que todo mundo escreveria, decidindo em silêncio uma coisa que o pedido não decidiu — e que só aparece quando o valor vai para um CSV, ou para uma comparação de string em outro sistema.
O que a spec custa
Na tarefa truncar, o prompt em prosa curta gastou 94 tokens de entrada. A mesma tarefa em spec gastou 556 — cerca de 6× — para uma diferença de acerto que, contra a prosa longa, não apareceu.
Com modelo local, esses 462 tokens a mais são tempo de prefill: 4 ms a mais por geração nesta placa (mediana de 141 ms contra 145 ms, cinco medições de cada). Com modelo de API, são conta no fim do mês, multiplicada por toda chamada em que a spec entra no contexto.
O custo é o mesmo nos dois braços informados, porque a informação é a mesma. O que a medição diz é que ele compra o salto de 46% para 90% — e não compra nada além disso pelo fato de estar em tabela.
Aider, Ollama e a armadilha que invalida a medição
Na prática eu não converso com o modelo por fetch. Dirijo o modelo local pelo Aider:
# direto no Ollama
aider --model ollama_chat/qwen2.5-coder:32b --read docs/specs/truncar.md src/texto.js
# ou por um gateway compatível com OpenAI
export OPENAI_API_BASE=http://localhost:4000/v1
aider --model openai/qwen2.5-coder:32b --read docs/specs/truncar.md src/texto.js
O --read continua sendo o detalhe que mais importa: carrega a spec no contexto sem torná-la editável. Agente que pode editar a spec “resolve” divergência reescrevendo a referência, que é o pior desfecho possível.
E é por isso que esta bancada não mediu através do Aider. O Aider acrescenta repo map, formato de edição e retentativa. Medindo por ele, o número seria do Aider, não do modelo. A bancada fala com o modelo por API crua, um turno, sem histórico — ela responde “o modelo entendeu o requisito?”, não “a ferramenta terminou a tarefa?”. São perguntas diferentes e a segunda não isola nada.
Onde os modelos pequenos quebram de verdade em 2025, no uso diário, não é escrever a função: é aplicar o patch no formato de busca-e-substituição que o Aider espera. O bloco volta com a indentação trocada, ou com um trecho de contexto que não bate byte a byte com o arquivo, e a edição falha. O código estava certo. Isso não aparece em taxa de acerto de função nenhuma — e é a maior parte do atrito real.
Limites do experimento
- Dez funções puras pequenas não são tarefa de repositório real. Não há dependência entre arquivos, convenção do projeto, código legado para respeitar. A seção “fora de escopo” da spec, que eu chamei em julho de a que mais paga, quase não tem o que fazer aqui — não existe vizinho para o modelo refatorar de passagem. É plausível que a estrutura pague justamente onde este experimento não olha.
- Um turno só, sem laço de agente. Sem teste rodando, sem erro voltando, sem correção. Boa parte do valor de um critério de aceite executável está exatamente no laço que esta bancada não tem.
- Modelos locais de 24 a 32B. Nada diz que o resultado se transporta para modelo grande de API, nem para modelo menor.
- Comparação de string é frágil, e a
formatarBRLprova. Um caractere invisível zerou uma tarefa inteira. Onde meu teste é rígido demais, eu reprovo código que serviria; onde é frouxo, aprovo código que não serve. Os 3,6 pontos entrespeceprosa longasão pequenos o bastante para caber nessa fragilidade. - Três repetições dão ideia de dispersão, não intervalo de confiança. Eu não fiz teste estatístico.
O que eu mudei de ideia
Escrevi em julho que a spec fazia o agente acertar mais. Está errado, do jeito que escrevi. O que faz o modelo acertar mais é eu ter decidido e escrito o que acontece quando o valor é negativo, quando a entrada é null, quando a página passa do fim. A spec é o lugar onde eu costumo fazer isso — não é o mecanismo.
O que continua de pé do post anterior: a seção de casos de borda é o que carrega o resultado. Foi lá que os 44 pontos apareceram. O que cai: a ideia de que a tabela, os cabeçalhos e o markdown fazem diferença para o modelo. Para o modelo, pelo que medi, não fazem.
Então para que serve o arquivo, se não é acerto por geração? Isto aqui é julgamento, não medição — não testei nada do que vem abaixo:
- Versionamento. Spec no prompt morre com a sessão. No repositório, ela entra no diff e alguém revisa a decisão antes de ela virar comportamento.
- Revisão contra referência. A pergunta na revisão deixa de ser “isso parece certo?” e vira “isso bate com o arquivo ao lado?”. A segunda tem resposta.
- Reuso entre ferramentas. O mesmo arquivo serve Aider, Claude Code, Cursor e eu lendo. Prosa colada num chat não serve nem a segunda vez.
- Sobrevida. O modelo troca a cada poucos meses. O arquivo que diz o que o sistema faz continua valendo depois da troca; o prompt afinado para um modelo específico, não.
A estrutura serve para mim, e para a próxima pessoa que abrir o arquivo. Isso me parece suficiente para continuar escrevendo spec. Só não é o que eu disse que era.
Se você for repetir: pnpm bancada, com --modelos, --amostras, --temperatura e --retomar. As tarefas estão em scripts/bancada-spec/tarefas.mjs, com os três prompts lado a lado — a parte que mais vale a pena revisar antes de acreditar em qualquer número deste post.