TRL documentation

Intégration d'OpenEnv pour l'entraînement de LLM avec des environnements

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v1.17.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Intégration d'OpenEnv pour l'entraînement de LLM avec des environnements

OpenEnv est un framework open source pour définir, déployer et interagir avec des environnements dans le cadre de l’apprentissage par renforcement (RL) et des workflows agentiques. Il fournit des API standardisées pour l’interaction avec les environnements et prend en charge l’exécution des environnements en tant que serveurs backend (via WebSocket ou exécution conteneurisée). Vous pouvez trouver une collection d’environnements OpenEnv prêts à l’emploi sur le Hugging Face Hub.

Ce guide explique comment intégrer OpenEnv avec TRL. Pour en savoir plus sur OpenEnv lui-même, consultez la documentation OpenEnv.

Exemples OpenEnv prêts à l’emploi : grpo_echo (minimal), grpo_catch, grpo_wordle, grpo_sudoku, grpo_multi_env, grpo_browsergym, grpo_carla, et async_grpo_harbor (local et HF Jobs).

Quand utiliser les environnements

GRPOTrainer peut être utilisé pour entraîner des agents. Pour les tâches agentiques, il prend en charge deux modes : tools, où le modèle peut appeler des fonctions externes mais où chaque appel est sans état et indépendant, et environments, qui maintiennent un état entre les tours, permettant une véritable interaction multi-tours où les actions de l’agent façonnent les observations futures. Utilisez des environnements lorsque la continuité est importante — par exemple, pour naviguer dans un jeu, parcourir une page web, ou pour toute tâche où ce que l’agent observe ensuite dépend de ce qu’il a fait auparavant.

Choisir une intégration d'environnement

OpenEnv est le chemin natif documenté ici. Deux intégrations supplémentaires — OpenReward et Harbor — respectent le même contrat environment_factory et sont interchangeables au niveau de TRL. Consultez la comparaison des intégrations d’environnements dans le guide GRPO pour choisir celle dont l’écosystème correspond à votre tâche.

Installation

Les environnements OpenEnv sont hébergés sous forme d'espaces Hugging Face, qui sont également des dépôts Git installables via 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"

Cela installe le client d’environnement (par exemple, EchoEnv) qui communique avec le serveur d’environnement distant via WebSocket, ainsi que les modèles d’action/observation et toutes les dépendances requises (y compris openenv).

Vous pouvez trouver la commande d’installation pour tout environnement sur sa page HF Space. Cliquez sur le menu ⋮ (trois points) et sélectionnez « Utiliser cet espace » pour afficher les instructions d’installation.

Vous pouvez également installer le package principal depuis PyPI avec pip install "openenv>=0.3.1", mais notez que les dépendances spécifiques à l'environnement peuvent nécessiter une installation séparée.

Pour le développement, vous pouvez cloner le dépôt OpenEnv et l’installer localement :

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

Chaque script d’environnement dans TRL inclut des métadonnées de dépendances en ligne (PEP 723), ce qui vous permet également de les exécuter directement avec uv :

uv run examples/grpo_echo/grpo_echo.py

Cela installe automatiquement le package d'environnement requis dans un environnement virtuel isolé.

Démarrage rapide

La façon la plus rapide de comprendre l'intégration est un exemple complet. Le script echo.py entraîne un modèle avec l'environnement Echo, qui récompense les complétions en fonction de leur longueur de texte :

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

from trl import GRPOConfig, GRPOTrainer

ENV_URL = "https://huggingface.co/proxy/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()

C’est tout. Voici ce qui se passe en coulisses :

  1. environment_factory=EchoToolEnv : le formateur crée une instance EchoToolEnv par génération (passez la classe, pas une instance).
  2. reset() est appelé au début de chaque épisode pour initialiser l'état. Renvoie une chaîne d'observation (ou None).
  3. Découverte des outils : le formateur détecte toutes les méthodes publiques de l'instance d'environnement (ici, echo()) et les expose en tant qu'outils d'appel de fonction. Chaque méthode doit disposer d'une docstring appropriée avec des arguments typés, que le formateur utilise pour construire le schéma de l'outil.
  4. Boucle multi-tours : le formateur génère une complétion, analyse les appels d’outils, exécute echo(), ajoute le résultat et génère à nouveau, jusqu’à ce que le modèle cesse d’appeler des outils ou que max_completion_length soit atteint.
  5. Fonction de récompense : lit env.reward de chaque instance d'environnement après l'épisode (avant la réinitialisation de l'environnement).
# 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://huggingface.co/proxy/openenv-echo-env.hf.space

Voici la courbe de récompense issue de l'entraînement :

Vous pouvez explorer d'autres exemples OpenEnv prêts à l'emploi dans la note en haut de cette page.

Fonctionnement de environment_factory

Le GRPOTrainer de TRL prend en charge l’entraînement dans des environnements interactifs grâce à l’argument environment_factory. S’il est fourni, le formateur gère automatiquement la boucle d’appels d’outils multi-tours : il génère des complétions, analyse les appels d’outils, les exécute dans l’environnement et renvoie les résultats au modèle. Le tout sans code de déroulement personnalisé.

Exigences de la classe Environment

Votre classe d'environnement doit respecter ces règles :

  • __init__(self) (facultatif) : s'il est fourni, il ne doit prendre aucun argument. Utilisez-le pour initialiser l'état ou les clients. Si vous avez besoin d'une configuration externe (par exemple, une URL), capturez-la depuis la portée englobante ou des variables de niveau module.
  • reset(self, **kwargs) : appelé au début de chaque épisode. Reçoit toutes les colonnes du jeu de données en tant qu'arguments nommés. Retourne une observation sous forme de chaîne de caractères (ou None en l'absence d'observation initiale).
  • Méthodes d’outil : toute méthode publique (ne commençant pas par _) autre que reset est automatiquement exposée en tant qu’outil. Chaque méthode d’outil doit avoir une docstring avec des descriptions Args:, car l’entraîneur utilise celles-ci pour générer le schéma de l’outil pour le modèle.

Conseils pour les classes d'environnement

  • État pour la récompense : vous pouvez stocker n'importe quel état sur l'instance de l'environnement (par exemple, self.reward, self.done, etc.) et y accéder dans votre fonction de récompense via le paramètre environments. Consultez le guide de démarrage rapide pour un exemple de ce motif.
  • Gestion des erreurs : si une méthode d'outil lève une exception (par exemple, ValueError("Game over.")), l'entraîneur la capture et renvoie le message d'erreur au modèle en tant que réponse d'outil. C'est la méthode recommandée pour signaler qu'une action est invalide ou que l'épisode est terminé.
ENV_URL = "https://huggingface.co/proxy/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"

Les outils doivent être des méthodes individuelles avec des noms descriptifs et des arguments typés (par exemple, guess(word: str), move(direction: str)). Nous ne recommandons pas l'utilisation de méthodes génériques comme step(action), car le modèle a besoin de noms d'outils et de descriptions d'arguments significatifs pour apprendre l'appel d'outils.

Fonctions de récompense

Les fonctions de récompense reçoivent le paramètre environments (une liste d’instances d’environnements), ce qui vous permet d’accéder à tout état stocké pendant l’épisode :

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

Pour plus d'informations sur les fonctions de récompense, consultez GRPO - Fonctions de récompense personnalisées.

Conseils pour les fonctions de récompense

Voici quelques éléments que nous avons trouvés utiles lors de notre travail avec les environnements OpenEnv et GRPO :

  • Vérifiez que les groupes diffèrent. GRPO compare les complétions au sein d’un groupe, de sorte que le classement relatif compte plus que les valeurs absolues, et un groupe où chaque complétion reçoit la même récompense ne fournit aucun signal. Une récompense binaire (1,0 en cas de succès, 0,0 sinon) est claire lorsque le modèle réussit parfois. S’il réussit rarement, ajoutez une note partielle : l’exemple Sudoku ajoute la fraction de cases remplies à la récompense de résolution, l’exemple Wordle ajoute les verts de la meilleure devinette.
  • Vérifiez l’état final, pas le chemin parcouru. Lorsque c’est possible, laissez l’environnement évaluer le résultat (par exemple, « le modèle a-t-il résolu l’énigme ? ») plutôt que de vérifier s’il a suivi une séquence d’actions spécifique. Cela laisse au modèle la liberté de découvrir ses propres stratégies.
  • Testez votre récompense avant l’entraînement. Exécutez manuellement quelques épisodes (voir le cahier d’exemples Wordle) pour vous assurer que l’environnement renvoie des récompenses cohérentes. Si un modèle performant ne parvient pas à obtenir un score supérieur à une ligne de base aléatoire, le signal de récompense peut nécessiter un ajustement.

max_completion_length dans les épisodes multi-tours

Le paramètre max_completion_length limite le nombre total de jetons sur l'ensemble de la conversation multi-tours (toutes les générations du modèle + les résultats des outils combinés), et non pas une seule génération. Pour les environnements comportant de nombreux tours (par exemple, le Sudoku avec des dizaines de coups), il peut être nécessaire de l'augmenter :

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

Si les épisodes sont interrompus prématurément (le modèle s’arrête en cours de partie), c’est probablement la cause.

Longues exécutions contre un Space

Un Space peut fermer les WebSockets qui restent ouverts pendant des heures (nous avons constaté que cela se produisait environ toutes les 2 heures). Les clients OpenEnv jusqu’à la version 0.11.0 se reconnectent alors à une nouvelle session d’environnement, et si une partie était en cours, la prochaine step() s’exécute sur une partie que le client n’a jamais réinitialisée, sans erreur. Depuis OpenEnv 0.12.0, les clients lèvent SessionLostError à la place, de sorte que la partie échoue. Pour éviter que ces coupures n’affectent l’entraînement, ouvrez un nouveau client dans reset(), afin que chaque connexion dure un épisode, et définissez MAX_CONCURRENT_ENVS légèrement au-dessus du nombre de parties jouées simultanément, car les anciennes sessions sont libérées de manière asynchrone :

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

Exemple avancé : Wordle

Entraînons un modèle pour jouer à Wordle à l’aide de l’environnement TextArena. Cela illustre l’interaction multi-tours, la gestion des retours cumulés, une récompense découpée en plusieurs termes et la fin d’un épisode via des exceptions.

Le tutoriel Wordle d’OpenEnv couvre tout le pipeline : les données SFT, le réchauffement SFT, cette étape GRPO et l’évaluation, avec le matériel et les coûts. La version notebook de cet exemple est grpo_wordle.ipynb.

L'environnement TextArena

TextArena est une collection open source de jeux textuels compétitifs conçue pour évaluer les capacités de raisonnement des LLM à l’aide de jeux textuels tels que Wordle, Snake, Tic-Tac-Toe et bien d’autres.

image of TextArena

Pourquoi Wordle ?

Wordle constitue un bon benchmark pour le RL basé sur l’environnement, car il nécessite un raisonnement sur les retours, est purement textuel et les modèles de 1 Md de paramètres peuvent s’y améliorer.

Comment fonctionne Wordle ? Wordle est un jeu de devinette où le joueur doit deviner un mot de 5 lettres en 6 essais. Après chaque essai, l'environnement fournit un retour d'information lettre par lettre :

G U E S S
X G Y X X

X = lettre absente du mot, G = position correcte (vert), Y = position incorrecte (jaune). Ici, « U » est correct et bien placé, « E » est dans le mot mais mal placé.

Commencer par un échauffement SFT

Qwen3-1.7B gagne presque jamais une partie, si bien que le GRPO à partir du modèle de base a peu à apprendre : un groupe de parties où aucune n’est gagnée ne fournit aucun signal. L’exemple part de openenv/Qwen3-1.7B-Wordle-SFT, entraîné pendant une époque sur 1 000 parties jouées par un solveur dans le même format d’appel d’outils, avec un court raisonnement avant chaque essai.

Classe Environment

La classe WordleEnv encapsule le client TextArena, expose guess() en tant qu’outil et conserve ce dont la récompense a besoin :

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

Choix de conception clés :

  • reset() renvoie le message de jeu initial en tant que première observation perçue par le modèle.
  • guess() est l’unique outil. Le modèle l’appelle à chaque tour avec un mot de 5 lettres.
  • Tronçonnage du feedback cumulé : TextArena renvoie l'historique complet de la partie à chaque tour. Nous ne conservons que la nouvelle partie pour éviter de répéter le contexte.
  • Exception on done : si le modèle tente de deviner après la fin du jeu, guess() lève une ValueError. L'entraîneur intercepte cette exception et renvoie "Game over." au modèle en tant que réponse d'outil, et l'incrémente dans tools/failure_frequency.

Fonctions de récompense et entraînement

La récompense propre à l’environnement verse un gain identique après 1 ou 6 tentatives, et n’attribue un crédit partiel qu’à la fin d’une partie. Avec la réflexion activée, une politique peut apprendre à réfléchir moins et à utiliser les 6 tentatives pour atteindre la fin. Trois termes de récompense évitent cela :

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()

Un gain rapporte d’autant plus qu’il survient rapidement (1,42 en une seule tentative, 1,0 en six), une perte est compensée par la meilleure estimation de la partie, qu’elle ait pris fin ou non, et chaque mouvement invalide coûte 0,1. TRL journalise chaque terme séparément, de sorte que rewards/reward_win/mean augmente avec le taux de victoire.

Exécution de l'exemple

Chaque étape joue 64 parties, soit une session d’environnement chacune. Dupliquez le Space openenv/wordle et définissez la variable MAX_CONCURRENT_ENVS=64 sur votre copie.

colocate
server

Mode Colocate (1 GPU, recommandé)

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

Cela exécute vLLM dans le même processus que l’entraînement, n’exigeant qu’un seul GPU. Lors de notre exécution, 150 étapes ont pris environ 2 heures 15 minutes sur une A100 80 Go.

Résultats

À partir de notre exécution, avec une seule exécution d'entraînement par modèle, sur les mêmes 30 mots joués de manière gloutonne :

ModèleVictoiresTentatives par victoireEssais incorrects
Qwen3-1.7B0/30-24
+ SFT10/303.241
+ SFT + GRPO (modèle)20/304.13

GRPO a principalement éliminé les mots répétés et inventés et a remporté plus de parties. La plupart des nouvelles victoires ont nécessité de 4 à 6 essais, ce qui a augmenté le nombre d’essais par victoire. Les 30 mots proviennent des 156 noms dont TextArena tire ses secrets, que les modèles ont également vus lors de l’entraînement, et une nouvelle exécution se situera à quelques victoires près (deux exécutions SFT avec la même recette ont remporté 10 et 13 parties). Regardez les trois modèles jouer dans la Wordle Arena et consultez les courbes d’entraînement sur Trackio.

Entraînement multi-environnements

Vous pouvez entraîner un modèle unique sur plusieurs environnements simultanément. Cela est utile lorsque vous souhaitez qu'un modèle apprenne différentes compétences en parallèle. Par exemple, jouer à Wordle (raisonnement linguistique) et à Catch (raisonnement spatial) dans la même session d'entraînement.

L’idée clé est de créer une classe d’environnement méta qui encapsule plusieurs environnements et achemine chaque échantillon vers le bon en utilisant une colonne du jeu de données.

Fonctionnement

  1. Ajoutez une colonne "env" (ou similaire) à votre jeu de données qui identifie l'environnement auquel chaque échantillon appartient.
  2. Dans reset(**kwargs), lisez kwargs["env"] pour sélectionner l'environnement actif pour cet épisode.
  3. Exposer les outils de tous les environnements ; le formateur découvre toutes les méthodes publiques.
  4. Utilisez des fonctions de récompense distinctes par environnement, en renvoyant None pour les échantillons qui n’appartiennent pas à cet environnement. TRL gère les valeurs None avec nansum/nanmean.

Exemple : Wordle + Catch

Le script grpo_multi_env.py entraîne simultanément sur Wordle et 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. ..."""
        ...

Principaux motifs :

  • Initialisation paresseuse des clients : créez les clients dans reset(), et non dans __init__(), afin d'éviter les connexions WebSocket inutiles.
  • Fermer avant de rouvrir : Fermez le client précédent avant d'en créer un nouveau afin d'éviter les erreurs de capacité du serveur.
  • Routing des kwargs : la colonne "env" du jeu de données est transmise à reset() en tant qu'argument par mot-clé.
  • Tous les outils sont exposés simultanément : le modèle voit guess, move et stay comme des outils disponibles, quel que soit l'environnement actif. S'il appelle le mauvais outil (par exemple move pendant Wordle), la méthode lève une ValueError que l'entraîneur gère proprement. En pratique, les modèles apprennent à utiliser les bons outils en se basant sur le prompt système.

Fonctions de récompense par environnement

Chaque fonction de récompense renvoie None pour les échantillons provenant d'autres environnements :

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 convertit None en nan en interne et utilise nansum/nanmean pour l'agrégation, de sorte que chaque échantillon n'est noté que par sa fonction de récompense pertinente.

Jeu de données avec routage d'environnement

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

Exécution de l’exemple multi-environnement

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

Lors de l’entraînement sur plusieurs environnements, surveillez les métriques par fonction de récompense (train/reward_func_0, train/reward_func_1, etc.) plutôt que la métrique combinée train/reward. La métrique combinée alterne entre les environnements et peut sembler bruitée.

Exécution des environnements

Lors de l'utilisation de environment_factory, le formateur se connecte automatiquement au serveur d'environnement. Il suffit que le serveur soit en cours d'exécution. Il existe trois façons d'exécuter un serveur d'environnement OpenEnv :

space
docker
local

Se connecter à un Space Hugging Face distant (le plus simple)

La plupart des scripts d'exemple utilisent par défaut un Space hébergé (aucune configuration requise) :

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

Pour l’entraînement, dupliquez le Space sur votre propre compte afin d’éviter les problèmes de concurrence. L’entraîneur ouvre N connexions WebSocket simultanées (une par génération), et les Spaces partagés peuvent ne pas prendre en charge cette configuration. Consultez Concurrence du serveur pour plus de détails.

Catalogue des environnements

La meilleure façon d'explorer le catalogue actuel des environnements maintenus est de consulter le catalogue officiel d'OpenEnv.

Pour créer votre propre environnement, consultez le guide sur Building Your Own Environment with OpenEnv. Les environnements sont étroitement intégrés au Hub, vous permettant de publier de nouveaux environnements pour que la communauté puisse les réutiliser.

Concurrence du serveur

Lors de l’utilisation de environment_factory, le formateur crée N instances d’environnement (une par génération), chacune ouvrant une connexion WebSocket vers le serveur. Par défaut, les serveurs OpenEnv n’autorisent qu’une seule session concurrente, ce qui provoque des échecs pendant l’entraînement.

Pour prendre en charge l'entraînement parallèle, configurez le serveur pour la concurrence :

  1. Dans votre fichier d’environnement, déclarez la prise en charge des sessions concurrentes :
SUPPORTS_CONCURRENT_SESSIONS: bool = True
  1. Dans votre application serveur, définissez la limite de concurrence :
app = create_app(
    create_my_environment,
    MyAction,
    MyObservation,
    max_concurrent_envs=64,  # match or exceed generation_batch_size
)

max_concurrent_envs doit être ≥ generation_batch_size (dont la valeur par défaut est per_device_train_batch_size × gradient_accumulation_steps). Par exemple, avec gradient_accumulation_steps=64 et une taille de lot de 1, il faut au moins 64 sessions concurrentes.

environment_factory par rapport à rollout_func

GRPOTrainer prend en charge deux approches pour l'entraînement basé sur des environnements :

  • environment_factory (recommandé) : vous définissez une classe d’environnement avec des méthodes d’outils, et le formateur gère automatiquement la génération, l’analyse des appels d’outils et la boucle multi-tours. C’est l’approche utilisée tout au long de ce guide.
  • rollout_func : vous écrivez vous-même la boucle complète de génération et d’interaction avec l’environnement. Cela offre un contrôle total sur la manière dont les complétions sont produites, dont les outils sont exécutés et dont les récompenses sont calculées.

Utilisez rollout_func lorsque environment_factory ne convient pas à votre cas d’usage. Par exemple, pour des serveurs d’agents externes où un serveur externe gère la boucle de génération et son propre protocole d’interaction agent-environnement.

Entraînement sur des harnais : entraînement d'agents de codage réels (Harbor)

Les intégrations ci-dessus sont white-box : TRL gère lui-même la boucle multi-tours. Il échantillonne chaque tour, analyse les appels d’outils, les exécute et renvoie les résultats.

Certains agents ne peuvent pas être pilotés de cette manière car ils possèdent leur propre boucle. Un harnais d’agent de codage de production tel que opencode ou claude-code dispose de son propre planificateur, de son propre ensemble d’outils, de sa propre gestion du contexte et de sa propre condition d’arrêt. Vous souhaitez entraîner cet agent précis, et non une réimplémentation de celui-ci.

À cet effet, TRL propose un parcours expérimental boîte noire (propriétaire de la boucle) basé sur experimental.async_grpo.AsyncGRPOTrainer : HarnessAsyncGRPOTrainer pilote une fabrique de sessions OpenEnv. Les agents et leurs environnements isolés proviennent de Harbor, servis via OpenEnv avec openenv harbor serve, de sorte que chaque agent installé par Harbor est entraînable de la même manière : vous en choisissez un avec --harness. Consultez examples/async_grpo_harbor pour un exemple complet, y compris un lanceur Hugging Face Jobs.

Ce chemin nécessite openenv>=0.7.0, la première version à inclure l’API de capture TrainingTrace : pip install "openenv>=0.7.0".

Fonctionnement

TRL ne procède pas à un échantillonnage de chaque tour ici. L'agent s'exécute jusqu'à son terme de manière autonome, et TRL lit ensuite ce qu'il a fait :

  1. Le serveur OpenEnv Harbor démarre l’agent sur une tâche Harbor dans un bac à sable (par exemple E2B). Chaque appel au modèle effectué par l’agent passe par le proxy de capture du serveur vers votre serveur vLLM, qui enregistre les identifiants de jetons de prompt et de complétion de son propre moteur, les logprobs échantillonnés et un masque de perte par jeton.
  2. Lorsque l’agent s’arrête, le vérificateur propre à la tâche évalue le résultat, et TRL reçoit la TrainingTrace validée d’OpenEnv et construit les lignes d’entraînement à partir de ses identifiants de jetons et de ses masques. TRL ne rétokenise pas les invites capturées.
  3. GRPO s’entraîne sur ces lignes. La récompense est propagée à chaque jeton entraîné via l’avantage relatif au groupe, et les nouveaux poids sont resynchronisés dans le même serveur vLLM.

Chaque déroulement s’exécute dans sa propre session isolée, avec son propre bac à sable et sa propre session de capture. L’isolement est important pour deux raisons : le proxy doit capturer exactement les jetons de ce déroulement, et un déroulement ne doit pas interférer avec un autre. Le paramètre max_concurrent_rollouts contrôle le nombre de déroulements exécutés simultanément, et le MAX_CONCURRENT_ENVS du serveur doit être au moins égal à max_concurrent_rollouts + 1 (la fabrique conserve une connexion supplémentaire pour les métadonnées de la tâche).

Câblage

HarnessAsyncGRPOTrainer prend le modèle, la configuration et le jeu de données comme experimental.async_grpo.AsyncGRPOTrainer, avec une fabrique de sessions à la place de reward_funcs, tools et environment_factory. Sans harness_adapter, l’agent possède sa propre boucle. L’entraîneur construit la fabrique de sessions avec sa propre politique d’échantillonnage, vous devez donc lui passer un appelable tel que partial(HarborSessionFactory, ...) plutôt qu’une fabrique déjà construite. Chaque session renvoie une TrainingTrace OpenEnv contenant les jetons exacts du moteur, les logprobs du comportement et les masques préremplis. rollout_reward_fn est facultatif ; sans lui, TRL utilise la récompense du vérificateur de la session.

from functools import partial

from harbor_env.harness import HarborSessionFactory

from trl.experimental.async_grpo 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()

Ce que vous devez définir

OpenEnv gère la validation des captures et la sélection des jetons. TRL convertit le masque de séquence complète du producteur en son masque de complétion interne, regroupe les lignes résultantes et applique GRPO. Il ne reconstruit pas les jetons ni ne déduit quels appels appartiennent à l’agent.

rollout_reward_fn : comment évaluer un déroulement

Callable[[HarnessRolloutOutcome], float | None]

TRL ne sait pas ce que le succès signifie pour votre tâche, vous convertissez donc chaque déroulement terminé en une récompense scalaire. La fonction reçoit un HarnessRolloutOutcome qui décrit ce que l'agent a fait :

  • env_reward (float | None) : la récompense issue du vérificateur de la tâche, ou None si le déroulement n’a pas pu être évalué.
  • completion (list[dict]) : la transcription finale du message.
  • trace (list[TraceEntry]) : les enregistrements de capture OpenEnv sélectionnés, y compris les tours masqués à zéro.
  • tool_call_count et tool_failure_count (int) : le nombre d'appels d'outils effectués par l'agent et le nombre d'appels qui semblaient être des échecs.
  • tool_calls_by_name (dict[str, int]) : nombre d'appels par outil, par exemple {"bash": 3, "edit": 2}.
  • timed_out (bool) : indique si l'agent a épuisé son budget de temps.

Retourner un float, ou retourner None pour marquer le rollout comme non notable, afin qu’il soit exclu de la référence du groupe plutôt que d’être compté comme un zéro. Si vous ne passez pas cette fonction, la valeur brute de env_reward est utilisée telle quelle. L’exemple Harbor conserve la justesse du vérificateur comme signal principal et n’ajoute un petit bonus d’efficacité des outils que lorsque la tâche est résolue.

Pourquoi ne pas simplement utiliser le vérificateur ? Le vérificateur est la responsabilité de l’environnement et répond à une seule question, « à quel point le résultat était-il correct », ce qui le garde propre et réutilisable pour l’évaluation. La récompense sur laquelle vous entraînez le modèle est une décision distincte, prise au moment de l’entraînement (binariser le score, pénaliser les comportements dégénérés, exclure les déroulements non évaluables). Elle a également besoin de signaux que le vérificateur ne voit jamais, car le vérificateur n’examine que le résultat final, tandis que rollout_reward_fn reçoit également la trajectoire (nombre d’outils, timed_out, la trace). Par exemple, un déroulement peut passer certains tests sans jamais exécuter bash ; seul rollout_reward_fn peut le constater et l’en pénaliser.

L'éligibilité des jetons provient d'OpenEnv

session.fetch_training_trace() renvoie un TrainingTrace validé. Chaque tour possède un node_id, des prompt_token_ids, des completion_token_ids, des per_token_logps et un loss_mask obligatoire couvrant le prompt et la complétion. Les positions du prompt sont nulles. Les masques de complétion partiels sont conservés, y compris les zéros à l’intérieur d’une réponse de l’assistant.

Par exemple, le prompt [10, 11], la complétion [20, 21, 22] et le masque [0, 0, 1, 0, 1] entraînent sur les jetons 20 et 22. Le jeton 21 reste en contexte. Une complétion entièrement masquée est conservée pour le contexte et la comptabilisation de l’utilisation.

Le producteur exclut les appels auxiliaires et les tentatives abandonnées. Cette API ne comporte pas de paramètres train_turn_fn ou agent_turn_fn. Les intégrations existantes doivent déplacer la sélection dans leurs masques OpenEnv. Le vérificateur de la tâche reste indépendant de l’éligibilité des jetons.

Les captures malformées échouent à la frontière OpenEnv et arrêtent l’entraînement. Les échecs de transport restent non notables. Les historiques réécrits démarrent de nouvelles lignes, préservant les jetons et les logprobs échantillonnés. Plusieurs lignes partagent toujours la récompense de la rollout ; cette API ne modifie pas le pondération de GRPO.

Prérequis

Le serveur OpenEnv Harbor possède le jeu de données et les environnements d'exécution. Il a besoin des identifiants du backend d'environnements d'exécution (par exemple E2B_API_KEY) :

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

Le serveur vLLM doit exposer l'appel d'outils et les identifiants de jetons réels, et activer la synchronisation des poids NCCL afin que l'agent accède toujours à la politique actuelle. Les déroulements et les mises à jour des poids doivent utiliser le même serveur 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"}'

L’entraînement Loop-owning se trouve sous trl.experimental et son API peut évoluer. L’exemple fixe la révision d’OpenEnv avec laquelle il a été testé ; consultez son README pour la configuration exacte.

Update on GitHub