Skip to content

从零构建一个 RL 项目

本页速览 RL 项目从问题定义到上线的完整流水线:环境选型(自建 vs 现成)、基线先行、训练循环、评估协议、日志与可复现、部署与监控;"先跑通再调优"的节奏。

从零构建一个 RL 项目 ​

一句话定位:这一页讲清楚一个 RL 项目从"脑子里有个问题"到"策略上线跑起来"的完整流水线——它解决"我知道算法、但不知道项目该按什么顺序做"的迷茫,适合第一次真正上手 RL 项目的工程师,读完后你能画出一张自己的项目路线图,并避开最常见的返工点。

RL 项目最大的特点,是成本结构的倒置。监督学习项目里,最贵的环节是数据标注;RL 项目里,最贵的是"环境与奖励",其次才是算法本身。Google 的 RL 系统解剖 里有一句被反复引用的话:环境即产品——一个 RL 项目 80% 的成本、时间和失败风险都集中在环境与奖励设计上,而不是那颗 PPO 算法上。本页就从这个事实出发,把项目按顺序拆成八个步骤。

一、项目流程总览:八步流水线 ​

┌────────────────────────────────────────────────────────────────┐
│  RL 项目八步流水线                                              │
│                                                                │
│  第0步  该不该用 RL?            ← 决策清单,先拦掉 30% 的错误项目 │
│     │                                                         │
│  第1步  环境:选型 / 自建 / 封装    ← 投入产出比最高的一步        │
│     │                                                         │
│  第2步  奖励与观测设计            ← "奖励即规格",写歪就全白费    │
│     │                                                         │
│  第3步  基线:随机 / 启发式 / 监督   ← 没有基线不许上算法         │
│     │                                                         │
│  第4步  训练循环最小骨架           ← 先跑通,再谈调优            │
│     │                                                         │
│  第5步  评估与记录                ← 一套评估协议,贯穿始终        │
│     │                                                         │
│  第6步  可复现性加固              ← seed / 配置 / 依赖锁定       │
│     │                                                         │
│  第7步  部署与监控                ← 策略服务、漂移、回滚          │
│     │                                                         │
│  迭代节奏:第4~6步是一天一转的闭环,第1~2步错了要回到上游         │
└────────────────────────────────────────────────────────────────┘

这八步不是瀑布。真正会反复走的,是第 4、5、6 步组成的内环(训练→评估→修复复现问题,一天转好几圈),以及偶尔从内环跳回第 1、2 步(发现环境或奖励有问题)。第 0 步只走一次,但要走对。

下面按顺序展开。文中与评估协议、框架选型、调参方法、常见陷阱等页互有引用,建议配合阅读。

二、第 0 步:该不该用 RL ​

RL 不是锤子。项目立项时先用一张决策清单把不合适的项目拦在门外,省下的时间和钱是最多的。一张经过实践检验的清单:

问题是否说明
问题能写成"状态 → 动作 → 奖励"的循环吗?继续换方法没有序列决策结构,就用监督/规则
存在可反复试错的执行通道吗(模拟器/沙箱/低代价线上)?继续换方法RL 靠试错,试错成本决定成败
奖励能定义清楚吗?会不会被钻空子?继续先做奖励设计奖励写歪,算法越强崩得越惨,见奖励工程
有 10^4 以上的交互样本预算吗?继续考虑离线 RL/BC样本太少连基线都赢不了
简单启发式/规则已经接近目标了吗?大概率不该用 RL继续能写规则就写规则
业务上能接受策略"先坏一阵再变好"吗?继续换方法RL 前中期表现波动大,见RL 设计原则

最常见的第一类错误

把"可以用 RL"当成"应该用 RL"。真实案例:某团队为仓库货物分拣做"智能调度",需求是"比人工规则快 5%",但每条真实订单都要在仿真里重放一遍,一次评估要 40 分钟——样本效率根本不支持训练,最后用一条排序启发式就达到了目标。先问"成本最低的解是什么",再问"RL 能不能做得更好"。

三种"不是 RL 也要做"的判定标准:

  1. 无序列性:每步决策独立、一步定终身 → 分类/回归/多臂老虎机即可(多臂老虎机 是 RL 的退化形式)。
  2. 无试错通道:决策后果不可逆、无模拟器 → 用离线数据的行为克隆或离线 RL,绝不上在线 RL。
  3. 可精确建模:动力学方程写得出来、规模小 → 动态规划/最优控制(RL vs 相邻领域)。

什么时候 RL 是明确正确的答案

模拟器廉价(游戏、机器人仿真)、奖励天然存在(分数、胜率、收益)、动作空间定义清晰,且问题的解空间大到你无法手写规则——满足两条以上,RL 通常值得试。参考学习路径里的项目建议。

三、环境:选型、自建、封装 ​

环境是 RL 项目最大的单一风险源。它决定你训练一天能拿多少样本、评估结果可不可信、策略能不能迁移。

1. 三个来源 ​

来源适用场景成本风险
现成环境库项目本身就是"跑算法"(学习、复现、对比)低环境 bug 少但未必贴合业务
现成环境 + 定制业务与公开环境结构相近(如调度≈车间作业)中定制部分引入新 bug
完全自建业务独有(推荐、交易、物理系统)高环境即产品,错误全埋在这里

环境库的选择看数据集与工具档案:Gymnasium 是事实标准接口,MuJoCo/Brax 管连续控制,Brax 能上 GPU,Isaac/仿真平台管机器人。无论选哪个,第一件事是给环境写一个 30 行的"冒烟测试":随机动作跑 1000 步,确认 step 返回值维度、done 触发条件、奖励取值范围都符合预期。

2. 自建环境的三条纪律 ​

自建环境(推荐模拟器、调度仿真等)是"环境即产品"说的主角,三条纪律缺一不可:

  1. 先定接口,再写逻辑。直接继承 gymnasium.Env,实现 reset、step、observation_space、action_space。Gymnasium API 细节见渐进式教程。
  2. 观测与动作用 spaces 显式声明,并做 space.contains() 校验。维度写错(比如把离散动作写成连续)是训练静默失败的顶级原因。
  3. 环境与训练代码分离。环境是一个独立进程/服务,训练代码只通过接口访问。这样环境换实现(Python 版 → C++ 版 → 分布式版)不碰算法代码。

3. 一个最小自建环境示例 ​

下面是一个"1 维小车向目标移动"的迷你环境,展示接口骨架:

python
import gymnasium as gym
import numpy as np
from gymnasium import spaces

class ReachGoalEnv(gym.Env):
    """小车一维运动:状态是位置与速度,动作是推力,目标是停在 [0,0] 附近。"""

    metadata = {"render_modes": []}

    def __init__(self):
        super().__init__()
        # 动作:连续推力,范围 [-1, 1]
        self.action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,), dtype=np.float32)
        # 观测:位置、速度
        self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=(2,), dtype=np.float32)
        self._state = np.zeros(2, dtype=np.float32)
        self._step_count = 0

    def reset(self, *, seed=None, options=None):
        super().reset(seed=seed)          # 必须调用,用于给 np_random 播种
        self._state = np.array([self.np_random.uniform(-1, 1), 0.0], dtype=np.float32)
        self._step_count = 0
        return self._state.copy(), {}

    def step(self, action):
        action = np.clip(action, -1.0, 1.0).item()
        pos, vel = self._state
        vel = vel + 0.1 * action           # 简单动力学
        pos = pos + vel
        self._state = np.array([pos, vel], dtype=np.float32)
        self._step_count += 1

        # 奖励:越近越好,且鼓励停稳
        reward = -abs(pos) - 0.5 * abs(vel)
        # 终止条件:到达目标附近或步数耗尽
        terminated = abs(pos) < 0.05 and abs(vel) < 0.05
        truncated = self._step_count >= 200
        info = {"dist": abs(pos)}
        return self._state.copy(), reward, terminated, truncated, info

接口规范要记牢

新版 Gymnasium 的 reset 返回 (obs, info),step 返回 (obs, reward, terminated, truncated, info),其中 terminated(任务达成)与 truncated(超时等外力截断)必须分开——把超时当成"任务失败"或"任务成功"会系统性歪曲学习信号。详见教程页的坑位清单。

4. 环境的验收清单 ​

自建环境交付前,逐条过一遍:

  • [ ] 随机策略 1000 次 rollout 不抛异常,观测/奖励类型与空间声明一致
  • [ ] 奖励有界(或至少方差有界),便于后续设计奖励归一化
  • [ ] 同一 seed 下 reset 产生相同的初始状态序列
  • [ ] 手写一个简单启发式,确认环境"可达成"(避免环境本身无解)
  • [ ] 单步 step 性能已测(每秒步数),决定能否支撑目标样本预算

四、第 2 步与第 3 步:奖励设计、基线先行 ​

1. 奖励设计要早于算法选型 ​

"奖励即规格"——奖励函数是产品需求唯一的机器可读表达。这一步做错,后面所有算法工作都白费,详见奖励工程。实践层只有一句话:先写一份"奖励设计文档",列出每个奖励项的来源、取值、为什么这样取、可能被怎么钻空子,再动手写代码。

2. 基线先行:没有基线,不许上算法 ​

很多项目一上来就跑 SAC/PPO,结果学不动,也不知道是算法问题还是环境问题。正确顺序是给"难度阶梯":

层级策略目的预期表现
0随机策略验证环境可交互、奖励可采集给出"地板"
1手写启发式(规则/Greedy/线性)验证问题本身可解给出"参考线"
2监督式预热(行为克隆/监督基线)有离线数据时快速摸到合理水平给出"廉价上界"
3简单 RL(DQN/REINFORCE)验证 RL 信号通路至少打平启发式
4复杂 RL(PPO/SAC + 调优)追求上限显著超过启发式

为什么要先跑启发式

启发式像一把"尺子":它能直接告诉你"这个环境的奖励信号够不够密、可不可学"。如果启发式能轻松拿到 80 分而 RL 只拿到 30 分,问题几乎一定在奖励或观测设计,不在算法。

一个非常常见的错误是跳过第 1 步直接上 PPO,调了两周超参后才发现手写规则本来就更好。在第 1 步上省下的时间,一定会在第 4~6 步里加倍还回去。

五、训练循环的最小骨架 ​

环境与基线就绪后,就可以搭"训练循环"。原则是最小骨架 + 增量打磨:第一版代码只要满足"能跑、能存模型、能出学习曲线"三个条件,任何优化(向量化、并行、分布式)都留到确认信号通路正确之后。

1. 最小骨架:不依赖框架的通用循环 ​

python
"""RL 训练循环最小骨架:只依赖 gymnasium + numpy。
目标:跑通、存模型、出曲线。算法替换不影响骨架结构。"""

import gymnasium as gym
import numpy as np

class RandomPolicy:
    """占位策略:换成任何算法的 .act(obs) 接口即可。"""
    def __init__(self, env):
        self.env = env
    def act(self, obs):
        return self.env.action_space.sample()

def collect_episode(env, policy, max_steps=500):
    """跑一集,返回 (总回报, 步数)。"""
    obs, _ = env.reset()
    total_reward, steps = 0.0, 0
    for _ in range(max_steps):
        action = policy.act(obs)
        obs, reward, terminated, truncated, _ = env.step(action)
        total_reward += reward
        steps += 1
        if terminated or truncated:
            break
    return total_reward, steps

def train(env_id, num_episodes=2000, seed=0):
    env = gym.make(env_id)
    # 1. 环境播种(每个环境都要播)
    env.reset(seed=seed)
    policy = RandomPolicy(env)

    returns = []
    for ep in range(num_episodes):
        # 2. 每集重播环境种子,保证可复现
        env.reset(seed=seed + ep)
        total_reward, _ = collect_episode(env, policy)
        returns.append(total_reward)

        # 3. 日志:每 100 集打印均值(先粗后细)
        if (ep + 1) % 100 == 0:
            mean = float(np.mean(returns[-100:]))
            print(f"episode {ep+1:>5d} | mean_return(100) = {mean:.2f}")

    # 4. 存模型 + 存曲线
    np.save("returns.npy", np.array(returns))
    print("done, returns saved to returns.npy")
    env.close()

if __name__ == "__main__":
    train("CartPole-v1")

这个骨架的三个设计决策值得记住:

  1. 策略对象只有 act(obs) 一个接口——DQN、REINFORCE、PPO 都能以相同方式插进来,替换算法时训练循环几乎不动。算法内部实现参照价值学习与策略梯度两页。
  2. 每集 reset 都重播种——把环境随机性与算法随机性分开,问题出现时能定位到层。
  3. 日志先粗后细——第一版只打印窗口均值;确认信号正确后,再逐步加 TD 误差均值、熵、KL 等诊断量(见调参实践)。

2. 从骨架到产品:三层演进 ​

阶段做什么什么时候做
骨架版单进程、每集逐步,确认能学到东西第一天
向量版gymnasium.vector 或 SubprocVecEnv 并行采集单进程太慢时
框架版换用 SB3/CleanRL/RLlib 重写或参照骨架证明可行后

不要急于上框架

先亲手写一次 50 行的训练循环,再去看框架对比。直接上框架最常见的后果是:算法跑起来了,但你不理解日志里每个量是什么,出了 bug 无从下手。

六、评估与记录 ​

评估协议在项目一开始就要定,而不是训练完再补——因为你训练中的每一次"看起来变好了"的决策,都在依赖一个评估判断。 完整协议见评估实践,这里给最小可用的三件事:

  1. 固定评估函数:训练过程中每 N 个时间步,用 num_eval_episodes 个固定种子跑评估,记录"评估回报均值 ± 四分位区间",与训练回报分开记录。
  2. 学习曲线持久化:每次训练把 (timestep, train_return, eval_return, seed) 追加进 CSV,这是后续所有诊断的原材料。
  3. 每次实验写一条实验记录:config(超参、seed、环境)、git commit、日志路径。没有记录的实验等于没做。

评估的核心概念——样本效率 vs 最终性能、多 seed 矩阵、IQR——都来自评估与基准那一页;实践层的落地模板在从零搭一套 RL 评估。

七、可复现性加固 ​

RL 的可复现问题比监督学习严重一个量级:同样的代码与超参,换机器、换 GPU、甚至换 NumPy 版本都可能得到不同的曲线。加固手段按性价比排序:

1. 三层 seed 管理 ​

层播什么代码
全局random、numpy、torchrandom.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
环境Gymnasium 环境env.reset(seed=s); env.action_space.seed(s)
算法内部回放采样、噪声、策略采样框架自带 seed(s) 接口(SB3 的 PPO(..., seed=...))

全局 seed 不是银弹

PyTorch 的某些算子在 GPU 上不保证确定(如 atomicAdd 型操作),多卡/并行采样更不可能复现到比特级。务实目标不是"逐比特复现",而是同一配置下的性能分布可复现(同 mean/IQR),这需要多 seed 而不是单 seed,见调参实践的 seed 陷阱。

2. 配置管理:Hydra ​

超参数散落在代码里是 RL 项目最大的卫生问题。用配置系统把"实验"与"代码"分离:

yaml
# config/train.yaml(Hydra 配置示例)
seed: 42
env_id: CartPole-v1

algorithm:
  name: ppo
  learning_rate: 3.0e-4
  gamma: 0.99
  gae_lambda: 0.95
  clip_range: 0.2
  ent_coef: 0.0
  n_steps: 2048
  batch_size: 64

evaluation:
  eval_episodes: 10
  eval_freq: 10000   # 每多少时间步评估一次
  seeds: [0, 1, 2]   # 多评估种子

命令行覆盖某个字段(python train.py algorithm.learning_rate=1e-3),每个实验的完整 config 随日志一起存档。这样"复现一个结果"退化成"重跑一份 config"。

3. 依赖与版本锁定 ​

  • requirements.txt / pyproject.toml 锁到小版本(RL 与 NumPy/Gymnasium 版本耦合极深)。
  • 记录 gymnasium, numpy, torch, stable-baselines3 版本号到实验记录。
  • 有条件就 docker build 固化镜像,或者用 uv/poetry 的 lockfile。

八、部署与监控 ​

训练出好策略只是项目的一半。上线环节的常见失败模式是:离线分数很高,线上全面拉胯。根因通常是分布漂移和评估与部署不一致。

1. 策略服务的形态 ​

场景服务形态示例
离线批量决策批处理任务,定期跑排产、批量推荐
在线单步决策低延迟推理服务广告出价、实时控制
在线环境交互环境在业务侧,策略被回调机器人、对话系统

通用做法:把策略导出成纯推理格式(ONNX/TorchScript/JAX),推理服务只依赖"观测 → 动作"的前向函数,不含任何训练依赖。训练循环、回放缓冲区、优化器都不出现在服务里。

2. 监控三件套 ​

  1. 动作分布:线上动作的均值/方差/离散度分布漂移 = 观测分布变了。
  2. 观测分布:对观测做统计(均值、分位数、哈希近似)与训练分布对比。
  3. 业务指标:奖励往往不能线上直采(延迟奖励、人工评分),必须同时盯替代业务指标。
text
漂移检测流程(每周巡检):
 收集本周线上观测 → 与训练集观测做分布对比(KS 检验/直方图)
  → 差异显著? → 是:触发重训评估;否:继续观察

3. 回滚与安全护栏 ​

  • 策略带上版本号,模型服务保留 N 个历史版本,随时一键回滚。
  • 上线前先跑"影子模式"(shadow deployment):策略只记录动作、不实际执行,与现有系统对比一周。
  • 业务侧保留规则兜底:策略动作异常(越界、NaN、超时)时回退到规则策略。安全相关讨论见RL 设计原则与常见陷阱。

九、迭代节奏:先跑通,再调优 ​

最后给一张可执行的节奏表。RL 项目大多数失败的共同点,不是"算法不行",而是在没跑通之前就开始调优,或者在跑通之后没有纪律地乱调。

阶段时长建议交付物退出条件
第 0~2 步1~2 周决策清单、环境验收单、奖励设计文档环境跑通、启发式有分数
第 3~4 步3~5 天骨架版训练循环、随机/启发式基线曲线能存模型、能出学习曲线
第 5~6 步3~5 天评估脚本、CSV 日志、Hydra 配置一次命令能完整复现一个实验
第 7 步与训练并行部署管线、监控告警影子模式跑通
调优循环按周每轮只改一个变量、记实验记录达到业务验收标准

一天一转的纪律

把"改代码 → 跑实验 → 看曲线 → 记结论"当成一个循环,一天至少完成一转。RL 实验动辄数小时,最怕的是同时改 5 个变量跑 5 个小时,最后不知道是哪个变量起作用。单变量实验、先跑通再调优、留实验记录——这三条能省下你整个项目 60% 的时间。

延伸阅读 ​

参考资料 ​

  • Sutton, R. S. & Barto, A. G. (2018). Reinforcement Learning: An Introduction, 2nd ed. MIT Press.(全书,尤其第 1、2、8 章关于智能体-环境接口与表格方法的讨论;免费在线版见 incompleteideas.net/book/the-book-2nd.html)
  • Gymnasium 官方文档(Farama Foundation):gymnasium.farama.org,Env 接口参考 gymnasium.farama.org/api/env/
  • Stable-Baselines3 官方文档:stable-baselines3.readthedocs.io
  • Henderson, P. et al. (2018). Deep Reinforcement Learning that Matters. AAAI 2018. arXiv:1709.06560(RL 可复现性问题的经典论述)
  • Islam, R. et al. (2017). Reproducibility of Benchmarked Deep Reinforcement Learning Tasks. arXiv:1708.04133(seed 敏感性实证)
  • Engstrom, L. et al. (2020). Implementation Matters in Deep RL: A Case Study on PPO and TRPO. ICLR 2020. arXiv:1805.08292(代码实现细节比论文公式更影响性能)
  • Irpan, A. (2018). Deep Reinforcement Learning Doesn't Work Yet. alexirpan.com/2018/02/14/rl-hard.html(为什么 RL 项目容易失败的著名博客)
  • Hydra 官方文档:hydra.cc