Compreensão de código
Compreender código para gerar código melhor
Ao criar o FiboCode, passei a querer compreender melhor o meu projeto. Reflexões sobre linguagem natural, arquitetura e trabalho continuado com AI.
Não gosto da sensação de não compreender o meu próprio projeto.
Os requisitos foram definidos por mim. As funcionalidades eram as que eu queria. Depois de a AI gerar o código, confirmei que funcionava. Mas, se alguém me perguntar por que módulos passa uma funcionalidade, por que foi implementada assim ou o que outra alteração pode afetar, nem sempre consigo responder com clareza.
Ao desenvolver o FiboCode, isto tem-se tornado cada vez mais importante para mim.
Claro que quero que a AI escreva mais código por mim. Mas pretendo continuar este projeto. Quando terminar a funcionalidade de hoje, amanhã surgirá outra ideia. Não quero começar cada alteração a rezar para que não estrague outra coisa.
Corrige-se A e aparece um problema em B. Corrige-se B e surge um novo erro em A. É fácil continuar a enviar os sintomas à AI e esperar pela próxima explicação e correção.
Mas, ao fim de algumas voltas, sentir-me-ia cada vez menos seguro. Cada alteração parece razoável isoladamente. Juntando tudo, em que se transformou o sistema?
Quero perceber isso. Ao seguir esta pergunta, comecei a sentir que compreender código e gerá-lo estão mais ligados do que pensava.
Programar em linguagem natural levou-me a pensar
Da linguagem máquina à linguagem assembly e depois às linguagens de alto nível, fomos acrescentando abstrações.
Muitos detalhes que antes tratávamos à mão passaram para as ferramentas. Podemos expressar funcionalidades mais complexas com menos código, sem começar sempre pela base.
Agora subimos mais um degrau: descrevemos requisitos em linguagem natural e pedimos à AI que gere o código.
À primeira vista, pode parecer apenas outra maneira de nos exprimirmos. Mas há uma mudança que, para mim, importa.
Assembladores e compiladores transformam código segundo regras explícitas. Os programas podem ter erros, claro, mas a transformação não precisa de adivinhar o que eu pretendia fazer.
O modelo precisa de adivinhar.
A partir das minhas palavras, do código que vê e do contexto, infere o que quero e escolhe uma implementação. A mesma frase pode produzir resultados diferentes. Mesmo um resultado sempre igual pode continuar a implementar um requisito que nunca expressei bem.
Mais tarde li um artigo de Birgitta Böckeler que também aborda esta diferença: os grandes modelos de linguagem geram através de inferência e não podem ser tratados simplesmente como compiladores de linguagem natural. Era próximo do que eu andava a pensar.
Então, o que podemos fazer?
Requisitos mais claros, convenções escritas e testes aos comportamentos importantes devem reduzir alguns desvios. Se o desenho já explica como os módulos colaboram e que classes e métodos intervêm, a AI tem mais pontos de apoio.
Mas a documentação tem de corresponder ao código. Uma descrição detalhada de um desenho alterado há muito pode causar mais problemas do que não ter documentação.
Também não creio que estas práticas eliminem toda a incerteza da geração. Mas deixam menos coisas para o modelo adivinhar no momento.
Sei realmente o que quero alterar?
Seguindo este raciocínio, chego a outro problema: as pessoas também têm dificuldade em explicar bem os requisitos.
Sobretudo quando não conhecem o código.
Posso saber exatamente onde quero mais um botão ou que operação devia ser mais cómoda. Mas talvez não saiba em que módulo existente se enquadra, por que processamento deve passar ou que comportamentos antigos têm de se manter.
Nessa situação, mesmo que repita à AI «não afetes as outras funcionalidades», como deve interpretar essa frase?
Por exemplo, «uniformiza isto» pode referir-se à apresentação ou à junção da lógica de processamento de dados. O âmbito das alterações será muito diferente conforme a interpretação.
Se nem eu consigo explicar os limites, o modelo tem de os completar a partir do que vê. Pode alterar mais do que esperava ou contornar uma convenção que eu nem sabia que existia.
Depois encontro um problema e peço que o corrija. À medida que as alterações se acumulam, o pedido inicial, as restrições acrescentadas e as relações que já existiam no código podem deixar de caber intactos no contexto.
Receio acabar com código que funciona aqui e ali, mas está cheio de remendos deixados pela correção anterior. Parece que só a AI aceita continuar a alterá-lo. As pessoas quase não se atrevem a tocar-lhe.
Esse código faz-me pensar em AI slop. E entregá-lo de novo à AI também não significa que o modelo compreenda todo o sistema. Pode continuar a produzir explicações sem que nós o tenhamos finalmente entendido.
Por isso, sinto cada vez mais que explicar um requisito com precisão começa por conhecer a forma geral do projeto.
Não preciso de perceber todos os pormenores logo de início. Mas quero saber onde vai entrar a alteração que estou a pedir.
Quero falar com a AI ao nível da arquitetura
Não considero que utilizar AI obrigue a rever à mão todas as linhas geradas.
Quando um projeto se torna complexo, a nossa atenção é limitada. Prefiro começar pela estrutura: os módulos principais, as responsabilidades de cada um, onde entra uma funcionalidade, que processamento percorre e onde altera dados e estados.
Quero organizar outra camada de compreensão do sistema acima da implementação em linguagem de alto nível. Por agora chamo-lhe «camada de arquitetura», sem me prender muito ao nome.
O importante é que corresponda ao código real.
Se vejo um módulo, consigo encontrar a implementação. Se vejo o percurso de uma funcionalidade, sei que classes e métodos atravessa. Se quero ajustar um comportamento, posso seguir as relações até aos pontos que preciso de observar.
Assim tenho assuntos mais concretos para discutir com a AI.
Se sei, por exemplo, que uma operação já passa por uma fila de tarefas comum, posso discutir se a nova capacidade deve seguir esse caminho. Se a AI acrescenta outra gestão de estados, posso perguntar por que não serve a existente e se as duas poderão interferir mais tarde.
Sem estas perguntas, talvez aceitasse qualquer implementação que funcionasse. Agora tenho pelo menos a oportunidade de detetar cedo quando as coisas ficam desnecessariamente complicadas.
Penso que estas comparações também podem melhorar a minha forma de programar.
Há várias maneiras de implementar uma funcionalidade. Por que escolheu este projeto esta solução? Uma abstração pode parecer elegante, mas, se cada alteração obriga a mexer em muitos sítios, estará a ajudar? Outro módulo é simples e resistiu a várias mudanças de requisitos sem perturbar o resto. O que fez bem?
São perguntas que merecem tempo. À medida que formo o meu próprio critério, torna-se mais fácil dizer à AI o que ajustar e compreender uma alternativa que proponha.
Claro que a minha compreensão da arquitetura também pode estar errada. As ligações num diagrama precisam de ser confirmadas no código. As funcionalidades que julgo intactas também exigem testes e execução real. Perante estados importantes ou limites complexos, posso aprofundar a implementação.
Quero chegar, aos poucos, a saber onde procurar na maioria das situações e também a reconhecer o que ainda não compreendo.
É uma das razões para desenvolver a análise de código do FiboCode.
Quero ligar código, documentos e diagramas de relações para encontrar a estrutura numa implementação desconhecida e acompanhar o percurso de uma funcionalidade. Com essa compreensão, sentir-me-ia mais tranquilo ao discutir alterações com a AI.
Em What Is Code?, Unmesh Joshi também explica como boas abstrações, tipos, testes e regras explícitas podem orientar o modelo a compreender e alterar código. Gosto desta direção: preservar no projeto, tanto quanto possível, a nossa compreensão do sistema, para que a alteração seguinte a possa aproveitar.
Ainda quero dedicar tempo a ler código
A AI escreve depressa. É tentador dar-lhe também a tarefa seguinte.
As funcionalidades ficam prontas umas atrás das outras e o projeto parece avançar. Mas também me pergunto o que compreendo melhor depois desse trabalho todo.
Se apenas fizer pedidos e aceitar resultados até o projeto se tornar demasiado complexo, posso acabar por gastar muito tempo em reparações ou a começar de novo. O tempo poupado pode desaparecer, juntamente com o custo das chamadas ao modelo e o esforço de voltar a conhecer toda uma implementação.
Prefiro passar parte desse tempo a ler código com calma.
Não tem de ser muito de uma vez. Posso começar por perceber por que um módulo foi dividido assim, por que uma funcionalidade precisa destes passos ou que técnica desconhecida a AI utilizou desta vez. Se não perceber, posso perguntar, procurar indícios no código e experimentar uma pequena alteração para verificar a minha interpretação.
Ao procurar material, encontrei uma experiência de aprendizagem da Anthropic. Depois de concluírem tarefas de programação desconhecidas com AI, os participantes tiveram uma classificação média inferior num teste imediato à do grupo que programou à mão. A experiência é pequena e não permite dizer como as competências evoluirão a longo prazo. Mas fez-me prestar mais atenção a saber se realmente aprendi alguma coisa depois de terminar uma tarefa.
Espero que essa compreensão se vá acumulando. Quando encontrar um desenho semelhante, talvez reconheça o que está a acontecer. Quando a AI propuser uma solução, poderei fazer uma pergunta fundamentada.
Ainda tenho de explorar se o FiboCode consegue ajudar bem nisso. Mas é uma grande parte da minha motivação para desenvolver a análise de código.
Quero continuar a escrever código com AI. E quero continuar a conseguir explicar como o projeto funciona quando tiver crescido.
Uma imagem simples desta ideia:
Outras leituras relacionadas
- O guia de revisão de código da Google, sobretudo a discussão do desenho geral e da complexidade, merece ser comparado com o próprio projeto.
- A discussão da Thoughtworks sobre dívida cognitiva no código descreve como a implementação e a compreensão da equipa se podem afastar.
- O artigo científico da experiência da Anthropic apresenta os métodos e resultados com mais pormenor.