Статьи

Разбираем AutoCodeBenchmark от Tencent: нейросеть, которая сама придумывает и проверяет задачи по программированию

2025-08-20 10:24 LLM Опенсорс

Недавно релизнулся интересный open-source проект от Tencent — AutoCodeBenchmark. Это целый фреймворк, который автоматизирует создание, проверку и даже описание задач по программированию. По сути, это конвейер, где на входе — простейший кусок кода, а на выходе — полноценный бенчмарк с задачами, решениями и тестами на 20 языках.

Давайте заглянем под капот и разберем, как эта штука устроена. Тут вам и LLM, и куча Python-скриптов, и хитрая "песочница" для безопасного выполнения кода.

Общая архитектура: два столпа системы

Если посмотреть на репозиторий, то вся система стоит на двух китах:

  1. AutoCodeGen — это "фабрика" контента. Здесь происходит магия: из простых "семян" (seeds) кода с помощью LLM выращиваются полноценные задачи. Этот компонент отвечает за всю логику генерации.
  2. MultiLanguageSandbox — это "испытательный полигон". Безопасная среда, где сгенерированный код компилируется и выполняется. Без этой песочницы вся система была бы просто очередной говорилкой, а так у нее есть кулаки — возможность проверить, работает ли то, что она нагенерировала.

Взаимодействие этих двух компонентов и есть сердце AutoCodeBenchmark. AutoCodeGen выступает в роли "креативного директора", который ставит задачи LLM, а MultiLanguageSandbox — в роли "ОТК" (отдела технического контроля), который безжалостно отбраковывает нерабочий код. Этот цикл "генерация-проверка" и позволяет на выходе получать качественный контент.

А теперь разберем каждый компонент в деталях.

AutoCodeGen: Фабрика задач по программированию

Это мозг всей операции, настоящий конвейер, который превращает простейшие идеи в полноценные учебные материалы. Процесс разбит на четкие, последовательные шаги, где каждый скрипт в директории AutoCodeGen/src выполняет свою конкретную функцию. Давайте пройдемся по всему этому пайплайну.

Шаг 1: Посев. Начинаем с простого

Все начинается с "семян" — seeds. В директории AutoCodeGen/data/seeds/ лежат jsonl файлы для нескольких языков. Внутри каждого — до смешного простые примеры кода.

Например, python.jsonl содержит:

{"text": "def add_two_numbers(a, b):\n    return a + b", "language": "python"}

Это наша отправная точка. Идея в том, что даже из такой примитивной функции можно "вырастить" что-то более интересное.

Шаг 2: Эволюция. Просим LLM усложнить задачу

Скрипт build_msg_for_solution.py берет этот seed и, используя шаблон из templates/gen_code_solution_templates/, формирует промпт для LLM.

[!INFO] Суть промпта: "Эй, LLM. Вот тебе простая функция. Преврати ее во что-то более сложное и осмысленное. А еще напиши для нее два набора тестов: demo_testing() с парой простых примеров и full_testing() с кучей кейсов, включая пограничные. Выдай все в трех отдельных блоках кода."

LLM (call_api.py отправляет запрос) получает эту инструкцию и начинает творить. Например, из простого сложения двух чисел она может сделать функцию для суммирования элементов в сложных вложенных структурах данных или что-то в этом роде.

Затем extract_three_code_blocks.py парсит ответ модели, извлекая из него три части:

  1. canonical_solution: Усложненная версия исходной функции.
  2. demo_test_func: Простые демонстрационные тесты.
  3. full_test_func: Полный набор тестов для проверки.

Шаг 3: Первая проверка в песочнице. Работает ли то, что получилось?

Теперь в дело вступает MultiLanguageSandbox. Скрипт call_sandbox.py берет сгенерированные canonical_solution и оба тестовых набора (demo_test_func и full_test_func) и отправляет их на выполнение.

[!NOTE] Важный момент: На этом этапе тесты еще не являются классическими unit-тестами с assert. Они просто вызывают функцию с разными входами и печатают результат в консоль. Цель этой проверки — убедиться, что сгенерированный код в принципе запускается, не падает с ошибкой и выдает какой-то предсказуемый вывод.

Результат этого запуска (то, что было напечатано в консоль) сохраняется. Если код упал или отработал некорректно, вся заготовка (задача + решение + тесты) отправляется в брак.

Шаг 4: Генерация верифицированных тестов

И вот тут начинается самое интересное. Скрипт build_msg_for_test.py берет успешно прошедшие проверку данные и формирует новый промпт для LLM.

[!TIP] Суть промпта: "Смотри, LLM. Вот функция (canonical_solution). Вот тестовые вызовы (demo_test_input) и вот реальный результат их выполнения из песочницы (demo_test_output). Сделай из этого нормальные unit-тесты с assert."

Мы не просто просим нейросеть написать тесты из головы. Мы даем ей фактические, верифицированные данные о поведении кода и просим обернуть их в формат assert. Это на порядок повышает качество и корректность финальных тестов.

После ответа модели скрипт extract_two_code_blocks.py снова парсит результат, но на этот раз извлекает уже финальные demo_test_func и full_test_func в виде полноценных unit-тестов.

Шаг 5: Генерация человекочитаемой задачи

Когда у нас есть верифицированное решение и верифицированные тесты, остается последний шаг — придумать для всего этого осмысленное задание для человека.

Скрипт build_msg_for_question.py формирует финальный промпт.

[!INFO] Суть промпта: "Окей, LLM, последний рывок. Вот тебе готовый код решения и тесты к нему. Придумай и напиши полноценное условие задачи, которое бы соответствовало этому коду. Опиши, что нужно сделать, формат ввода-вывода и приведи примеры из demo_test."

extract_question.py извлекает из ответа модели текст задачи в тегах <question>, и на этом конвейер завершает свою работу.

Итог конвейера AutoCodeGen

На выходе из этого многоступенчатого процесса мы получаем полный и, что самое главное, проверенный пакет данных:

  • question: Текстовое описание задачи.
  • canonical_solution: Эталонное решение на одном из исходных языков.
  • demo_test_func: Демонстрационные тесты (часто для примеров в условии).
  • full_test_func: Полный набор тестов для проверки решения.
  • language: Язык программирования.

А дальше эти пакеты можно использовать как основу для перевода на другие языки (для этого есть build_msg_for_translation.py) или для оценки моделей. Но прежде чем код куда-то пойдет, он должен пройти через "чистилище" — песочницу.

MultiLanguageSandbox: Безопасный полигон для кода

Если AutoCodeGen — это мозг, то MultiLanguageSandbox — это бронированная комната, где этот мозг проводит свои рискованные эксперименты. Главная задача этого компонента — выполнить потенциально небезопасный, сгенерированный нейросетью код так, чтобы он не навредил основной системе, и вернуть достоверный результат его работы.

Архитектура песочницы построена вокруг Flask-сервера (sandbox.py), который принимает HTTP-запросы на выполнение кода. Внутри используется сложная система, чтобы обеспечить и безопасность, и поддержку множества языков.

Ключевые компоненты песочницы

  1. code_config.yaml: Это конфигурационный файл, который является сердцем мультиязычной поддержки. Для каждого языка здесь прописаны свои правила игры:

    • compile_cmd и compile_flags: Как компилировать код (если это компилируемый язык).
    • execute_cmd и execute_flags: Как запускать код.
    • file_extension и file_name_template: Как называть файлы.
    • handler: Указание на специальный обработчик для языков, требующих особого подхода (например, dotnet_handler для C# или rust_handler для Rust).
    • error_check: Список строк, наличие которых в выводе будет считаться ошибкой теста (например, "AssertionError").

    [!TIP] За счет этого файла добавление нового языка в песочницу сводится к описанию его правил в YAML, а не к переписыванию логики на Python. Очень гибко.

  2. sandbox.py (Flask App): Точка входа. Принимает JSON-запрос с кодом, языком и параметрами. Главный эндпоинт — /submit. Он оркестрирует весь процесс внутри песочницы.

  3. code_splicer.py: Умный "сшиватель" кода. Перед выполнением часто нужно объединить код решения (func_code) и код тестов (main_code). Этот модуль делает это не тупым сложением строк, а с учетом синтаксиса языка:

    • Для C# или Java он корректно объединяет using/import директивы.
    • Для Go — обрабатывает package main и import (...).
    • Для PHP — следит за тегами <?php ?>.
    • И так далее для десятка других языков.
  4. code.py (CodeStore): Управляет временным рабочим окружением для каждого запроса. Он создает уникальную директорию, сохраняет туда исходный код, а для языков со сложной структурой проекта (вроде Rust с его Cargo.toml или C# с .csproj) разворачивает необходимый шаблон проекта.

  5. safe_subprocess.py: Самая важная часть с точки зрения безопасности. Это не просто обертка над стандартным subprocess. Этот модуль запускает код от имени специального, максимально ограниченного в правах пользователя sandbox (uid=1000, gid=1000).

    • Изоляция пользователя: Процесс с кодом не имеет прав root.
    • Таймауты: Жестко контролирует время выполнения и убивает процесс, если он превышает лимит.
    • Ограничение вывода: Чтобы избежать "логарифмической бомбы", ограничивает максимальный объем stdout и stderr.
    • Убийство группы процессов: Важный нюанс. Запущенный код может порождать дочерние процессы. safe_subprocess убивает всю группу процессов (os.killpg), чтобы ни один "потомок" не остался висеть в системе.
  6. executor.py: Непосредственный исполнитель. Он берет language_config из YAML, формирует команды для компиляции и запуска и передает их в safe_subprocess. После выполнения анализирует exit_code, stdout и stderr, чтобы определить исход: PASSED, COMPILATION_ERROR, RUNTIME_ERROR и т.д.

Особый случай: JVM Pool Manager для Java

Запуск JVM — процесс довольно медленный и ресурсоемкий. Если для каждого Java-теста запускать новую виртуальную машину, производительность сильно упадет. Разработчики AutoCodeBenchmark решили эту проблему элегантно.

В jvm_pool_manager.py реализован пул постоянно работающих JVM-процессов.

  • При старте песочницы запускается несколько (по умолчанию 8) JVM-процессов (WorkerMain.java), каждый из которых слушает свой порт.
  • Когда приходит запрос на выполнение Java-кода, executor не запускает новый процесс, а отправляет код по HTTP на свободный порт из пула.
  • JVM-воркер получает код, компилирует и выполняет его внутри себя, а затем возвращает результат.
  • Это многократно ускоряет обработку Java-задач, так как нет накладных расходов на постоянный запуск и остановку JVM.
  • Менеджер пула также следит за "здоровьем" воркеров и перезапускает их, если они упали или выполнили слишком много задач (для борьбы с утечками памяти).

Заключение

AutoCodeBenchmark — это не просто очередной датасет. Это мощный и хорошо продуманный инструмент для генерации датасетов. Его архитектура показывает, как можно эффективно сочетать мощь генеративных моделей с надежностью и безопасностью изолированных сред выполнения.

Ключевые выводы, которые можно сделать из разбора этого проекта:

  1. LLM + Sandbox = Синергия: Сами по себе LLM склонны к "галлюцинациям" и генерации нерабочего кода. Песочница дает им мгновенную обратную связь, позволяя итеративно улучшать качество и отбраковывать мусор.
  2. Двухэтапная генерация тестов: Идея сначала получить фактический вывод кода, а потом на его основе генерировать assert'ы — ключевой элемент, обеспечивающий корректность тестов. Модель не фантазирует, а работает с фактами.
  3. Автоматизация — это масштабируемость: Такой подход позволяет генерировать задачи для десятков языков, просто описывая их правила в конфиге. Сделать это вручную было бы на порядки дороже и дольше.