Привет, Хабр! Меня зовут Михаил, в Циане я занимаюсь развитием культуры и developer experience. Архитектура у нас микросервисная, за каждый микросервис отвечает конкретная команда. В любой команде обычно есть микросервисы, которые помогают ей достигать собственных целей, и микросервисы, которые достались по наследству.
Наш бэкенд написан на Python и C#, и иногда в одной команде используются микросервисы на двух языках. Я считаю, что это не самый удобный расклад: лучше все-таки иметь один стек в рамках одной команды. Если, например, в команде с питонистами и единственным шарпистом последний уходит в отпуск, то при поломке сервиса на C# остальной команде придется этого шарписта ждать. Либо срочно вызывать на подмогу другого шарписта.

Можно переписать все микросервисы на один язык. Довольно трудоемкая задача, если заниматься этим вручную. Разработчику нужно погрузиться в микросервис, максимально покрыть тестами бизнес-логику и аккуратно все переписать. Не забывая, что делать один в один нужно не всегда, поскольку архитектурные паттерны Python и C# различаются.
Для переписывания сервисов разумно привлечь на помощь LLM. Далее я расскажу, как за неделю своей частичной занятости я с помощью LLM переписал сервис, который потребовал бы для этого два месяца от живого специалиста.
Сразу уточню: к началу миграции у нас уже был набор готовых инструкций для LLM по переносу микросервисов. Как подготовить исходный микросервис, как написать функциональные тесты, как обеспечить максимально полное покрытие — дотошные скилы LLM, с помощью которых можно перенести микросервис на другой язык и проверить корректность всей бизнес-логики.
Пока LLM неделю переносила сервис, я параллельно занимался другими задачами. Возвращался к ней лишь время от времени, каждый час-полтора. Нейросеть делала задачу и возвращалась с итогами. Я смотрел код: обычно все было хорошо, потому что тесты она уже прогнала. По необходимости подстраивал скилы, чтобы объяснить ИИ какие-то неочевидные вещи. По времени миграция заняла неделю, но если оценивать чисто мое время над проектом, в сумме получится дня два, не больше.
Разделение по этапам и стейт-машина
Я начинал создавать скилы с самого простого промпта: вот тебе микросервис на C#, построй план переноса и перепиши его на Python. Разумеется, в итоге ничего не заработало, потому что LLM не знала ничего ни про нашу платформу, ни про наш SDK. Это нужно рассказать LLM дополнительно. Это понятная и не самая интересная часть работы.
Что оказалось гораздо интересней, так это структурирование скилов. LLM в работе должна двигаться понятными человеку шагами. Когда я начал составлять их список, то увидел, что получается многовато. Разработчик в проектах проходит по некоторому дереву решений, с помощью которого определяет, что ему делать дальше в конкретном состоянии проекта.
Подобное дерево я выстроил и для LLM. В нем получилось очень много листьев. Мне как живому разработчику сложно держать в голове всю эту чащобу. Поэтому я разложил этапы работы по трем отдельным скилам: подготовка исходного сервиса, переписывание его на другой язык и ревью нового сервиса с оглядкой на старый код, чтобы находить различия.
Внутри каждого из скилов LLM сначала определяет свое положение в графе — для этого используется стейт-машина. Каждому положению соответствует некоторый набор инструкций. Нейросеть их подключает, выполняет, фиксирует новое состояние в чек-листе и начинает выполнять инструкции, привязанные к этому состоянию. Подход через стейт-машину помог нам укладываться в ограничения контекста: его можно очищать с переходом в каждое новое состояние. Это я делал вручную после проверки текущего результата.
Подготовка микросервиса к миграции
Первый скил — это подготовка микросервиса. Здесь LLM анализирует микросервис и составляет реестр всех внешних эндпойнтов — точек, через которые можно с микросервисом взаимодействовать: HTTP API-ручки, отчетные события, фоновые задачи. Каждый эндпойнт, по сути, инкапсулирует кусочек бизнес-логики. В данном случае нейросеть насчитала у микросервиса 64 эндпойнта.
Далее по каждому эндпойнту LLM делает вот что:
Полностью анализирует его бизнес-логику и по итогам строит граф с описанием этой логики на более высоком уровне, чем написан код.
Валидирует этот граф, сопоставляя его с кодом, при необходимости правит граф.
Оценивает имеющиеся тесты эндпойнта: насколько и как качественно они покрывают граф.
Пишет тест-планы по непокрытым частям эндпойнта и снова проверяет тесты.
Обработав все эндпойнты, LLM запускает итоговые функциональные тесты. Так удачно сложилось, что фреймворк функциональных тестов у нас един для Python и C#. В нем микросервис тестируется как черный ящик, поэтому язык значения не имеет.
Миграция с C# на Python
Далее вступает в дело скил миграции. Он готовит шаблон микросервиса на Python, прогоняет без изменений функциональные тесты C# и смотрит, что они падают именно на бизнес-логике — ошибка 404 или отсутствие ручки. Так мы понимаем, что инфраструктурных проблем из-за самого микросервиса нет.
Затем начинается сама миграция. Скил упорядочивает все эндпойнты от простого к сложному и строит чек-лист миграции. Порядок от простого к сложному нужен, чтобы на первых шагах не столкнуться с необходимостью переписать половину микросервиса. Лучше начинать с простого.
После переписывания на Python скил еще раз прогоняет тесты для каждого эндпойнта, ревьюит код, заливает в отдельную ветку, предлагает мне посмотреть, что получилось, и скорректировать по необходимости. Здесь в проверке учитываются не только общие правила, которые легко проверить сторонними чекерами, линтерами, но и те правила, что приняты именно у нас в командах.
Для такой проверки у нас есть отдельный инструмент, так что мы можем не раздувать контекст LLM правилами. Скил отправляет код в наш линтер, забирает ответ и чинит по нему код. Инструкция для LLM получается очень простой, и нам не нужно углубляться в промпт-инжиниринг. Кроме того, мы по своему опыту знаем, что от запуска к запуску LLM может интерпретировать контекст по-разному: сегодня что-то учтет, а завтра уже нет. Отдельная же проверка через наш инструмент обеспечит здесь стабильность: правила у нас вполне детерминированные, и скил будет прогонять проверки, пока не получит одобрение от нашего линтера. Для контекста LLM лучше оставлять правила, которые работают не всегда, а в зависимости от ситуации.
Полная проверка и передача кода
После миграции LLM проходит по всему списку эндпойнтов и проверяет, что исходная реализация на C# и итоговая на Python совпадают. По своему опыту могу сказать, что какие-то небольшие, но важные расхождения проскакивают все предыдущие тесты и обнаруживаются здесь. Обычно проверка повторяется два-три раза до полного совпадения. Затем LLM заканчивает работу, и новая кодовая база отправляется живым разработчикам. Они проверяют все на тестовом окружении, стейджинге и затем уже заливают на прод.
Что может пропустить LLM
С помощью LLM мы пока мигрировали два микросервиса, и на первом столкнулись с проблемой: iOS-приложение при обращении к сервису начало получать ошибку. Почему это стало неожиданностью? Потому что все события при раскатке микросервиса — в HTTP API на эндпойнтах — попадают в Swagger, в некоторый контракт. Этот контракт есть у самого микросервиса, и все пользователи микросервиса обращаются к нему через клиента, код которого сгенерирован на основе этого самого контракта.
Вот как это работает. Разработчик говорит: я хочу в рамках некоторого микросервиса обращаться к некоему другому микросервису, а именно к такой-то ручке. Происходит кодогенерация из JSON, создается полный слепок полей и типов с учетом обязательности и необязательности. Так общение разработчика с целевым микросервисом абстрагируется.
Эта схема применяется везде, поэтому мы в инфраструктуре может суммировать, в какие другие микросервисы ходит каждый микросервис, какие ручки использует, какие данные отдает и забирает. Эту общую картину анализирует отдельный инструмент и при выкатке некоего микросервиса в прод проверяет, нет ли в его контракте изменений, которые ломают взаимодействие с клиентами. Так мы можем убедиться, что даже при косяках бизнес-логики межсервисное взаимодействие не ломается.
Так вот, в истории с iOS-приложением мы успешно прогнали этот цикл и убедились, что никаких изменений по контрактам нет. Но потом выяснилось, что в iOS-приложении есть какой-то кастомный код, который дергает микросервис и падает. До этого код спокойно дергал такой же микросервис на C#, почему же с Python появилась ошибка?
Оказалось, что в контракте есть ручка, которая в числе прочих полей и в С#, и в Python принимает на вход ID типа int. А iOS-приложение отправляет не сам integer, а строчку с integer внутри. C# из-за специфики сериализации/десериализации просто конвертирует эту строку в integer и пропускает дальше. В Python у нас с этим строже, поэтому и возникала ошибка. А поскольку этот клиент не был сгенерирован, разницу контрактов на проде мы не увидели.
Что помогло решить проблему быстро? Мой опыт работы с описанными выше скилами. Я просто склонировал iOS-приложение, сказал, где ошибка, и попросил разобраться. А приложение большое, там много нашей бизнес-логики. И LLM быстро определила проблему: вот здесь C# конвертирует, а Python нет.
Улучшайте код, а не промпты
Этот кейс подвел меня к одной хорошей практике: держать в отдельной папке для LLM референсные микросервисы с примерами кода, где нейросеть может сама поразбираться. Если вы используете какой-нибудь SDK, его тоже стоит в эту папку сложить. Далее можно будет в инструкции отправлять LLM в эту папку на поиск нужной информации. По моему опыту, это гораздо быстрее и эффективней, чем писать множество инструкций с критериями «правильного кода».
LLM хорошо умеет вычленять правила написания из самого кода. Если в референсе везде есть docstring, то и в новом коде они появятся с высокой вероятностью. Если рядом есть код, который определенным образом оформляет инструкции, она сделает так же. Вообще, по моему опыту, если LLM делает что-то не так, то, скорее всего, где-то рядом есть код, в котором это так же плохо написано. Так что советую больше инвестировать не в простыню инструкций для LLM, а в улучшение вашей текущей кодовой базы, из которой AI сам почерпнет лучшие практики.
DamirMur
Если тебе llm помогла переехать с C# на Python, то что мешало давать задание чинить микросервис llm на c#.
На python конечно много чего написано и пишется, но нет строгой типизации, куча ошибок в библиотеках, потому что это язык для начинающих, типа basic. Я недавно перешёл на Go, который кроме типизации, ещё и мало места занимает. Язык особо не учу, всё равно llm пишет. За счёт типизации, ошибок гораздо меньше и отладка соответственно меньше токенов жрет.