После прошлого поста в комментариях несколько раз возникал вопрос: а кто вообще пишет весь этот код и насколько здесь участвуют нейросети? Вопрос, в общем, закономерный. Объём того, что небольшая команда успевает одновременно разрабатывать и поддерживать, действительно довольно большой.
Масштаб, о котором обычно не пишут
В прошлой статье речь шла в основном про транспортный уровень — uTLS, decoy-трафик, pacing и всё, что связано с прохождением DPI. Это удобно рассматривать как отдельную техническую задачу, но внутри проекта это только один слой.
На моей части лежат клиенты под пять операционных систем: Windows, macOS, Linux, Android и iOS. Даже когда собственно транспортная логика у них общая, вокруг неё неизбежно появляется платформенный код. У каждой системы свои сетевые API, свои ограничения на фоновую работу, свои модели пермишенов и свой способ интегрировать всё это с остальной системой.
Кроме клиентов есть сетевая инфраструктура: control- и exit-узлы, арбитр, подписанный манифест узлов и обвязка вокруг этого хозяйства. Есть метрики, алертинг, ротация ключей и конфигов. Есть анализаторы трафика. Всё это не существует независимо друг от друга: изменение протокола довольно быстро превращается в изменения на нескольких платформах, серверных компонентах и в тестах.
И это только то, чем занимаюсь непосредственно я.
Рядом существуют партнёрские приложения, использующие наш SDK, сайт, документация и экосистема вокруг клиента. Ими занимаются другие люди, но с точки зрения общего объёма разработки они никуда не исчезают.
Команда при этом небольшая. Поэтому довольно быстро возникает банальная арифметическая проблема: задач больше, чем люди способны последовательно написать руками за разумное время. Именно здесь у нас появились LLM — в основном Claude, иногда модели OpenAI.
Не как ещё один архитектор и не как человек, которому можно сказать «сделай мне анти-DPI систему», а как инструмент для довольно определённого класса инженерной работы.
Что реально делегируется
Со временем довольно хорошо стало понятно, какие задачи имеет смысл отдавать модели, а какие проще сразу делать самому.
Кросс-платформенные порты
Один из самых очевидных случаев — перенос уже существующей и понятной логики между платформами.
Предположим, transport-слой на Go уже работает, поведение определено, интерфейсы понятны и основные проблемы решены. Теперь эквивалентную часть нужно сделать для Swift в iOS-клиенте или Kotlin в Android.
Инженерной неопределённости здесь значительно меньше, чем при создании исходной реализации. Зато появляется много механической работы: другой язык, другие типы, другие библиотечные вызовы, платформенные API, обработка ошибок.
Такой первый проход LLM обычно делает быстрее человека. Причём речь не о том, чтобы принять результат и забыть. Код после этого всё равно приходится читать, собирать, запускать и проверять на граничных случаях. Но начинать уже приходится не с пустого файла.
Для небольшой команды разница существенная. Особенно когда одно изменение нужно последовательно протащить через несколько платформ.
Протокольная рутина
Есть задачи, которые сами по себе требуют аккуратности, но после того, как принцип уже определён, не требуют каждый раз заново принимать архитектурное решение.
Например, ротация ClientHello-фингерпринтов под разные браузеры. Нужно аккуратно воспроизводить наборы TLS-расширений и их порядок для разных профилей Chrome, Firefox, Edge и Safari.
Здесь есть содержательная часть работы: понять, что именно мы хотим воспроизводить, зачем это нужно и как эта логика должна вести себя внутри нашего транспорта. Эту часть определяю я.
А дальше возникает серия реализаций одного и того же принципа. Условно: профиль для одного браузера уже сделан, теперь нужно аналогично реализовать другой. Задача вида «сделай то же самое для Firefox 141 с такими-то заданными параметрами» для модели подходит очень хорошо.
Человеку такая работа довольно быстро становится скучной, а скука в протокольном коде обычно приводит не к философским открытиям, а к переставленному расширению или пропущенному условию.
LLM скучно не становится.
Тесты и фикстуры
Ещё один большой класс задач — тесты.
Написать несколько проверок основного сценария обычно не проблема. Гораздо неприятнее систематически добивать кривые входные данные, различные варианты ошибок парсинга, пограничные случаи протокольных сообщений и моки сетевого слоя.
Это достаточно формализуемая работа. Если модели дать существующий интерфейс, описать ожидаемое поведение и показать уже имеющиеся тесты, она неплохо генерирует следующий слой покрытия.
Здесь, конечно, есть очевидная ловушка: автоматически написанный тест совершенно не обязательно проверяет именно то, что мы думаем. Поэтому тест модели тоже является кодом и тоже требует ревью.
Но писать очередную серию однотипных фикстур руками обычно нет большого смысла.
Рефакторинг
Очень похожая история с техническим долгом.
Поменять интерфейс в одном месте легко. После этого выясняется, что его используют ещё в десятках файлов. Или нужно везде добавить контекст с таймаутом. Или убрать разбросанную по коду константу и перенести её в конфигурацию.
Смысл изменения уже понятен. Творческой задачи почти нет. Есть большой объём операций, каждая из которых по отдельности элементарна, но одну из них очень легко забыть.
Для LLM это хороший режим работы.
Причём здесь особенно полезна не какая-то способность модели «понимать архитектуру», а совершенно прозаическая способность без раздражения делать двадцать раз однотипное изменение и потом пройтись ещё раз по получившемуся коду.
Первый проход по понятным багам
Есть ещё отладка, но здесь результат сильно зависит от типа проблемы.
Если есть воспроизводимый крэш, стектрейс и достаточно локализованное несоответствие между фактическим поведением и ожидаемым, модель часто действительно находит причину быстрее меня. Особенно если проблема находится внутри обычного прикладного кода, а все необходимые куски контекста можно ей показать.
В таких случаях очень удобно дать ей логи, соответствующий участок кода и спросить, где расходится логика.
Это не означает, что найденная причина автоматически правильная. Но как первый проход по проблеме это часто экономит время.
Совсем иначе всё работает, когда баг возникает из взаимодействия нескольких компонентов, проявляется только в определённых сетевых условиях или причина находится не там, где виден симптом. Там преимущество модели довольно быстро исчезает.
Что не делегируется
Граница при этом существует довольно чёткая. За время использования LLM она у нас не размылась, а, скорее, стала понятнее.
Первое — архитектура.
Какие типы узлов вообще нужны? Что должен знать control-узел, а чего он знать не должен? Как разнести доверие между арбитром и клиентом? Что произойдёт, если один из control-узлов будет скомпрометирован?
Модель вполне способна предложить ответы на такие вопросы. Иногда очень убедительно сформулированные.
Проблема в том, что убедительность здесь вообще не является нужным критерием.
Нужно понимать всю систему, историю принятых раньше решений, реальные ограничения и последствия изменения. Значительная часть этого контекста не находится в одном исходнике и не помещается в нормально сформулированную локальную задачу.
Поэтому архитектурные решения остаются человеческой работой.
Второе — сама постановка задачи.
LLM особенно хороша тогда, когда задача уже хорошо определена. Чем точнее известно, что должно получиться, тем полезнее инструмент.
Но кто-то до этого должен обнаружить проблему и решить, что именно нужно менять.
Модель сама не приходит утром с мыслью: «Кажется, нам пора пересмотреть вот эту часть системы, потому что через некоторое время здесь возникнет проблема». Она отвечает на поставленные вопросы. В лучшем случае может заметить что-то рядом с задачей, которую сейчас рассматривает.
Выбор самой задачи остаётся за человеком.
И, наконец, есть мотивация.
Это звучит менее технически, но в реальной разработке имеет довольно практическое значение. У человека есть собственный интерес в том, чтобы система работала. Есть ответственность за результат, репутация, понимание того, зачем вообще существует проект и что произойдёт, если определённая его часть будет работать плохо.
У модели собственного интереса к результату нет.
Это не претензия к модели. От компилятора тоже никто не ждёт мотивации. Просто это одна из причин, почему схема «отдал задачу LLM и забыл» у нас не работает.
Почему архитектор с 14 языками всё равно отдаёт модели код
У нас в компании есть архитектор с тридцатилетним стажем, который знает 14 языков программирования.
И тем не менее в довольно большом классе задач Claude пишет код лучше него.
Здесь важно уточнить, что я понимаю под словом «лучше». Я не утверждаю, что модель стала более сильным инженером, чем человек с тридцатилетним опытом. Если дать им неясную задачу и попросить самостоятельно определить архитектуру решения, сравнение будет совсем другим.
Но если задача уже определена, интерфейсы известны, а работа состоит в том, чтобы аккуратно реализовать понятную логику, сделать порт, написать серию тестов или протащить рефакторинг через множество файлов, результат модели часто оказывается лучше как заготовка для дальнейшей работы.
Она быстрее пишет этот объём кода и меньше устаёт на механических операциях.
Человеку на десятом похожем изменении уже очень хочется решить, что одиннадцатое ничем принципиально не отличается и можно посмотреть на него чуть менее внимательно. Модель такого желания не испытывает.
При этом у неё свои способы ошибаться.
Она может уверенно применить стандартное решение там, где конкретный случай как раз нестандартный. Может не увидеть ограничение, которое очевидно человеку из контекста проекта. Может построить совершенно логичную реализацию на неверном предположении, которое никто явно не сформулировал в задаче.
Поэтому тезис «Claude пишет код лучше нашего архитектора» и тезис «архитектор нам больше не нужен» для меня вообще не противоречат друг другу. Более того, первый как раз требует второго: чем больше кода можно быстро получить от модели, тем важнее человек, который понимает, какой из этого кода вообще имеет смысл писать.
Что получилось на практике
В итоге разделение труда у нас возникло довольно приземлённое.
Люди занимаются тем, где много неопределённости: анализируют проблему, выбирают направление, проектируют архитектуру, определяют ограничения и принимают решения, последствия которых выходят за пределы одного файла или одного компонента.
LLM получает задачи, где неопределённость уже сильно уменьшена: реализовать известную схему, перенести её на другую платформу, покрыть тестами, провести рефакторинг, разобрать локальный баг.
Конечно, абсолютной границы здесь нет. Иногда модель подсказывает хорошее архитектурное решение. Иногда задача, казавшаяся механической, внезапно упирается в платформенную особенность и возвращается человеку.
Но как рабочее разделение это оказалось вполне устойчивым.
И именно оно позволяет небольшой команде поддерживать такой объём компонентов. Без LLM нам пришлось бы либо существенно уменьшить фронт работ, либо растянуть разработку на сроки, которые для этого проекта не имеют смысла.
Итог
Поэтому короткий ответ на вопрос из комментариев такой: да, значительная часть кода проекта создаётся с активным использованием LLM. Основной инструмент у нас Claude, для части задач используем модели OpenAI.
Мы не пытаемся поручить модели проект целиком. Она хорошо работает там, где можно дать достаточно полный контекст и достаточно точно определить требуемый результат: кросс-платформенные порты, протокольная рутина, тесты, рефакторинг, локальная отладка.
Высокоуровневая аналитика, постановка задачи, архитектура, приоритеты и ответственность за результат остаются на людях. По крайней мере в нашей работе именно это разделение сейчас оказалось наиболее практичным.
Если здесь есть люди, которые используют LLM примерно в таком же режиме — сетевые протоколы, low-level код, кросс-платформенная разработка — интересно сравнить опыт. Особенно интересно, какие задачи вы сначала пытались отдавать модели, а потом решили всё-таки вернуть людям.
ссылка на оригинал статьи https://habr.com/ru/articles/1068410/