Testei 17 modelos locais com meu agente de código. Fiquei com 2
Mais de 500 execuções para descobrir qual modelo local aguenta o serviço de um agente de código: o método, as surpresas e os dois que sobraram.
Baixei modelos demais. Esse era o problema. Tinha tanto modelo no disco que cada um que eu experimentava parecia ótimo num dia e inútil no outro, e eu já não sabia mais se estava escolhendo ou só tentando. Então parei de achar e fui medir.
Uso um agente de código que eu mesmo escrevi, o donk. Ele lê arquivos, procura texto, edita e roda comandos num repositório, em nome de um modelo qualquer. A graça de ter feito isso é poder trocar o modelo por trás: um da nuvem quando a tarefa pede, um local quando o código é meu e não quero que saia da máquina. O que eu queria saber era qual local aguenta o serviço.
Rodei tudo num PC com um Ryzen AI 9 HX 470 e 64 GB de memória. Ele tem três lugares onde um modelo pode rodar: a CPU, a placa de vídeo integrada (uma Radeon 890M) e uma NPU, aquele chip de IA que agora vem em tudo quanto é notebook. Os modelos eram servidos pelo Lemonade.
Como eu medi
Nada de ler a resposta e achar que parecia certa. Cada tarefa tinha um verificador automático no fim. Se era pra corrigir um defeito, os testes do projeto tinham que passar, e o modelo não podia ter mexido nos testes. Se era pra achar uma função, a resposta tinha que ser exatamente arquivo:linha. Se era configuração, um script lia o arquivo e conferia cada detalhe pedido.
Cada execução começava numa cópia limpa do projeto. Anotei acertos, tempo e a velocidade de escrita do modelo, em tokens por segundo.
Fiz em funil, porque não ia gastar uma tarde com um modelo que já tinha falhado no básico. Primeiro, o modelo carrega e consegue usar as ferramentas do agente? Dos 17, 14 conseguiram. Depois, cinco tarefas de código num projeto de teste que montei só pra isso: achar uma função, implementar outra, renomear um nome em nove arquivos, corrigir um defeito, achar um valor numa tabela enorme. Ficaram 9, pela regra de que saía quem acertava menos de 8 em 10 e ainda demorava mais de 100 segundos por acerto. Escolhi três campeões para a etapa seguinte, com configuração de sistema, perguntas de arquitetura e defeitos plantados em três projetos meus. Um deles, o Qwen3-14B, caiu aí, e eu trouxe de volta o Qwen3.5-9B para comparar. O desempate final, com 13 tarefas, três vezes cada uma, foi entre Qwen3.6-35B, Ornith-9B e Qwen3.5-9B. Contando tudo, passou de 500 execuções (só a segunda fase foram 160, e o desempate, 117).
A NPU ficou de fora
Era a parte que mais me interessava e foi a que menos rendeu. Cinco dos modelos que rodam nela simplesmente não usam as ferramentas do agente. Eu desconfiava de instalação mal feita, então comparei o texto que chega ao modelo com e sem a lista de ferramentas. Tinha o mesmo tamanho. A NPU funciona, o modelo é que ignora as ferramentas, e um agente sem ferramentas não lê nem edita nada.
Um modelo da NPU funcionou. Acertou 8 de 10 tarefas, mas levava 381 segundos por acerto. O Ornith-9B, o mais rápido da placa de vídeo, levou 40 nessa mesma fase. Por enquanto, a NPU fica pra outra hora.
Pensar mais não ajudou
Alguns modelos têm um modo de raciocínio, em que escrevem um rascunho antes de responder. Eu rodava todos com isso desligado e comecei a achar que podia estar prejudicando os que foram feitos pra pensar. Testei com e sem em cinco modelos.
Pra quem já acertava, não mudou nada, só gastou mais texto e mais tempo. O Qwen3-14B ficou pior: caiu de 6 acertos em 6 pra 4, e ficou quase cinco vezes mais lento, porque entrou num ciclo repetindo a mesma edição. Mantive desligado.
O melhor em código foi o pior em configuração
O Qwen3-14B acertou todas as 10 execuções de código da segunda fase (cinco tarefas, duas vezes cada). Eu esperava que fosse finalista. Na configuração de sistema ele acertou 4 das 10 execuções.
O jeito de errar é o que mais me incomodou. Pedi pra um serviço do systemd reiniciar sozinho, rodar com outro usuário e ter uma proteção ligada. Ele fez uma única edição e respondeu que estava tudo pronto. Fui conferir na sessão e era um pedaço só.
Nas perguntas de arquitetura foi parecido. Perguntei se um serviço com uma única instância devia usar Redis ou um cache dentro do próprio processo, e ele recomendou o Redis, sem notar que isso põe um servidor inteiro no caminho pra ganhar quase nada. Saiu da lista. Daí tirei uma coisa que parece óbvia mas que eu só tinha entendido de verdade agora: ser bom numa categoria de tarefa não diz nada sobre as outras, e quem testa só código descobre isso tarde.
Quem ficou
| Qwen3.6-35B | Ornith-9B | |
|---|---|---|
| Acertos no desempate (de 39) | 38 | 32 |
| Tempo por acerto | 48 s | 31 s |
| Velocidade de escrita | 12,6 tok/s | 16,9 tok/s |
| Perguntas de arquitetura (nota de 0 a 1) | 0,79 | 0,85 |
| Defeitos em projetos reais (de 18) | 14 | 13 |
| Espaço em disco | 21,7 GB | 6,2 GB |
O Qwen3.6-35B virou o padrão. É o que mais acerta, e foi o único que corrigiu o defeito mais difícil que plantei num dos meus projetos. Os outros dois finalistas, o Ornith-9B e o Qwen3.5-9B (este fez 33 acertos de 39, um a mais que o Ornith, mas saiu da lista por ser o mais lento dos três e errar sempre as tabelas grandes), tentaram até o limite de 20 turnos e não acharam a causa. No total dos projetos reais a diferença para o Ornith é de um acerto (14 contra 13), mas ela esconde o defeito difícil: 3 de 3 contra 0 de 3. O Ornith perdeu esse defeito nas três tentativas e mais duas localizações. Em compensação o Qwen3.6-35B é lento pra renomear um nome em muitos arquivos, uns 200 segundos, e pra uma tarefa de configuração de SSH, uns 80.
O Ornith-9B fica pras coisas pequenas: achar uma função, mexer num arquivo de configuração, responder uma dúvida. É menor, mais rápido e foi o melhor nas perguntas de arquitetura. Mas não consegue renomear o nome em nove arquivos. Acertou zero de três, e olha que o 35B fez três de três. Tem o ponto cego dele, e agora eu sei qual é.
Se quiser ver onde cada um acerta ou erra, nas 13 tarefas do desempate:
| Tarefa | Ornith-9B | Qwen3.6-35B |
|---|---|---|
| Achar uma função (duas tarefas) | 6 de 6 | 6 de 6 |
| Implementar uma função | 3 de 3 | 3 de 3 |
| Corrigir um defeito simples | 3 de 3 | 3 de 3 |
| Renomear em 9 arquivos | 0 de 3 | 3 de 3 |
| Achar um valor numa tabela enorme (três tamanhos) | 8 de 9 | 8 de 9 |
| Configuração do SSH | 1 de 3 | 3 de 3 |
| Outras quatro de configuração | 11 de 12 | 12 de 12 |
Os tempos por acerto da tabela anterior vêm do desempate. Os da fase de código, como os 40 segundos acima, são de outro conjunto de tarefas, por isso os números não se repetem.
Uma coisa que ajuda a explicar as velocidades: na placa de vídeo integrada, o que limita é a memória, não a conta. Por isso um modelo MoE, que usa só uma parte dos parâmetros a cada palavra, anda bem. O Qwen3.6-35B, com 35 bilhões de parâmetros, escreve a uns 13 tokens por segundo nas tarefas de código. Um modelo denso de 14 bilhões, bem menor, ficou em 4, nas mesmas tarefas e com o raciocínio desligado.
Rodar os dois ao mesmo tempo
Deu vontade de deixar os dois carregados e escolher por tarefa, então testei.
O Ornith-9B na CPU funciona. Numa rodada à parte, com 15 execuções de configuração, acertou todas na CPU e todas na placa de vídeo, escrevendo a 11 tokens por segundo contra 18. Mais lento, igual em acertos. (Aqui ele foi melhor no SSH do que no desempate, onde acertou 1 de 3: mais um lembrete de que poucas repetições dão ruído.)
Um na placa de vídeo e outro na CPU também funciona, mas um atrapalha o outro. A memória é a mesma pros dois, então cada um perdeu cerca de um terço da velocidade. Nenhum acerto a menos.
Os dois na placa de vídeo derrubaram minha sessão. Duas vezes. Na segunda vez fui olhar o registro do sistema, e as duas quedas tinham a mesma linha, segundos antes de a sessão cair: a placa de vídeo avisando que rejeitou um comando por falta de memória. Minha leitura é que a placa integrada divide a memória com o resto, e dois modelos grandes mais a interface gráfica não cabem juntos. Não sei se é só isso, mas deixei registrado e voltei pra um modelo por vez.
Antes disso eu ainda perdi parte do trabalho. O programa de teste e os resultados parciais estavam em /tmp, que some quando a máquina reinicia. Os números importantes já estavam anotados, mas passei a guardar tudo em disco.
O que eu não sei
Cada tarefa rodou duas ou três vezes por modelo. Diferença de um acerto, como 32 contra 33, é ruído, e eu tomei cuidado de não tirar conclusão disso. Medi só o que eu uso: código em Rust, arquivos de configuração e perguntas de design. Não sei como esses modelos vão em Python, em frontend ou em projetos com centenas de arquivos. Os números de velocidade valem pra esta máquina.
O raciocínio eu testei em cinco modelos. Num modelo feito pra pensar de verdade, pode ajudar mais do que vi. E eliminei alguns que talvez voltem: alguns da NPU que ainda não provei, e um que caiu por um problema no formato do prompt dele e poderia voltar corrigido.
Se você quiser repetir algo parecido, o caminho é mais simples do que parece. Escolha poucas tarefas que importam de verdade pro seu uso, escreva um verificador pra cada uma e comece com modelos demais, cortando a cada fase. O que me custou algumas horas pra descobrir coube numa tabela de duas colunas.