TRL documentation

Интеграция OpenEnv для обучения LLM с использованием сред

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Интеграция OpenEnv для обучения LLM с использованием сред

OpenEnv — это open-source-фреймворк для определения, развёртывания и взаимодействия с окружениями в задачах обучения с подкреплением (RL) и агентных сценариях. Он предоставляет стандартизированные API для взаимодействия с окружениями и поддерживает запуск окружений в качестве серверов на стороне бэкенда (через WebSocket или контейнеризованное выполнение). Коллекцию готовых к использованию окружений OpenEnv можно найти на Hugging Face Hub.

В этом руководстве описано, как интегрировать OpenEnv с TRL. Подробнее об OpenEnv см. в документации OpenEnv.

Готовые примеры OpenEnv: grpo_echo (минимальный), grpo_catch, grpo_wordle, grpo_sudoku, grpo_multi_env, grpo_browsergym, grpo_carla и async_grpo_harbor (локально и в HF Jobs).

Когда использовать окружения

GRPOTrainer можно использовать для обучения агентов. Для агентных задач поддерживаются два режима: tools, в котором модель может вызывать внешние функции, но каждый вызов является безсостоятельным и независимым, и environments, который сохраняет состояние между ходами, обеспечивая подлинное многоходовое взаимодействие, в котором действия агента формируют будущие наблюдения. Используйте environments, когда важна преемственность — например, при навигации в игре, просмотре веб-страницы или в любой задаче, где то, что агент увидит дальше, зависит от того, что он сделал ранее.

Выбор интеграции с окружением

OpenEnv — это нативный путь, описанный здесь. Ещё две интеграции — OpenReward и Harbor — соответствуют тому же контракту environment_factory и взаимозаменяемы на уровне TRL. См. сравнение интеграций сред в руководстве по GRPO, чтобы выбрать ту, экосистема которой лучше подходит для вашей задачи.

Установка

Среды OpenEnv размещаются в виде Hugging Face Spaces, которые также являются Git-репозиториями, устанавливаемыми через pip:

# Echo environment
pip install "openenv-echo-env @ git+https://huggingface.co/spaces/openenv/echo_env"

# Wordle (TextArena) environment
pip install "openenv-textarena @ git+https://huggingface.co/spaces/openenv/wordle"

# Catch (OpenSpiel) environment
pip install "openenv-openspiel-env @ git+https://huggingface.co/spaces/openenv/openspiel_env"

Эта команда устанавливает клиент окружения (например, EchoEnv), который взаимодействует с удалённым сервером окружения через WebSocket, а также модели действий и наблюдений и все необходимые зависимости (включая openenv).

Команду установки для любого окружения можно найти на его странице HF Space. Нажмите на меню ⋮ (три точки) и выберите «Use this Space», чтобы увидеть инструкции по установке.

Также можно установить базовый пакет из PyPI с помощью pip install "openenv>=0.3.1", но имейте в виду, что зависимости, специфичные для окружения, могут потребовать отдельной установки.

Для разработки вы можете клонировать репозиторий OpenEnv и установить его локально:

git clone https://github.com/openenv/OpenEnv.git
cd OpenEnv/envs/echo_env
pip install -e .

Каждый скрипт окружения в TRL содержит встроенные метаданные зависимостей (PEP 723), поэтому вы также можете запускать их напрямую с помощью uv:

uv run examples/grpo_echo/grpo_echo.py

Это автоматически устанавливает необходимый пакет среды в изолированную виртуальную среду.

Быстрый старт

Самый быстрый способ понять интеграцию — это полный пример. Скрипт echo.py обучает модель в среде Echo environment, которая поощряет завершения в зависимости от длины их текста:

from datasets import Dataset
from echo_env import EchoEnv
from echo_env.models import EchoAction

from trl import GRPOConfig, GRPOTrainer

ENV_URL = "https://openenv-echo-env.hf.space"

class EchoToolEnv:
    def __init__(self):
        self.env = EchoEnv(base_url=ENV_URL)
        self.reward = 0.0

    def reset(self, **kwargs) -> str | None:
        self.reward = 0.0
        return None

    def echo(self, message: str) -> str:
        """
        Echo the message back from the environment.

        Args:
            message: The message to echo

        Returns:
            The echoed message.
        """
        observation = self.env.step(EchoAction(message=message))
        self.reward = observation.observation.reward
        return observation.observation.echoed_message

def reward_func(environments, **kwargs):
    return [env.reward for env in environments]

dataset = Dataset.from_dict(
    {"prompt": [[{"role": "user", "content": "Try to echo 'Hello World!' in the environment."}]] * 64}
)

trainer = GRPOTrainer(
    model="Qwen/Qwen3-0.6B",
    train_dataset=dataset,
    reward_funcs=reward_func,
    args=GRPOConfig(
        chat_template_kwargs={"enable_thinking": False},
        log_completions=True,
    ),
    environment_factory=EchoToolEnv,
)
trainer.train()

Всё. Вот что происходит под капотом:

  1. environment_factory=EchoToolEnv: тренер создаёт один экземпляр EchoToolEnv на каждое поколение (передайте класс, а не экземпляр).
  2. reset() вызывается в начале каждого эпизода для инициализации состояния. Возвращает строку наблюдения (или None).
  3. Обнаружение инструментов: Тренер находит все публичные методы экземпляра среды (в данном случае, echo()) и предоставляет их как инструменты для вызова функций. Каждый метод должен иметь корректную docstring с типизированными аргументами, на основе которой тренер формирует схему инструмента.
  4. Многоходовый цикл: тренер генерирует ответ, парсит вызовы инструментов, выполняет echo(), добавляет результат и генерирует снова, пока модель не перестанет вызывать инструменты или не будет достигнута max_completion_length.
  5. Функция вознаграждения: читает env.reward из каждого экземпляра среды после эпизода (до сброса среды).
# Run the example
python examples/grpo_echo/grpo_echo.py

# Customize model and environment URL
python examples/grpo_echo/grpo_echo.py --model Qwen/Qwen3-0.6B --env-host https://openenv-echo-env.hf.space

Ниже приведена кривая вознаграждения из процесса обучения:

Вы можете найти больше готовых примеров OpenEnv в заметке в начале этой страницы.

Как работает environment_factory

GRPOTrainer в TRL поддерживает обучение в интерактивных средах с помощью аргумента environment_factory. При его указании тренер автоматически управляет многоходовым циклом вызова инструментов: он генерирует завершения, парсит вызовы инструментов, выполняет их в среде и возвращает результаты модели. Всё это без необходимости писать собственный код для rollout.

Требования к классу Environment

Класс вашего окружения должен соблюдать следующие правила:

  • __init__(self) (необязательно): если указан, не должен принимать аргументов. Используйте его для инициализации состояния или клиентов. Если требуется внешняя конфигурация (например, URL), захватите её из внешнего окружения или из переменных на уровне модуля.
  • reset(self, **kwargs): Вызывается в начале каждого эпизода. Получает все столбцы набора данных в виде именованных аргументов. Возвращает строковое наблюдение (или None, если начальное наблюдение отсутствует).
  • Методы инструментов: любой публичный метод (не начинающийся с _), кроме reset, автоматически становится инструментом. Каждый метод инструмента должен иметь docstring с описаниями в разделе Args:, так как тренер использует их для генерации схемы инструмента для модели.

Советы по классам окружения

  • Состояние для вознаграждения: вы можете хранить любое состояние в экземпляре среды (например, self.reward, self.done и т. д.) и обращаться к нему в функции вознаграждения через параметр environments. Пример этого подхода приведён в руководстве для быстрого старта.
  • Обработка ошибок: если метод инструмента вызывает исключение (например, ValueError("Game over.")), тренер перехватывает его и возвращает сообщение об ошибке модели в качестве ответа инструмента. Это рекомендуемый способ сообщить, что действие недействительно или эпизод завершён.
ENV_URL = "https://my-env.hf.space"

class MyEnv:
    def __init__(self):
        self.client = MyClient(base_url=ENV_URL)  # captured from enclosing scope
        self.reward = 0.0

    def reset(self, **kwargs) -> str | None:
        self.reward = 0.0
        return "Initial observation for the model"

    def my_tool(self, arg1: str, arg2: int) -> str:
        """
        Description of what this tool does.

        Args:
            arg1: Description of arg1
            arg2: Description of arg2

        Returns:
            The result message.
        """
        self.reward = 1.0
        return "Tool result"

Инструменты должны быть отдельными методами с описательными именами и типизированными аргументами (например, guess(word: str), move(direction: str)). Мы не рекомендуем использовать универсальные методы, такие как step(action), поскольку модели необходимы осмысленные имена инструментов и описания аргументов для обучения вызову инструментов.

Функции вознаграждения

Функции вознаграждения принимают параметр environments (список экземпляров сред), поэтому вы можете получить доступ к любому состоянию, сохранённому в ходе эпизода:

def reward_func(environments, **kwargs) -> list[float]:
    return [env.reward for env in environments]

Для получения дополнительной информации о функциях вознаграждения см. GRPO - Пользовательские функции вознаграждения.

Советы по функциям вознаграждения

Вот несколько моментов, которые, по нашему опыту, оказались полезными при работе с окружениями OpenEnv и GRPO:

  • Убедитесь, что группы различаются. GRPO сравнивает завершения внутри группы, поэтому относительный ранжирование важнее абсолютных значений, а группа, в которой каждое завершение получает одинаковое вознаграждение, не даёт сигнала. Бинарное вознаграждение (1.0 при успехе, 0.0 в противном случае) является чистым, когда модель иногда достигает успеха. Если это происходит редко, добавьте частичный кредит: в примере с Судоку к вознаграждению за решение добавляется доля заполненных клеток, в примере с Wordle — количество зелёных букв в лучшей догадке.
  • Оценивайте конечное состояние, а не путь. По возможности позволяйте среде оценивать результат (например, «решила ли модель головоломку?»), а не проверять, следовала ли она конкретной последовательности действий. Это даёт модели свободу находить собственные стратегии.
  • Проверяйте вознаграждение перед обучением. Проведите несколько эпизодов вручную (см. ноутбук с примером Wordle), чтобы убедиться, что среда возвращает осмысленные значения вознаграждения. Если способная модель не может набрать больше, чем случайная базовая линия, возможно, сигнал вознаграждения требует корректировки.

max_completion_length в многораундовых эпизодах

Параметр max_completion_length ограничивает общее количество токенов во всем многоходовом диалоге (все генерации модели + результаты инструментов вместе), а не только одну генерацию. Для сред с большим количеством ходов (например, Судоку с десятками ходов) может потребоваться увеличить это значение:

args = GRPOConfig(
    max_completion_length=4096,  # default is usually 256-1024, increase for long episodes
    # ...
)

Если эпизоды обрываются (модель останавливается в середине игры), это, вероятно, и есть причина.

Долгие прогоны против Space

Space может закрывать WebSocket-соединения, которые остаются открытыми в течение нескольких часов (мы наблюдали это примерно каждые 2 часа). Клиенты OpenEnv до версии 0.11.0 затем переподключаются к новой сессии окружения, и если игра была в процессе, следующий вызов step() выполняется в игре, которую клиент никогда не сбрасывал, без какой-либо ошибки. Начиная с OpenEnv 0.12.0, клиенты вместо этого вызывают SessionLostError, из-за чего игра завершается с ошибкой. Чтобы избежать обрывов во время обучения, создавайте нового клиента в reset(), чтобы каждое соединение длилось один эпизод, и установите MAX_CONCURRENT_ENVS немного выше количества одновременно играемых игр, поскольку старые сессии освобождаются асинхронно:

def reset(self, **kwargs) -> str | None:
    self.client.close()
    self.client = TextArenaEnv(base_url=env_url).sync()
    result = self.client.reset()
    ...

Расширенный пример: Wordle

Обучим модель игре в Wordle с использованием среды TextArena. Это демонстрирует многоходовое взаимодействие, обработку накопленной обратной связи, разделение вознаграждения на несколько слагаемых и завершение эпизода через исключения.

Учебник OpenEnv Wordle охватывает весь конвейер: данные для SFT, предварительный прогрев SFT, этот шаг GRPO и оценку, включая требования к аппаратному обеспечению и затраты. Версия этого примера в формате ноутбука — grpo_wordle.ipynb.

Среда TextArena

TextArena — это открытая коллекция соревновательных текстовых игр, предназначенная для оценки навыков рассуждения LLM с помощью текстовых игр, таких как Wordle, Snake, Tic-Tac-Toe и других.

image of TextArena

Зачем Wordle?

Wordle — хороший бенчмарк для RL на основе среды, поскольку он требует рассуждений на основе обратной связи, полностью текстовый, и модели с 1 млрд параметров способны улучшать свои результаты в этой игре.

Как работает Wordle? Wordle — это игра в угадывание слов, в которой игроку нужно угадать слово из 5 букв за 6 попыток. После каждой попытки среда предоставляет обратную связь по каждой букве:

G U E S S
X G Y X X

X = буквы нет в слове, G = буква на верном месте (зелёный), Y = буква в слове, но на неверном месте (жёлтый). Здесь «U» стоит на верном месте, а «E» есть в слове, но стоит не там.

Начните с прогрева SFT

Модель Qwen3-1.7B почти никогда не выигрывает в игру, поэтому GRPO, обучаемый на базовой модели, получает мало информации: группа игр, в которой нет ни одной победы, не дает сигнала. Пример начинается с openenv/Qwen3-1.7B-Wordle-SFT, обученной в течение одной эпохи на 1000 играх, сыгранных решателем в том же формате вызова инструментов, с кратким рассуждением перед каждым предположением.

Класс Environment

Класс WordleEnv инкапсулирует клиент TextArena, предоставляет guess() в качестве инструмента и сохраняет данные, необходимые для вычисления награды:

import re

from textarena_env import TextArenaAction, TextArenaEnv

FEEDBACK = re.compile(r"Feedback:\n([A-Z ]+)\n([GYX ]+)")

class WordleEnv:
    def __init__(self):
        self.client = TextArenaEnv(base_url="https://<user>-wordle.hf.space")

    def reset(self, **kwargs) -> str | None:
        result = self.client.reset()
        self._last_full_feedback = result.observation.messages[0].content
        self.done = False
        self.won = False
        self.num_guesses = 0
        self.best_greens = 0
        self.invalid_moves = 0
        return self._last_full_feedback

    def guess(self, guess: str) -> str:
        """
        Make a guess in the Wordle environment.

        Args:
            guess: The guessed word, formatted as '[abcde]'

        Returns:
            The feedback message from the environment.
        """
        if self.done:
            raise ValueError("Game over.")
        result = self.client.step(TextArenaAction(message=guess))
        _full_feedback = result.observation.messages[0].content
        feedback = _full_feedback[len(self._last_full_feedback):]
        self._last_full_feedback = _full_feedback
        if "invalid move" in feedback.lower():
            self.invalid_moves += 1
        else:
            self.num_guesses += 1
            match = FEEDBACK.search(feedback)
            if match:
                self.best_greens = max(self.best_greens, match.group(2).count("G"))
            self.won = result.done and result.reward >= 1.0
        self.done = result.done
        return feedback

Ключевые проектные решения:

  • reset() возвращает начальное сообщение игры в качестве первого наблюдения, которое видит модель.
  • guess() — единственный инструмент. Модель вызывает его каждый ход с пятибуквенным словом.
  • Порезка накопленной обратной связи: TextArena возвращает полную историю игры на каждом ходу. Мы вырезаем только новую часть, чтобы не дублировать контекст.
  • Исключение при завершении: если модель пытается сделать ход после окончания игры, guess() вызывает ValueError. Тренер перехватывает это и возвращает модели "Game over." в качестве ответа инструмента, а также учитывает это в tools/failure_frequency.

Функции вознаграждения и обучение

Собственная награда среды выплачивает одинаковую сумму за победу, независимо от того, было ли сделано 1 или 6 попыток, и начисляет частичные баллы только в конце игры. При включённом режиме размышления политика может научиться думать меньше и использовать все 6 попыток, чтобы дойти до конца. Три компонента награды предотвращают это:

from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer

def reward_win(environments, **kwargs) -> list[float]:
    return [1.0 + 0.5 * (1 - env.num_guesses / 6) if env.won else 0.0 for env in environments]

def reward_partial(environments, **kwargs) -> list[float]:
    return [0.0 if env.won else 0.1 * env.best_greens for env in environments]

def reward_invalid(environments, **kwargs) -> list[float]:
    return [-0.1 * env.invalid_moves for env in environments]

prompt = """You are an expert Wordle solver with deep knowledge of English vocabulary...
Use the tool `guess` to make a guess."""

dataset = Dataset.from_dict({"prompt": [[{"role": "user", "content": prompt}]] * 600})

trainer = GRPOTrainer(
    model="openenv/Qwen3-1.7B-Wordle-SFT",
    reward_funcs=[reward_win, reward_partial, reward_invalid],
    train_dataset=dataset,
    args=GRPOConfig(
        use_vllm=True,
        vllm_mode="colocate",
        chat_template_kwargs={"enable_thinking": True},
        max_completion_length=4096,
        max_tool_calling_iterations=12,
        vllm_importance_sampling_mode="token_truncate",
        num_generations=16,
        per_device_train_batch_size=8,
        gradient_accumulation_steps=8,
        learning_rate=1e-6,
        lr_scheduler_type="constant",
        max_steps=150,
    ),
    environment_factory=WordleEnv,
)
trainer.train()

Чем быстрее наступает победа, тем выше выплата (1.42 за одно угадывание, 1.0 за шесть); проигрыш приносит выплату за лучший ход в партии независимо от того, закончилась ли она, а каждый недействительный ход обходится в 0.1. TRL фиксирует каждый член отдельно, поэтому rewards/reward_win/mean растет вместе с частотой побед.

Запуск примера

Каждый шаг включает 64 игры, по одной сессии среды на игру. Скопируйте Space openenv/wordle и задайте переменную MAX_CONCURRENT_ENVS=64 в вашей копии.

colocate
сервер

Режим Colocate (1 GPU, рекомендуется)

python examples/grpo_wordle/grpo_wordle.py --vllm-mode colocate --env-url https://<user>-wordle.hf.space

Это запускает vLLM в том же процессе, что и обучение, требуя лишь одного GPU. В нашем запуске 150 шагов заняли около 2 часов 15 минут на одном A100 80GB.

Результаты

В ходе нашего запуска, с одним тренировочным прогоном на каждую модель, на одних и тех же 30 словах, сгенерированных жадным алгоритмом:

МодельПобедыУгадываний на победуНеверные предположения
Qwen3-1.7B0/30-24
+ SFT10/303.241
+ SFT + GRPO (модель)20/304.13

GRPO в основном устранил повторяющиеся и выдуманные слова и выиграл больше игр. Большинство новых побед было достигнуто за 4–6 попыток, поэтому среднее число попыток на победу выросло. 30 слов взяты из 156 существительных, из которых TextArena выбирает свои секреты; модели также видели эти слова во время обучения, и новый запуск даст результат с разницей в несколько побед (два запуска SFT с тем же рецептом выиграли 10 и 13 игр соответственно). Посмотрите, как играют три модели, в Wordle Arena и изучите кривые обучения на Trackio.

Обучение в нескольких средах

Вы можете обучать одну модель одновременно в нескольких средах. Это полезно, когда вы хотите, чтобы модель параллельно осваивала разные навыки. Например, игра в Wordle (языковое рассуждение) и Catch (пространственное рассуждение) в одном прогоне обучения.

Ключевая идея состоит в создании класса мета-окружения, который оборачивает несколько окружений и направляет каждый образец в правильное окружение с помощью столбца набора данных.

Как это работает

  1. Добавьте в ваш набор данных столбец "env" (или аналогичный), который указывает, к какой среде относится каждый пример.
  2. В reset(**kwargs) прочитайте kwargs["env"], чтобы выбрать активное окружение для данного эпизода.
  3. Опубликовать инструменты из всех сред; тренер обнаруживает все публичные методы.
  4. Используйте отдельные функции вознаграждения для каждой среды, возвращая None для образцов, которые не относятся к данной среде. TRL обрабатывает значения None с помощью nansum/nanmean.

Пример: Wordle + Catch

Скрипт grpo_multi_env.py обучает модель одновременно на Wordle и Catch:

class MultiEnv:
    def __init__(self):
        self._wordle_client = None
        self._catch_client = None
        self.active = None
        self.reward = 0.0
        self.done = False

    def reset(self, **kwargs) -> str | None:
        self.active = kwargs.get("env", "wordle")
        self.reward = 0.0
        self.done = False

        if self.active == "wordle":
            if self._wordle_client is not None:
                try:
                    self._wordle_client.close()
                except Exception:
                    pass
            self._wordle_client = TextArenaEnv(base_url=WORDLE_URL)
            result = self._wordle_client.reset()
            self._last_full_feedback = result.observation.messages[0].content
            self.reward = 0.0
            return self._last_full_feedback
        elif self.active == "catch":
            if self._catch_client is not None:
                try:
                    self._catch_client.close()
                except Exception:
                    pass
            self._catch_client = OpenSpielEnv(base_url=CATCH_URL)
            result = self._catch_client.reset()
            self.done = result.observation.done
            return _format_catch_obs(result.observation.info_state)

    # Wordle tool
    def guess(self, guess: str) -> str:
        """Make a guess in the Wordle environment. ..."""
        ...

    # Catch tools
    def move(self, direction: str) -> str:
        """Move the paddle left or right. ..."""
        ...

    def stay(self) -> str:
        """Do nothing and let the ball fall one step. ..."""
        ...

Ключевые паттерны:

  • Ленивая инициализация клиентов: создавайте клиентов в reset(), а не в __init__(), чтобы избежать лишних WebSocket-соединений.
  • Закрытие перед повторным открытием: Закройте предыдущий клиент перед созданием нового, чтобы избежать ошибок, связанных с пропускной способностью сервера.
  • маршрутизация kwargs: столбец "env" из набора данных передаётся в reset() как именованный аргумент.
  • Все инструменты доступны одновременно: модель видит guess, move и stay как доступные инструменты независимо от активного окружения. Если она вызывает неверный инструмент (например, move во время игры Wordle), метод выбрасывает ValueError, который тренер корректно перехватывает. На практике модели учатся использовать правильные инструменты на основе системного промпта.

Функции вознаграждения для каждой среды

Каждая функция вознаграждения возвращает None для образцов из других сред:

def wordle_reward(environments, **kwargs) -> list[float | None]:
    return [env.reward if env.active == "wordle" else None for env in environments]

def catch_reward(environments, **kwargs) -> list[float | None]:
    rewards = []
    for env in environments:
        if env.active != "catch":
            rewards.append(None)
        elif env.done:
            rewards.append(max(env.reward, 0.0))
        else:
            rewards.append(0.0)
    return rewards

TRL внутренне преобразует None в nan и использует nansum/nanmean для агрегации, поэтому каждая оценка вычисляется только соответствующей ей функцией вознаграждения.

Набор данных с маршрутизацией окружения

n = 500
dataset = Dataset.from_dict({
    "prompt": (
        [[{"role": "user", "content": wordle_prompt}]] * n
        + [[{"role": "user", "content": catch_prompt}]] * n
    ),
    "env": ["wordle"] * n + ["catch"] * n,
})

Запуск примера с несколькими средами

python examples/grpo_multi_env/grpo_multi_env.py \
    --wordle-url https://openenv-wordle.hf.space \
    --catch-url https://openenv-openspiel-env.hf.space \
    --vllm-mode colocate \
    --gradient-accumulation-steps 4 \
    --num-generations 8

При обучении в нескольких средах отслеживайте метрики по каждой функции вознаграждения (train/reward_func_0, train/reward_func_1 и т. д.), а не общую метрику train/reward. Общая метрика чередуется между средами и может выглядеть зашумлённой.

Запуск сред

При использовании environment_factory тренер автоматически подключается к серверу среды. Достаточно, чтобы сервер работал. Существует три способа запуска сервера среды OpenEnv:

space
docker
local

Подключение к удалённому Hugging Face Space (самый простой способ)

Большинство примеров скриптов по умолчанию используют размещённый Space (настройка не требуется):

env = EchoEnv(base_url="https://openenv-echo-env.hf.space")

Для обучения сделайте дубликат Space в своём аккаунте, чтобы избежать проблем с конкурентностью. Тренер открывает N одновременных WebSocket-соединений (по одному на каждое поколение), и общие Spaces могут не поддерживать это. Подробности — в разделе Конкурентность сервера.

Каталог сред

Лучший способ ознакомиться с текущим каталогом поддерживаемых сред — посетить официальный каталог OpenEnv catalog.

Чтобы создать собственное окружение, ознакомьтесь с руководством Создание собственного окружения с помощью OpenEnv. Окружения тесно интегрированы с Hub, поэтому вы можете публиковать новые окружения, чтобы сообщество могло их переиспользовать.

Параллелизм сервера

При использовании environment_factory тренер создаёт N экземпляров среды (по одному на каждое поколение), каждый из которых открывает WebSocket-соединение с сервером. По умолчанию серверы OpenEnv допускают только одну одновременную сессию, что приводит к сбоям во время обучения.

Для поддержки параллельного обучения настройте сервер на одновременную обработку запросов:

  1. В файле окружения объявите поддержку одновременных сессий:
SUPPORTS_CONCURRENT_SESSIONS: bool = True
  1. В вашем серверном приложении установите ограничение на параллелизм:
app = create_app(
    create_my_environment,
    MyAction,
    MyObservation,
    max_concurrent_envs=64,  # match or exceed generation_batch_size
)

max_concurrent_envs должно быть ≥ generation_batch_size (по умолчанию per_device_train_batch_size × gradient_accumulation_steps). Например, при gradient_accumulation_steps=64 и размере батча 1 требуется как минимум 64 одновременных сессий.

environment_factory и rollout_func

GRPOTrainer поддерживает два подхода к обучению на основе среды:

  • environment_factory (рекомендуется): вы определяете класс среды с методами инструментов, а тренер автоматически обрабатывает генерацию, парсинг вызовов инструментов и многоходовой цикл. Именно этот подход используется во всём руководстве.
  • rollout_func: вы сами пишете весь цикл генерации и взаимодействия с окружением. Это даёт полный контроль над тем, как создаются завершения, как выполняются инструменты и как вычисляются награды.

Используйте rollout_func, если environment_factory не подходит для вашей задачи. Например, для внешних серверов агентов, где внешний сервер управляет циклом генерации и собственным протоколом взаимодействия агента с окружением.

Обучение на harnesses: обучение реальным кодинг-агентам (Harbor)

Перечисленные выше интеграции являются white-box: TRL самостоятельно управляет многоходовым циклом. Он генерирует каждый ход, парсит вызовы инструментов, выполняет их и возвращает результаты.

Некоторые агенты нельзя управлять таким образом, поскольку они владеют собственным циклом. Промышленный каркас для кодинг-агента, такой как opencode или claude-code, имеет собственный планировщик, набор инструментов, управление контекстом и условие остановки. Вам нужно обучить именно этого агента, а не его переосуществление.

Для этого TRL предоставляет экспериментальный путь black-box (loop-owning), построенный на AsyncGRPOTrainer: HarnessAsyncGRPOTrainer управляет фабрикой сессий OpenEnv. Агенты и их песочницы поступают из Harbor и обслуживаются через OpenEnv с помощью openenv harbor serve, поэтому каждый агент, устанавливаемый Harbor, обучается одинаково: вы выбираете его с помощью --harness. См. examples/async_grpo_harbor для полного примера, включая загрузчик Hugging Face Jobs.

Этот путь требует openenv>=0.7.0, первого релиза с API захвата TrainingTrace: pip install "openenv>=0.7.0".

Как это работает

TRL здесь не выполняет выборку каждого хода. Агент работает до завершения самостоятельно, а TRL считывает результаты его действий:

  1. Сервер OpenEnv Harbor запускает агента на задаче Harbor в песочнице (например, E2B). Каждый вызов модели, который делает агент, проходит через прокси перехвата сервера на ваш сервер vLLM, который записывает собственные идентификаторы токенов промпта и завершения, сэмплированные logprobs и маску потерь для каждого токена.
  2. Когда агент останавливается, собственный верификатор задачи оценивает результат, а TRL получает валидированный TrainingTrace от OpenEnv и строит строки обучения на основе его идентификаторов токенов и масок. TRL не выполняет повторную токенизацию захваченных промптов.
  3. GRPO обучается на этих строках. Награда распространяется на каждый обучаемый токен через групповое относительное преимущество, а новые веса синхронизируются обратно в тот же сервер vLLM.

Каждый rollout выполняется в собственной изолированной сессии, со своим песочным окружением и собственной сессией перехвата. Изоляция важна по двум причинам: прокси должен перехватывать токены именно этого rollout, и один rollout не должен мешать другому. Настройка max_concurrent_rollouts определяет, сколько rollout выполняется одновременно, а значение MAX_CONCURRENT_ENVS сервера должно быть не меньше max_concurrent_rollouts + 1 (фабрика резервирует одно дополнительное соединение для метаданных задачи).

Подключение

HarnessAsyncGRPOTrainer принимает модель, конфигурацию и набор данных так же, как AsyncGRPOTrainer, но вместо reward_funcs, tools и environment_factory используется фабрика сессий. Если не указан harness_adapter, агент управляет своим циклом самостоятельно. Трейнер создает фабрику сессий со своей собственной политикой выборки, поэтому вместо готовой фабрики передается вызываемый объект, например partial(HarborSessionFactory, ...). Каждая сессия возвращает TrainingTrace OpenEnv, содержащий точные токены движка, лог-вероятности поведения и предзаполненные маски. Параметр rollout_reward_fn является необязательным; если он не задан, TRL использует награду верификатора сессии.

from functools import partial

from harbor_env.harness import HarborSessionFactory

from trl import AsyncGRPOConfig
from trl.experimental.async_grpo.openenv_harness import HarnessAsyncGRPOTrainer

factory = partial(
    HarborSessionFactory,
    "http://localhost:8200",       # the OpenEnv Harbor server
    split="<hf-dataset>",          # a Harbor task dataset the server serves
    harness="mini-swe-agent",      # or "opencode", "claude-code", ...
    sandbox="e2b",
    llm_url=vllm_url,
    model=model,
)
config = AsyncGRPOConfig(num_generations=8, max_concurrent_rollouts=8, vllm_server_base_url=vllm_url)
trainer = HarnessAsyncGRPOTrainer(
    model=model,
    harness_session_factory=factory,  # called by the trainer with its sampling policy
    args=config,
    train_dataset=dataset,
    rollout_reward_fn=my_reward,      # outcome -> float | None
)
trainer.train()

Что нужно определить

OpenEnv отвечает за валидацию захвата и выбор токенов. TRL преобразует маску полной последовательности производителя во внутреннюю маску завершения, упаковывает полученные строки и применяет GRPO. Он не восстанавливает токены и не определяет, какие вызовы принадлежат агенту.

rollout_reward_fn : как оценить rollout

Callable[[HarnessRolloutOutcome], float | None]

TRL не знает, что означает успех для вашей задачи, поэтому вы превращаете каждый завершённый rollout в скалярное вознаграждение. Функция принимает HarnessRolloutOutcome, описывающий действия агента:

  • env_reward (float | None): награда от верификатора задачи или None, если rollout не удалось оценить.
  • completion (list[dict]): итоговый протокол сообщений.
  • trace (list[TraceEntry]): выбранные записи захвата OpenEnv, включая ходы с нулевой маской.
  • tool_call_count и tool_failure_count (int): количество вызовов инструментов, сделанных агентом, и количество вызовов, похожих на неудачные.
  • tool_calls_by_name (dict[str, int]): количество вызовов для каждого инструмента, например {"bash": 3, "edit": 2}.
  • timed_out (bool): указывает, исчерпан ли временной бюджет агента.

Возвращает float или None, чтобы пометить rollout как не поддающийся оценке, в результате чего он исключается из группового базового значения, а не учитывается как ноль. Если эта функция не передана, используется исходное значение env_reward. В примере Harbor правильность верификатора сохраняется как основной сигнал, а небольшой бонус за эффективность использования инструментов добавляется только в том случае, если задача решена.

Почему бы не использовать верификатор? Верификатор выполняет роль среды и отвечает на один вопрос: «насколько корректным был результат», что позволяет сохранять его чистоту и переиспользовать для оценки. Награда, на которой вы обучаете модель, — это отдельное решение, принимаемое на этапе обучения (бинаризация оценки, штраф за вырожденное поведение, отбрасывание нерасцениваемых прогонов). Кроме того, для неё нужны сигналы, которые верификатор никогда не видит, поскольку верификатор анализирует только итоговый результат, тогда как rollout_reward_fn также получает траекторию (количество вызовов инструментов, timed_out, трассировку). Например, прогон может пройти часть тестов, но так и не выполнить bash; только rollout_reward_fn может это увидеть и наложить штраф.

Квалификация токенов определяется в OpenEnv

session.fetch_training_trace() возвращает валидированный объект TrainingTrace. У каждого хода есть уникальный node_id, prompt_token_ids, completion_token_ids, per_token_logps и обязательная loss_mask для промпта и завершения. Значения в позициях промпта равны нулю. Частичные маски завершения сохраняются, включая нули внутри ответа ассистента.

Например, промпт [10, 11], завершение [20, 21, 22] и маска [0, 0, 1, 0, 1] обучают на токенах 20 и 22. Токен 21 остаётся контекстом. Полностью замаскированное завершение сохраняется для контекста и учёта использования.

Producer исключает вспомогательные вызовы и отменённые повторы. В этом API отсутствуют параметры train_turn_fn и agent_turn_fn. Существующие интеграции должны перенести выбор в свои маски OpenEnv. Верификатор задачи остаётся независимым от допустимости токенов.

Некорректные захваты приводят к сбою на границе OpenEnv и останавливают обучение. Сбои транспорта остаются без возможности оценки. Переписанные истории начинают отдельные строки, сохраняя сэмплированные токены и logprobs. Несколько строк по-прежнему разделяют награду rollout; этот API не изменяет взвешивание в GRPO.

Метрики

Тренажёр harness записывает метрики инструментов для AsyncGRPOTrainer под теми же именами, с следующими отличиями:

  • Когда агент управляет своим циклом (без harness_adapter), сбои оцениваются по результатам инструментов в последнем запросе: результат считается сбойным, если его текст содержит error, failed, traceback или exception (без учёта регистра), а результаты с одинаковым инструментом и одинаковыми первыми 200 символами учитываются один раз. tools/latency_s и tools/<name>/latency_s не логируются, так как в трассировке OpenEnv нет меток времени.
  • При использовании harness_adapter вызов инструмента считается неудачным, если harness сообщает об ошибке. Время между ответом модели и следующим запросом распределяется поровну между вызовами инструментов в этом ходе и записывается как их tools/latency_s, поэтому в него входит собственное накладное время harness. Последний ход не измеряется.
  • tools/unknown_name_count, tools/parallel_calls_mean и tools/loop_exhausted_frac поступают из встроенного цикла инструментов и не записываются в журнал.
  • Сам по себе trainer не имеет инструментов, поэтому tools/call_frequency и tools/failure_frequency учитывают только те группы, в которых хотя бы один rollout вызвал инструмент.
  • rollout/correctness_mean записывается в журнал только при наличии rollout_reward_fn: это среднее значение env_reward верификатора по всем rollout'ам, для которых оно определено.

Требования

Сервер OpenEnv Harbor владеет набором данных и песочницами. Ему необходимы учётные данные бэкенда песочниц (например, E2B_API_KEY):

MAX_CONCURRENT_ENVS=9 openenv harbor serve --dataset <hf-dataset> --port 8200 --capture-port 8300 --expose gradio

Сервер vLLM должен поддерживать вызов инструментов и реальные идентификаторы токенов, а также включать синхронизацию весов NCCL, чтобы агент всегда обращался к текущей политике. Для генерации и обновления весов необходимо использовать один и тот же сервер vLLM:

vllm serve <model> \
    --enable-auto-tool-choice --tool-call-parser <parser> \
    --logprobs-mode processed_logprobs \
    --return-tokens-as-token-ids \
    --weight-transfer-config '{"backend":"nccl"}'

Обучение с управлением циклом находится в trl.experimental, и его API может измениться. В примере зафиксирована ревизия OpenEnv, с которой он был протестирован; точная настройка описана в его README.

Update on GitHub