构建Othello强化学习环境

akshay_pachaar 发布于 2026-07-08 阅读 18

文章详细介绍了如何使用Verifiers库构建一个Othello(黑白棋)强化学习环境。内容包括RL循环的四个组成部分(状态、动作、奖励、环境)、GRPO算法、对手引擎(随机和极小极大)、奖励函数设计(胜负、棋子优势、格式合规、无效移动惩罚)以及从数据生成、监督微调到强化学习的完整训练流程。代码示例清晰,结构易懂。

图像

Andrej Karpathy 用三个名词总结了 LLM 训练的整个历史:

  • 文本
  • 对话
  • 环境

预训练基于互联网文本,监督微调基于精选对话,而当前的强化学习时代则基于环境。

OpenAI 的 o1 通过使用可验证答案的数学和编程问题进行训练,验证了这一框架,而 DeepSeek-R1 则公开了方法。

业界现在将环境视为稀缺资源。据报道,Anthropic 曾讨论一年内花费超过 10 亿美元用于环境。

与此同时,有人通过一个类似 Hugging Face 的中心免费提供它们,该中心托管了 2500 多个开源环境。

今天,我们将使用他们的框架从头构建一个我们自己的环境。

什么是 RL 环境?

在构建任何东西之前,让我们先理解环境到底是什么。每个 RL 设置都是一个具有四个组成部分的循环,它们映射到模型交互的任何东西上:

  • 状态:模型当前看到的内容
  • 动作:模型从该状态做出的选择
  • 奖励:一个数字,表示该选择有多好
  • 环境:持有状态、接受动作并返回奖励的东西

图像

这个循环的难点始终在于奖励,传统上需要训练一个单独的模型来学习人类偏好。

DeepSeek-R1 通过 GRPO(群体相对策略优化)用一个简单的 Python 函数取代了整个模型。对同一个提示的多个答案进行评分,并促使模型倾向于那些超过群体平均值的答案。

以下是 GRPO 工作原理的图示:

图像

这就留下了环境这个仍然存在的障碍。奖励函数可以对最终状态进行评分,但必须有东西首先呈现任务、接受模型的动作、执行规则并产生最终状态。

前沿实验室将这一部分视为专有资产,这就是为什么很少有人真正了解工作环境的内部实现。

那个免费提供环境的团队是 Prime Intellect,他们的库 Verifiers 是我们将要构建的框架。(100% 开源)

该设计与模型无关,奖励完全可验证,相同的结构适用于任何你想要训练的回合制任务。

如何阅读本文

目标很简单。我们正在构建一个完整的 RL 环境,并且我们将以易于理解的方式来讲解,即使这是你第一次接触。

每个想法都附有实现它的代码,代码保持简短,以便在文章中流畅阅读。完整的、可运行的版本在最后分享。

我们将构建的游戏只是一个工作示例。当你完成时,你将能够采用这个确切的结构并使其适应你自己的用例。

Othello RL 环境:为什么选择棋盘游戏

Othello 是一款在 8x8 网格上进行的双人游戏。你放置一个棋子,使其在一条直线上夹住对手的棋子,每个被夹住的棋子都会翻转为你的颜色,当棋盘填满时,拥有更多棋子的人获胜。

这个翻转规则就是为什么一个单独的移动可以大幅改变比分,也是为什么角落如此重要(角落的棋子永远无法被翻转回来)。

图像

每个 RL 部分在这里都有对应。棋盘是状态,棋子放置是动作,最终结果提供奖励,而验证移动、翻转棋子、扮演另一方的游戏引擎就是环境。

LLM 扮演黑方,内置引擎扮演白方。每次黑方移动后,白方响应,更新后的棋盘返回给模型,重复此过程直到游戏结束。

图像

技术栈

三个工具使其工作,每个处理一层:

  • Verifiers:RL 框架。它定义环境、运行回合循环并处理评估。
  • Lightning AI:一个兼容 OpenAI 的推理 API,因此相同的代码可以调用托管模型(如 Claude 或 DeepSeek),无需特定于提供商的重新编写。
  • vLLM:在本地通过相同的 OpenAI 兼容端点提供开放权重模型。

共享接口是使环境与模型无关的原因。本地 Ministral-3B 和托管 GPT-4.1 只需更改模型名称即可在一行中互换,其他无需更改。

游戏循环

以下是模型在每个回合看到的内容:

图像

棋盘、分数和有效移动列表就是整个状态。模型对游戏了解的一切都来自这段文本。

它必须用一个 <think> 部分后跟一个 <move> 标签来响应,这迫使它在提交移动之前对局面进行推理。

图像

环境根据有效列表验证移动,将其应用于棋盘,让白方响应,并将更新后的棋盘发回。无效移动会收到错误消息并重试,同时对奖励施加惩罚。

所有这些都位于一个方法中。OthelloEnv 继承自 Verifiers 中的 MultiTurnEnv,后者处理循环、回合跟踪和终止,当模型发送移动时,基类调用你的 env_response

class OthelloEnv(MultiTurnEnv):
    def env_response(self, model_output, state):
        move = parse_move(model_output)

        if not is_valid(move, state.board):
            state.penalty += INVALID_MOVE_PENALTY
            return error_message(move), state          # 同一回合,重试

        state.board = apply_move(state.board, move, player="black")

        if not game_over(state.board):
            white_move = opponent_engine(state.board, state.difficulty)
            state.board = apply_move(state.board, white_move, player="white")

        return render_board(state.board), state

这是逻辑的轮廓;真实版本还处理棋盘解析和代码片段跳过的边缘情况。一旦我们端到端地理解了设置,稍后我会分享完整的工作代码。

内置对手引擎

内置引擎扮演白方,有两种模式:

图像

  • 随机 选择任何合法移动。适用于早期训练,因为模型只需做出合理决策即可获胜。
  • Minimax 模拟未来移动,通过角落控制、棋盘位置和可用移动对每个结果位置进行评分,并选择最佳最坏情况结果的移动。深度为 3 意味着它向前看三步,足以设置陷阱并避免明显的失误。

一个随机参数控制白方忽略其策略并随机选择的频率。较低的随机性意味着更一致、更严厉的玩法。

白方的移动是从当前棋盘状态和固定的游戏种子生成的,因此相同的位置总是产生相同的响应。这种确定性允许你在相同条件下比较不同的模型。

以下是相同内容的代码示例:

def opponent_engine(board, randomness, depth):
    if random.random() < randomness:               # 随机模式
        return random.choice(legal_moves(board))
    best_move = None                                 # Minimax 模式:尝试每个移动,
    best_score = float("-inf")                       # 保留得分最高的那个
    for move in legal_moves(board):
        score = minimax(apply_move(board, move), depth - 1, my_turn=False)
        if score > best_score:
            best_move, best_score = move, score
    return best_move

奖励函数

游戏结束时,四个信号组合成一个奖励:

图像

每个都是一个从最终状态读取某些内容并返回一个数字的函数,其中获胜信号是最简单的:

def win_reward_func(state):
    result = state.get("result")
    if result == "black":   # 模型的颜色
        return 1.0
    if result == "draw":
        return 0.5
    return 0.0              # 输掉,或者游戏从未结束

其他三个遵循相同形式,全部四个合并为一个分数:

def total_reward(state):
    return (
        win_loss_score(state)
        + piece_advantage(state)
        + format_compliance(state)
        - invalid_move_penalty(state)
    )

使用四个信号而不是单个胜负比特的原因是细粒度。训练早期,大多数游戏都是失败,对于纯粹的胜负奖励来说,它们看起来都相同,因此模型没有可攀登的梯度。

棋子优势将接近的失利与大比分失利分开,在模型开始获胜之前为其提供梯度。

格式合规性权重较低,因此干净的格式永远不会超过好的玩法。

无效移动惩罚有上限,这样一次糟糕的游戏不会淹没模型做对的所有事情。

每个分数都直接来自游戏状态和规则,不涉及评判模型或 LLM 评估器,因此奖励是完全确定且可复现的。

连接起来

图像

环境从不同的起始位置和对手难度生成游戏,模型通过上面的循环玩每个游戏,在游戏结束时计算并组合四个奖励。在评估期间,所有游戏的奖励、Token 使用量和回合数汇总到一个结果表中。

一个函数将所有内容连接起来,这就是 prime eval 命令在幕后调用的:

def load_environment(min_random_move_prob, max_random_move_prob, parse_think):
    dataset = generate_games(min_random_move_prob, max_random_move_prob)
    parser = XMLParser(fields=["think", "move"])
    rubric = Rubric(funcs=[...], weights=[1.0, 1.0, 0.2, 1.0])
    return OthelloEnv(dataset, parser, rubric)

数字显示的内容

运行评估是一个命令,对手设置作为参数传递:

prime eval run othello -m openai/gpt-4.1 -n 100 \
  -a '{"min_random_move_prob": 0.0, "max_random_move_prob": 0.0, "minimax_depth": 3}'

更改模型名称以测试其他模型,无论是通过 Lightning AI 托管还是在本地 vLLM 服务器上运行。以下是两个模型对阵两个对手的表现:

图像

从评估到训练

评估告诉你模型在哪里不足,训练分三个阶段解决它。同一个环境支持所有阶段:

图像

  1. 数据生成:让你最强的模型玩几百场游戏并保存结果。相同的 eval 命令直接写入数据集:

    prime eval run othello -m openai/gpt-4.1 -n 200 \
      --save-to-hf-hub --hf-hub-dataset-name your-username/othello-data
    

    在训练之前将其过滤为仅胜利和平局,这样你就不会在教模型你最强玩家的策略的同时,也教了它的错误。

  2. 监督微调:首先教授格式和有效移动。Ministral-3B 的回合数表现直接说明了这一点,因为不可靠的格式和非法移动是 RL 无法训练通过的噪声。

  3. RL 训练:这是策略改进的地方。从相同的起始位置进行多场游戏,每场使用评估中的相同奖励函数进行评分,模型向得分更高的 rollout 更新。

剥离到核心,这就是文章顶部的 GRPO 循环:

for prompt in batch:
    rollouts = [play_game(model, prompt) for _ in range(group_size)]
    rewards = [total_reward(r.final_state) for r in rollouts]
    advantage = rewards - mean(rewards)     # 相对于群体
    update_model(model, rollouts, advantage)

每个 rollout 都根据其自身群体的平均值进行评分,因此从给定位置赢得 10 场中 6 场的移动比旁边较弱的尝试获得更多奖励。

使这适应你自己的任务

一旦你移除 Othello 的具体细节,相同的 MultiTurnEnv 会为你提供一个适合任何回合制任务的骨架:

class TaskEnv(MultiTurnEnv):
    def env_response(self, model_output, state):
        action = parse_action(model_output)          # 你任务的语法
        if not is_valid(action, state):
            return error_message(action), penalize(state)
        state = apply_action(state, action)          # 你任务的规则
        if not task_complete(state):
            state = environment_step(state)          # 工具、API 调用或对手
        return render_state(state), state            # 你任务的显示

这并非特定于游戏。编码智能体将 apply_action 替换为对生成的代码运行测试套件,支持智能体将其替换为检查工具调用是否检索了正确的记录,研究任务将其替换为根据引用的来源验证声明。

使其适应归结为四个替换:

  • 任务逻辑:你领域内关于何为有效动作以及状态如何变化的规则
  • 响应引擎:模型响应任何东西,从基于规则的模拟器到实时 API 再到另一个模型
  • 奖励函数:保持模式(结果信号、部分信用、格式、惩罚)并替换领域逻辑
  • 状态呈现:模型需要看到的任何东西,无论是文件差异、对话记录还是工具的响应

底层的结构无论领域如何都保持不变:解析、验证、应用、响应、评分。评分规则就是设计,如果你把组件搞对,训练信号会自行处理。

亲自尝试

所有代码、设置说明以及现成的 GPU 来重现这些结果都在 Lightning AI Studio 模板中:构建自定义 RL 环境 →

查看 Lightning AI 推理 →

图像

感谢阅读!

干杯! :)

  • 原文链接: x.com/akshay_pachaar/sta...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论