Ir para o conteúdo principal
Anúncios

Benchmarking de agentes de codificação na base de código de vários milhões de linhas da Databricks

por Vinay Gaba, Ankit Mathur, Rishabh Singh, Patrick Wendell e Matei Zaharia

Na Databricks, a forma como desenvolvemos software está mudando rapidamente à medida que adotamos ativamente a AI na engenharia. O cenário de modelos e harnesses para criação de código expandiu-se rapidamente no último ano, oferecendo aos desenvolvedores mais opções do que nunca. Com mais opções, tornou-se cada vez mais importante entender quais agentes de codificação oferecem o melhor desempenho em tarefas reais de programação, bem como compreender como o desempenho das tarefas varia de acordo com o preço.

Este artigo compartilha os resultados e a metodologia do benchmark interno de codificação que criamos na Databricks, que avalia ferramentas em tarefas reais de programação que nossos engenheiros realizaram na base de código da Databricks. As tarefas envolveram edições em uma base de código de milhões de linhas que abrange várias linguagens populares (Python, Go, Typescript, Scala, etc.), e tanto as tarefas quanto as soluções foram revisadas cuidadosamente para garantir a precisão. Isso não pretende ser exaustivo, mas o exercício trouxe insights que já tornaram nossa equipe de engenharia significativamente mais eficiente com agentes de codificação. Abaixo, você pode ver a pontuação dos modelos e harnesses no benchmark geral:

Custo vs. Desempenho em nosso benchmark

Figura 1: Custo vs. Desempenho em nosso benchmark

As principais conclusões da nossa análise foram:

  1. A fronteira de Pareto para tarefas de codificação (ou seja, a melhor qualidade para um determinado custo) inclui modelos da OpenAI, Anthropic e de código aberto. Isso significa que, hoje, apenas uma combinação de ferramentas pode oferecer um desempenho de ponta.
  2. Modelos abertos, e o GLM 5.2 em particular, agora são capazes de lidar até mesmo com o nível mais alto de dificuldade de tarefas.
  3. O preço por token de um modelo é um indicador ruim dos custos reais gerados em tarefas de ponta a ponta. Modelos maiores podem ser muito mais eficientes no uso de tokens e apresentar custos gerais mais baixos.
  4. O harness a partir do qual um modelo é chamado afeta drasticamente o custo e a qualidade. Em muitos casos, harnesses simples como o Pi apresentaram o melhor desempenho em nossas cargas de trabalho.

Vamos nos aprofundar um pouco mais em cada um deles.

Modelos se agrupam em "níveis de capacidade" aproximados

Pequenas diferenças de alguns pontos nos resultados específicos costumam se equilibrar em tarefas do mundo real. Nós nos concentramos mais nos padrões temáticos que nos ajudam a decidir quais modelos usar para diferentes tarefas. De fato, os resultados mostraram um agrupamento claro dos modelos e harnesses em 3 níveis de capacidade.

Níveis de capacidade para modelos

Figura 2: Três níveis distintos de capacidade surgiram em nossos resultados gerais, com nuances sobre quais modelos foram eficazes em cada grupo

No limite superior de desempenho, vemos que os modelos mais inteligentes são muito eficazes na resolução de todos os tipos de problemas, mas são muito caros. Modelos de inteligência média e baixa ainda são altamente eficazes em tarefas comuns e, em muitos casos, também são significativamente mais baratos.

No dia a dia, os engenheiros realizam muitas atividades diferentes que variam significativamente em complexidade: tarefas operacionais comuns, como alterar uma flag ou atualizar configurações, não exigem modelos extremamente inteligentes, mas explorações de design mais profundas sim. No entanto, no passado, nossos modelos padrão eram sempre os mais caros. Com base nessa análise, determinamos que deveríamos direcionar mais trabalho para a classe de modelos Haiku e GPT 5.4 Mini.

Modelos abertos chegaram para a codificação

Há muito entusiasmo em relação ao GLM 5.2, e nossos resultados mostraram evidências de que o GLM pode ser um modelo de uso diário para muitos de nossos desenvolvedores. Ele ficou no nível superior de capacidade, empatado estatisticamente com o Opus 4.8 em qualidade, mas custando US$ 1,28 por tarefa, contra os US$ 1,94 do Opus.

As pontuações de qualidade do GLM são consistentes com o feedback qualitativo que recebemos de desenvolvedores internos que estão testando o GLM no desenvolvimento diário. Devido ao seu excelente desempenho em tarefas cotidianas de programação, temos focado em servir o GLM com o melhor desempenho, e as evidências mostram que é hora de começar a implantá-los como modelos principais para o dia a dia de codificação.

Preço por tarefa vs. preço por token

Os desenvolvedores costumam estimar por alto os custos de tokens para determinar o quão caro um modelo será ao concluir tarefas de programação. No entanto, descobrimos que os custos de tokens geralmente são um indicador ruim dos custos gerais das tarefas, devido à variação na eficiência de raciocínio entre os modelos. Isso reforça a necessidade de benchmarks no nível da tarefa, já que o formato e a complexidade da tarefa podem variar em diferentes contextos.

Como exemplo, o Sonnet 5 é cerca de 1,7 vez mais barato por token do que o Opus 4.8, mas, em nossas tarefas, descobrimos que o Sonnet custou US$ 2,09 por tarefa contra os US$ 1,94 do Opus, enquanto obteve uma pontuação seis pontos menor na conclusão de tarefas (81% contra 87%). Isso ocorreu principalmente porque o Sonnet 5 trabalhou por mais tempo e leu mais para chegar lá, consumindo 1,9 vez mais tokens.

Harnesses têm um grande impacto na eficiência

Quando executamos o mesmo modelo com o mesmo esforço de raciocínio em dois harnesses diferentes (Claude Code/Codex vs. Pi), observamos que o custo por tarefa diferiu significativamente (mais de duas vezes em alguns casos), enquanto a qualidade permaneceu a mesma. A principal diferença se resumiu a quanto contexto cada harness fornecia ao modelo a cada turno.

Impacto do harness na eficiência

O Pi enviou cerca de 3 vezes menos contexto por turno. Ele gerenciou melhor o contexto, mantendo um conjunto de trabalho mais enxuto e concluindo as tarefas em menos execuções.
Contexto total reenviado ao modelo por tarefa

A lição aqui não é que um harness é sempre mais barato ou que os harnesses nativos são piores. Em vez disso, a escolha do modelo é apenas uma peça do quebra-cabeça. Estabelecer essa flexibilidade é o motivo pelo qual investimos na Omnigent para tornar as trocas de modelo e harness integradas.

Por que criar seu próprio benchmark?

Benchmarks públicos como o SWE-Bench e o TerminalBench são úteis, mas não conseguem responder às perguntas que tínhamos. Há algumas razões para isso:

  • As tarefas são públicas, de modo que as soluções vazam para os dados de treinamento ao longo do tempo.
  • Descobrimos que os resultados não eram representativos para a nossa base de código, que abrange mais de 10 linguagens e muitos serviços escritos em Scala, Go, Rust, Java e Python, Bazel, Protobuf e muito mais.

Ao criar um benchmark em nossos próprios PRs, podemos tomar essas decisões com maior confiança de que não atrapalharemos nossos desenvolvedores ao implementar otimizações.

Como criamos o benchmark

Usamos o Unity AI Gateway para capturar logs de todas as nossas interações de codificação, o que nos permitiu analisar a complexidade das tarefas que os engenheiros enfrentam usando agentes de codificação. Houve uma diversidade significativa na complexidade das tarefas, sendo que cerca de um quarto foi classificado como trabalho de baixa complexidade e aproximadamente 60% como de média complexidade.

O que nossos engenheiros realmente pedem aos agentes de codificação

No entanto, os modelos caros são os modelos padrão que os engenheiros usam, por isso havia claramente uma enorme oportunidade para melhorar a eficiência.

Construção de tarefas

Nossos engenheiros mesclam milhares de alterações de código por dia, por isso já temos um excelente conjunto de dados para começar. Um bom pull request é um artefato rico, com commits que mostram a iteração do desenvolvedor, a revisão por humanos e testes que ajudam a verificar se uma alteração de código é fiel à sua intenção. No entanto, precisamos de várias verificações de qualidade e filtros para construir um benchmark de alta qualidade a partir deles:

  • Recência: Buscamos no histórico recente para que as tarefas reflitam como desenvolvemos hoje, incluindo os frameworks, padrões e convenções atualmente em uso.
  • Escrito por humanos: Commits de bots, contas de serviço, alterações totalmente geradas por AI e alterações geradas automaticamente foram filtrados.
  • Suíte de testes de alta qualidade associada: Filtramos os PRs que incluíam testes de alta qualidade para validar as alterações de código.
  • Autocontido: As alterações foram limitadas a alguns módulos.
  • Representativo de tarefas típicas: Selecionamos PRs de uma distribuição de tarefas em todo o stack: serviços de backend em Scala, código de sistemas em Rust, frontend em React e TypeScript, contratos de protobuf e gRPC e configurações do Bazel.
Plano passo a passo de construção de tarefas

Assim que tivemos os PRs candidatos, focamos em construir tarefas bem especificadas ao:

  1. Compreender a intenção e resumi-la como um prompt. Lemos o PR para entender para que ele realmente servia e, em seguida, descrevemos o resultado desejado. Geralmente, isso significava reescrever a descrição do PR indicando o problema ou objetivo, nomeando quaisquer restrições e removendo a descrição da solução. É importante remover, por exemplo, explicações sobre por que uma correção de bug é a correta, pois isso torna a tarefa fácil demais.
  2. Separar os testes relevantes. Os arquivos que não eram de teste representavam a alteração que o modelo precisa reproduzir por conta própria, por isso deixamos os arquivos de teste de lado e garantimos que podíamos compilar isso. Nosso sistema de build já consegue determinar quais testes dependem dos arquivos que foram alterados no PR original, então executamos todos esses targets de teste na íntegra.

O resultado desse exercício foi uma única tarefa no benchmark. Aqui está um exemplo simplificado:

Embora tenhamos usado scripts e AI para gerar tarefas candidatas, avaliamos cada amostra manualmente. Em alguns casos, descobrimos que os testes no PR original precisavam ser reescritos para permitir uma implementação alternativa ou para serem mais rigorosos, o que fizemos manualmente (sem AI). Da mesma forma, também encontramos casos que exigiram a melhoria da descrição da tarefa para torná-las bem especificadas.

Antes e depois da suíte de testes

Figura 3: Um antes e depois da nossa suíte de testes: o teste anterior baseava-se na verificação de correspondência exata de strings, o que resultou em algumas falhas quando o modelo tentou resolver a tarefa. Essa não era uma boa maneira de testar saídas não determinísticas, por isso ele foi reescrito para avaliar o comportamento em vez disso.

Instanciamos os harnesses e modelos de agentes de codificação usando suas configurações padrão prontas para uso, com todas as ferramentas comuns que os engenheiros da Databricks teriam disponíveis.

Processo de configuração e revisão

Quando o agente dizia explicitamente que havia concluído a tarefa, criávamos um checkpoint daquele código, aplicávamos o patch nos testes que haviam sido retidos e avaliávamos os testes para determinar se aquela tarefa era aprovada para aquela combinação de modelo + harness. Nós não usamos um juiz LLM para avaliar a correção, pois descobrimos que isso recompensa parecer correto em vez de estar correto.

Proteções adicionais

Proteções adicionais

Em nossos experimentos iniciais, algumas pontuações dos modelos pareciam boas demais para ser verdade, por isso inspecionamos manualmente os rastros para entender o que aconteceu nessas trajetórias dos agentes. O que vimos foi que, devido à nossa configuração original, a implementação "correta" ainda podia ser recuperada no histórico do Git da worktree! Cada tarefa tinha se originado de um commit mesclado (merged), então nada impedia que um agente com um shell avançasse pelo histórico do git para encontrá-la. Para corrigir esse problema, isolamos o histórico do git: durante a execução de cada rodada, desconectamos completamente a cópia de trabalho do repositório.

O que vem a seguir?

Começamos com uma pergunta simples: podemos usar agentes de codificação de forma mais eficiente? A resposta é um sim definitivo e, como podemos ser orientados por dados, podemos começar a criar recursos para selecionar os modelos certos automaticamente e acompanhar a eficiência.

Qualquer empresa pode fazer o mesmo. Qualquer equipe com um backlog de PRs mesclados já tem em mãos um benchmark no qual nenhum modelo foi treinado, avaliado pelos testes que sua própria equipe escreveu. Estamos adicionando ativamente mais tarefas (especialmente as mais difíceis) e planejamos testar cada novo agente/harness com elas para termos mais confiança em nossas escolhas.

Na Databricks, sempre fomos cautelosos com o aprisionamento tecnológico (lock-in), não apenas em relação a fornecedores, mas também a suposições que tornam as equipes menos flexíveis ao longo do tempo. Esse mesmo instinto moldou nossas primeiras apostas em formatos e padrões abertos, e molda como estamos abordando a AI agora: medir o que realmente funciona no código que entregamos, dar espaço para os engenheiros alternarem entre modelos e harnesses com proteções consistentes, e fazer otimizações para usar a AI de maneira eficaz.

Em um blog post futuro, falaremos mais sobre como estamos usando recursos de roteamento inteligente no Unity AI Gateway e no Omnigent para ajudar nossos desenvolvedores a usar os agentes mais inteligentes de forma eficiente.

(Esta publicação no blog foi traduzida utilizando ferramentas baseadas em inteligência artificial) Publicação original

Receba os posts mais recentes na sua caixa de entrada

Assine nosso blog e receba os posts mais recentes diretamente na sua caixa de entrada.