В этой статье я планирую исследовать, как можно использовать большие языковые модели (LLM) для миграции проектов между различными фреймворками. Применение LLM в задачах на уровне репозитория — это развивающаяся и всё более популярная область. Миграция кода со старых, устаревших фреймворков на новые является одной из ключевых задач в крупных корпоративных проектах.
Актуальность
Миграция проектов на новые фреймворки становится всё более актуальной по мере быстрого развития технологий и изменяющихся требований бизнеса. Устаревшие фреймворки могут ограничивать возможности команды, снижать производительность и увеличивать риски безопасности. С ростом проекта возникает необходимость в масштабируемости и поддерживаемости выбранного фреймворка. Под поддерживаемостью подразумеваются читабельность кода и удобство его обслуживания. Например, в крупных проектах использование чистого JavaScript может привести к ухудшению читаемости и удобства работы с кодом, поэтому многие разработчики переходят на TypeScript, который также обеспечивает защиту от ошибок типизации. В то же время, фреймворки, такие как Express.js, являются un-opinionated и предоставляют разработчикам множество возможностей, что делает их подходящими для начинающих проектов. Однако переход на opinionated фреймворки позволяет масштабировать проект и повышать его консистентность, а также защищает от ошибок.
Таким образом, в многих крупных корпоративных проектах переписывание легаси кода занимает значительную часть времени разработчиков, которое могло бы быть использовано для создания новой функциональности и разработки инновационных решений. В последнее время большие языковые модели (LLM) демонстрируют отличные результаты в генерации кода, и всё больше проектов на основе LLM автоматизируют задачи на уровне репозитория [1], такие как рефакторинг кода, написание юнит-тестов и миграция пакетов. Основываясь на проведенном исследовании [2], оценившем помощь больших языковых моделей (LLM) в миграции кода на новые версии Java, было показано, что разработчики, получающие поддержку от LLM, могут значительно эффективнее выполнять эту задачу. Использование LLM позволяет ускорить процесс миграции, улучшить качество кода и снизить количество ошибок, что делает переход на новые версии более плавным и менее затратным по времени.
Перформанс LLM в трансляции кода
Недавнее исследование [3], посвященное эффективности различных больших языковых моделей (LLM) в переводе кода, показало, что такие модели, как GPT-4 и Claude, достигают 40-80% уровня успеха при решении различных задач перевода и генерации кода. В частности, приведены результаты, согласно которым лучшая LLM смогла успешно перевести код из реальных проектов на Rust в 47% бенчмарков. При этом корректность перевода и перформанс моделей снижались для проектов с более чем 100 строками кода. Эти результаты подчеркивают не только потенциал и возможности языковых моделей в переводе кода, но и существующие лимиты, которые важно учитывать. Существующие модели CodeGen могут служить хорошей основой для будущих решений, так как они демонстрируют хорошие показатели в переводе кода. Однако, учитывая снижение эффективности при работе с большим количеством строк кода, целесообразно рассмотреть возможность разбиения проекта на части и миграции его поэтапно.
Согласно данным проведенного исследования перформанса различных больших языковых моделей [4], можно наблюдать хорошие результаты у GPT-4 и Code Llama-3. На графиках видно, что при работе с большими текстами GPT-4 демонстрирует лучшие показатели, в то время как для решения специфичных задач, особенно связанных с кодом, Code Llama-3 показывает более высокие результаты.

Выбор фреймворков для миграции
Решение обобщенной задачи миграции с любого фреймворка на любой другой является слишком трудоемким процессом. Однако при более конкретной задаче с выбранными фреймворками появляется возможность настраивать и обучать модель с большей точностью, а также использовать особенности выбранных фреймворков и существующие руководства по миграции проектов.
Во-первых, было решено отказаться от фронтенд-фреймворков, так как в этом случае сложно проводить тестирование и оценку успешности миграции. Хотя существуют хорошие решения для e2e тестирования интерфейсов, такие как Playwright или Hermione, решение задач для бэкенд-фреймворков является более детерминированным и проще в тестировании и оценке производительности.
Во-вторых, необходимо выбирать фреймворки, миграция на которые распространена. Например, миграция с Flask на Django является редким решением, так как эти фреймворки имеют слишком много различий в применении, и миграция между ними происходит нечасто.
Конечным выбором стала миграция с Express.js на Nest.js. Express.js — это un-opinionated, минималистичный фреймворк для приложений на Node.js. Он очень популярен и существует уже много лет, фактически являясь стандартным каркасом для Node.js, поэтому много легаси-кода в энтерпрайзе написано на Express. Благодаря своей минималистичности и функциональности, многие начинающие проекты также используют его.
В то время как Nest.js — это быстро набирающий популярность новый фреймворк для Node.js, который поддерживает TypeScript и является opinionated, что делает его более консистентным и подходящим для больших проектов. Такие возможности, как поддержка Fastify (который быстрее Express), делают его более масштабируемым решением.
Актуальность фреймворков можно оценить, например, по количеству звезд на GitHub, и оба фреймворка, Express.js и Nest.js, в настоящее время являются одними из самых популярных. Интересно отметить, что за последний год Nest.js значительно увеличил свою популярность и по количеству звезд уже обходит Express.js. Это свидетельствует о растущем интересе разработчиков к Nest.js и его функциональным возможностям, что может быть связано с его поддержкой TypeScript и более структурированным подходом к разработке. На момент написания статьи количество звезд на GitHub для Express.js составляет примерно 65K, а для Nest.js — около 68K. Актуальные данные можно посмотреть в гитхабе репозитории со статистикой по Node js фреймворкам [5]
Среди наиболее распространенных методов решения сложных и специфичных задач с использованием LLM выделяются подходы RAG и fine-tuning модели. Эти методы оказывают различное влияние на поведение модели, и их можно применять одновременно. В этой связи давайте подробнее рассмотрим каждый из них.
Retrieval Augmented Generation
Генерация с дополненной выборкой (RAG) — это процесс оптимизации выходных данных большой языковой модели (LLM), при котором перед получением ответа осуществляется обращение к надежной базе знаний за пределами источников обучающих данных.
При миграции кода проект можно разбить на части, так как обработка всего проекта в одном запросе может быть сложной и привести к ухудшению производительности LLM, как мы выяснили ранее. Важно иметь информацию о проекте в целом, и именно эту информацию можно передавать с помощью RAG.
Для предоставления информации о проекте можно использовать различные виды интерпретации кода, такие как:
Abstract Syntax Tree (AST): представляет структуру программы в виде дерева, что позволяет анализировать синтаксис и семантику кода.
Intermediate Representation (IR): промежуточное представление, которое упрощает анализ и оптимизацию кода.
Data Flow: анализирует, как данные перемещаются через программу, что помогает понять зависимости между переменными.
Control Flow: показывает порядок выполнения инструкций, что важно для понимания логики программы.
Все эти виды представления программ предоставляют различную информацию, которая может быть использована при миграции проекта.

При использовании RAG можно обратиться к наиболее распространенному пайплайну, который активно применяется в различных проектах [8]:
Индексация данных: эффективная организация данных для быстрого доступа к ним.
Обработка входящих запросов: анализируем запрос клиента, чтобы определить, какие данные необходимо дополнить.
Поиск и ранжирование: с помощью алгоритмов поиска извлекаем необходимые данные для улучшения контекста запроса.
Дополнение промпта: используя полученные данные, обогащаем оригинальный запрос.
Генерация ответа: передаем дополненный промпт модели, которая генерирует ответ. Этот пайплайн позволяет значительно улучшить качество ответов, обеспечивая более точное и контекстуализированное взаимодействие с пользователем.

Pre-training vs Fine-tuning
Aspect |
Pre-training |
Fine-tuning |
---|---|---|
Определение |
Обучение на большом объеме немаркированных данных |
Адаптация модели под конкретную, определенную задачу |
Необходимые данные |
Обширный и разнооборазный набор немаркированных текстовых данных |
Меньший набор маркированных текстовых данных, специфичных для определенной задачи |
Цель |
Формирование общих языковых знаний |
Специализировать модель под определенную задачу |
Процесс |
Сбор данных, обучение на большом датасете, предсказание следующего слова/ последовательности слов |
Сбор данных под конкретную задачу, модификация последнего слоя модели, обучение на основе новых данных, генерация ответов на основе задач |
Модификация модели |
Вся модель обучена |
Последний слой адаптирован под решение определенной задачи |
Вычислительные затраты |
Высокие (большой датасет, сложная модель) |
Ниже (датасет меньше, файн тюнинг слоев) |
Длительность обучения |
От пары недель до нескольких месяцев |
От пары дней до нескольких недель |
Типы Fine-tuning
Unsupervised Fine-tuning
Unsupervised fine-tuning подразумевает использование немаркированных данных. Этот подход часто применяется, когда получение маркированных датасетов может быть затратным или когда нет необходимости в них. Основная цель — адаптация модели к определенной сфере, что позволяет улучшить понимание нюансов и терминологии данной области.
Однако данный метод показывает себя неэффективно в случаях, когда требуется адаптация под конкретную задачу. В нашем случае речь идет о миграции кода с Express.js на Nest.js, и поэтому использование unsupervised fine-tuning будет нецелесообразным.
Supervised Fine-tuning
Supervised fine-tuning — это подход, при котором модель обучается на маркированных данных, адаптированных под конкретную задачу. Поскольку этот метод фокусируется на адаптации к определенной задаче, он будет эффективен для решения нашей проблемы. Однако, несмотря на высокую эффективность, данный метод требует наличия маркированных данных, которые могут быть сложными и затратными в получении.
Instruction Fine-tuning
Также стоит рассмотреть технику, часто используемую для адаптации моделей под конкретные задачи, — это instruction fine-tuning. Этот подход подразумевает использование данных, содержащих инструкции для решения поставленных задач. В отличие от традиционного supervised fine-tuning, который в основном фокусируется на парах input и output, в данном методе мы также можем передавать контекст задачи, что способствует улучшению понимания нюансов и особенностей.
Подготовка данных
The Stack
Можно использовать существующие открытые датасеты, такие как The Stack [9] от BigCode Project. Этот специализированный датасет предназначен для обучения больших языковых моделей для работы с кодом и содержит более 3 ТБ кода, включая около 500 ГБ кода на JavaScript и более 100 ГБ на TypeScript. В нашей задаче необходимо получить датасет кода на Express.js, переведенного на Nest.js. Файлы с кодом, использующим фреймворк Express.js, можно определить по строкам вида:
require('express');
import ... from 'express';
Таким образом, мы можем извлечь из данного датасета код на Express.js и сформировать набор input-значений для обучения.
GitHub
У GitHub есть удобное REST API и CLI для работы с репозиториями. По тегам express
и javascript
на GitHub хранится более 50 тысяч репозиториев. В данном случае это полноценные проекты на фреймворке Express.js, а не просто набор кода без контекста, что придает этому подходу значительные преимущества. С помощью GitHub REST API можно извлечь репозитории по тегам express
и javascript
, получив таким образом датасет проектов на Express.js.
Преимущество данного подхода заключается в том, что полученный датасет можно использовать не только для тестирования модели, но и, например, для обучения с подкреплением, что мы рассмотрим далее.
Подготовка ответов
Имея датасет с кодом на Express.js, необходимо создать набор ответов, а именно переведенный код на Nest.js. Один из вариантов — это ручное составление ответов, когда разработчики самостоятельно пишут код. Однако этот подход является неэффективным и затратным по времени. Второй вариант — автоматизировать процесс перевода. Если бы задача перевода с одного фреймворка на другой была настолько тривиальной, что ее можно было бы решить простыми алгоритмами без использования больших языковых моделей, мы бы не занимались этой проблемой. Поэтому мы будем рассматривать генерацию ответов с помощью LLM.
Синтетическая генерация данных
Если бы задача перевода с одного фреймворка на другой была настолько тривиальной, что ее можно было бы решить простыми алгоритмами без использования больших языковых моделей, мы бы не занимались этой проблемой. Поэтому мы будем рассматривать генерацию ответов с помощью LLM. Есть две основные техники:
Prompt Engineering
С помощью составления специальных промптов, которые инструктируют LLM, можно генерировать датасет ответов на основе существующего датасета input‑значений, а также полностью создавать все данные, включая входные. Этот подход можно дополнительно улучшить, применяя различные методы валидации ответов. Это может включать как простую проверку кода на наличие ошибок, так и запуск юнит‑тестов, если они доступны.
Multi-Step Generation
Более интересный вариант генерации с использованием LLM — это отправка итеративных запросов к модели, которые постепенно приводят к получению ответа. Например, в нашей задаче можно разделить процесс на этапы, такие как рефакторинг кода, добавление комментариев, выделение маршрутов, сервисов, мидлварин и т. д. Этот метод позволяет получать данные высокого качества, а также инструкции и описания всех шагов, которые можно использовать для instruction fine‑tuning. В данном подходе также можно внедрить валидацию, чтобы повысить качество данных.
GPT-migrate
Существует открытое решение GPT‑Migrate [10], которое использует Prompt Engineering и модель GPT-4 для автоматической миграции кода между фреймворками. Это решение также генерирует юнит‑тесты для полученного кода. На данный момент проект находится на стадии alpha‑разработки и не готов к использованию в production, так как на сложных бенчмарках и с трудными языками он требует человеческой помощи. Тем не менее, проект демонстрирует хорошую архитектуру промптов и показывает неплохие результаты с «легкими» языками, такими как Python и JavaScript, на простых бенчмарках.
Таким образом, данное решение может быть использовано для подготовки датасета ответов. Одним из направлений для улучшения является повышение точности модели, что можно достичь через дообучение. В будущем можно воспользоваться бенчмарками этого проекта и их методами юнит‑тестирования для оценки и тестирования полученной модели.
Reinforcement Learning
Еще одним подходом, используемым для дообучения моделей, является Reinforcement Fine-Tuning. Этот метод подразумевает использование среды, которая оценивает работу модели, на основе чего происходит ее обучение. Данный подход хорошо подходит для нашей задачи, так как в качестве среды могут выступать проверки на ошибки компиляции и юнит-тестирование.

Сравнение использования этого метода с обычным supervised fine-tuning показывает, что результаты значительно выше.

Evolution Startegies
В качестве альтернативы методу обучения с подкреплением можно рассмотреть подход Evolution Strategies. Этот метод позволяет обучать модель без использования обратного распространения ошибки (backpropagation) и предоставляет возможность распараллеливания процесса обучения. Награду можно рассчитывать на основе предупреждений, ошибок в рантайме и результатов юнит-тестирования.
Исследование [12] показывают, что алгоритм Evolution Strategies в некоторых случаях обучается быстрее и демонстрирует результаты, не уступающие результатам методов обучения с подкреплением. Учитывая возможность распараллеливания обучения, этот подход может значительно оптимизировать затраты времени на обучение. В данном исследовании в качестве метода обучения с подкреплением рассматривался алгоритм TRPO (Trust Region Policy Optimization), а также задача MuJoCo Control Task.

План работы
Необходимо собрать датасет входных данных, более детально рассмотрев подходы к парсингу существующего датасета The Stack и репозиториев на GitHub. Все собранные данные будут полезны; например, датасет проектов с GitHub окажется ценным при тестировании и обучении с подкреплением.
Далее следует применить supervised fine-tuning с инструкциями и оценить полученные результаты. В завершение можно использовать методы Reinforcement Learning и Evolution Strategies для сравнения результатов, либо сосредоточиться только на Evolution Strategies, так как этот метод менее затратен.
Также необходимо провести тестирование модели с пмощью GPT-Migrate. При достижении хороших показателей модели возможно реализовать фронтенд в виде плагина для IDE.
Источники
[1] CodePlan: Repository-level Coding using LLMs and Planning. Ramakrishna Bairi, Atharv Sonwane, Aditya Kanade, Vageesh D C, Arun Iyer, Suresh Parthasarathy, Sriram Rajamani, B. Ashok, Shashank Shet (https://arxiv.org/pdf/2309.12499)
[2] Evaluating Human-AI Partnership for LLM-based Code Migration (https://assets.amazon.science/bc/ec/8213526e4857b6fa09af53b10c66/evaluating-human-ai-partnership-for-llm-based-code-migration.pdf)
[3] Towards Translating Real-World Code with LLMs: A Study of Translating to Rust. Hasan Ferit Eniser, Hanliang Zhang, Cristina David, Meng Wang, Maria Christakis, Brandon Paulsen, Joey Dodds, Daniel Kroening (https://arxiv.org/pdf/2405.11514)
[4] (https://arxiv.org/pdf/2407.21783)
[5] (https://github.com/VanoDevium/node-framework-stars)
[6] Large Language Models As Evolution Strategies. Robert Tjarko Lange, Yingtao Tian, Yujin Tang (https://arxiv.org/pdf/2312.10997)
[8] The Ultimate Guide to Fine-Tuning LLMs from Basics to Breakthroughs: An Exhaustive Review of Technologies, Research, Best Practices, Applied Research Challenges and Opportunities. Venkatesh Balavadhani Parthasarathy, Ahtsham Zafar, Aafaq Khan, Arsalan Shahid (https://arxiv.org/pdf/2408.13296)
[9] The Stack: 3 TB of permissively licensed source code. Denis Kocetkov, Raymond Li, Loubna Ben Allal, Jia Li, Chenghao Mou, Carlos Muñoz Ferrandis, Yacine Jernite, Margaret Mitchell, Sean Hughes, Thomas Wolf, Dzmitry Bahdanau, Leandro von Werra, Harm de Vries (https://arxiv.org/pdf/2211.15533)
[10] GPT-Migrate (https://github.com/joshpxyne/gpt-migrate/tree/main)
[11] REFT: Reasoning with REinforced Fine-Tuning. Trung Quoc Luong∗, Xinbo Zhang∗, Zhanming Jie*, Peng Sun†, Xiaoran Jin, Hang Li (https://arxiv.org/pdf/2401.08967v1)
[12] Evolution Strategies as a Scalable Alternative to Reinforcement Learning. Tim Salimans, Jonathan Ho, Xi Chen, Szymon Sidor, Ilya Sutskever (https://arxiv.org/pdf/1703.03864)