Можно перевести две сотни функций, увидеть зелёные тесты и решить, что миграция закончена. На практике это ещё ничего не доказывает. В реальной миграции с Go на Python в старом реестре было 329 команд, а в новом нашлось пересечение лишь по 201. Ещё 128 команд существовали только на стороне Go.
Здесь легко спутать «перевели» и «перенесли». Корректно переписать одну функцию — сильная сторона модели, и с этим она справляется хорошо. Но полнота миграции — вопрос другого рода: это операция над множествами. И именно такие операции модели лучше не поручать: здесь она особенно убедительно способна создать ложное ощущение порядка.
Ниже — реальный баланс миграции Go-to-Python и схема, при которой цифры считает скрипт, а модель лишь помогает разобраться в причинах.
Три цифры, с которых начинается проверка
В старом реестре Go было 23 платформы и 329 команд. Набор команд, который новый Python-код получает из деклараций argparse, пересекается со старым реестром ровно по 201 команде. Значит, 128 команд есть только в старой реализации: их либо не перенесли в Python, либо не оставили даже в виде заглушек.
329 = 201 + 128. В этом вычитании нет никакой технической сложности, но именно оно единственное во всей миграции отвечает на вопрос «всё ли готово». Когда вы переводите функции по одной, этой картины не видно. Пропущенная команда — ошибка отсутствия: нет исключения, нет упавшего теста, нет сообщения об ошибке. Просто имени, которое должно существовать, нет. Оно ни разу не попало в чат, а вы можете смотреть на двести зелёных галочек и не заметить проблему.
Почему не стоит маскировать пробелы заглушками и прокси
В середине миграции очень хочется обозначить ещё не сделанные команды: поставить заглушку с raise NotImplementedError или прокси совместимости, который вызывает старый бинарник, — чтобы «каталог эндпоинтов выглядел полным». Не делайте этого. Пустая оболочка обходится дороже честного пробела по трём причинам.
Заглушка ломает сверку. Имя команды попадает в новое множество, разница становится равна 0, и кажется, что работа закончена. Пробел честно горит красным, а заглушка рисует зелёную ложь: вместо «осталось 128» вы видите «всё на месте».
Прокси совместимости навсегда закрепляет невычищенную зависимость. Он пересылает вызов старому бинарнику Go, а значит, старый рантайм уже нельзя удалить. Смысл миграции — отказаться от прежнего стека; проксирующий слой позволяет ему въехать под вывеской «временной совместимости» и остаться навсегда.
Недоделанный эндпоинт вводит вызывающую сторону в заблуждение. Agent или человек видит его в каталоге, предполагает, что тот работает, делает вызов и получает runtime_unavailable. Или, что ещё хуже, ложный успех с незаметно пустым результатом.
Честный пробел на деле дешевле всего: diff сразу подсвечивает его красным, и всем видно, сколько работы осталось. Это тот же принцип, что и порог доказательности при реверс-инжиниринге приложений: пометить что-то как «пока непригодно к использованию» всегда дешевле, чем выпустить недостроенную версию.
Минимальный скрипт для сверки миграции
Вся суть сверки укладывается в одно правило: наборы команд с обеих сторон нужно получать из деклараций, а не переписывать вручную. Ручной список «уже перенесённого» создаёт третий источник истины, который неизбежно расходится с кодом. Через две недели именно он обычно и оказывается первой проблемой.
На новой стороне, в Python, единственный источник истины — декларации argparse в cli.py каждой платформы. Модуль catalog обходит подкоманды и экспортирует набор {platform/command}, который выводится через python -m reverse describe --format json. О том, почему декларация может быть единственным источником истины и как каталог строится автоматически, рассказывает материал об интерфейсе как коде. На старой стороне, в Go, уже есть map platform -> command — неизменяемый allowlist, скомпилированный в бинарник, — поэтому выгрузить JSON той же структуры несложно.
Когда есть два JSON-файла, всё остальное сводится к операциям над множествами:
# Both sides' command sets derive from declarations, not transcription.
# Transcribe by hand and you've added a third source of truth that will drift.
import json
from collections import Counter
def ids(path):
doc = json.load(open(path))
return {f"{p['name']}/{c['name']}"
for p in doc["platforms"] for c in p["commands"]}
old = ids("go-registry.dump.json") # old registry: immutable platform->command allowlist
new = ids("python-catalog.dump.json") # python -m reverse describe --format json
missing = old - new # old side only: each one needs a keep-or-drop verdict
added = new - old # new side only: new capability, logged separately
kept = old & new # intersection: migrated, but still check for semantic drift
assert missing | kept == old # every old-side item classified, none dropped
by_platform = Counter(pc.split("/")[0] for pc in missing) # goes straight into the README table
Скрипт выполняется за несколько миллисекунд, ничего не стоит, работает детерминированно и даёт 100% точный результат. В missing лежат те самые 128 команд. Если сгруппировать их по платформам, получится такая таблица:
Платформа | Неперенесённые команды |
|---|---|
xiaohongshu | 33 |
tiktok | 30 |
hotspot | 21 |
douyin | 19 |
8 | |
7 | |
bilibili | 5 |
zhihu | 3 |
1 | |
netease_music | 1 |
Итого | 128 |
На этом шаге модели делать нечего.
Почему поручать модели построчное сравнение дорого и ненадёжно
Можно не писать скрипт, вставить оба списка в чат и спросить: «Какие из 329 команд не встречаются среди этих 201?» Но почти неизбежно произойдут три вещи.
Во-первых, модель пропустит часть элементов. На длинном списке она не вычисляет разность множеств элемент за элементом, а опирается на приблизительное «выглядит похоже». Хвост списка размывается, и ответ, внешне кажущийся полным, недосчитывается на десяток команд. Во-вторых, она начнёт выдумывать: назовёт отсутствующими команды, которые есть с обеих сторон, или сочтёт перенесёнными действительно пропущенные. Модель воспроизводит форму отчёта о сверке, а не обязательно выполняет саму разность. В-третьих, результат нельзя воспроизвести: повторите запрос с теми же данными — и список пропусков изменится. Сверка, которая каждый раз выдаёт другой результат, не является сверкой.
По стоимости это тоже бессмысленно: скрипту нужны несколько миллисекунд, а модели для сравнения потребуются несколько сотен тысяч токенов и несколько раундов самопроверки. Это дорого, медленно и ненадёжно. Отдать операции над множествами инструменту, который умеет выполнять операции над множествами, — самая бесспорная мысль во всей статье.
Правильная роль модели: объяснять расхождения, а не искать их
Скрипт выдаёт 128 фактов вида «не перенесено», но факт сам по себе не является решением. Для каждой команды нужен вердикт: сохранять или убирать, а у вердикта должна быть причина. Вот здесь начинается работа модели.
Пусть она объясняет, почему команда не была перенесена, — по одной за раз. Это мёртвый код? Вышестоящий эндпоинт закрыли? Работу отложили? Или наиболее сложный вариант: команду не удалили, а объединили с другой — имя исчезло, но возможность осталась. Такое скрытое соответствие «объединено, а не удалено» невозможно обнаружить по одному списку пропусков: нужно одновременно читать оба реестра и сопоставлять их.
Даже 201 команда из пересечения не гарантируют безопасности. Перенос не означает сохранения семантики: у одноимённой команды мог незаметно измениться default, поменяться смысл пагинации, два кода ошибок могли превратиться в один. Это семантический дрейф, и он опаснее пробела: diff зелёный, а команда вообще не попадает в missing. Чтобы обнаружить дрейф, модели нужно прочитать обе реализации и ответить на вопрос «эквивалентно ли это поведение», а затем подтвердить вывод дифференциальным тестированием — сравнением фикстур на третьем этапе четырёхэтапного процесса. Умение посмотреть на уже признанный успешным перевод и всё же сказать «здесь изменилось поведение» — ровно то, о чём говорится в разделе о контрдоказательствах в статье о fingerprinting. Слабая модель в такой ситуации лишь повторит: «миграция успешно выполнена».
Разделение ответственности получается простым: скрипт отвечает на вопрос «есть ли это», а рассуждение требуется для вопросов «нужно ли это сохранять» и «не изменилось ли поведение». В этом случае 4 команды, подсвеченные diff красным, после проверки оказались нужными и были восстановлены как полноценные новые команды. Скрипт устанавливает факт, модель объясняет, человек принимает решение — у каждого слоя своя задача.
Какую модель использовать на каждом этапе
Все четыре уровня ниже относятся к слою объяснения. В слое принятия факта — то есть при вычислении diff — модель не используется вовсе. В этом и проходит граница между этим подходом и другими статьями про «AI-миграции».
Задача | Необходимые возможности | Выбор | model id |
|---|---|---|---|
Одновременно загрузить оба реестра и найти соответствия вида «не удалено, а объединено с другим» | Длинный контекст, чтение полных деклараций обеих сторон за один раз | Kimi K3 |
|
Первичный вердикт keep-or-drop для 128 отсутствующих элементов, структурированный черновик | Низкая стоимость, сотни вызовов с высокой параллельностью | Claude Sonnet 5 |
|
Оценить семантический дрейф: команда перенесена, но изменилось ли поведение; прочитать обе реализации | Сильное рассуждение и готовность сказать: «здесь есть изменение» | Claude Opus 5 |
|
Команда перенесена, но фикстура не совпадает: объяснить расхождение по параметрам или форме ответа | Средний уровень рассуждений для атрибуции причин | GPT-5.6 Sol |
|
Особенно стоит протестировать третий уровень. Оценка семантического дрейфа проверяет, способна ли модель возразить против уже признанного успешным перевода, и именно здесь смена модели сильнее всего влияет на итог. Протокол такой:
Возьмите собственную реальную миграцию между двумя языками и получите множество
missingскриптом. На этом шаге модель не нужна.Вручную разметьте 10–15 элементов, сформировав ground truth: убрать, сохранить, объединено с другой командой, отложено. Это будет контрольной выборкой.
Передайте один и тот же запрос «объясни keep-or-drop для каждого элемента» в
claude-opus-5и в дешёвый уровень. Оцените две вещи: ссылается ли объяснение на конкретный факт в коде или выдаёт вязкое «возможно, устарело», а также сколько соответствий «объединено с другой командой» находит каждая модель.Количество найденных скрытых соответствий и будет основанием решить, можно ли доверить модели первичный проход.
Проблема не в выборе модели, а в цене переключения
Четыре модели от трёх поставщиков — это три SDK, три схемы авторизации и три формата ошибок. Переписывать клиента при каждом переходе между уровнями невыгодно, поэтому многие используют одну модель на всём пути. А на проверке семантического дрейфа, где особенно нужен сильный уровень рассуждений, остаются на дешёвой модели, получающей лишь расплывчатые объяснения, и пропускают весь дрейф, который выглядит зелёным.
AIReiter убирает эту прослойку сложности: один ключ, один OpenAI-совместимый интерфейс, все четыре уровня за ним, а для переключения достаточно изменить поле model в теле запроса.
# Semantic-drift review / per-item keep-or-drop: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<both implementations + this command migration status, ask if behavior is equivalent>"}]
}'
# First pass on 128 missing items in bulk: change the model field, leave the rest
# "model": "claude-sonnet-5"
# Diff attribution when a fixture won't match:
# "model": "gpt-5.6-sol"
Если вы уже используете OpenAI SDK, укажите для base_url адрес https://aireiter.com/api/v1 — больше ничего менять не нужно. В Anthropic SDK отправляйте запрос на POST /api/v1/messages, используя тот же ключ.
Ценообразование хорошо ложится на этот процесс: первичный проход одновременно обрабатывает сотни элементов и повторяется на каждом цикле миграции, поэтому высокопараллельный claude-sonnet-5 оказывается самым дешёвым. Проверка семантического дрейфа — это десяток сложных случаев, которые снова и снова задаются claude-opus-5, и она самая дорогая в расчёте на элемент. Оба уровня относятся к Claude, поэтому скидка 30% приходится как раз на самые плотные и дорогие участки процесса. Расхождения фикстур объясняет gpt-5.6-sol — GPT за полцены.
Попробовать без регистрации: сначала вручную прогоните несколько отсутствующих элементов и посмотрите, замечает ли модель случаи «объединено с другой командой». После этого решайте, стоит ли встраивать её в процесс.
Итог
«Переведено» — иллюзия, возникающая на уровне отдельных функций. «Перенесено» устанавливает diff. Операции над множествами выполняет скрипт, объяснения даёт модель, решения принимает человек. Этот порядок нельзя перемешивать — и особенно нельзя отдавать модели принятие факта.
Есть ещё один шаг, который проще всего пропустить: список отсутствующих команд должен попасть в README и оставаться видимым в долгую. Число 128 остаётся там, пока не станет 0 либо пока у каждой команды не появится записанное объяснение: «не переносим, потому что X». Сверка, существующая только в обсуждении какого-то PR, — не сверка: следующий человек, который возьмётся за проект, её не увидит и снова наступит на все те же 128 проблем. В этом сходятся идеи этой статьи, материала об интерфейсе как коде и статьи о том, почему не стоит строить единую response Model: пусть единственный источник истины говорит сам за себя, а выводы не разлетаются по памяти отдельных людей.