TRL documentation

Integración de OpenEnv para entrenar LLMs con entornos

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

Integración de OpenEnv para entrenar LLMs con entornos

OpenEnv es un marco de código abierto para definir, desplegar e interactuar con entornos en el aprendizaje por refuerzo (RL) y en flujos de trabajo agénticos. Proporciona APIs estandarizadas para la interacción con entornos y permite ejecutarlos como servidores de backend (mediante WebSocket o ejecución en contenedores). Puedes encontrar una colección de entornos de OpenEnv listos para usar en el Hugging Face Hub.

Esta guía cubre cómo integrar OpenEnv con TRL. Para más información sobre OpenEnv en sí, consulte la documentación de OpenEnv.

Ejemplos de OpenEnv listos para usar: grpo_echo (mínimo), grpo_catch, grpo_wordle, grpo_sudoku, grpo_multi_env, grpo_browsergym, grpo_carla y async_grpo_harbor (local y HF Jobs).

Cuándo usar entornos

GRPOTrainer se puede utilizar para entrenar agentes. Para tareas agénticas, admite dos modos: tools, donde el modelo puede llamar a funciones externas, pero cada llamada es sin estado e independiente, y environments, que mantienen el estado entre turnos, lo que permite una interacción genuina de varios turnos en la que las acciones del agente determinan las observaciones futuras. Utilice entornos cuando la continuidad sea importante, por ejemplo, al navegar por un juego, al explorar una página web o en cualquier tarea donde lo que el agente observa a continuación dependa de lo que hizo antes.

Elegir una integración de entorno

OpenEnv es la ruta nativa documentada aquí. Dos integraciones adicionales, OpenReward y Harbor, cumplen con el mismo contrato environment_factory y son intercambiables a nivel de TRL. Consulta la comparación de integraciones de entornos en la guía de GRPO para elegir la cuyo ecosistema se ajuste a tu tarea.

Instalación

Los entornos de OpenEnv se alojan como Hugging Face Spaces, que también son repositorios Git instalables con 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"

Esto instala el cliente de entorno (p. ej., EchoEnv) que se comunica con el servidor de entorno remoto a través de WebSocket, junto con los modelos de acción/observación y todas las dependencias necesarias (incluido openenv).

Puedes encontrar el comando de instalación para cualquier entorno en su página de HF Space. Haz clic en el menú ⋮ (tres puntos) y selecciona “Use this Space” para ver las instrucciones de instalación.

También puedes instalar el paquete principal desde PyPI con pip install "openenv>=0.3.1", pero ten en cuenta que las dependencias específicas del entorno pueden necesitar instalarse por separado.

Para el desarrollo, puedes clonar el repositorio de OpenEnv e instalarlo localmente:

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

Cada script de entorno en TRL incluye metadatos de dependencias en línea (PEP 723), por lo que también puedes ejecutarlos directamente con uv:

uv run examples/grpo_echo/grpo_echo.py

Esto instala automáticamente el paquete de entorno requerido en un entorno virtual aislado.

Inicio rápido

La forma más rápida de entender la integración es un ejemplo completo. El script echo.py entrena un modelo con el Echo environment, que otorga recompensas a las finalizaciones en función de su longitud de texto:

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

Eso es todo. Esto es lo que ocurre por debajo:

  1. environment_factory=EchoToolEnv: El entrenador crea una instancia de EchoToolEnv por generación (pasa la clase, no una instancia).
  2. reset() se llama al inicio de cada episodio para inicializar el estado. Devuelve una cadena de observación (o None).
  3. Detección de herramientas: El entrenador detecta todos los métodos públicos de la instancia del entorno (en este caso, echo()) y los expone como herramientas de llamada a funciones. Cada método debe tener una docstring adecuada con argumentos tipados, que el entrenador utiliza para construir el esquema de la herramienta.
  4. Bucle de varios turnos: El entrenador genera una respuesta, analiza las llamadas a herramientas, ejecuta echo(), añade el resultado y vuelve a generar, hasta que el modelo deja de llamar a herramientas o se alcanza max_completion_length.
  5. Función de recompensa: Lee env.reward de cada instancia de entorno después del episodio (antes de que el entorno se restablezca).
# 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

A continuación se muestra la curva de recompensa del entrenamiento:

Puedes explorar más ejemplos de OpenEnv listos para usar en la nota en la parte superior de esta página.

Cómo funciona environment_factory

El GRPOTrainer de TRL admite el entrenamiento en entornos interactivos a través del argumento environment_factory. Cuando se proporciona, el entrenador gestiona automáticamente el bucle de llamadas a herramientas de varios turnos: genera completados, analiza las llamadas a herramientas, las ejecuta contra el entorno y devuelve los resultados al modelo. Todo esto sin código de rollout personalizado.

Requisitos de la clase Environment

Su clase de entorno debe seguir estas reglas:

  • __init__(self) (opcional): Si se proporciona, no debe aceptar argumentos. Úsalo para inicializar el estado o los clientes. Si necesitas configuración externa (p. ej., una URL), captúrala desde el ámbito envolvente o desde variables a nivel de módulo.
  • reset(self, **kwargs): Se llama al inicio de cada episodio. Recibe todas las columnas del conjunto de datos como argumentos de palabra clave. Devuelve una observación en formato de cadena (o None si no hay observación inicial).
  • Métodos de herramienta: cualquier método público (que no comience por _) distinto de reset se expone automáticamente como herramienta. Cada método de herramienta debe tener una docstring con descripciones de Args:, ya que el trainer utiliza estas para generar el esquema de la herramienta para el modelo.

Consejos para las clases de entorno

  • Estado para la recompensa: Puede almacenar cualquier estado que desee en la instancia del entorno (p. ej., self.reward, self.done, etc.) y acceder a él en su función de recompensa a través del parámetro environments. Consulte la guía de inicio rápido para ver un ejemplo de este patrón.
  • Gestión de errores: si un método de herramienta lanza una excepción (p. ej., ValueError("Game over.")), el entrenador la captura y devuelve el mensaje de error al modelo como respuesta de la herramienta. Esta es la forma recomendada de indicar que una acción no es válida o que el episodio ha terminado.
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"

Las herramientas deben ser métodos individuales con nombres descriptivos y argumentos tipados (p. ej., guess(word: str), move(direction: str)). No recomendamos el uso de métodos genéricos como step(action), ya que el modelo necesita nombres de herramientas y descripciones de argumentos significativos para aprender a llamar herramientas.

Funciones de recompensa

Las funciones de recompensa reciben el parámetro environments (una lista de instancias de entorno), por lo que puedes acceder a cualquier estado almacenado durante el episodio:

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

Para obtener más información sobre las funciones de recompensa, consulte GRPO - Custom Reward Functions.

Consejos para las funciones de recompensa

Algunas cosas que nos han resultado útiles al trabajar con entornos de OpenEnv y GRPO:

  • Asegúrate de que los grupos difieran. GRPO compara las finalizaciones dentro de un grupo, por lo que el ranking relativo importa más que los valores absolutos, y un grupo donde todas las finalizaciones reciben la misma recompensa no proporciona ninguna señal. Una recompensa binaria (1.0 para éxito, 0.0 en caso contrario) es clara cuando el modelo tiene éxito a veces. Cuando rara vez lo hace, añade crédito parcial: el ejemplo de Sudoku añade la fracción de celdas completadas a la recompensa por resolverlo, y el ejemplo de Wordle añade los verdes de la mejor conjetura.
  • Comprueba el estado final, no el camino. Siempre que sea posible, deja que el entorno juzgue el resultado (p. ej., “¿el modelo resolvió el rompecabezas?”) en lugar de verificar si siguió una secuencia específica de acciones. Esto le da al modelo la libertad de descubrir sus propias estrategias.
  • Prueba tu recompensa antes de entrenar. Ejecuta algunos episodios manualmente (consulta el cuaderno de ejemplo de Wordle) para confirmar que el entorno devuelve recompensas coherentes. Si un modelo capaz no logra puntuar más alto que una línea base aleatoria, es posible que la señal de recompensa necesite ajustes.

max_completion_length en episodios de varios turnos

El parámetro max_completion_length limita el número total de tokens en toda la conversación de varios turnos (todas las generaciones del modelo + los resultados de las herramientas combinados), no solo una única generación. Para entornos con muchos turnos (p. ej., Sudoku con decenas de movimientos), es posible que necesites aumentarlo:

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

Si los episodios se están interrumpiendo (el modelo se detiene a mitad de la partida), esta es probablemente la causa.

Ejecuciones prolongadas contra un Space

Un Space puede cerrar WebSockets que permanecen abiertos durante horas (lo observamos aproximadamente cada 2 horas). Los clientes de OpenEnv hasta la versión 0.11.0 se reconectan entonces a una nueva sesión de entorno, y si una partida estaba en curso, el siguiente step() se ejecuta en una partida que el cliente nunca reinició, sin ningún error. Desde OpenEnv 0.12.0, los clientes lanzan SessionLostError en su lugar, por lo que la partida falla. Para mantener las desconexiones alejadas del entrenamiento, abra un nuevo cliente en reset(), de modo que cada conexión dure un episodio, y establezca MAX_CONCURRENT_ENVS un poco por encima del número de partidas jugadas simultáneamente, ya que las sesiones antiguas se liberan de forma asíncrona:

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

Ejemplo avanzado: Wordle

Entrenemos un modelo para jugar a Wordle utilizando el entorno TextArena. Esto demuestra la interacción de varios turnos, el manejo de retroalimentación acumulativa, una recompensa dividida en varios términos y la terminación del episodio mediante excepciones.

El tutorial de Wordle de OpenEnv cubre todo el flujo de trabajo: los datos de SFT, el calentamiento de SFT, este paso de GRPO y la evaluación, con el hardware y los costos. La versión en cuaderno de este ejemplo es grpo_wordle.ipynb.

El entorno TextArena

TextArena es una colección de código abierto de juegos competitivos basados en texto, diseñada para evaluar las habilidades de razonamiento de los LLM mediante juegos textuales como Wordle, Snake, Tres en raya y más.

image of TextArena

¿Por qué Wordle?

Wordle es un buen benchmark para el RL basado en entornos porque requiere razonar sobre la retroalimentación, es puramente textual y los modelos de 1B parámetros pueden mejorar en él.

¿Cómo funciona Wordle? Wordle es un juego de adivinanzas de palabras en el que el jugador debe adivinar una palabra de 5 letras en 6 intentos. Después de cada intento, el entorno proporciona retroalimentación letra por letra:

G U E S S
X G Y X X

X = no está en la palabra, G = posición correcta (verde), Y = posición incorrecta (amarillo). Aquí, “U” es correcta y está en su lugar, “E” está en la palabra pero en una posición equivocada.

Comenzar con un calentamiento de SFT

Qwen3-1.7B casi nunca gana una partida, por lo que GRPO a partir del modelo base tiene poco de lo que aprender: un grupo de partidas en las que ninguna se gana no proporciona ninguna señal. El ejemplo parte de openenv/Qwen3-1.7B-Wordle-SFT, entrenado durante una época con 1.000 partidas jugadas por un solver en el mismo formato de tool-calling, con un razonamiento breve antes de cada intento.

Clase Environment

La clase WordleEnv encapsula el cliente de TextArena, expone guess() como herramienta y conserva lo que necesita la recompensa:

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

Decisiones de diseño clave:

  • reset() devuelve el mensaje inicial del juego como la primera observación que ve el modelo.
  • guess() es la única herramienta. El modelo la llama en cada turno con una palabra de 5 letras.
  • Segmentación de retroalimentación acumulativa: TextArena devuelve el historial completo del juego en cada turno. Extraemos solo la parte nueva para evitar repetir el contexto.
  • Excepción en done: Si el modelo intenta adivinar después de que el juego termina, guess() lanza un ValueError. El entrenador captura esto y devuelve "Game over." al modelo como respuesta de la herramienta, y lo cuenta en tools/failure_frequency.

Funciones de recompensa y entrenamiento

La recompensa propia del entorno paga una victoria de la misma manera ya sea tras 1 o 6 intentos, y solo otorga crédito parcial al final de una partida. Con el razonamiento activado, una política puede aprender a razonar menos y usar los 6 intentos para llegar al final. Tres términos de recompensa evitan que eso ocurra:

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 resultado ganador otorga una recompensa mayor cuanto antes se produzca (1.42 en un solo intento, 1.0 en seis), una derrota paga según la mejor suposición del juego, independientemente de si este terminó o no, y cada movimiento inválido cuesta 0.1. TRL registra cada término por separado, por lo que rewards/reward_win/mean aumenta con la tasa de victorias.

Ejecutar el ejemplo

Cada paso juega 64 partidas, una sesión de entorno cada una. Duplica el Space openenv/wordle y establece la variable MAX_CONCURRENT_ENVS=64 en tu copia.

colóquese
server

Modo Colocate (1 GPU, recomendado)

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

Esto ejecuta vLLM en el mismo proceso que el entrenamiento, lo que requiere solo una GPU. En nuestra ejecución, 150 pasos tomaron aproximadamente 2 horas y 15 minutos en una A100 de 80 GB.

Resultados

En nuestra ejecución, con una ejecución de entrenamiento por modelo, con las mismas 30 palabras reproducidas de forma codiciosa:

ModeloVictoriasAciertos por victoriaConjeturas incorrectas
Qwen3-1.7B0/30-24
+ SFT10/303.241
+ SFT + GRPO (modelo)20/304.13

GRPO eliminó en gran medida las palabras repetidas e inventadas y ganó más partidas. La mayoría de las nuevas victorias requirieron de 4 a 6 intentos, por lo que el número de intentos por victoria aumentó. Las 30 palabras provienen de los 156 sustantivos de los que TextArena extrae sus secretos, que los modelos también vieron durante el entrenamiento, y una nueva ejecución se situará a unas pocas victorias de distancia (dos ejecuciones de SFT con la misma receta ganaron 10 y 13 partidas). Observa a los tres modelos jugar en la Wordle Arena y consulta las curvas de entrenamiento en Trackio.

Entrenamiento en múltiples entornos

Puedes entrenar un modelo único en varios entornos simultáneamente. Esto es útil cuando quieres que un modelo aprenda diferentes habilidades en paralelo. Por ejemplo, jugar a Wordle (razonamiento lingüístico) y Catch (razonamiento espacial) en la misma ejecución de entrenamiento.

La idea clave es crear una clase de meta-entorno que envuelva múltiples entornos y enrute cada muestra al correcto utilizando una columna del conjunto de datos.

Cómo funciona

  1. Agrega una columna "env" (o similar) a tu conjunto de datos que identifique a qué entorno pertenece cada muestra.
  2. En reset(**kwargs), lea kwargs["env"] para seleccionar el entorno activo para ese episodio.
  3. Exponer herramientas de todos los entornos; el trainer descubre todos los métodos públicos.
  4. Utilice funciones de recompensa separadas por entorno, devolviendo None para las muestras que no pertenecen a ese entorno. TRL maneja los valores None con nansum/nanmean.

Ejemplo: Wordle + Catch

El script grpo_multi_env.py entrena simultáneamente en Wordle y 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. ..."""
        ...

Patrones clave:

  • Inicialización perezosa de clientes: Crea los clientes en reset(), no en __init__(), para evitar conexiones WebSocket innecesarias.
  • Cerrar antes de reabrir: Cierre el cliente anterior antes de crear uno nuevo para evitar errores de capacidad del servidor.
  • Enrutamiento de kwargs: La columna "env" del conjunto de datos se pasa a reset() como un argumento de palabra clave.
  • Todas las herramientas se exponen simultáneamente: el modelo ve guess, move y stay como herramientas disponibles, independientemente del entorno activo. Si llama a la herramienta incorrecta (por ejemplo, move durante Wordle), el método lanza un ValueError que el entrenador captura de forma adecuada. En la práctica, los modelos aprenden a usar las herramientas correctas en función del system prompt.

Funciones de recompensa por entorno

Cada función de recompensa devuelve None para las muestras de otros entornos:

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 convierte None en nan internamente y utiliza nansum/nanmean para la agregación, de modo que cada muestra solo se puntúa con su función de recompensa relevante.

Dataset con enrutamiento de entorno

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

Ejecutar el ejemplo de multi-entorno

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

Al entrenar en varios entornos, monitorea las métricas por función de recompensa (train/reward_func_0, train/reward_func_1, etc.) en lugar de la métrica combinada train/reward. La métrica combinada alterna entre entornos y puede parecer ruidosa.

Ejecución de los entornos

Al usar environment_factory, el trainer se conecta automáticamente al servidor de entorno. Solo es necesario que el servidor esté en ejecución. Hay tres formas de ejecutar un servidor de entorno de OpenEnv:

space
docker
local

Conectar a un Space remoto de Hugging Face (el más simple)

La mayoría de los scripts de ejemplo usan un Space alojado por defecto (no requiere configuración):

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

Para el entrenamiento, duplica el Space en tu propia cuenta para evitar problemas de concurrencia. El trainer abre N conexiones WebSocket simultáneas (una por generación) y los Spaces compartidos pueden no admitir esto. Consulta Server concurrency para más detalles.

Catálogo de entornos

La mejor manera de explorar el catálogo actual de entornos mantenidos es visitar el catálogo oficial de OpenEnv.

Para crear tu propio entorno, consulta la guía sobre Building Your Own Environment with OpenEnv. Los entornos están estrechamente integrados con el Hub, por lo que puedes publicar nuevos entornos para que la comunidad los reutilice.

Concurrencia del servidor

Al usar environment_factory, el trainer crea N instancias de entorno (una por generación), cada una abre una conexión WebSocket con el servidor. De forma predeterminada, los servidores de OpenEnv solo permiten 1 sesión concurrente, lo que provocará fallos durante el entrenamiento.

Para admitir el entrenamiento paralelo, configure el servidor para la concurrencia:

  1. En su archivo de entorno, declare el soporte para sesiones concurrentes:
SUPPORTS_CONCURRENT_SESSIONS: bool = True
  1. En tu aplicación de servidor, establece el límite de concurrencia:
app = create_app(
    create_my_environment,
    MyAction,
    MyObservation,
    max_concurrent_envs=64,  # match or exceed generation_batch_size
)

max_concurrent_envs debe ser ≥ generation_batch_size (que por defecto es per_device_train_batch_size × gradient_accumulation_steps). Por ejemplo, con gradient_accumulation_steps=64 y un tamaño de lote de 1, se necesitan al menos 64 sesiones concurrentes.

environment_factory vs rollout_func

GRPOTrainer admite dos enfoques para el entrenamiento basado en entornos:

  • environment_factory (recomendado): define una clase de entorno con métodos de herramientas, y el entrenador se encarga automáticamente de la generación, el análisis de llamadas a herramientas y el bucle de varios turnos. Este es el enfoque utilizado en toda esta guía.
  • rollout_func: Escribes tú mismo todo el bucle de generación e interacción con el entorno. Esto te da un control total sobre cómo se producen las finalizaciones, cómo se ejecutan las herramientas y cómo se calculan las recompensas.

Use rollout_func cuando environment_factory no se adapta a su caso de uso. Por ejemplo, servidores de agentes externos donde un servidor externo controla el bucle de generación y gestiona su propio protocolo de interacción entre el agente y el entorno.

Entrenamiento en arneses: entrenamiento de agentes de codificación reales (Harbor)

Las integraciones anteriores son de caja blanca: TRL controla el bucle de varios turnos por sí mismo. Muestrea cada turno, analiza las llamadas a herramientas, las ejecuta y devuelve los resultados.

Algunos agentes no pueden controlarse de esta manera porque gestionan su propio bucle. Un marco de agente de codificación de producción como opencode o claude-code tiene su propio planificador, conjunto de herramientas, gestión de contexto y condición de parada. Lo que se desea es entrenar ese agente exacto, no una reimplementación de él.

Para ello, TRL ofrece una ruta experimental de caja negra (con control del bucle) basada en experimental.async_grpo.AsyncGRPOTrainer: HarnessAsyncGRPOTrainer controla una fábrica de sesiones de OpenEnv. Los agentes y sus entornos de ejecución provienen de Harbor, servidos a través de OpenEnv con openenv harbor serve, de modo que cada agente que instala Harbor se puede entrenar de la misma manera: se selecciona uno con --harness. Consulte examples/async_grpo_harbor para un ejemplo completo, incluido un lanzador de Hugging Face Jobs.

Esta ruta requiere openenv>=0.7.0, la primera versión con la API de captura TrainingTrace: pip install "openenv>=0.7.0".

Cómo funciona

TRL no muestrea cada turno aquí. El agente se ejecuta hasta su finalización por sí mismo, y TRL lee lo que hizo:

  1. El servidor OpenEnv Harbor inicia el agente en una tarea de Harbor dentro de un sandbox (por ejemplo, E2B). Cada llamada al modelo que realiza el agente pasa a través del proxy de captura del servidor hacia tu servidor vLLM, que registra los ids de tokens de prompt y completion del propio motor, los logprobs muestreados y una máscara de pérdida por token.
  2. Cuando el agente se detiene, el verificador propio de la tarea puntúa el resultado, y TRL recibe el TrainingTrace validado de OpenEnv y construye las filas de entrenamiento a partir de sus ids de tokens y máscaras. TRL no vuelve a tokenizar los prompts capturados.
  3. GRPO entrena en esas filas. La recompensa se propaga a cada token entrenado a través de la ventaja relativa al grupo, y los nuevos pesos se sincronizan de vuelta en el mismo servidor de vLLM.

Cada rollout se ejecuta en su propia sesión aislada, con su propio sandbox y su propia sesión de captura. El aislamiento es importante por dos razones: el proxy debe capturar exactamente los tokens de ese rollout, y un rollout no debe interferir con otro. La configuración max_concurrent_rollouts controla cuántos rollouts se ejecutan al mismo tiempo, y el MAX_CONCURRENT_ENVS del servidor debe ser al menos max_concurrent_rollouts + 1 (la fábrica mantiene una conexión extra para los metadatos de la tarea).

Configuración

HarnessAsyncGRPOTrainer recibe el modelo, la configuración y el conjunto de datos de la misma manera que experimental.async_grpo.AsyncGRPOTrainer, pero con una fábrica de sesiones en lugar de reward_funcs, tools y environment_factory. Sin un harness_adapter, el agente controla su propio bucle. El entrenador construye la fábrica de sesiones con su propia política de muestreo, por lo que se le pasa una función invocable, como partial(HarborSessionFactory, ...), en lugar de una fábrica ya construida. Cada sesión devuelve un TrainingTrace de OpenEnv que contiene los tokens exactos del motor, los logprobs de comportamiento y las máscaras prellenadas. rollout_reward_fn es opcional; si no se proporciona, TRL utiliza la recompensa del verificador de la sesión.

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

Lo que necesitas definir

OpenEnv es responsable de la validación de la captura y la selección de tokens. TRL convierte la máscara de secuencia completa del productor en su máscara de finalización interna, empaqueta las filas resultantes y aplica GRPO. No reconstruye tokens ni infiere qué llamadas pertenecen al agente.

rollout_reward_fn : cómo puntuar un rollout

Callable[[HarnessRolloutOutcome], float | None]

TRL no sabe qué significa el éxito para tu tarea, por lo que conviertes cada rollout finalizado en una recompensa escalar. La función recibe un HarnessRolloutOutcome que describe lo que hizo el agente:

  • env_reward (float | None): la recompensa del verificador de la tarea, o None cuando el rollout no pudo puntuarse.
  • completion (list[dict]): el transcurso final del mensaje.
  • trace (list[TraceEntry]): los registros de captura de OpenEnv seleccionados, incluyendo los turnos con máscara cero.
  • tool_call_count y tool_failure_count (int): cuántas llamadas a herramientas realizó el agente y cuántas parecían ser fallos.
  • tool_calls_by_name (dict[str, int]): llamadas por herramienta, por ejemplo {"bash": 3, "edit": 2}.
  • timed_out (bool): si el agente agotó su presupuesto de tiempo.

Devuelve un float o devuelve None para marcar el rollout como no puntuable, de modo que se descarte de la línea base del grupo en lugar de contarse como un cero. Si no pasas esta función, se utiliza el env_reward crudo tal cual. El ejemplo de Harbor mantiene la corrección del verificador como señal principal y añade un pequeño bono de eficiencia de herramientas solo cuando la tarea se resuelve.

¿Por qué no usar directamente el verificador? El verificador es responsabilidad del entorno y responde a una sola pregunta, “¿cuán correcto fue el resultado?”, lo que lo mantiene limpio y reutilizable para la evaluación. La recompensa sobre la que se entrena es una decisión separada, propia del momento de entrenamiento (binarizar la puntuación, penalizar el comportamiento degenerado, descartar los *rollouts* que no se pueden puntuar). Además, necesita señales que el verificador nunca ve, ya que el verificador solo inspecciona el resultado final, mientras que rollout_reward_fn también recibe la trayectoria (número de herramientas, timed_out, la traza). Por ejemplo, un *rollout* puede pasar algunas pruebas pero nunca ejecutar bash; solo rollout_reward_fn puede ver eso y penalizarlo.

La elegibilidad de los tokens proviene de OpenEnv

session.fetch_training_trace() devuelve un TrainingTrace validado. Cada turno tiene un node_id, prompt_token_ids, completion_token_ids, per_token_logps y un loss_mask obligatorio sobre el prompt y la completion. Las posiciones del prompt son cero. Se conservan las máscaras de completion parciales, incluidos los ceros dentro de una respuesta del asistente.

Por ejemplo, el prompt [10, 11], la completion [20, 21, 22] y la mask [0, 0, 1, 0, 1] entrenan en los tokens 20 y 22. El token 21 permanece como contexto. Una completion completamente enmascarada se conserva para el contexto y la contabilidad de uso.

El productor excluye las llamadas auxiliares y los reintentos descartados. Esta API no tiene parámetros train_turn_fn ni agent_turn_fn. Las integraciones existentes deben mover la selección a sus máscaras de OpenEnv. El verificador de la tarea permanece independiente de la elegibilidad de los tokens.

Las capturas malformadas fallan en el límite de OpenEnv y detienen el entrenamiento. Las fallas de transporte siguen siendo no puntuables. Los historiales reescritos inician filas separadas, preservando los tokens y logprobs muestreados. Múltiples filas aún comparten la recompensa del rollout; esta API no cambia el ponderado de GRPO.

Requisitos

El servidor OpenEnv Harbor es el propietario del conjunto de datos y de los entornos de pruebas. Necesita la credencial del backend de entornos de pruebas (por ejemplo, E2B_API_KEY):

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

El servidor de vLLM debe exponer la invocación de herramientas y los ids de tokens reales, y habilitar la sincronización de pesos de NCCL para que el agente siempre acceda a la política actual. Los rollouts y las actualizaciones de pesos deben usar el mismo servidor de 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"}'

El entrenamiento con propiedad del bucle se encuentra en trl.experimental y su API puede cambiar. El ejemplo fija la revisión de OpenEnv con la que se probó; consulte su README para la configuración exacta.

Update on GitHub