分析 screenshot-to-code 循环的收敛问题:结构相同的渲染可能像素差异巨大,而像素差分在错误足够大时信号饱和到近乎平坦,导致优化实质上变成随机游走。
生成标记、渲染、与目标比对、修正、重复。每个 screenshot-to-code(截图转代码)演示都有这个循环。决定它是否值得运行的有三个因素,但没有一个是模型本身。
仓库:https://github.com/dev48v/render-loop — 开源,MIT许可,25个测试用例,仅用标准库,不依赖浏览器。整个循环在你的浏览器里运行:https://dev48.infy.uk/agentlab/vol2-01-render-loop.html
两次渲染可以在结构上完全相同,却在数千个像素上有差异——字体推进方式不同,一条边落到了半个像素上。所以分数永远到不了零。
比这个下限更糟糕的是饱和问题。一旦候选结果的框与目标框不再重叠,进一步的误差几乎不会改变像素分数:
Structural 持续攀升。Pixel 已经压平到每步 0.008——剩余信号相差 441 倍。由这个数字驱动的循环是在随机游走的基础上多加几步而已。
结构比较会将框与框一一匹配,当布局一致时恰好达到零,这个属性才让循环能够终止。
assert structural_diff(boxes, boxes).value == 0.0
total += (missing + extra) * 10.0 # a whole missing element is not a nudge
这个 * 10 很关键:如果"尺寸错误"和"完全缺失"得分相同,算法会乐于通过删除元素来减少误差。
每一次修正都可能让情况变糟。返回最后一次尝试结果的循环,返回的是它最后一次犯错产生的输出。
report = run_loop(TARGET, scripted([good, awful, awful]), patience=5)
assert report.best_source == good
assert report.attempts[-1].score > report.best_score # 51.13 vs 0.25
一个变量。这是整个项目里最便宜的正确性属性,大多数实现都缺少它,因为在一次成功的运行中两者是一样的。
说起来容易,做起来比听起来更容易出错——一个共享对象就够了。proposer(提议者)收到的是上一版的源码、其渲染后的框和一个分数。永远不是目标标记。
def test_the_generator_never_receives_the_target_source():
run_loop(TARGET, spy, max_attempts=2)
assert TARGET not in seen # only the previous source
assert seen[0] == "" # and the first call starts from nothing
仓库里有一个 peeking() 生成器会拿到答案。它存在纯粹是为了作为对照:它在第一次尝试时就匹配成功,而套件里其他任何测试都不允许这样。没有一个展示作弊样子的对照测试,一个"诚实循环是诚实的"测试就毫无意义。
matched · no_improvement · budget · gave_up · unrenderable
无法渲染的目标会在生成任何候选结果之前就停止——没有信号可以参照,任何输出都是猜测。无法渲染的输出是失败的尝试而非崩溃,因为生成器发出损坏的标记是正常情况。
而且故意为之,出厂的默认生成器无法解决一般目标,所以 python -m render_loop 最终会以诚实的 no_improvement 结束,而不是一个针对自己例子调优过的演示。
Agent Lab Vol 2 在这里开启。Vol 1 是五个懂得何时停止的工具;Vol 2 从一个懂得何时自己的反馈信号在对它说谎的工具开始:https://dev48.infy.uk/agentlab.php