EE-VibeCoding

Преамбула

Преамбула

Есть у меня один пет-проект, NutriLog, демонстрация интеграции веб-приложения и кастомного GPT-чата. Частью этого проекта является двуязычный (en, ru) SSR-сайтик на десяток страниц на базе шаблонизатора Mustache - чисто для SEO. Две недели назад я вернулся из отпуска и подумал, а не автоматизировать ли мне переводы страничек с одного языка на другой? Переводы я делал вручную, через ChatGPT Web UI - модель показывала себя прекрасно. Переводила только контент и правильно внедряла его в код шаблона. Плюс я решил убить и второго "зайца" — сделать проект с использованием лишь одной своей библиотеки "@teqfw/di". Вытащить наружу и показать в явном виде всё то "волшебство", что спрятано "под капотом" в моём персональном фреймворке TeqFW (не волнуйтесь, этот пост не о нём :). Таким образом, в перспективе нарисовалась разработка простой мультиязычной файловой CMS с автоматизацией переводов через LLM с основного языка на поддерживаемые.

Посоветовался с "Игорь Ивановичем" (ИИ) и пришёл к выводу, что в качестве шаблонизатора лучше использовать Nunjucks, а в качестве веб-серверов — express или fastify. После чего, не спеша, в довольно спокойном темпе, при помощи LLM за две недели создал и развернул демонстрационный сайт на движке, который я, не особо фантазируя, назвал TeqCMS. В этом посте я делюсь своими выводами, сделанными по результатам этого двухнедельного спринта.

Разделение труда: идея и реализация

Я не считаю, что Модель (LLM) способна к творчеству. У неё нет собственных целей или намерений. Она начинает действовать только в ответ на внешнее побуждение. При этом Модель — не просто инструмент. В отличие от отвертки или компилятора, она непредсказуема, вариативна и способна предлагать альтернативные решения.

Я воспринимаю Модель одновременно и как инструмент, и как партнёра. В нашем взаимодействии роли разделены: я формулирую цели и направления, Модель помогает исследовать возможные пути. Она предлагает варианты, уточняет, показывает, чего я не заметил. Финальные решения — всегда за мной.

Но суть глубже: я не просто использую Модель, чтобы получить результат. Работая с Моделью, я постепенно переформатирую своё мышление. Уточняю термины. Пересматриваю структуру. Переосмысляю границы архитектуры. Это не только разработка приложения — это и разработка разработчика. Самопрограммирование при помощи ИИ.

Текст как язык общения с LLM

Само название LLM (Large Language Model) говорит о том, что основой взаимодействия с Моделью является язык. Чтобы Модель меня поняла, я должен выразить свои намерения, наблюдения и сомнения в текстовой форме. Даже если я передаю изображение или звук, Модель сначала интерпретирует их как текст: она распознаёт, описывает и структурирует восприятие в языковом представлении.

Когда речь идёт о долговременной работе над проектом, текст становится не просто средством общения, а носителем контекста. Я использую Markdown-файлы: они одинаково хорошо воспринимаются и человеком, и моделью. В этих файлах я фиксирую цели, архитектурные решения, черновики кода, разметку интерфейсов и всё, что формирует представление о проекте.

В GPT Plus я пользуюсь функцией проектов — это механизм, который позволяет связать диалоги общей инструкцией (системным промптом) и набором файлов. Системная инструкция задаёт стиль и намерение, а файлы проекта формируют расширенный контекст, доступный модели в каждом запросе. Это позволяет вести осмысленную и непрерывную разработку — не от запроса к запросу, а от замысла к реализации.

Проект в GPT объединяет различные диалоги

Перед кодом — диалог

Вот сравнительная таблица максимального контекста для популярных моделей — она была составлена GPT-чатом по моей просьбе:

Модель

Макс. контекст (токенов)

Макс. вход (токенов)

Макс. выход (токенов)

GPT-4o (OpenAI)

128 000

~126 000

~4 000

Claude 3 Opus

200 000

~195 000

~4 000

Gemini 1.5 Pro

1 000 000+

~980 000+

~10 000

Claude 3 Sonnet

200 000

~195 000

~4 000

Обратите внимание на вот это: входной контекст у моделей на порядок больше выходного.

Даже если мы можем подать в модель 100 000 токенов кода, она сможет сгенерировать лишь 4 000 — и это создаёт архитектурные ограничения на этапе проектирования.

Прежде чем поручать Модели генерацию кода, необходимо согласовать с ней не только цели и бизнес-задачи, но и структурное разбиение проекта — так, чтобы каждый логический блок укладывался в объём, удобный для обработки. Практически это означает: не более 100K токенов на один пакет.

Для ориентира — текущие размеры пакетов в моём проекте:

  • @flancer32/teq-web: ~9.5K tokens
  • @flancer32/teq-tmpl: ~8.3K tokens
  • @flancer32/teq-cms: ~10.2K tokens

Этот этап разработки можно назвать программированием когнитивной рамки: мы формулируем архитектурные границы, фиксируем границы пакетов, их назначение и связи. По сути, мы создаём систему координат, в которой Модель сможет действовать эффективно и предсказуемо. Именно на этом уровне определяются количество пакетов и их назначение, границы ответственности, допустимые и недопустимые действия, будущие точки расширения.

Результатом становятся один или несколько Markdown-документов, которые затем используются как часть контекста при генерации кода — через Web-интерфейс или API. Эти документы — не вспомогательные, а архитектурно-значимые артефакты, обеспечивающие воспроизводимость, масштабируемость и когнитивную согласованность проекта. Именно эти документы становятся кандидатами в README.md для npm-пакетов.

Разработка в изоляции

В какой-то момент становится понятно, что архитектурный каркас проекта достаточно стабилен — и можно переходить к реализации. Мы начинаем наполнять структуру кодом, при этом сохраняя возможность работы с фрагментами системы независимо от целого.

Внедрение зависимостей

Именно на этом этапе становится критически важным использовать технику позднего связывания и внедрения зависимостей (DI). Каждый файл, генерируемый Моделью, должен укладываться в 4K токенов, что означает: проект фактически представляет собой граф es6-модулей с явными связями.

В своих проектах я использую библиотеку @teqfw/di, в которой зависимости между модулями указываются через параметры конструктора. Пример:

`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,

}

) {}

}

`Такой подход позволяет Модели точно понимать, в каком окружении работает код, и какие зависимости необходимо учитывать. Пока их количество не превышает 20–25, весь набор можно подать модели в одном входном контексте.

Интерфейсы как контракты

Когда разрабатываются модули нижнего уровня, реализации многих зависимостей ещё не существует — но их интерфейсы уже можно описать. Это позволяет продолжать генерацию кода, даже не имея полного представления о деталях.

Пример: CMS-пакет использует объект data для рендеринга шаблона, но сам шаблон, набор переменных и шаблонизатор ( Nunjucks, Mustache и т. д.) могут подключаться позже. Тем не менее, мы определяем интерфейс 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');

}

}

`Этот интерфейс уже можно использовать в модуле, а реализация будет внедрена в runtime контейнером зависимостей.

Юнит-тестирование и моки

Поскольку работа идёт на уровне изолированных модулей, необходимо проверять каждую часть без зависимости от остального проекта. DI делает это возможным — окружение можно замещать мок-объектами, включая нативные Node.js-модули.

Пример: зависимость от node:fs в боевом коде:

`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)) {...}

}

}

`А вот как выглядит мок в юнит-тесте:

`const checkedPaths = [

'/abs/app/root/tmpl/web/en-US/welcome.html',

];

container.register('node:fs', {

existsSync: (p) => checkedPaths.includes(p),

});

`Таким образом, Модель может создавать и тестировать код автономно, файл за файлом — без знания всех реализаций, без интеграции в полноценное приложение. Это критически важно в начале разработки, когда большая часть системы ещё отсутствует.

Юнит-тесты здесь играют особую роль: они становятся интерфейсом обратной связи между Человеком и Моделью. Они позволяют Человеку убедиться, что Модель правильно интерпретировала задачу, а сгенерированный код выполняет нужные действия. По мере роста системы важность юнит-тестов снижается, уступая место интеграционным — но на ранних этапах они незаменимы.

Код как носитель смысла

При генерации исходников особое внимание стоит уделить их документированию. Я использую JSDoc-аннотации не только для того, чтобы IDE лучше ориентировались в структуре проекта и помогали человеку понимать код, но прежде всего — для того, чтобы зафиксировать когнитивный контекст, в котором этот код был сгенерирован Моделью.

Такой подход делает возможным рефакторинг: Модель (или другой разработчик) может, сопоставив документацию с новым контекстом проекта, определить, насколько текущая реализация соответствует изменившимся условиям. JSDoc становится якорем архитектурных ожиданий.

Пример — интерфейс 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.

*/

`При документировании важно помнить, что этот код будет анализироваться в будущем — в изменившемся контексте, возможно даже другой Моделью. JSDoc должен содержать достаточно информации, чтобы Модель могла не только безопасно отрефакторить код, но и принять архитектурное решение: сохранить, переписать или изолировать фрагмент в соответствии с изменившимися условиями.

Документированный код становится не только средством исполнения, но и носителем замысла — мостом между прошлым и будущим состоянием проекта.

Заключение

LLM-first — это не про игры с промптами. Это про структуру, архитектуру и согласование позиций между Человеком и Моделью. Я уверен, что смогу управлять рефакторингом своей CMS — и что Модель будет в этом процессе на моей стороне, а не против меня.

Может ли вайбкодинг привести к такому же результату?

Если да — покажите мне этот результат.

Talk is cheap. Show me the code.— Linus Torvalds