跳转至

扩展加载

扩展是普通 Python 包,用来向 GeneLab 注册机器人、环境、任务和 RL 后端。真实项目应使用这种方式构建。

为什么扩展要分离

把下游项目放在 src/genelab/ 外,可以避免框架变成项目专用代码集合。团队也能独立 version、install 和发布自己的机器人包。

genelab.extensions API

genelab.extensions 是扩展所需一切的唯一稳定导入路径。底层实现位于 genelab.registry(机器人 / 环境 / 任务)和 genelab.rl.backends(后端),但 genelab.extensions 是规范入口:

from genelab.extensions import (
    register_robot, register_env, register_task, register_backend,
    ROBOTS, ENVS, TASKS,        # 三个注册表本身
    Runnable, Backend,          # 两个 Protocol 契约
)
扩展类型 注册函数 存放于
机器人 cfg register_robot(name, factory, *, description, …) ROBOTS
环境 cfg register_env(name, factory, *, description, …) ENVS
任务 register_task(name, factory, *, description, …) TASKS
RL 后端 register_backend(backend) backend.cfg_type 索引

机器人 / 环境 / 任务的 register_* 接收稳定的 name 和一个零参 factory,按需构建 config(注册表保持惰性)。 register_backend 接收一个构建完毕的后端对象,并按它所拥有的 agent-config 类型来索引。

发现机制

机制 适用场景
genelab.extensions 分组的 entry point 已安装包和日常工作流。
CLI --import MODULE 临时本地模块或调试 entry-point 加载。
程序内加载 嵌入式应用。

所有机制最终做同一件事:注册函数调用 register_robotregister_envregister_taskregister_backend

Runnable 任务契约

任务工厂产出的每个值都必须满足 genelab.extensions.Runnable —— 这是 CLI 的 play / train 命令所依赖的契约:

class Runnable(Protocol):
    cfg: object
    def play(self, *, max_steps: int | None = None) -> None: ...
    def train(self) -> None: ...

因此任务类型通过 .cfg 暴露其配置,并实现 play() / train()。内置任务类型已满足该契约;第三方任务类型应实现相同形状。

play 接受一个仅限关键字的 max_steps——由 --max-steps CLI 标志透传的硬上界。当它不为 None 时,无论是否开启 viewer、也无论软配置 simulation.steps 为何,播放循环都必须在该步数后停止;None (普通 task.play() 调用传入的值)则交由软配置决定(headless 时以 simulation.steps 为界,开启 viewer 时一直运行到关闭窗口)。本身不驱动步进循环的任务可以接受并忽略它。

Backend 契约

一个后端拥有一个 RL 库,按任务 agent config 的类型被选中(见 RL runner)。契约是 genelab.extensions.Backend

@runtime_checkable
class Backend(Protocol):
    name: str
    cfg_type: type
    def train(self, ctx: TrainContext) -> Path: ...
    def play(self, ctx: PlayContext) -> None: ...
    def make_inference_setup(self, ctx: PlayContext) -> InferenceSetup: ...

TrainContext / PlayContext(位于 genelab.rl.backends)打包了已解析好的 env、各配置、seed、log_dir 以及 profiler 选项,因此库相关代码全部留在后端内部。

一个会自我注册并记录一次运行的最小后端如下:

import json
from dataclasses import dataclass
from pathlib import Path

from genelab.extensions import register_backend
from genelab.rl.backends import PlayContext, TrainContext
from genelab.rl.backends.base import InferenceSetup
from genelab.rl.config import BackendConfig


@dataclass
class EchoAgentCfg(BackendConfig):
    """该后端拥有的 agent-config 类型;任务把 ``cfg.agent`` 设为它的实例。"""

    max_iterations: int = 10


class EchoBackend:
    name = "echo"
    cfg_type = EchoAgentCfg

    def train(self, ctx: TrainContext) -> Path:
        log_dir = ctx.log_dir or Path("logs") / ctx.task_id
        log_dir.mkdir(parents=True, exist_ok=True)
        (log_dir / "run.json").write_text(
            json.dumps({"task": ctx.task_id, "iterations": ctx.max_iterations})
        )
        return log_dir

    def play(self, ctx: PlayContext) -> None:
        ctx.env.reset()

    def make_inference_setup(self, ctx: PlayContext) -> InferenceSetup:
        raise NotImplementedError("EchoBackend 不支持 eval / export")


def register() -> None:
    register_backend(EchoBackend())

register 暴露为包的 entry point(或通过 --import 调用),任何 cfg.agentEchoAgentCfg 的任务都会 路由到 EchoBackendmake_inference_setup 仅在 genelab eval / genelab export 时需要;只做训练的后端可以 不实现它。

扩展契约

扩展应能作为包导入,保持注册阶段导入轻量,暴露无参 register() hook,并避免重复加载时重复注册。

继续阅读