Привет, Хабр! Про модели искусственного интеллекта сейчас не говорит только ленивый. Высказывается множество мнений и нередко они оказываются на противоположных полюсах: от полного скепсиса до убеждённости, что произошла новая научно-техническая революция. Жизненный опыт подсказывает, что истина где-то по-середине и инструмент будет полезным ровно настолько, насколько ты умеешь им пользоваться. В относительно недавнем интервью генеральный директор Microsoft Сатья Наделла заявил, что примерно 20-30 % кода в Microsoft уже сейчас генерируется ИИ и разработчикам надо будет адаптироваться. Мне тоже захотелось попробовать внедрить такого помощника в свои рабочие процессы и посмотреть, что из этого получится.
Несколько слов о себе. В IT я с середины нулевых, занимаюсь web-разработкой. Был опыт управления небольшими командами, но потом я всё равно вернулся к разработке. В свободное от работы время занимаюсь детским образовательным проектом chikipooki.com, о котором писал ранее.

Одна голова хорошо, а две лучше
Прежде чем переходить к экспериментам, я задумался о форме взаимодействия. Если принять во внимание точку зрения руководителя Microsoft о том, что роль инженера будет смещаться к контролю и проектированию, то логичным, на мой взгляд, первым шагом стала парная разработка. Честно говоря, у меня никогда не было подобного опыта в классическом определении данного понятия. Методика представлялась слишком идеализированной, подходящей разве что для проекта из палаты мер и весов, с бесконечными ресурсами и идеально подобранной командой.
В теории можно попытаться свести вместе двух людей с одинаковым темпом, уровнем вовлечённости и ответственности. Но закладывать под это двойной бюджет всегда казалось сомнительной затеей: слишком высокая цена ради практики, эффективность которой видна лишь на больших и долгоживущих проектах.
И даже цитата Мартина Фаулера о пользе такого подхода мне не отзывалась:
Многие поначалу реагируют на парное программирование, считая, что работать вдвоём – это расточительство. Но это расточительство только в том случае, если самая сложная часть программирования – набор текста. На самом деле, программирование – это непрерывный процесс понимания того, как работает код, и принятия решений о том, как его лучше всего изменить. Два человека, работающие вместе, часто могут добиться большего прогресса в решении такой задачи, чем по отдельности. Я определённо заметил, что гораздо быстрее проникаю в суть, имея под рукой второй мозг, и гораздо реже погружаюсь в аналитические дебри, на которые можно легко потратить часы.
В начале августа OpenAI выкатила Chat GPT-5 (далее по тексту модель) и я решил, что пришло время для экспериментов. Почему второй мозг, о котором говорил Фаулер, не может быть моделью искусственного интеллекта? Модель не устанет, не потеряет контекст, не будет спорить (если сам не попросишь) и безошибочно вспомнит детали проекта в нужный момент. Идеальный напарник.
Почему именно такой формат? Мне кажется, что если появился настолько мощный инструмент, то это отличный шанс углубить знания и улучшить навыки программирования. Поэтому, начиная эксперимент, я хотел ответить на несколько вопросов:
Можно ли получить действительно рабочий продукт? Что-то среднее между телеграм-ботом и очередной социальной сетью.
Можно ли попутно разобраться с чем-то новым? И насколько это новое будет «новым»?
Сколько свободы можно дать модели и как часто мне надо будет вмешиваться?
Пострадают ли мои когнитивные навыки и скажется ли это на качестве кода, написанного без помощи модели?
Постановка задачи
Почти десять лет назад мне в руки попала книга камрада @Milfgard о граблях российского бизнеса и неожиданных решениях, где на первой странице было написано: если есть возможность писать – пиши.

Я и писал. За это время накопилось немало заметок, черновиков, набросков и прочих полезных вещей, которые хранятся в файловой системе.
Поиск по файлам работает неплохо, но этого объективно мало. Поэтому в качестве экспериментального проекта я решил сделать мини-дневник, который поможет навести порядок в заметках и превратить их в удобный инструмент для работы и размышлений.
Мне не хотелось писать «в стол», поэтому проект должен приносить реальную пользу, как минимум мне. А чтобы извлечь из него максимум, я решил строить его в стиле Чистой архитектуры и TDD. Приведу краткие определения этих понятий (в моём понимании):
TDD – это подход к разработке, при котором программист сначала пишет тест, описывающий ожидаемое поведение системы ещё до реализации, а потом – минимальный код, чтобы тест прошёл. Т.е. TDD помогает писать только необходимый и тестируемый код. Классический цикл выглядит следующим образом:
Тест → Код → Рефакторинг.Чистая архитектура – это подход, в котором система строится слоями вокруг домена. В центре находится бизнес-логика, а все остальные слои зависят от неё, но не наоборот. Обычно выделяют домен, слой приложения, инфраструктуру и представление. При этом зависимости направлены внутрь, к домену.
Первое знакомство
Обсудив с моделью идею проекта, обозначив свои ожидания и попросив её следовать лучшим практикам, мы приступили к созданию документации: с правилами, требованиями, соглашениями и т.д. Когда дело дошло до архитектуры приложения, меня ждал первый сюрприз. Я рассчитывал увидеть классическую линейную схему зависимостей: Представление → Инфраструктура → Приложение → Домен. Но модель предложила концентрический вариант: Представление → Приложение → Домен ← Инфраструктура. Ну что ж, тем интереснее будет идти разработка.
Второй сюрприз возник, когда мы обсуждали метрики качества кода. Модель сразу предложила подключить PHPStan, но ничего не сказала про PHPCS или PHPMD, хотя эти инструменты обычно рассматриваются вместе. Это показалось необычным и даже любопытным. Забегая вперёд скажу, что обе утилиты я подключил в самом конце, чтобы убедиться насколько «чистым» оказался код, написанный нашим тандемом.
Далее в предметную область была введена доменная сущность, описывающую одну заметку в системе: Entry(id, title, body, date, createdAt, updatedAt). Мы договорились реализовать стандартные CRUD-операции для этой сущности и считать их успешную реализацию критерием завершения первого этапа работы.
Рабочий процесс
Работа над проектом проходила в формате регулярных живых бесед. Я старался выделять на это несколько часов каждый день, фиксируя время в блокнотике, чтобы ничего не пропустить. Общение с моделью не было похоже на классический чат, скорее на длительное техническое повествование. Было много обсуждений и уточнений, делались паузы на размышления поэтому вперёд мы двгались маленькими шагами.
Чтобы сохранить порядок и не потерять контекст, для каждого нового вопроса я создавал отдельный чат. Все они нумеровались по простому паттерну: [DL] yyyy.mm.dd – Day N. Как выяснилось позже, это решение оказалось удивительно практичным: скачав архив переписки, можно было легко «грепнуть» нужные чаты и быстро восстановить логику принятия решений.
Старт был стремительным. Кода появлялось так много, что рабочий процесс практически сразу превратился в непрерывный code review. Первые дни по вечерам даже чувствовалась усталость, но не из-за сложности задач, а из-за постоянного переключения внимания между диффами, уточнениями и правками.
При этом чёткого понимания структуры, зависимостей и связей между компонентами ещё не было: модель предлагала решения быстро, а я не всегда успевал осмыслить, как они вписываются в систему. Ирония в том, что поначалу проект казался тривиальным: простая сущность Entry, CRUD-операции – два притопа, три прихлопа. Но на практике удержать целостность системы оказалось куда труднее, чем я ожидал. И это стало третьим сюрпризом.
Наладить процесс помогли короткие комментарии после каждой рабочей сессии. Эта проблема, кстати, хорошо видна в листе учёта времени.

Здесь, кстати, проявились и два любопытных побочных эффекта:
Во-первых, как только цикл TDD стал более-менее устойчивым, каждые десять минут захотелось коммитить. Постоянный диалог с моделью дробил работу на очень мелкие шаги и многократное повторение шаблона
изменение → коммит → вознаграждениесформировало поведенческую петлю. Потом, правда, когда характер работы изменился, это «неприятное» чувство пропало.Во-вторых, заметно обострилось ощущение времени. Работая небольшими отрезками и фиксируя затраты в рабочем листе я всё время видел, сколько уже было сделано за конкретный промежуток. В результате внутренний таймер начал подстраиваться под внешний и заводя будильник на 45 минут, я смотрел на часы за несколько минут до сигнала.
Если говорить о количественных метриках, то эксперимент продолжался 38 дней и на него ушло немногим больше 120 часов.
На графике ниже показано распределение коммитов по дням. Хорошо видно, что работа шла волнами: периоды спокойных доработок сменялись днями интенсивной разработки, когда изменениям подвергались крупные куски системы. За это время на бэкенде мы выделили и реализовали пять основных сценариев пользовательского взаимодействия с дневником (создание, список, просмотр, обновление и удаление записи), привели архитектуру к слоистому виду и унифицировали валидацию и формат API-ответов. Написали юнит-, интеграционные и функциональные тесты.

Такая картина меня неприятно удивила. Почему вообще пришлось несколько раз вовращаться к одним и тем же моментам и почему нельзя было написать нормально с первого? Почему модель не подстраховала и не предложила идеальные абстракции когда разработчик не продумал (в данном случае осознанно!) архитектуру заранее? Почему границы ответственности компонентов, зависимости и интерфейсы менялись по мере написания тестов и сценариев?
Для себя я выделил два фактора:
Ограниченное контекстное окно модели не позволяло ей удерживать долгосрочные планы и ранее принятые договорённости. Приходилось постоянно проверять, сопоставлять и корректировать ответы.
В какой-то момент возникло ощущение, что «ещё немного и мы сделаем идеальный проект», и поэтому стоит применять только самые лучшие решения. Это спорный подход: подобные попытки усложняли код, удлиняли разработку и порождали технический долг.
На графике хорошо видно: периоды крупных пиков соответствуют моментам, когда приходилось переносить компоненты между слоями, заменять абстракции, упрощать модули или убирать прежние решения, которые перестали быть актуальными.

Обратная сторона сотрудничества
Эксперимент быстро вскрыл ряд неприятных моментов.
Во-первых, код получался разнородным. Модель постоянно переключалась между разными форматами тестовых данных, стилем комментариев, наименованием переменных: $entry, $entryData, $testEntryData, $seed, $canary. Ни разу она, кстати, не предложила вынести повторяющиеся участки кода в отдельную функцию. В итоге проект выглядел так, будто над ним работали разные люди, не знакомые друг с другом. Довольно много времени уходило на унификацию и увлекательный рефакторинг тестов.
Во-вторых, несмотря на уговоры следовать лучшим практикам, PHPStan стабильно находил ошибки даже не в самых сложных вещах: типовая строгость, nullable, несовпадение сигнатур. Что-то я вычищал вручную, но многое добавилось в технический долг.
В-третьих, хотя мы и договорились использовать архитектурные слои, сценарии, валидаторы, доменные сервисы и репозитории модель регулярно об этом «забывала» и предлагала положить интерфейс в другой слой, неправильно внедрить зависимость или же реализовать «Порт» и «Адаптер», которых у нас никогда не было.
Все эпизоды, когда модель уходила в сторону, предлагала несогласованные решения или просто теряла контекст, я помечал стоп-словом, называя всё происходящее ерундой. Это позволило собрать небольшую статистику. Ниже на графике распределение таких случаев по дням.

Также стоит отметить, что если в запросе появлялась эмоциональная окраска, такая как: капслок, восклицательные знаки, саркастические формулировки, то качество ответов заметно ухудшалось. Модель стремилась к деэскалации и тратила силы на это, а не на решение задачи.
Итоги эксперимента
Эксперимент длился 38 дней и был посвящён не только разработке, но и проверке рабочей гипотезы: сможет ли модель выступить в роли полноценного технического напарника и насколько это повлияет на процесс принятия решений и темп работы.
Ниже мои комментарии по каждому из вопросов:
Можно ли получить рабочий продукт, используя модель как напарника?
Да. При условии, что ты ясно понимаешь, чего хочешь добиться, умеешь формулировать задачи и двигаешься небольшими шагами. В этом режиме модель делала разработку быстрее. В ходе эксперимента я несколько раз поймал себя на мысле, что надо чётче формулировать задания. Без внятного ТЗ, результат все мы знаем какой.
Можно ли попутно разобраться с чем-то новым?
Сильно ограниченно. Модель располагает огромным арсеналом аргументов, уверенно ими оперирует и довольно жёстко отстаивает свою позицию. Если нет собственного фундамента в предметной области, спорить попросту не о чем. Один из примеров: обсуждая структуру тестов для очередного сценария, модель настаивает, что RequestDTO обязательно надо покрыть тестами, а в другом случае, что тестировать не нужно. Если слепо следовать этим рекомендациям, в одном месте мы напишем тесты для геттеров, а в другом можем пропустить логику или инварианты. Оба подхода возможны, но если в проекте выработан свой стиль, то ему и надо следовать. Коллеги, а какого варианта придерживаетесь вы?
Сколько свободы можно предоставить модели в процессе разработки?
Немного. Даже имея под рукой проектную документацию, правила и договорённости у модели было своё видение: местами полезное, местами неожиданное, местами не совпадающее с моим. Ни о каком карт-бланше не может быть и речи.
Смогу ли я дальше писать код без участия модели?
Да, безусловно. Рутинный код и раньше все искали на StackOverflow, копировали и переиспользовали, а проектные решения надо было придумывать самостоятельно. В этом плане модель для меня не открыла ничего нового, просто сделала процесс быстрее и удобнее. Перед началом эксперимента я вспоминал старый анекдот про доктора Ватсона, который хотел отучить Холмса курить трубку весьма… экзотическим способом,
и немного опасался, что ситуация повторится: Холмс курить так и не бросит, а вот Ватсон без трубочки уже не сможет. Но практика показала, что сможет. Возможно, до выхода следующей версии модели.
Планы
Следующим шагом в развитии проекта станет расширение предметной области за счёт новых сущностей: Теги, Люди, Места и Вложения. Каждая из них добавляет небольшой объём логики, но по своей структуре они будут похожи друг на друга, как воробьи. Поэтому ожидаю, что этот этап займёт заметно меньше времени, чем работа над первой сущностью.
Коллеги, поделитесь статистикой: сколько у вас уходит времени на реализацию одной такой сущности вместе с покрытием её тестами?
Ретроспектива
Формально это нельзя назвать классической парной разработкой, хотя бы потому, что мы не могли поменяться ролями. Но по сути работа шла именно в паре: мы вместе проектировали, обсуждали решения и двигались вперёд небольшими шагами. Но глаз замыливается и пару раз я прозевал ну уж слишком откровенные ляпы. Здесь мне не хватило помощи человека, который бы делал ревью уже моих изменений свежим взглядом.
Следовать TDD не получилось. Тесты появлялись либо параллельно, либо уже после того, как становилось понятно, что именно нужно проверять. Я думаю, что в большей степени это произошло из-за поиска самых лучших решений и огромного потока кода, выдаваемого моделью. Моё контекстное окно с этим не справлялось. В итоге ритм получился TDD-подобным: частые коммиты, регулярный рефакторинг, небольшие шаги, но без строгого test-first.
Образцово-показательного проекта тоже не вышло. Линтеры находят множество замечаний, а в ряде мест код выглядит откровенно небрежно. Например, ошибки формируются через захардкоженные строковые сообщения. Из-за этого те же строки приходится дублировать в тестах, что приводит к хрупким и плохо поддерживаемым контрактам.

И если подобные недочёты можно сравнительно быстро поправить, хотя в самом начале я просил модель следовать лучшим практикам и соблюдать стандарты, то логические несоответствия выявить сложнее и они обязательно сыграют злую шутку. Например, структурной единицей на уровне приложения был Сценарий. Он отвечал за оркестрацию всего процесса: вызов валидатора, сервисов, репозитория, а также за корректную обработку непредвиденных ситуаций. По идее, его тесты должны были проверять остановку сценария и отсутствие побочных эффектов, если где-то внутри произойдёт ошибка. Валидатор же следовало тестировать на полном наборе недопустимых значений и ожидаемых для них доменных кодов ошибок.
Однако модель поступила иначе и перенесла матрицу проверок валидатора на уровень Сценария. Для каждого доменного кода ошибки появился отдельный тест, хотя ответственность Сценария – это корректная реакция на сам факт сбоя. При этом для самого валидатора был создан один обобщённый тест, который проверяет только базовый набор правил. По большому счёту валидатор протестирован, но протестирован не там: вместо того, чтобы полноценно проверить правила именно в его собственных юнит-тестах, все проверки переехали в тесты сценария использования, где им не место.
Очень скромный тест валидатора:
final class AddEntryValidatorTest extends Unit
{
use EntryFieldsDomainDataProvider;
/**
* Validation rules: invalid business input yields DomainValidationException
* with the correct error code.
*
* @dataProvider provideInvalidDomainAddEntryCases
*
* @param array<string,string> $overrides
* @param string $expectedCode
* @return void
*/
public function testValidateThrowsOnDomainViolations(array $overrides, string $expectedCode): void
{
// Arrange
$data = EntryTestData::getOne();
$data = array_merge($data, $overrides);
$request = AddEntryRequest::fromArray($data);
// Expect
$this->expectException(DomainValidationException::class);
$this->expectExceptionMessage($expectedCode);
// Act
$this->validator->validate($request);
}
}
Один из нескромных тестов сценария:
final class AC06_MissingDateTest extends BaseAddEntryUnitTest
{
use EntryValidationAssertions;
/**
* DATE_REQUIRED must stop execution before persistence.
*
* @return void
*/
public function testMissingDateFailsWithDateRequired(): void
{
// Arrange
$dataset = AddEntryDataset::ac06EmptyDateSanitized();
$errorCode = 'DATE_REQUIRED';
$validator = $this->makeValidatorThrows($errorCode);
$repo = $this->makeRepo();
// Expect
$this->expectDateRequired();
// Act
$request = $dataset['request'];
$useCase = $this->makeUseCase($repo, $validator);
$useCase->execute($request);
// Assert
$this->assertRepoUntouched($repo);
}
}
Буду ли я дальше пользоваться такими инструментами? Да. Они идеальны для быстрого старта, когда нужно проверить идею или собрать прототип. Ну зачем вручную писать очередную фабрику, если модель штампует их гораздо быстрее?
Но при этом я не готов делегировать ей серьёзные задачи и внедрять в зрелые проекты. Дело даже не столько в качестве программного продукта, сколько в отсутствии ответственности. Я всерьёз опасаюсь момента, когда в продакшен уйдёт огромный массив «нового» кода, который был недовычитан и недотестирован просто из-за своего объёма и ограниченных человеческих ресурсов. Ошибка в расчёте скидки или неверная рекомендация в кинотеатре никому всерьёз не повредит, но в более важных системах подобная беспечность уже неприемлема.
Хочется верить, что те самые оставшиеся 70 % кода в Microsoft по-прежнему будут писать инженеры, и новые версии их программных продуктов будут нас только радовать.