外观
从零构建一个 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 也要做"的判定标准:
- 无序列性:每步决策独立、一步定终身 → 分类/回归/多臂老虎机即可(多臂老虎机 是 RL 的退化形式)。
- 无试错通道:决策后果不可逆、无模拟器 → 用离线数据的行为克隆或离线 RL,绝不上在线 RL。
- 可精确建模:动力学方程写得出来、规模小 → 动态规划/最优控制(RL vs 相邻领域)。
什么时候 RL 是明确正确的答案
模拟器廉价(游戏、机器人仿真)、奖励天然存在(分数、胜率、收益)、动作空间定义清晰,且问题的解空间大到你无法手写规则——满足两条以上,RL 通常值得试。参考学习路径里的项目建议。
三、环境:选型、自建、封装
环境是 RL 项目最大的单一风险源。它决定你训练一天能拿多少样本、评估结果可不可信、策略能不能迁移。
1. 三个来源
| 来源 | 适用场景 | 成本 | 风险 |
|---|---|---|---|
| 现成环境库 | 项目本身就是"跑算法"(学习、复现、对比) | 低 | 环境 bug 少但未必贴合业务 |
| 现成环境 + 定制 | 业务与公开环境结构相近(如调度≈车间作业) | 中 | 定制部分引入新 bug |
| 完全自建 | 业务独有(推荐、交易、物理系统) | 高 | 环境即产品,错误全埋在这里 |
环境库的选择看数据集与工具档案:Gymnasium 是事实标准接口,MuJoCo/Brax 管连续控制,Brax 能上 GPU,Isaac/仿真平台管机器人。无论选哪个,第一件事是给环境写一个 30 行的"冒烟测试":随机动作跑 1000 步,确认 step 返回值维度、done 触发条件、奖励取值范围都符合预期。
2. 自建环境的三条纪律
自建环境(推荐模拟器、调度仿真等)是"环境即产品"说的主角,三条纪律缺一不可:
- 先定接口,再写逻辑。直接继承
gymnasium.Env,实现reset、step、observation_space、action_space。Gymnasium API 细节见渐进式教程。 - 观测与动作用
spaces显式声明,并做space.contains()校验。维度写错(比如把离散动作写成连续)是训练静默失败的顶级原因。 - 环境与训练代码分离。环境是一个独立进程/服务,训练代码只通过接口访问。这样环境换实现(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")这个骨架的三个设计决策值得记住:
- 策略对象只有
act(obs)一个接口——DQN、REINFORCE、PPO 都能以相同方式插进来,替换算法时训练循环几乎不动。算法内部实现参照价值学习与策略梯度两页。 - 每集
reset都重播种——把环境随机性与算法随机性分开,问题出现时能定位到层。 - 日志先粗后细——第一版只打印窗口均值;确认信号正确后,再逐步加 TD 误差均值、熵、KL 等诊断量(见调参实践)。
2. 从骨架到产品:三层演进
| 阶段 | 做什么 | 什么时候做 |
|---|---|---|
| 骨架版 | 单进程、每集逐步,确认能学到东西 | 第一天 |
| 向量版 | gymnasium.vector 或 SubprocVecEnv 并行采集 | 单进程太慢时 |
| 框架版 | 换用 SB3/CleanRL/RLlib 重写或参照 | 骨架证明可行后 |
不要急于上框架
先亲手写一次 50 行的训练循环,再去看框架对比。直接上框架最常见的后果是:算法跑起来了,但你不理解日志里每个量是什么,出了 bug 无从下手。
六、评估与记录
评估协议在项目一开始就要定,而不是训练完再补——因为你训练中的每一次"看起来变好了"的决策,都在依赖一个评估判断。 完整协议见评估实践,这里给最小可用的三件事:
- 固定评估函数:训练过程中每 N 个时间步,用
num_eval_episodes个固定种子跑评估,记录"评估回报均值 ± 四分位区间",与训练回报分开记录。 - 学习曲线持久化:每次训练把
(timestep, train_return, eval_return, seed)追加进 CSV,这是后续所有诊断的原材料。 - 每次实验写一条实验记录:config(超参、seed、环境)、git commit、日志路径。没有记录的实验等于没做。
评估的核心概念——样本效率 vs 最终性能、多 seed 矩阵、IQR——都来自评估与基准那一页;实践层的落地模板在从零搭一套 RL 评估。
七、可复现性加固
RL 的可复现问题比监督学习严重一个量级:同样的代码与超参,换机器、换 GPU、甚至换 NumPy 版本都可能得到不同的曲线。加固手段按性价比排序:
1. 三层 seed 管理
| 层 | 播什么 | 代码 |
|---|---|---|
| 全局 | random、numpy、torch | random.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. 监控三件套
- 动作分布:线上动作的均值/方差/离散度分布漂移 = 观测分布变了。
- 观测分布:对观测做统计(均值、分位数、哈希近似)与训练分布对比。
- 业务指标:奖励往往不能线上直采(延迟奖励、人工评分),必须同时盯替代业务指标。
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% 的时间。
延伸阅读
- RL 系统解剖 —— 一个完整 RL 系统的六层架构,本页流水线的理论底座
- 渐进式教程:Gymnasium 三版跑起来 —— 本页骨架版的完整血肉,三版代码跟着跑一遍
- 从零搭一套 RL 评估 —— 本页第 6 步的完整展开:实验矩阵、IQR、公平性陷阱
- 框架与工具怎么选 —— 骨架版成熟后,如何升级到 SB3/RLlib/CleanRL
- 常见陷阱与反模式 —— 本页各步骤对应的翻车点与检测方法
- 数据集与工具档案 —— 环境库、基准套件、训练工具链的一站式索引
参考资料
- 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