跳到主要内容

多轮循环、停止条件与工具调用

这章讲什么: 上一章的打分接在「一条轨迹已经跑完」之后。这一章补上前半段——rollout() 本体:模型说一句、环境回一句的循环怎么转,什么时候停,工具调用怎么执行,一条轨迹的数据结构长什么样。

1. MultiTurnEnv:rollout 循环的实现处

上一章说 Environment.rollout 是抽象方法。它的实际实现在 MultiTurnEnvverifiers/envs/multiturn_env.py)——几乎所有具体环境都继承它(SingleTurnEnv、ToolEnv 都是它的子类)。

循环骨架

MultiTurnEnv.rollout:164,标了 @final 子类不能改)核心是一个 while 循环:

init_state() 建工作台

setup_state() 环境特定初始化(可选)

┌──► while not is_completed(state): ← 停止条件全过一遍,没命中就继续
│ get_prompt_messages(state) ① 拼出当前该发给模型的对话
│ get_model_response(state) ② 调模型,拿回答
│ add_model_response(state, ...) ③ 把回答存成一个 trajectory step
└────────┘
│ 循环退出(某个停止条件命中)
cleanup(state) → render_completion() 拼出最终 completion

两个抽象接缝留给子类填:

  • env_response(messages, state):57 抽象):模型说完一句后,环境回什么。ToolEnv 在这里执行工具、返回工具结果;游戏环境在这里返回下一步棋盘。
  • setup_state / get_prompt_messages:可选覆盖,处理非线性对话。

依据:MultiTurnEnv.rolloutverifiers/envs/multiturn_env.py:164)、循环体 rollout_loop:174-:214)、env_response 抽象(:57)。

整条循环被一个总超时罩住

整个 rollout_loopasyncio.wait_for(..., timeout=self.timeout_seconds) 包住(:217),超时就 mark_timed_out 标记完成——保证单条 rollout 不会永远挂着。

2. 停止条件:用装饰器声明,按优先级检查

「什么时候停」不是写死的 if,而是一组用 @vf.stop 装饰的方法。这是本库很有辨识度的设计。

怎么写

# 示意,非源码:声明一个停止条件
@vf.stop(priority=100) # 优先级高的先查
async def has_error(self, state) -> bool:
return state.get("error") is not None

怎么被发现和执行

  • @vf.stop 只是给方法打个标记属性(verifiers/decorators.py:32)。
  • 环境初始化时,discover_decorated(self, "stop")verifiers/decorators.py:8)用 inspect.getmembers 扫出所有带标记的方法,priority 降序、同级按名字排序
  • 每轮循环开头,is_completedverifiers/envs/environment.py:657@final)挨个 await 这些条件,任一返回 True 就停,并把 stop_condition 记进 state(_render_stop:638)。

MultiTurnEnv 自带哪些停止条件

停止条件触发优先级
has_errorstate 里有 error100(最先查)
prompt_too_long提示词超长被截断0
max_turns_reached轨迹步数 ≥ max_turns0
max_total_completion_tokens_reached输出 token 超预算0
has_final_env_responseenv_response 主动发了「终局回话」0

子类可以再加。比如 ToolEnv 加了 no_tools_called(模型这轮没调工具就停,verifiers/envs/tool_env.py:121)。

这种「声明式停止条件 + 优先级」的好处:加一个新的终止规则不用碰循环主体,写个带 @vf.stop 的方法即可,且 error 检查天然排在最前。同族还有 @vf.cleanup / @vf.setup / @vf.teardown / @vf.reward / @vf.metric,都是同一套「装饰器打标 + discover_decorated 收集 + 按优先级跑」的机制(verifiers/decorators.py)。

3. SingleTurnEnv:多轮的退化

SingleTurnEnvverifiers/envs/singleturn_env.py)几乎没代码——它就是 MultiTurnEnv(max_turns=1),并把 env_response 声明成「不该被调用」。因为一轮就撞上 max_turns_reached 停了,环境永远不需要回话。「单轮」是「多轮」的一个特例,这是很干净的抽象复用。

4. ToolEnv:工具调用怎么落地

ToolEnvverifiers/envs/tool_env.py)是「让模型调工具」的环境。它把普通 Python 函数变成模型能调的工具。

从函数到工具

构造时(:74):

  • 传入一组 Python 函数 tools
  • convert_func_to_tool_def 把每个函数的签名 + docstring 转成工具定义 schema(tool_defs),发给模型让它知道有哪些工具。
  • tool_map 建立「工具名 → 函数」的查找表。

执行工具(env_response 的实现)

模型回一句、里面带 tool_calls 时,env_response:145)逐个执行:

模型回答里的每个 tool_call
│ 解析 name + JSON arguments

call_tool(name, args, id) ← 从 tool_map 找到函数,await 执行

├─ 成功 → ToolMessage(role="tool", content=结果, tool_call_id=id)
└─ 抛异常 → 看是否在 stop_errors 里
├─ 是 → raise,终止 rollout
└─ 否 → 用 error_formatter 把异常转成文本,当工具结果塞回去

关键设计:工具报错默认不中断,而是把错误信息当成工具返回喂回给模型(:171-:180),让模型有机会自我纠正——这更贴近真实 agent 的行为。只有 stop_errors 里指定的异常才会真的终止。

ToolEnv 还自动叠加一个 ToolMonitorRubric:13),给每个工具记调用次数当 metric——这就是上一章 RubricGroup 合并多 rubric 的用武之地。

依据:ToolEnv.env_responseverifiers/envs/tool_env.py:145)、call_tool:132)、no_tools_called:121)、ToolMonitorRubric:13)。StatefulToolEnvverifiers/envs/stateful_tool_env.py)在此之上支持「工具需要读写 state」的情形。

5. State 与 TrajectoryStep:一条轨迹的数据模型

State:可变工作台

Stateverifiers/types.py:444)是 dict 的子类,rollout 全程往里塞东西。它有个巧妙设计:prompt/answer/info/example_id 这几个「输入字段」的读写会自动转发到内部的 input 子字典__getitem__/__setitem__:471-:492),既保持了「输入」和「运行时状态」的结构分离,又让访问很顺手(state["answer"] 直接拿到)。

State 里主要装什么:

字段是什么
prompt / answer / info来自输入的题目
client / model / sampling_args这次用哪个模型端点、怎么采样
trajectory轨迹:一步步的对话记录(下面细说)
completion最终「模型这一路说的话」(去掉初始 prompt)
reward / advantage / metrics打分结果
timing各阶段计时
error / stop_condition出错信息、因何停止

TrajectoryStep:一步的记录

每次模型回答存成一个 TrajectoryStepverifiers/types.py:297):这一步的 prompt(发过去的对话)、completion(模型回的)、原始 responsetokens(训练要的 token ids + mask + logprobs)、reward/advantage(分组打分回填)、is_truncated

为什么每步都存 token 级信息? 因为这就是 RL 训练器要的原料——TrajectoryStepTokens:253)里的 prompt_ids / completion_ids / completion_mask / completion_logprobs 正是 token-in-token-out 训练的输入。评测时用不上,训练时是命根子。

add_model_responseverifiers/envs/multiturn_env.py:139)负责把一次模型回答解析成 TrajectoryStep 并追加进 trajectory

6. 把两章串起来:完整 rollout 时序

MultiTurnEnv.rollout()
├─ init_state() 建 State 工作台 [01章]
├─ setup_state() 环境特定初始化
└─ while not is_completed(): 查所有 @vf.stop 条件
├─ get_prompt_messages() 拼当前对话(含 env_response 回话)
├─ get_model_response() 调 Client → 模型 [01章 Client]
└─ add_model_response() 存成 TrajectoryStep
├─ cleanup() → render_completion() 拼最终 completion
└─(回到 run_rollout)rubric.score_rollout() 打分 [02章]

7. 代码地图

主题文件符号
多轮循环verifiers/envs/multiturn_env.pyMultiTurnEnv.rollout / rollout_loop
环境回话接缝verifiers/envs/multiturn_env.pyMultiTurnEnv.env_response(抽象)
自带停止条件verifiers/envs/multiturn_env.pyhas_error / max_turns_reached / max_total_completion_tokens_reached
停止条件检查verifiers/envs/environment.pyEnvironment.is_completed / _render_stop
装饰器打标 + 收集verifiers/decorators.pystop / discover_decorated
单轮环境verifiers/envs/singleturn_env.pySingleTurnEnv
工具环境verifiers/envs/tool_env.pyToolEnv.env_response / call_tool
有状态工具verifiers/envs/stateful_tool_env.pyStatefulToolEnv
运行时状态verifiers/types.pyState
轨迹一步verifiers/types.pyTrajectoryStep / TrajectoryStepTokens