同一段提示词生成的网页忽好忽坏 / WebCraftBench 该怎么固定输入?

文章导读
同一段提示词两次跑出结构完整度差很多,通常不是提示词本身在变,而是生成和评分链路上几个会漂移的量没有被固定:随机种子与采样参数、任务抽样顺序、页面渲染的等待条件与视口尺寸、以及超时设置。把这些量在运行脚本里钉住并把本次输入落盘,两次运行的差异才有资格归因到模型或提示词。
📋 目录
  1. Ⅰ 在运行脚本里固定随机种子与采样参数
  2. Ⅱ 把任务抽样顺序固定下来并落盘
  3. Ⅲ 统一网页渲染的等待条件与视口尺寸
  4. Ⅳ 跑两轮空对照,确认基线是否稳定
  5. Ⅴ 把改动后的差异逐条对到任务 id 上
A A

同一段提示词两次跑出结构完整度差很多,通常不是提示词本身在变,而是生成和评分链路上几个会漂移的量没有被固定:随机种子与采样参数、任务抽样顺序、页面渲染的等待条件与视口尺寸、以及超时设置。把这些量在运行脚本里钉住并把本次输入落盘,两次运行的差异才有资格归因到模型或提示词。

判断方向是先把环境变量钉死,再谈模型差异。适用场景:同一提示词、同一 WebCraftBench 任务集,两次结果不可比。操作动作:固定 seed 与采样参数、复用同一份任务 id 列表、统一渲染等待与视口、先跑两轮空对照。验证方式:空对照两轮分数接近且失败任务重合度高,再动提示词。风险边界:接口若不暴露 seed 或不支持确定性采样,只能缩小波动,不能指望完全复现。

在运行脚本里固定随机种子与采样参数

先让两次生成在源码层面可比。做法是把本轮所有影响输出的参数收进一个配置对象,在调用模型前打印并写入运行目录,而不是散落在脚本各处。

run_cfg = {
  'benchmark': 'WebCraftBench',
  'task_ids': [],            # 由任务清单文件填充,见下一节
  'seed': None,              # 占位:接口支持 seed 时填固定整数
  'temperature': None,       # 占位:以实际接口文档为准
  'top_p': None,             # 占位
  'max_tokens': None,        # 占位:值不确定就不要写死
  'timeout_s': None,         # 占位
}

# 调用前落盘
write_json('runs/<run_id>/run_cfg.json', run_cfg)
resp = model.generate(prompt, **run_cfg)

参数名一律以实际接口文档为准,这里给的是字段位而非官方命名。凡是接口文档里没确认的字段,留成 None 或 TODO,并在日志里标注未确认,不要凭印象写死一个默认值——写死猜测值比留空更糟,因为它会伪装成已固定。如果所用接口根本不支持固定 seed,就在 run_cfg 里显式标注 seed_unsupported,后续看波动时把这部分排除。

把任务抽样顺序固定下来并落盘

WebCraftBench 这类任务集如果在每轮启动时重新随机抽样,即使模型完全没变,两轮跑的任务也不是同一批,分数差异就无从归因。要求每轮把实际执行的任务 id 按顺序写成文件,下一轮直接复用这份文件。

# 第一轮:抽一次并落盘
python run_bench.py `--out` runs/0001 `--sample` 40 `--dump-tasks` runs/0001/tasks_run.txt

# 后续所有轮次:复用同一份列表,不再重新抽样
python run_bench.py `--out` runs/0002 `--tasks` runs/0001/tasks_run.txt

tasks_run.txt 每行一个任务 id,顺序也保留,便于按位对比。需要换任务集时新建一份列表文件并改运行编号,不要覆盖旧文件,否则之前的对照记录就失去参照。

统一网页渲染的等待条件与视口尺寸

页面还没渲染完就开始截图和评分,会制造出一批假失败,表现为同一任务时好时坏。等待条件和视口尺寸都要写进配置,不要用工具默认值。

render:
  viewport:
    width: 1440
    height: 900
    device_scale_factor: 1
  wait:
    until: networkidle     # 或 load,取值以所用工具支持为准
    extra_delay_ms: 800    # 固定延时,补字体与动画
    timeout_ms: 30000
  screenshot:
    full_page: true

验证方式是同一页面连拍两张截图做对比:逐像素比对或感知哈希都行,两张基本一致再开始评分。若两次截图仍有明显差异,说明页面里还有异步内容在变,可以加大 extra_delay_ms、屏蔽动画,或把动态时间戳一类元素在评分前剔除。视口尺寸一旦改变,布局和分数都会变,所以尺寸必须和截图一起进 run_cfg。

跑两轮空对照,确认基线是否稳定

在改任何提示词之前,先用完全相同的配置和任务列表跑两轮,量出同一输入下的天然波动。

python run_bench.py `--config` cfg/base.yaml `--tasks` runs/0001/tasks_run.txt `--out` runs/base_a
python run_bench.py `--config` cfg/base.yaml `--tasks` runs/0001/tasks_run.txt `--out` runs/base_b

把两轮结果记成一张表,留作后续对照的底稿:

  • 轮次:base_a / base_b
  • 总分:各自记录
  • 失败条数:各自记录
  • 失败任务 id:写成列表文件
  • 备注:是否中途报错、是否超时

如果两轮总分差异已经很大、失败任务几乎不重合,说明环境还没稳定,此时比较提示词改动没有意义,先回到前三节把种子、任务列表、渲染等待逐项确认。

把改动后的差异逐条对到任务 id 上

改动生效后,不要只看总分涨跌,要把失败任务集合做 diff,看变化集中在哪些任务。

python report.py `--run` runs/base_a `--list-failed` > runs/base_a/failed.txt
python report.py `--run` runs/after_a `--list-failed` > runs/after_a/failed.txt
diff runs/base_a/failed.txt runs/after_a/failed.txt

解读规则可以先用这套:两轮都失败的任务属于重合部分,通常与该改动无关,优先怀疑任务本身的固有难度或环境问题;改动后新出现的失败是最该查的回归,说明这条改动在特定任务上有副作用;从失败列表里消失的任务,代表该改动在这类任务上有效,但最好再跑一轮同配置确认它不是随机波动。如果失败任务几乎整体平移、重合度很低,往往是基线不稳而不是改动有效,回到空对照重新判断。