数据截至 (上游 commit 20daedc47558)
三大模块:环境 ↔ LLM 怎么缝合
本章讲什么:RAGEN 把“让 LLM 在环境里多轮交互”这件事拆成三个职责分明的模块。看懂这三个模块怎么 传接数据,是看懂 StarPO 主循环的前提。
3.1 为什么要拆三块
要解决的小问题。 LLM 只会“吞 token、吐 token”;环境只会“收动作、吐状态+奖励”。两边语言不通, 中间需要一层翻译;而且为了训练效率,还得同时跑几百个环境(一个 batch 里有很多并行轨迹)。
思路。 把这三件事分开:
| 模块 | 只管 | 不管 |
|---|---|---|
EnvStateManager | 并行环境池的 reset/step/收集指标 | token、prompt 拼装 |
ContextManager | 状态↔token 双向翻译、loss mask、奖励张量 | 环境怎么 step |
LLMAgentProxy | 调度两者 + 调 LLM 生成 | 具体怎么算 token / 怎么 step 环境 |
三者都在 agent_proxy.py:193 的 LLMAgentProxy.__init__ 里被一次性实例化(train/val 各一套 ctx/es 管理器)。
3.2 Environment State Manager:一批环境怎么管
环境不是一个,是一整阵。 EnvStateManager 的核心是一个 self.envs 列表,每个条目是一个环境实例
加元数据。它按“env_groups 个组 × group_size 个副本”的结构创建(es_manager.py:69 的 _init_env_instances)。
这个组结构很重要:同一组里的所有副本用同一个 seed,也就是面对同一个初始状态,只是采样出不同
轨迹——这正是 GRPO / 奖励方差过滤赖以工作的“组”。看 reset 里怎么撑 seed(es_manager.py:115):
# 示意,非源码(改编自 es_manager.py:_expand_seed)
# env_groups=3, group_size=2:同组同 seed,跨组 seed+1
# 结果:[seed, seed, seed+1, seed+1, seed+2, seed+2]
def _expand_seed(seed):
seeds = [[seed + i] * group_size for i in range(env_groups)]
return sum(seeds, []) # 拍平
一个环境条目里有什么。 每个 entry 是个 dict(es_manager.py:84):tag(如 SimpleSokoban)、group_id、
env_id、env(真环境对象)、status(EnvStatus,记 truncated/terminated/num_actions/rewards)。
step 做什么。 EnvStateManager.step(es_manager.py:172)收一批 {env_id, llm_response, actions},
对每个环境依次执行动作。几个关键细节:
- 动作要过一道“查表”。
_extract_map_valid_actions(es_manager.py:384)把文本动作(如"down")映到 环境的动作码;不在action_lookup里的被丢弃。 - 无效动作有惩罚。若解析出的动作个数和管理器接受的不一致,记一笔
format_penalty(默认 -0.1,es_manager.py:235)。 - 做完的环境不再输出。
step只把没结束的环境放进env_outputs(es_manager.py:285)——这样下一轮 生成只针对还活着的轨迹,省 GPU。
并行加速。 若环境 parallel_friendly 且 max_workers>1,同 tag 的 reset/step 会走一个 ThreadPoolExecutor
(es_manager.py:104)。对 Sokoban 这种生成谜题费时、或 Search 这种要走网络的环境很有用。
收尾算指标。 get_rollout_states(es_manager.py:290)把每条轨迹的 success/num_actions/自定义指标
聚成 cache['metrics'],还会算 pass@k(k=group_size):同组里只要有一条成功就计 1(es_manager.py:356)。
3.3 环境接口:Gym 风格的 BaseEnv
所有环境只需实现两个方法。 ragen/env/base.py:5 的 BaseEnv 是个 ABC,必须实现:
reset(seed, **kwargs)→ 返回渲染后的初始状态(文本或图像)。约定:同 seed 同环境。step(action)→ 返回(observation, reward, done, info)。
info 里的键会被当成指标收集(如 sokoban 的 action_is_effective/success,sokoban/env.py:54)。
两种动作空间。 BaseDiscreteActionEnv(离散,如 Sokoban/FrozenLake)和 BaseLanguageBasedEnv(文本,
如 Countdown)。Sokoban 还同时继承了 gym_sokoban 的 GymSokobanEnv(sokoban/env.py:14)——复用现成的游戏逻辑,
只包一层文本渲染。
注册一个新环境很便宜。 在 ragen/env/__init__.py:23 的 REGISTERED_ENVS 和 REGISTERED_ENV_CONFIGS
两个字典里加一行,再在 config/envs.yaml 的 custom_envs 里写个 tag 条目(指定 env_type、env_instruction、
max_actions_per_traj 等)即可。注意:WebShop / Search / Alfworld 是“可选依赖”,__init__.py:49 用 try/except ImportError 包住——装了才注册。
3.4 Context Manager:状态 ↔ token 的双向翻译
这是工程含量最高的一块(ctx_manager.py 有 1600+ 行)。它两个方向都要管。
方向一:环境输出 → LLM 输入(get_lm_inputs)
get_lm_inputs(ctx_manager.py:1350)是总入口,按 prepare_for_update 和 context_window_mode 分派:
prepare_for_update=False(推理)→_build_infer_samples:把历史拼成 chat 消息,末尾补上<think>(或<answer>)作为生成提示(ctx_manager.py:1260)。prepare_for_update=True(训练)→ 按模式分派给_build_samples_full/_build_single_turn_samples/_build_limited_multi_turn_samples。
每轮状态怎么拼成 prompt。 看 _build_turn_state_content(ctx_manager.py:616):每轮都拼一段
“Turn N: + State + 还剩几个动作 + 要求严格输出 <think>…</think><answer>…</answer> 格式”。系统提示由
_build_system_content(ctx_manager.py:541)拼,里面嵌了该环境的 instruction(含动作表、grid 词表等,
在 _init_prefix_lookup,ctx_manager.py:115)。