desenvolvimento de pares sem vibecoding / Habr

desenvolvimento de pares sem vibecoding / Habr

Preâmbulo

Eu tenho um projeto de estimação, NutriLog, uma demonstração da integração de um aplicativo Web e um bate-papo GPT personalizado. Parte deste projeto é um site SSR bilíngue (en, ru) de uma dúzia de páginas baseado em um mecanismo de modelo Bigode – puramente para SEO. Duas semanas atrás, voltei de férias e me perguntei se deveria automatizar a tradução de páginas de um idioma para outro. Fiz as traduções manualmente, através do ChatGPT Web UI – o modelo se mostrou perfeitamente. Traduzi apenas o conteúdo e o implementei corretamente no código do modelo. Além disso, decidi matar o segundo “Lebre” — para fazer um projeto usando apenas uma de suas bibliotecas “@teqfw/di“. Puxe para fora e mostre de forma explícita tudo o que “magia“, o que está oculto “Sob o capô” na minha estrutura pessoal TeqFW (Não se preocupe, este post não é sobre ele:). Assim, no futuro, foi delineado o desenvolvimento de um CMS de arquivo multilíngue simples com a automação de traduções via LLM do idioma principal para os suportados.

Eu consultei “Igor Ivanovich” (AI) e chegou à conclusão de que é melhor usar Nunjuckse como servidores web, expressar ou Fixar. Depois disso, lentamente, em um ritmo bastante calmo, com a ajuda do LLM, em duas semanas criei e implantei um site de demonstração na engine, que eu, sem muito fantasiar, chamei TeqCMS. Neste post, compartilho minhas descobertas deste sprint de duas semanas.

Divisão do trabalho: ideia e implementação

Não acho que o Modelo (LLM) seja criativo. Não tem objetivos ou intenções próprias. Ele começa a agir apenas em resposta a um impulso externo. Ao mesmo tempo, o Modelo não é apenas uma ferramenta. Ao contrário de uma chave de fenda ou de um compilador, é imprevisível, variável e capaz de oferecer soluções alternativas.

Percebo o Modelo como uma ferramenta e um parceiro. Em nossa interação, os papéis são separados: formulo metas e direções, o Modelo ajuda a explorar possíveis caminhos. Ela oferece opções, esclarece, mostra o que eu não percebi. As decisões finais são sempre minhas.

Mas o resultado final é mais profundo: eu não uso o Model apenas para obter resultados. Trabalhando com o Modelo, eu gradualmente Eu reformatei meu pensamento. Estou esclarecendo os termos. Estou revisando a estrutura. Eu repenso os limites da arquitetura. Não é apenas desenvolvimento de aplicativos, é também desenvolvimento de desenvolvedores. Autoprogramação com IA.

Texto como Língua de Comunicação com LLM

O próprio nome LLM (Large Language Model) sugere que a base da interação com o Modelo é a linguagem. Para que o Modelo me entenda, devo expressar minhas intenções, observações e dúvidas em forma textual. Mesmo que eu transmita uma imagem ou um som, o Modelo primeiro o interpreta como texto: ele reconhece, descreve e estrutura a percepção em uma representação linguística.

Quando se trata de trabalho de longo prazo em um projeto, o texto se torna não apenas um meio de comunicação, mas portador do contexto. Eu uso arquivos Markdown: eles são igualmente bem compreendidos tanto pela pessoa quanto pelo modelo. Nesses arquivos, registro metas, soluções arquitetônicas, rascunhos de código, layout de interface e tudo o que forma uma ideia do projeto.

No GPT Plus, uso o recurso de projetos, que é um mecanismo que me permite vincular diálogos a uma instrução comum (prompt do sistema) e a um conjunto de arquivos. A instrução do sistema define o estilo e a intenção, e os arquivos do projeto formam o Contexto estendidodisponível para o modelo em cada solicitação. Isso permite um desenvolvimento significativo e contínuo – não de solicitação para solicitação, mas do conceito à implementação.

O projeto no GPT reúne diferentes diálogos

Antes do código — o

Aqui está uma tabela de comparação de contexto máximo para modelos populares – ela foi compilada pelo chat GPT a meu pedido:

Modelo

Contexto (de tokens)

Login (Tokens)

Saída máxima (tokens)

GPT-4o (OpenAI)

128 000

~126 000

~ 4 000

Claude 3 Opus

200 000

~195 000

~ 4 000

Gêmeos 1.5 Pro

1 000 000+

~ 980 000+

~ 10 000

Claude 3 Soneto

200 000

~195 000

~ 4 000

Preste atenção a isso: O contexto de entrada dos modelos é uma ordem de magnitude maior do que o contexto de saída.
Mesmo que possamos alimentar o modelo com 100.000 tokens de código, ele só será capaz de gerar 4.000 — e isso cria restrições arquitetônicas no estágio de design.

Antes de confiar ao Modelo a geração de código, é necessário concordar com ele não apenas sobre as metas e objetivos de negócios, mas também sobre o Divisão do projeto estrutural — para que cada bloco lógico se encaixe em um volume conveniente para processamento. Na prática, isso significa: não mais do que 100 mil tokens por pacote.

Para referência, aqui estão os tamanhos atuais dos pacotes no meu projeto:

Esse estágio de desenvolvimento pode ser chamado de Programação da estrutura cognitiva: formulamos limites arquitetônicos, registramos os limites dos pacotes, sua finalidade e conexões. Na verdade, estamos criando um sistema de coordenadas no qual o Modelo pode agir de forma eficiente e previsível. É neste nível que são determinados o número de pacotes e sua finalidade, os limites de responsabilidade, ações permitidas e inaceitáveis, futuros pontos de expansão.

O resultado é um ou mais documentos Markdown, que são usados como parte do contexto ao gerar código, seja por meio de uma interface da Web ou de uma API. Esses documentos não são auxiliares, mas Artefatos arquitetonicamente significativosque garantem a reprodutibilidade, escalabilidade e consistência cognitiva do design. São esses documentos que se tornam candidatos a README.md para pacotes npm.

Desenvolvimento isolado

Em algum momento, fica claro que a estrutura arquitetônica do projeto é bastante estável – e você pode passar para a implementação. Começamos a preencher a estrutura com código, mantendo a capacidade de trabalhar com fragmentos do sistema independentemente do todo.

Injeção de dependência

É nesta fase que se torna extremamente importante usar a técnica Ligação tardia e Injeção de dependência (DI). Cada arquivo gerado pelo Modelo deve caber em tokens 4K, o que significa que o projeto é na verdade um gráfico de módulos es6 com conexões explícitas.

Em meus projetos, eu uso o @teqfw/di, em que as dependências entre os módulos são especificadas por meio de parâmetros de construtor. Exemplo:

export default class Fl32_Tmpl_Back_Service_Load {
    /**
     * @param {Fl32_Tmpl_Back_Logger} logger - Logger for exceptions
     * @param {Fl32_Tmpl_Back_Act_File_Find} actFind - Action to find files
     * @param {Fl32_Tmpl_Back_Act_File_Load} actLoad - Action to load files
     */
    constructor(
        {
            Fl32_Tmpl_Back_Logger$: logger,
            Fl32_Tmpl_Back_Act_File_Find$: actFind,
            Fl32_Tmpl_Back_Act_File_Load$: actLoad,
        }
    ) {}
}

Essa abordagem permite que o Model entenda exatamente em qual ambiente o código está sendo executado e quais dependências precisam ser consideradas. Desde que o número de modelos seja menor que 20-25, todo o conjunto pode ser apresentado ao modelo em um único contexto de entrada.

Interfaces como contratos

Quando os módulos de nível inferior são desenvolvidos, muitas implementações de dependência ainda não existem, mas suas interfaces já podem ser descritas. Isso permite que você continue gerando código mesmo sem ter uma compreensão completa dos detalhes.

Exemplo: Um pacote CMS usa o data para renderizar o modelo, mas o próprio modelo, o conjunto de variáveis e o mecanismo de modelagem (Nunjucks, Mustache, etc.) podem ser incluídos posteriormente. No entanto, definimos a interface Fl32_Cms_Back_Api_Adapter:

/**
 * @interface
 */
export default class Fl32_Cms_Back_Api_Adapter {
    /**
     * @param {object} args - Parameters object.
     * @param {import('node:http').IncomingMessage | import('node:http2').Http2ServerRequest} args.req - The HTTP(S) request object.
     * @returns {Promise} Rendering context for the template engine.
     * @throws {Error} If the method is not implemented by the application.
     */
    async getRenderData({req}) {
        throw new Error('Method not implemented');
    }
}

Essa interface já pode ser usada no módulo, e a implementação será injetada no tempo de execução por um contêiner de dependência.

Teste de unidade e simulações

Como o trabalho é feito no nível de módulos isolados, é necessário verificar cada parte sem depender do restante do projeto. A DI torna isso possível — o ambiente pode ser substituído por objetos fictícios, incluindo módulos Node.js nativos.

Exemplo: Dependência de node:fs No código de combate:

export default class Fl32_Tmpl_Back_Act_File_Find {
    /**
     * @param {typeof import('node:fs')} fs
     */
    constructor(
        {
            'node:fs': fs,
        }
    ) {
      const {existsSync} = fs;
      ...
      if (plain.startsWith(root) && existsSync(plain)) {...}
    }
}

E aqui está a aparência da simulação no teste de unidade:

const checkedPaths = (
    '/abs/app/root/tmpl/web/en-US/welcome.html',
);
container.register('node:fs', {
    existsSync: (p) => checkedPaths.includes(p),
});

Assim, o Modelo pode criar e testar código de forma autônoma, arquivo por arquivo – sem conhecimento de todas as implementações, sem integração em um aplicativo completo. Isso é crítico no início do desenvolvimento, quando a maior parte do sistema ainda está faltando.

Os testes de unidade desempenham um papel especial aqui: eles se tornam uma interface de feedback entre a Pessoa e o Modelo. Eles permitem que o Humano se certifique de que o Modelo interpretou corretamente a tarefa e que o código gerado executa as ações necessárias. À medida que o sistema cresce, a importância dos testes unitários diminui, dando lugar aos testes de integração — mas, nos estágios iniciais, eles são indispensáveis.

Código como portador de significado

Ao gerar códigos-fonte, atenção especial deve ser dada à sua documentação. Eu uso anotações JSDoc não apenas para ajudar os IDEs a navegar melhor no estrutura do projeto e ajudar uma pessoa a entender o código, mas acima de tudo para capturar o Contexto cognitivoem que este código foi gerado pelo Modelo.

Essa abordagem torna possível a refatoração: o modelo (ou outro desenvolvedor) pode mapear a documentação para o novo contexto do projeto para determinar o quanto a implementação atual corresponde às condições alteradas. O JSDoc está se tornando uma âncora das expectativas arquitetônicas.

Exemplo — Interface do adaptador CMS:

/**
 * Application adapter interface for the CMS plugin.
 *
 * This adapter connects the plugin to the application-specific logic.
 * It allows the application to analyze the incoming HTTP request and
 * return the data and rendering options required to process the page
 * using the selected template engine.
 *
 * The plugin interacts with this interface only, without knowledge of the implementation.
 *
 * @interface
 */
export default class Fl32_Cms_Back_Api_Adapter {
    /* eslint-disable no-unused-vars */
    /**
     * Analyze the incoming request and provide data and rendering options for the template engine.
     *
     * This method is called on every HTTP request handled by the CMS plugin.
     * The application must extract context-specific information (e.g., locale, route data, user agent)
     * and prepare a structured result that will be passed to the template renderer.
     *
     * @param {object} args - Parameters object.
     * @param {import('node:http').IncomingMessage | import('node:http2').Http2ServerRequest} args.req - The HTTP(S) request object.
     * @returns {Promise} Rendering context for the template engine.
     * @throws {Error} If the method is not implemented by the application.
     */
    async getRenderData({req}) {
        throw new Error('Method not implemented');
    }
}

/**
 * @typedef {object} Fl32_Cms_Back_Api_Adapter.RenderData
 * @property {object} data - Variables used in the template (e.g., page metadata, content blocks, user info).
 * @property {object} options - Template engine options (e.g., layout, partials, flags).
 * @property {Fl32_Tmpl_Back_Dto_Target.Dto} target - Render target metadata including template path, type, and localization context.
 */

Ao documentar, é importante lembrar que esse código será analisado no futuro, em um contexto alterado, talvez até por um modelo diferente. O JSDoc deve conter informações suficientes para que o Modelo possa não apenas refatorar o código com segurança, mas também tomar uma decisão arquitetônica: salvar, reescrever ou isolar o fragmento de acordo com as condições alteradas.

O código documentado torna-se não apenas um meio de execução, mas também o portador da ideia – uma ponte entre o estado passado e futuro do projeto.

Conclusão

Com base em minha própria experiência, posso dizer com confiança: o uso do LLM no desenvolvimento é justificado e realmente eficaz. Em duas semanas, usando o ChatGPT Plus e a API DeepSeek, criei um CMS de arquivo multilíngue com automação de tradução. Este projeto está funcionando agora – https://cms.teqfw.com, e seu código é aberto: https://github.com/flancer32/teq-cms-demo

LLM-first não é sobre jogos com prompts. Trata-se de estrutura, arquitetura e coordenação de posições entre o Homem e o Modelo. Estou confiante de que serei capaz de gerenciar a refatoração do meu CMS – e que o Modelo estará no processo do meu lado, não contra mim.

A codificação de vibração pode levar ao mesmo resultado?
Se sim, mostre-me esse resultado.

Falar é fácil. Mostre-me o código.
— Linus Torvalds

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *