用单步动作正确率去预估 MineExplorer 这类分钟级长程任务的整段表现,经常会对不上,问题通常不在模型某一次突然变差,而在两个指标统计的对象根本不是一回事:单步指标统计的是「在给定状态下选对下一个动作」的条件正确率,长程指标统计的是「从初始状态走到终局,整条轨迹是否达成任务目标」。前者是局部条件概率,后者是整段路径的联合结果,中间任何一个决策点偏离,都可能让整段判定为未完成。要判断项目该看哪一层,先数清任务里有多少个决策点,再看评测脚本的成功条件是怎么写的。
如果项目要判断的是 agent 能否独立完成多步目标,就不该只用单步动作正确率下结论。建议按三步走:用任务分解表数清决策点,分别统计单步正确率与整段完成率并看两者的落差,再对照 MineExplorer 公开任务说明确认失败样本的判定口径。单步指标适合做动作层回归和问题定位,整段完成率才对应长程能力,两者不宜互相换算,也不适合用单步分数对外描述长程表现。
把一个任务拆成动作序列,数清中间有多少个决策点
以「采集木材并合成一把木镐」为例,任务目标往下拆两层就能看出决策链的长度:目标层是拿到木镐;子目标层是找到可砍的树、拿到足够原木、合成木板、合成木棍、在工作台合成木镐;每个子目标再落到单步动作,比如转向、接近、持续挖掘、拾取掉落物、打开背包、在合成格摆放材料。每一步都有独立的偏离位置:视角没对准会让挖掘无效,掉落物落在远处会漏拾取,背包已满会让拾取静默失败,合成格摆错位置会得到别的产物,天黑后敌对生物打断会改变状态。这些偏离单独看都不算「动作错」,但都会改变后续状态分布。
下面是一段任务分解的配置骨架,放在评测仓的任务定义目录下,字段名按实际评测脚本替换即可,作用是让「决策点数量」变成可数的东西,而不是靠读代码时凭感觉估:
task: craft_wooden_pickaxe
success: inventory_has("wooden_pickaxe", 1) # 终局判定条件,需与公开任务说明一致
subgoals:
- id: find_tree
check: visible_block_tag("log")
- id: gather_logs
check: inventory_count("log") >= 3
- id: craft_planks
check: inventory_count("planks") >= 4
- id: craft_sticks
check: inventory_count("stick") >= 2
- id: craft_pickaxe
check: inventory_has("wooden_pickaxe", 1)
max_steps: 6000 # 与评测配置保持一致,超时按未完成处理
数决策点时建议按子目标数乘以每个子目标的动作步数估算,而不是只数最终成功那一步。这样得到的数量通常远大于人直觉里的「几步就能做完」,也解释了为什么单步分数看着不错、整段却经常卡在中间。
对照单步动作正确率和整段任务完成率
两个指标的落差来自统计口径不同。单步指标的分母是被评估的动作步,判定依据是与参考动作或可行动作集合比对;整段指标的分母是 episode,判定依据是终局状态是否满足任务成功条件。同一批 episode 里,前者会把大量中间步算进去,后者只看收尾那一刻的结果。
| 对比维度 | 单步动作正确率 | 整段任务完成率 |
|---|---|---|
| 统计单位 | 单个「状态—动作」对 | 单条 episode 或整条轨迹 |
| 判定依据 | 动作与参考或可行动作比对 | 终局状态是否满足成功条件 |
| 对早期偏航的敏感度 | 低,错误可能被后续步稀释 | 高,早期偏航会拉长轨迹或直接终止 |
| 回答的问题 | 动作层是否学会 | 目标层能否走完 |
| 常见失真 | 分数高但执行不连贯 | 分数低但说不出坏在哪一步 |
落差可以这样推导:设单步在评估状态分布下的正确率为 p,整段需要 n 个关键动作都做对,在各步近似独立的假设下,整段完成率的上界约为 p 的 n 次方,随 n 增大单调下降。实际值通常比这个上界更低,因为单步评估用的状态分布往往比长程实际遇到的状态更「干净」——真实轨迹会走进模型自己造成的偏态状态,比如背包已满、工具已损坏、身处封闭空间。要验证这一点,不必先改模型,只要在日志里同时保留两列数:每步的动作判定结果和每条 episode 的终局结果,成对统计后看落差是否集中在长 episode 上。下面是一个可直接替换字段名的统计骨架:
def summarize(episodes):
step_total = step_ok = 0
ep_total = ep_success = 0
for ep in episodes:
ep_total += 1
if ep.get('outcome') == 'success': # 终局判定来自评测脚本
ep_success += 1
for s in ep.get('steps', []):
step_total += 1
if s.get('action_match'): # 单步判定来自动作比对
step_ok += 1
return {
'n_episodes': ep_total,
'episode_success': ep_success / ep_total if ep_total else None,
'step_acc': step_ok / step_total if step_total else None,
}
把这段放在离线分析脚本里跑日志,不改评测逻辑,只做对照;如果两者接近,说明任务 horizon 还短,单步指标够用;如果落差大且随 horizon 拉长而扩大,就该改用整段指标做主线。
列出中途失败却仍被记成完成的几种情形
判定口径的边界比指标本身更容易出错。以下三类情形在没有写清成功条件时最容易被误记,处理方式需要以 MineExplorer 公开任务说明中的成功条件与 episode 切分规则为准;说明没写清的部分,建议在自有评测配置里显式定义并留档,不要靠肉眼判断。
| 情形 | 常见记法 | 风险点 |
|---|---|---|
| 部分完成 | 任务定义里若区分了分级目标,按最细的成功条件记 partial,不计入 success | 把 partial 算成 success 会系统性高估长程能力 |
| 重试后成功 | 按 episode 边界切分,重试作为新 episode;或单独记 attempts 与最终结果 | 把多次尝试并成一次,会低估实际步数消耗 |
| 中途改计划 | 只看终局是否满足目标,改计划本身不判错 | 用参考轨迹逐步比对会把合理改道误判为失败 |
验证方式是抽查:从日志里各抽几条被记为 success 的 episode,回放看终局状态是否真的满足配置里的 success 表达式,同时确认没有因为超时截断、异常退出或奖励条件写宽而被判过。这一步通常比调指标公式更有价值。
按项目目标决定看哪一层指标
选型不必纠结哪个指标更「正确」,先看这一轮要回答什么问题。短程任务与长程任务的适用场景可以这样分:
- 短程任务更适合:动作层回归、感知或操作接口是否接通、单步策略调参、迭代节奏快的实验。主线看单步正确率加短 horizon 完成率。
- 长程任务更适合:目标分解、状态跟踪与记忆、错误恢复、存在阶段依赖的多步流程。主线看整段完成率,单步指标退为定位工具。
判断顺序建议固定为四步:第一步,看任务成功是否需要跨过多个子目标,是则归为长程;第二步,看单步动作空间是否固定、正误是否容易定义,否则单步指标本身噪声就大;第三步,看失败样本是否需要人工判断,需要则在评估前先把成功条件写进配置;第四步,看这个数字是否要对外描述能力,要则用整段指标并注明判定口径。走完这四步再决定主看哪一层,比先跑一轮再解释落差更省事。