Генератор или агент: что выгоднее генерировать по правилам
Часть кода пишется по правилам: DTO, миграции, клиенты API, типы. Агент сделает это каждый раз немного по-разному и за токены. Генератор сделает одинаково, бесплатно и мгновенно. Разберём, где граница.
После урока вы сможете
- объяснить, что такое детерминированный генератор и почему это важно;
- написать простой генератор «спецификация плюс шаблон равно код»;
- решать, что отдать генератору, что агенту, а что делать вместе;
- защитить сгенерированный код от ручных правок и проверить генерацию в CI.
Формочка для печенья и лепка руками
Сто одинаковых печений проще выдавить формочкой, чем лепить руками. Руками лепят торт на заказ: он один, и он особенный.
Главная мысль: то, что делается по правилам, делайте правилами. Агента оставьте там, где нужны понимание и решения.
От простого к сложному
Шаблон плюс данные равно код
Детерминированный (предсказуемый) значит: тот же вход даёт тот же выход, байт в байт. Модель из урока 1 так не умеет: выбор токенов случайный. Обычная функция умеет.
Генератор кода — это функция. На вход — описание (спецификация). Внутри — шаблон. На выход — текст программы.
const entity = { name: "Order", fields: [{ name: "id", type: "number" }, { name: "email", type: "string" }] };
const ts = `export interface ${entity.name} {
${entity.fields.map(f => ` ${f.name}: ${f.type};`).join("\n")}
}`;
// export interface Order {
// id: number;
// email: string;
// }
Где живут генераторы и где граница с агентом
Вы почти наверняка уже пользовались генераторами:
- OpenAPI (описание API) превращается в клиентов, DTO и заготовки контроллеров;
- JSON Schema превращается в типы TypeScript, Python, PHP;
- описание сущности превращается в Entity, миграцию, репозиторий (в Symfony это
make:entityиdoctrine:migrations:diff); - protobuf превращается в клиентов и серверы gRPC.
Генератору отдавайте: CRUD, DTO, контракты API, маппинги, типы, миграции. Всё, что повторяется и следует правилу.
Агенту оставляйте: нестандартную бизнес-логику, рефакторинг, разбор багов, склейку систем. Всё, где нужно понять смысл.
Гибрид работает лучше всего. Агент пишет или правит спецификацию (это короткий понятный файл). Генератор делает из неё код. Агент дописывает бизнес-логику в оставленные для неё места.
Мини-генератор SQL на четырёх языках — шаблонные строки
Один и тот же генератор: JSON с описанием сущности на входе, CREATE TABLE на выходе.
import { readFileSync, writeFileSync } from "node:fs";
const SQL = { int: "INTEGER", string: "VARCHAR", text: "TEXT", bool: "BOOLEAN", decimal: "NUMERIC(12, 2)", datetime: "TIMESTAMP" };
const snake = s => s.replace(/[A-Z]/g, c => "_" + c.toLowerCase());
function column(f) {
let type = SQL[f.type];
if (f.type === "string") type += `(${f.length ?? 255})`;
const parts = [snake(f.name), type];
if (f.primary) parts.push("PRIMARY KEY");
else if (!f.nullable) parts.push("NOT NULL");
if (f.unique) parts.push("UNIQUE");
return " " + parts.join(" ");
}
const e = JSON.parse(readFileSync("entities/order.json", "utf8"));
const sql = `-- GENERATED from entities/order.json. Do not edit.\nCREATE TABLE ${e.table} (\n${e.fields.map(column).join(",\n")}\n);\n`;
writeFileSync(`migrations/${e.table}.sql`, sql);
import { readFileSync, writeFileSync } from "node:fs";
type FieldType = "int" | "string" | "text" | "bool" | "decimal" | "datetime";
interface Field { name: string; type: FieldType; length?: number; nullable?: boolean; unique?: boolean; primary?: boolean }
interface Entity { name: string; table: string; fields: Field[] }
const SQL: Record<FieldType, string> = { int: "INTEGER", string: "VARCHAR", text: "TEXT", bool: "BOOLEAN", decimal: "NUMERIC(12, 2)", datetime: "TIMESTAMP" };
const snake = (s: string): string => s.replace(/[A-Z]/g, c => "_" + c.toLowerCase());
function column(f: Field): string {
const type = f.type === "string" ? `VARCHAR(${f.length ?? 255})` : SQL[f.type];
const parts = [snake(f.name), type];
if (f.primary) parts.push("PRIMARY KEY");
else if (!f.nullable) parts.push("NOT NULL");
if (f.unique) parts.push("UNIQUE");
return " " + parts.join(" ");
}
const e: Entity = JSON.parse(readFileSync("entities/order.json", "utf8"));
writeFileSync(`migrations/${e.table}.sql`, `CREATE TABLE ${e.table} (\n${e.fields.map(column).join(",\n")}\n);\n`);
<?php
// bin/generate-sql.php entities/order.json
$sqlTypes = ['int' => 'INTEGER', 'text' => 'TEXT', 'bool' => 'BOOLEAN', 'decimal' => 'NUMERIC(12, 2)', 'datetime' => 'TIMESTAMP'];
$snake = fn (string $s): string => strtolower(preg_replace('/[A-Z]/', '_$0', $s));
$e = json_decode(file_get_contents($argv[1]), true, flags: JSON_THROW_ON_ERROR);
$columns = [];
foreach ($e['fields'] as $f) {
$type = $f['type'] === 'string' ? sprintf('VARCHAR(%d)', $f['length'] ?? 255) : $sqlTypes[$f['type']];
$parts = [$snake($f['name']), $type];
if ($f['primary'] ?? false) {
$parts[] = 'PRIMARY KEY';
} elseif (!($f['nullable'] ?? false)) {
$parts[] = 'NOT NULL';
}
if ($f['unique'] ?? false) {
$parts[] = 'UNIQUE';
}
$columns[] = ' ' . implode(' ', $parts);
}
file_put_contents("migrations/{$e['table']}.sql", "CREATE TABLE {$e['table']} (\n" . implode(",\n", $columns) . "\n);\n");
import json
import re
import sys
from pathlib import Path
SQL = {"int": "INTEGER", "text": "TEXT", "bool": "BOOLEAN", "decimal": "NUMERIC(12, 2)", "datetime": "TIMESTAMP"}
def snake(s: str) -> str:
return re.sub(r"[A-Z]", lambda m: "_" + m.group().lower(), s)
def column(f: dict) -> str:
t = f"VARCHAR({f.get('length', 255)})" if f["type"] == "string" else SQL[f["type"]]
parts = [snake(f["name"]), t]
if f.get("primary"):
parts.append("PRIMARY KEY")
elif not f.get("nullable"):
parts.append("NOT NULL")
if f.get("unique"):
parts.append("UNIQUE")
return " " + " ".join(parts)
e = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
body = ",\n".join(column(f) for f in e["fields"])
Path(f"migrations/{e['table']}.sql").write_text(f"CREATE TABLE {e['table']} (\n{body}\n);\n", encoding="utf-8")
Готовый генератор по OpenAPI — пример команды
Писать свой генератор нужно не всегда. Для OpenAPI есть готовый openapi-generator с шаблонами под десятки языков. Ниже пример: фрагмент спецификации и команда генерации клиента.
openapi: 3.0.3
info: { title: Shopik API, version: 1.0.0 }
paths:
/orders/{id}:
get:
operationId: getOrder
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
responses:
"200":
description: Заказ
content:
application/json:
schema: { $ref: "#/components/schemas/Order" }
components:
schemas:
Order:
type: object
required: [id, email, totalKopecks]
properties:
id: { type: integer }
email: { type: string, format: email }
totalKopecks: { type: integer }
comment: { type: string, nullable: true }
# пример: TypeScript-клиент для фронтенда npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o src/api # пример: PHP-клиент для другого сервиса npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g php -o generated/shopik-client
Защита сгенерированного кода и экономика
Сгенерированный код не правят руками. Следующая генерация перезапишет правку, и она тихо пропадёт. Правят спецификацию или шаблон. Как это закрепить:
- в начале файла метка
GENERATEDи откуда он получен; - сгенерированные файлы лежат в отдельной папке (например,
generated/); - в правилах агента: «файлы с меткой GENERATED не правь, правь спецификацию и запусти генератор»;
- хук (урок 4) запрещает агенту запись в такие файлы;
- CI запускает генерацию и проверяет, что дифф пустой:
make generate && git diff --exit-code.
Где детерминированность ломается. Дата генерации в шапке файла, случайные id, порядок ключей словаря, разные версии генератора у разных разработчиков. Каждое из этого даёт дифф без изменений спецификации, и CI-проверка становится бесполезной. Версию генератора фиксируйте, дату не пишите, поля сортируйте стабильно.
Проверка генерации в CI — Makefile и GitHub Actions
# Makefile generate: node bin/generate-sql.mjs entities/*.json npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o src/api check-generated: generate git diff --exit-code -- migrations/ src/api/ || (echo "Сгенерированный код устарел: запустите make generate" && exit 1)
# .github/workflows/ci.yml (фрагмент) - name: Сгенерированный код совпадает со спецификацией run: make check-generated
Генератор в деле
Живой генератор: одна спецификация, четыре языка
интерактив 1 из 3Меняйте JSON слева. Код справа пересобирается сразу. Типы: int, string, text, bool, decimal, datetime. Флаги: primary, nullable, unique, length. Попробуйте добавить поле, сломать JSON или указать неизвестный тип.
Генератор, агент или вместе?
интерактив 2 из 3Для каждой задачи выберите исполнителя. После выбора появится объяснение.
Калькулятор: оцените свою задачу — четыре ползунка
Два прогона: агент и генератор
интерактив 3 из 3Одна и та же спецификация, два запуска подряд. Смотрим, совпадёт ли результат байт в байт.
Прогон 1
Прогон 2
Песочницы с автопроверкой
Здесь вы сами пишете генераторы. Тесты сравнивают результат со строкой байт в байт, как это делает CI.
Песочница 1. Генератор TypeScript-интерфейса
JSНапишите generateTsInterface(entity). Типы: int → number, decimal → string (деньги не храним во float), string и text → string, bool → boolean, datetime → string. Если nullable — добавить | null. Отступ два пробела, в конце перевод строки. Сущность-пример уже лежит в ORDER.
Песочница 2. Генератор CREATE TABLE
JSНапишите generateSqlTable(entity) и помощник toSnake(name). Правила:
- имя колонки — snake_case:
totalKopecks→total_kopecks; - типы:
int→INTEGER,string→VARCHAR(length)(по умолчанию 255),text→TEXT,bool→BOOLEAN,decimal→NUMERIC(12, 2),datetime→TIMESTAMP; primary→PRIMARY KEY(без NOT NULL); иначе неnullable→NOT NULL;unique→UNIQUEв конце;- колонки через
,\n, отступ два пробела, в конце);и перевод строки.
Песочница 3. Проверка детерминированности
JSЭто мини-версия CI-проверки. Напишите checkDeterminism(generator, input, runs = 3): запустить генератор runs раз и сравнить каждый результат с первым. Всё совпало — { ok: true }. Иначе — { ok: false, line: N }, где N — номер первой отличающейся строки (с единицы) в первом несовпавшем прогоне.
Проверьте себя
Что значит «детерминированный генератор»?
Поэтому результат можно проверять в CI сравнением: если дифф не пустой, значит спецификация и код разошлись.
Какую задачу выгоднее отдать генератору, а не агенту?
Повторяемо, по правилу, обновляется регулярно. Остальные две задачи требуют понимания: это работа агента.
Агент поправил руками файл с меткой GENERATED. Что произойдёт?
Поэтому в правилах агента пишут «не правь GENERATED», а хук может запретить такую запись.
Почему не стоит писать дату генерации в шапку файла?
Вы сами поймали это в песочнице 3. То же с случайными id и нестабильным порядком полей.
Задание на своём проекте — 45 минут
- Найдите в проекте код, который повторяется по шаблону хотя бы 5 раз: DTO, маппинги, типы ответов API, однотипные контроллеры.
- Опишите один такой случай как JSON или YAML: только данные, без логики.
- Напишите генератор на своём языке по образцу из объяснения. Проверьте, что он воспроизводит существующий файл байт в байт или с понятными отличиями.
- Добавьте в шапку метку GENERATED, а в правила агента строку «такие файлы не правь, правь спецификацию».
- Добавьте в CI шаг «генерация и
git diff --exit-code».
Отметьте, что усвоили
- Могу объяснить, что такое детерминированность и почему модель ею не обладает.
- Понимаю формулу «спецификация плюс шаблон равно код».
- Знаю готовые генераторы: OpenAPI, JSON Schema, make:entity, миграции.
- Различаю задачи для генератора, для агента и для гибрида.
- Знаю, как защитить сгенерированный код: метка, папка, правило агента, хук, CI.
- Знаю, что ломает детерминированность: даты, случайные id, порядок полей, версии.
- Решил песочницы: TypeScript-интерфейс, CREATE TABLE и проверку детерминированности.
Что почитать и где попробовать
Материалы на русском отмечены зелёной меткой. Все ссылки проверены 18 октября 2026 года.
Почитать
- Swagger (OpenAPI 3.0)подходы code-first и design-first, инструменты Swagger, включая Codegen. рус.
- База, которую нужно знать про JSON Schemaот простой схемы до anyOf/allOf/oneOf и валидации. рус.
- Кодогенерация DTO: зачем она нужна и как её настроитьопыт Яндекс Go: DTO из схемы вместо ручного кода. рус.
- Генерация кода для бекенда. Что генерировать, как и зачем?что выгодно генерировать на бэкенде и как устроить проект с генерацией. рус.
- OpenAPI Generatorофициальный генератор клиентов и серверов по OpenAPI, список генераторов (в т.ч. PHP). англ.
Песочницы по теме
- Swagger Editorнаписать OpenAPI-описание и сразу увидеть документацию; оттуда же можно сгенерировать код.
- quicktypeвставить JSON или JSON Schema и получить классы на PHP, TypeScript и др. — один вход, всегда один выход.
- JSON Schema Validatorпроверить схему и данные до запуска генератора.
Запустить код из урока
- TypeScript PlaygroundJS и TS из примеров урока: вставить и запустить кнопкой Run.
- 3v4l.orgPHP-примеры урока на любой версии PHP.
- JupyterLitePython-примеры урока прямо в браузере, без установки.