Преамбула
Есть у меня один пет-проект, 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-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<Fl32_Cms_Back_Api_Adapter.RenderData>} 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<Fl32_Cms_Back_Api_Adapter.RenderData>} 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 в разработке обосновано и действительно эффективно. За две недели, используя ChatGPT Plus и DeepSeek API, я создал мультиязычную файловую CMS с автоматизацией переводов. Этот проект работает прямо сейчас — https://cms.teqfw.com, а его код открыт: https://github.com/flancer32/teq-cms-demo
LLM-first — это не про игры с промптами. Это про структуру, архитектуру и согласование позиций между Человеком и Моделью. Я уверен, что смогу управлять рефакторингом своей CMS — и что Модель будет в этом процессе на моей стороне, а не против меня.
Может ли вайбкодинг привести к такому же результату?
Если да — покажите мне этот результат.
Talk is cheap. Show me the code.
— Linus Torvalds
supercat1337
Спасибо за статью. Интересно! А что если скармливать ИИ не сам js-код, а d.ts файлы? Мне кажется, что количество токенов можно уменьшить в разы. d.ts файлы они и код описывают, и комментарии содержат. Например, DeepSeek выдавал вполне сносные выводы по скормленным d.ts файлам.
flancer Автор
Я не использую TypeScript в разработке, я заменяю его JSDoc-аннотациями. Да, вы правы, что количество токенов можно уменьшить, если использовать описание интерфейса вместо его реализации. При достаточно большом объёме проекта это может быть отличной опцией - отделять описания интерфейсов. Можно даже использовать TS-нотацию в них.
supercat1337
Так я тоже пишу на JS с комментариями на jsdoc. Только в дополнение с помощью typescript я генерирую d.ts.