Осваивая разработку с помощью AI-агентов, я написал веб-приложение для собственной ежедневной работы. Проект получился довольно сложным: с динамическим обновлением интерфейса и поиском, модальными окнами, автосохранением, интеграциями с внешними системами, модулями машинного обучения, текстовым анализом, фоновыми задачами и автоматическим деплоем. Объём кода составил около 150 000 строк, из которых примерно 120 000 написаны на Rust, а остальные — на TypeScript и Terraform.
Весь этот код сгенерировали агенты — в основном Claude Code и частично Cursor. За редкими исключениями я почти не открывал и не читал исходные файлы.
В процессе разработки я начал замечать странности. Когда в терминале мелькнула правка 4000-й строки в одном файле, я решил посмотреть на код ближе. Выяснилось, что слой доступа к данным разросся до 6000 строк. С каждой новой функцией он продолжал расти. В коде каждого запроса, чтения или записи повторялись настройка HTTP-запроса, кодирование и декодирование JSON. В итоге весь слой доступа к данным оказался в одном файле на 17 155 строк Rust.
Эксперимент по рефакторингу
Файл на 17 155 строк содержал весь слой доступа к данным. Это был изолированный модуль с чётким интерфейсом, который требовалось сохранить.
Внутри файла было много повторяющегося кода. Общие операции редко выносили в функции или отдельные структуры. Не было и собственного мини-языка (domain-specific language, DSL), который упростил бы построение запросов.
При этом модуль имел чёткий внешний интерфейс, который требовалось сохранить. Можно было менять внутреннее устройство модуля, не затрагивая код, который к нему обращался. Поэтому он хорошо подходил для эксперимента с рефакторингом.
В кодовой базе, которую поддерживают AI-агенты, цель рефакторинга проста: потратить токены сейчас, чтобы снизить их расход на все последующие изменения. Эксперимент должен был проверить, уменьшится ли стоимость работы с кодом по мере структурирования монолитного файла.
Каждый новый запуск агента начинался без памяти о предыдущих попытках. Поэтому после каждого шага рефакторинга можно было открыть отдельную сессию и повторить тот же промпт. В отличие от разработчика, агент не накапливал знания о задаче, которые могли бы повлиять на следующий результат.
Схема эксперимента выглядела так:
-
Подготовить общий план рефакторинга по строгим правилам дисциплины рефакторинга.
-
Сформулировать типовую задачу по добавлению фичи в виде одного промпта.
-
Измерить базовую стоимость изменений: запустить отдельного субагента с этим промптом и запросить отчёт о потраченных токенах.
-
Отменить внесённые изменения.
-
Повторять в цикле:
-
Выполнить один шаг рефакторинга.
-
Запустить свежего субагента с точно таким же промптом и записать расход токенов.
-
Отменить изменения фичи.
-
-
Зафиксировать расход токенов, время выполнения и количество строк кода после каждого шага.
Промпт для типовой задачи и применённые шаги рефакторинга приведены в приложении в конце статьи.
Важное уточнение: Claude не предоставляет надёжного способа считать токены в реальном времени, хотя показывает их количество, сообщает расход за сессию и использует его при расчёте стоимости. Я исхожу из того, что это временное ограничение. В эксперименте субагент сообщал количество полученных и отправленных символов. Затем я оценивал расход с помощью библиотеки tiktoken, исходя примерно из 4 символов на токен.
Результаты измерения
|
Шаг |
Строк в слое данных |
Макс. строк в файле |
Всего строк Rust |
Входные токены |
Выходные токены |
Время (с) |
|---|---|---|---|---|---|---|
|
Baseline |
17 155 |
17 155 |
50 359 |
159 564 |
1 705 |
342 |
|
Step 1 (FirestoreClient) |
16 706 |
16 706 |
49 910 |
155 205 |
1 723 |
530 |
|
Step 2 (extract_doc_id, new_link) |
16 562 |
16 562 |
49 766 |
159 227 |
2 105 |
574 |
|
Step 3 (link-query helpers) |
16 567 |
16 567 |
49 771 |
154 054 |
2 105 |
524 |
|
Step 4 (FakeStore predicates) |
16 577 |
16 577 |
49 781 |
154 146 |
2 060 |
654 |
|
Step 5 (value ctors) |
16 469 |
16 469 |
49 673 |
171 251 |
2 036 |
1 353 |
|
Step 6 (FieldsBuilder) |
16 469 |
16 469 |
49 673 |
171 251 |
2 036 |
1 353 |
|
Step 7 (queries.rs) |
16 474 |
15 670 |
49 678 |
151 850 |
1 800 |
587 |
|
Step 8 (traits.rs) |
16 508 |
13 845 |
49 712 |
132 558 |
1 723 |
446 |
|
Step 9 (traits/ split) |
16 508 |
13 845 |
49 712 |
132 558 |
1 723 |
446 |
|
Step 10 (codec.rs) |
16 521 |
12 846 |
49 725 |
131 871 |
1 750 |
540 |
|
Step 11 (fake_store.rs) |
16 535 |
11 122 |
49 739 |
133 016 |
2 460 |
600 |
|
Step 12 (store/ split) |
16 550 |
9 269 |
49 754 |
104 080 |
2 050 |
490 |
|
Step 13 (co-locate tests) |
16 550 |
9 269 |
49 754 |
104 080 |
2 050 |
490 |
|
Step 14 (complete fake_store.rs) |
16 553 |
7 225 |
49 757 |
107 205 |
2 453 |
523 |
|
Step 15 (store/ split) |
16 608 |
3 695 |
49 812 |
27 360 |
2 113 |
454 |
Ключевые метрики здесь — общий объём слоя данных в строках, размер самого большого файла в этом слое и количество входных токенов, затраченных на типовое изменение.

На графике отображены четыре показателя. Точка 0 — это базовое состояние (Baseline), а последующие точки показывают метрики после каждого этапа рефакторинга:
-
Data Access Layer (LOC)— количество строк кода во всём слое доступа к данным. Сначала весь слой находился в одном исходном файле, а к концу эксперимента его код был распределён по 19 файлам на Rust. -
Largest File (LOC)— количество строк в крупнейшем файле слоя. Вначале этим файлом был весь исходный монолит. В конце крупнейшим файлом оказалась вспомогательная библиотека тестов. -
Input Tokens— входные токены, потраченные субагентом на типовое изменение. -
Output Tokens— выходные токены, сгенерированные субагентом при написании кода.
Как рефакторинг снижает расход токенов
В этом прогоне объём входных токенов оставался высоким, пока размер крупнейшего файла не начал сокращаться. После разделения файла расход токенов резко упал.
Между исходным кодом и финальным этапом рефакторинга количество входных токенов на выполнение одной и той же задачи снизилось со 159 564 до 27 360. Экономия составила 132 204 токена, или 83%. И это не одноразовый эффект: каждое следующее изменение в этом модуле теперь обходится значительно дешевле.
Сколько это в деньгах? По действовавшей на момент публикации цене Sonnet 5 — $3 за 1 млн входных токенов — экономия составляет около 39,7 цента на одну задачу. На первый взгляд немного. Но будет ли эффект накапливаться при отладке, разработке более сложных функций и частых итерациях? Можно ли получить похожую экономию после рефакторинга других частей системы? И сколько токенов потребует сам рефакторинг? Этот эксперимент не отвечает на такие вопросы.
Вероятная причина экономии — агенту приходилось читать меньше кода. Общий размер слоя данных практически не изменился и остался на уровне 16,5 тыс. строк. Чтобы сэкономить токены, агент должен был находить минимальный набор файлов, необходимый для изменения. Результаты эксперимента указывают, что это происходило. Сообщения Claude Code о ходе работы и отчёты о прочитанных файлах также показывали, что с каждым шагом агент обращался ко всё меньшим и более точным фрагментам репозитория.
Простая нарезка монолита на случайные файлы вряд ли дала бы такой результат. Если файлы разделены без понятной логики, агенту всё равно придётся прочитать многие из них в поисках нужной функции. Самый заметный эффект возник на последнем шаге, но предыдущие рефакторинги подготовили это разделение. Я не планировал такой результат заранее. Процесс развивался обычным для рефакторинга способом: сначала агент устранял локальные повторы, а затем разделил код на модули, когда стала видна общая повторяющаяся логика.
Рефакторинг почти не повлиял на количество выходных токенов: объём генерируемого агентом кода оставался примерно одинаковым. Выходные токены стоили в 5 раз дороже входных, но их требовалось гораздо меньше. Мне нужен более сложный пример изменения, чтобы проверить, можно ли сократить и этот расход. Кроме того, недетерминированная генерация создаёт шум, который может скрывать небольшое влияние структуры кода на количество выходных токенов.
Особенности и ограничения процесса
В ходе эксперимента проявились характерные черты работы с агентами:
-
Без подробного плана Claude не справился с рефакторингом. Агент не смог самостоятельно изучить большой файл, выбрать подходящие приёмы и выстроить последовательность изменений. В процессе разработки он получал отдельную команду провести рефакторинг, но так и не улучшил этот файл. Человеку пришлось задать направление и затем вести агента по шагам. При подготовке плана
Claude.aiсразу предложил выделитьFirestoreClient, аClaude Codeначал с более локального приёма Extract Function — выделения функции. -
С выполнением плана тоже возникли проблемы. Claude применял изменения с помощью скриптов на Python, которые вызывали
grepиsed. Скрипты регулярно ошибались при обработке отступов. В первом проходе агент также пропустил самое полезное изменение — разделение реализаций хранилища на отдельные модули. Этот рефакторинг пришлось завершить позже в два дополнительных этапа. Поэтому в плане 13 шагов, а в таблице результатов — 15. -
Инфраструктура влияла на длительность. Весь эксперимент занял около 8 часов и почти не требовал моего участия. Я вмешался только через 6 часов 40 минут, когда агент пропустил важный шаг. Сначала причиной задержки казался медленный Wi-Fi в отеле. Однако дальнейшая проверка показала, что временный кэш сборки
cargoсильно разросся и замедлил тесты.
Дальнейшие шаги
Я не посчитал точный расход токенов на сам процесс рефакторинга. По общему расходу за время работы можно назвать только верхнюю границу — около 5 млн токенов. В эту оценку входят две попытки составить план, проектирование эксперимента и типового изменения, а также другие задачи. В будущих исследованиях стоит отдельно измерять стоимость рефакторинга.
Это один эксперимент на новом проекте без накопленного legacy-кода. Я разрабатываю и поддерживаю его один. Результат выглядит интересным первым шагом, но не общим доказательством экономической выгоды рефакторинга. Дальше стоит проверить более сложные изменения, рефакторинг других частей системы, непрерывный рефакторинг и относительную пользу разных подходов.
В этом эксперименте рефакторинг помог агенту читать меньше кода и снизил стоимость типового изменения. Но сколько стоил сам рефакторинг и повторится ли эффект в других условиях, пока неизвестно.
Приложения
Приложение 1. Промпт типового изменения
Этот промпт передавался каждому субагенту в абсолютно одинаковых условиях вместе с документацией по архитектуре:
You are working in the Rust project at
~/dev/your-project-name.Add a new
ItemWatchStorepublic async trait to the Firestore layer, following existing patterns exactly. The trait must have three methods:
async fn watch_item(&self, item_id: &str, user_id: &str) -> Result<()>
async fn unwatch_item(&self, item_id: &str, user_id: &str) -> Result<()>
async fn watched_items_for_user(&self, user_id: &str) -> Result<Vec<String>>Watches are stored in a
item_watchesFirestore collection. Each document has fields:itemId(string),userId(string),createdAt(timestamp). There is no Rust struct for a watch record — the methods returnVec<String>(item ids).Implement the trait for both
FakeStore(using an in-memoryVec<(String, String)>field added toFakeStoreInner) andFirestoreStore(using the same HTTP patterns used for other store impls in this file).At the very end of your response, output exactly this JSON block (fill in real values):
{ "files_read": [ {"path": "src/firestore.rs", "chars": 123456}, ... ], "response_chars": 7890 }Do NOT commit the change. Stop after writing the code.
Приложение 2. Сокращённый перевод плана рефакторинга
Промпт для генерации плана рефакторинга:
Following the strict definition that a refactoring is a provably correctness preserving series of code edits, and using Martin Fowler’s 2nd edition of Refactoring as the source, examine
@src/firestore.rs. This is a 17K LoC Rust file. No file should be that long. It is almost certainly not using an internal language to build and manage queries. Produce and describe, but don’t execute, a sequence of refactorings that would massively reduce the line count of that file, without changing the interface at all.
Claude составил подробный план с предполагаемыми изменениями. Каждый шаг можно было проверить отдельно, и после каждого шага запускались тесты. Приведённые ниже 13 пунктов не совпадают с 15 этапами измерения в таблице: агент сначала пропустил разделение реализаций хранилища на файлы, а затем выполнил его в два дополнительных этапа.
Шаг 1 — Extract Class: FirestoreClient (Fowler §7.5) + Extract Function × 4 (Fowler §6.1)
Выделение HTTP-транспорта (reqwest::Client, project_id, MetadataAuth) из FirestoreStore в отдельную структуру FirestoreClient.
Оценка: реализации FirestoreStore сократятся примерно на 1200 строк, а новый FirestoreClient добавит около 120 строк.
Шаг 2 — Extract Function: extract_doc_id и new_link (Fowler §6.1)
Выделение повторяющегося разбора ID документа и фабричной функции для создания Link.
Оценка экономии: около 500 строк.
Шаг 3 — Extract Function: вспомогательные функции конвейера запросов связей (Fowler §6.1)
Выделение функций для двух повторяющихся операций: собрать документы связей из результатов запроса и найти единственный целевой ID.
Оценка экономии: около 200 строк.
Шаг 4 — Extract Function: предикаты связей в FakeStoreInner (Fowler §6.1)
Выделение двух методов для повторяющихся проходов по связям в тестовом хранилище.
Оценка экономии: около 120 строк.
Шаг 5 — Replace Inline Code with Function Call: конструкторы значений Firestore (Fowler §8.5)
Замена более 128 встроенных выражений json!({"stringValue": ...}) и похожих конструкций вызовами четырёх приватных функций.
Оценка экономии: около 80 строк.
Шаг 6 — Extract Class: FieldsBuilder (Fowler §7.3)
Создание билдера для словарей полей документов.
Оценка экономии: около 500-600 строк.
Шаг 7 — Move Function: src/firestore/queries.rs (Fowler §8.1)
Вынос 32 констант LinkQuery и связанных типов в отдельный файл queries.rs.
Основной файл сократится примерно на 800 строк.
Шаг 8 — Move Function: src/firestore/traits.rs (Fowler §8.1)
Вынос 17 описаний публичных трейтов и связанных типов ошибок в traits.rs.
Основной файл сократится примерно на 1900 строк.
Шаг 9 — Move Function: разделение traits.rs на каталог traits/ (Fowler §8.1)
Разделение traits.rs на четыре доменных файла: planning.rs, content.rs, people.rs и system.rs.
Шаг 10 — Move Function: src/firestore/codec.rs (Fowler §8.1)
Вынос функций кодирования и декодирования документов, FieldsBuilder и конструкторов значений в codec.rs.
Основной файл сократится примерно на 500 строк.
Шаг 11 — Move Function: src/firestore/fake_store.rs (Fowler §8.1)
Вынос FakeStore, FakeStoreInner и реализаций всех 18 трейтов в fake_store.rs.
Основной файл сократится примерно на 4700 строк.
Шаг 12 — Move Function: разделение реализаций FirestoreStore по файлам (Fowler §8.1)
Разделение реализаций FirestoreStore на доменные файлы в каталоге src/firestore/store/. Вместо одного файла примерно на 10 000 строк должны получиться 10 файлов размером от 120 до 650 строк.
Шаг 13 — Move Function: перенос тестов к соответствующим модулям (Fowler §8.1)
Перенос тестов из общего блока непосредственно к тестируемым модулям. Сами тесты при этом не меняются.
ссылка на оригинал статьи https://habr.com/ru/articles/1065178/