多轮循环、停止条件与工具调用
这章讲什么: 上一章的打分接在「一条轨迹已经跑完」之后。这一章补上前半段——
rollout()本体:模型说一句、环境回一句的循环怎么转,什么时候停,工具调用怎么执行,一条轨迹的数据结构长什么样。
1. MultiTurnEnv:rollout 循环的实现处
上一章说 Environment.rollout 是抽象方法。它的实际实现在 MultiTurnEnv(verifiers/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.rollout(verifiers/envs/multiturn_env.py:164)、循环体rollout_loop(:174-:214)、env_response抽象(:57)。
整条循环被一个总超时罩住
整个 rollout_loop 被 asyncio.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_completed(verifiers/envs/environment.py:657,@final)挨个 await 这些条件,任一返回 True 就停,并把stop_condition记进 state(_render_stop,:638)。
MultiTurnEnv 自带哪些停止条件
| 停止条件 | 触发 | 优先级 |
|---|---|---|
has_error | state 里有 error | 100(最先查) |
prompt_too_long | 提示词超长被截断 | 0 |
max_turns_reached | 轨迹步数 ≥ max_turns | 0 |
max_total_completion_tokens_reached | 输出 token 超预算 | 0 |
has_final_env_response | env_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:多轮的退化
SingleTurnEnv(verifiers/envs/singleturn_env.py)几乎没代码——它就是 MultiTurnEnv(max_turns=1),并把 env_response 声明成「不该被调用」。因为一轮就撞上 max_turns_reached 停了,环境永远不需要回话。「单轮」是「多轮」的一个特例,这是很干净的抽象复用。
4. ToolEnv:工具调用怎么落地
ToolEnv(verifiers/envs/tool_env.py