Logo NascimentoIgor Nascimento / Blog
Engenharia

IA, dívida técnica e a ilusão de que gerar código é resolver problema

Uma migração urgente, um código que funcionava, e cerca de quatro registros por segundo. Com milhares de registros e centenas de tabelas, isso vira dias. A correção foi uma mudança na forma de buscar e associar dados, e a mesma migração passou a rodar em minutos. Uma conversa sobre por que o problema não era Python, não era a IA, e era exatamente o que o fundamento ensina a enxergar.

EngenhariaIA aplicada12 min de leituraIgor Nascimento

O caso, como ele chegou

Recentemente acompanhei um caso simples que reforçou algo que venho pensando há tempo: o problema não é usar IA para programar. É usar IA sem entender o que está sendo gerado.

Havia uma migração de dados urgente. O código funcionava, estava em Python, tinha sido escrito rápido e passava nos testes. Lia registros de uma origem, associava cada um a dados de referência, e gravava no destino. Nada exótico. Mas havia um detalhe na forma como ele fazia a associação: em um ponto crítico, para cada registro processado, ele percorria uma coleção inteira procurando a correspondência. Funcionava. Só que fazia isso para cada registro.

Resultado: cerca de quatro registros por segundo.

Pode parecer aceitável. Afinal, funciona, e numa amostra de teste com algumas dezenas de linhas ninguém percebe. Mas faça a conta com o volume real: milhares de registros por tabela, centenas de tabelas. A quatro por segundo, uma migração que deveria caber numa janela de manutenção vira algo que leva dias. E migração que leva dias não é lenta, é inviável, porque a janela fecha, o sistema de origem continua recebendo escrita, e você começa a migrar um alvo que se move.

A forma do problema

O que estava acontecendo tem nome, e é o primeiro capítulo de qualquer curso de estrutura de dados. Associar dois conjuntos percorrendo um deles para cada item do outro é uma operação quadrática: com n registros de um lado e m do outro, são n × m comparações. Dobrou o volume, quadruplicou o tempo. É o tipo de custo que fica invisível em dados pequenos e explode em dados reais, e por isso é tão traiçoeiro: o código que passa no teste e o código que trava a produção são o mesmo código. Só o tamanho da entrada mudou.

A mudança foi simples na estratégia de busca e associação. Em vez de procurar a correspondência de cada registro varrendo a coleção, construir uma vez um índice em memória, um dicionário com a chave de associação, e a partir daí cada busca custa tempo constante. O ponto crítico passou de O(n) por registro para O(1) por registro, e o total de quadrático para linear. A mesma migração passou a processar milhares de registros em minutos.

# a forma do antes: para cada registro, varrer a referência
for reg in registros:
    ref = next((r for r in referencias if r.chave == reg.chave), None)
    gravar(reg, ref)

# a forma do depois: indexar uma vez, buscar em tempo constante
por_chave = {r.chave: r for r in referencias}
for reg in registros:
    gravar(reg, por_chave.get(reg.chave))

E isso não veio de mágica. Veio de base. De entender estrutura de dados. De entender complexidade algorítmica. De saber identificar o gargalo real, em vez de chutar. De uma base construída em estudos e em exercícios de lista, pilha, hash, busca e ordenação que, na época, pareciam distantes de qualquer problema de verdade.

O trade-off do índice, para não parecer de graça

O dicionário troca tempo por memória: a referência inteira precisa caber na RAM do processo que migra. Para centenas de milhares de linhas é trivial; para centenas de milhões, não é, e aí a resposta muda: processar em lotes ordenados pela chave, ou empurrar a associação para o banco, que tem índice e join otimizado há quarenta anos. Também há o custo de construir o índice, que é linear e se paga na primeira centena de buscas. A escolha certa depende do tamanho, e saber disso faz parte do mesmo fundamento que apontou o problema.

"Python é lento" e outras explicações confortáveis

Hoje vejo muita gente dizendo que "Python é lento" ou que "precisa trocar de linguagem" diante de um problema como esse. Às vezes a linguagem importa, e eu não vou fingir que não: há cargas em que a diferença entre Python e uma linguagem compilada é de uma ordem de grandeza e decide o projeto. Mas repare no que aconteceu aqui. A mesma linguagem, o mesmo hardware, a mesma base de dados, e a diferença entre horas e minutos. Nenhuma linguagem do mundo salva um algoritmo quadrático em volume grande; ela só muda a constante. Trocar Python por Go com o mesmo laço aninhado teria dado, talvez, quarenta registros por segundo. Em vez de dias, ainda seriam muitas horas.

A explicação "a linguagem é lenta" é confortável porque transfere a responsabilidade para fora. A explicação "o código foi pensado errado" exige olhar para dentro. E a segunda é a que resolve.

Onde a IA entra nessa história

Vivemos uma fase em que a IA gera código em segundos e entrega respostas boas, funcionais, com nome de variável decente e tratamento de erro. O código do caso era exatamente assim: legível, correto, testado. E tinha um gargalo quadrático no meio, porque o modelo escreveu a forma mais direta de resolver o problema descrito, e a forma mais direta de associar duas coleções é percorrer uma dentro da outra.

Esse é o risco do uso desenfreado. Não é que a IA gere código errado. É que ela gera código que funciona, e "funciona" é um critério fraco demais para sistemas reais. O problema de complexidade não aparece na primeira execução, nem no teste unitário, nem na demonstração. Ele aparece depois:

A IA pode gerar dívida técnica em escala industrial justamente porque remove o atrito. Antes, escrever um laço aninhado sobre duas coleções grandes exigia digitar, e enquanto se digitava, algo na cabeça dizia "isso vai ser lento". Agora o código aparece pronto, e a hora de desconfiar passou sem ninguém notar.

A frase que resume

IA é uma ferramenta poderosa. Mas não substitui fundamento. Ela acelera quem sabe o que está fazendo. E acelera a dívida técnica de quem não sabe. A velocidade é a mesma nos dois casos; só a direção muda.

O que mudaria o desfecho

Olhando para trás, o gargalo teria sido pego por qualquer uma de três coisas, e nenhuma delas é sofisticada:

  1. Um teste com volume representativo. Não um teste de carga formal; bastava rodar a migração numa amostra de alguns milhares de registros e olhar o relógio. A curva quadrática se denuncia em segundos.
  2. Um profiler. Dez minutos com o profiler padrão do Python apontam a linha que consome tudo. Medir antes de otimizar é a regra mais repetida da engenharia e a mais ignorada.
  3. A revisão com as perguntas certas. Não "isso funciona?", que o teste responde, mas as perguntas que o teste não responde.

E talvez seja aí que está o diferencial do desenvolvedor daqui para a frente. Não vai ser pedir código para a IA, porque isso todo mundo faz. Vai ser olhar para o código que veio e perguntar:

Essas perguntas não exigem genialidade. Exigem fundamento, e exigem o hábito de fazê-las mesmo quando o código parece bom. Principalmente quando parece bom, porque código que parece bom não é revisado com atenção.

O que eu faria diferente

Trataria código gerado exatamente como trato código de um colega júnior competente: confio na sintaxe, confio na intenção, e reviso a complexidade, os limites e o comportamento com dados grandes. Não por desconfiança da ferramenta. Por respeito ao sistema que vai rodar aquilo em produção.

Resumo da conversa