dev.to #ai短讯
ButterflyBench:我改了一个指令,AI 还变了什么?
作者提交 Kaggle 基准测试挑战时发现,修改提示词读取 JSON 而非 CSV 时,多个模型自动调整了分隔符和表头设置。作者意识到自己的评分代码错误地假设其他参数必须保持不变,这一发现比排行榜分数更有价值。
这是 Kaggle 基准测试挑战的参赛作品
我进行了哪些基准测试
我的一个测试提示词写道:“读取 JSON 文件而不是 CSV。”
在我的多次重跑中,有三个模型的回答还同时设置了分隔符和表头行为未指定。它们并没有错。JSON 文件确实没有分隔符也没有表头行。我的评分代码错了,因为它假设其他 19 个设置必须保持原样。这个错误教会我的比任何排行榜数字都多,而这正是这篇文章真正要讨论的内容:检查模型是否只改变了你要求它改变的部分,对模型而言和对编写测试的人来说,都比听起来更难。
在我自己处理提示词和代码的工作中,我不断遇到同样的问题:当我改变一件事时,还有什么会出问题?助手可以执行你要求的编辑,但仍然移动了你从未提及的东西。因此,我构建了 ButterflyBench 来直接测量这一点。
这个名字来源于蝴蝶效应概念:微小的变化会在其他地方产生效应,我想衡量在 AI 规范中这种情况发生了多少。
工作原理。每个场景都从命令行数据工具的相同 20 个设置的规范开始。它涵盖 Python 版本、输入和输出格式、排序、编码、超时、分隔符、表头行、日志级别等,并以纯英语描述给模型。在一到十个回合中,模拟用户对其进行编辑。用户可能:
- 设置值(“给每个文件 60 秒超时。”)
- 撤销上一次更改,或间接描述的更改(“撤销关于日期打印方式的更改。”)
- 撤销“仍在生效的最早更改”
- 重做刚刚撤销的更改
- 删除要求(“排序不再重要。”)
- 说一些必须不改变任何东西的话,比如问题或对队友设置的评论
在每个回合之后,模型必须以 JSON 形式重新陈述完整规范。我还提前声明了三个耦合规则:TSV 输入强制使用制表符分隔符,并行处理关闭进度条,HTML 输出强制使用 UTF-8。这样,必需的副作用就不会被误认为是附带损害。
没有 LLM 裁判。一个小 Python 预言机在每个回合后重播活动更改并计算出完全正确的规范。Kaggle 分数是最终规范完全匹配的 40 个场景的比例。离线我还计算变更半径(未被请求而移动的设置的数目)、撤销后的恢复能力以及模型出错的第一个回合。
共有 40 个场景:6 个是我手动编写的,34 个是使用固定随机种子从模板生成的。它们涵盖了通过间接描述撤销(6 个)、撤销仍在生效的最早更改(6 个)、耦合规则(6 个)、干扰项(5 个)、撤销最后一次(4 个)、取消(4 个)、长链(4 个)、重做(3 个),以及各一个的取消后撤销和间接引用。
我并不声称发明了其中任何内容。例如,在多轮指令遵循方面已有深入研究,如 Multi-IF、MultiChallenge 和 EvolIF。我想要的是小而可复现的东西,带有确切的答案键,其中核心问题是附带变化。
我的第一次试点太简单了。它有 10 个设置和一到三次编辑的场景,Gemini 3.7 Flash 得了 5 分中的 5 分。我用 20 个设置、撤销、重做、间接引用、干扰项和耦合规则重建了它,那个版本开始区分模型。
测试的模型
我运行了来自四个提供商的七个模型:Gemini 3.7 Flash、Grok 4.20(推理和非推理版本)、Claude Sonnet 5、Claude Haiku 4.5、GPT-5.4 mini 和 gpt-oss-20b。
我是根据一条在查看任何结果之前就定好的规则来选择它们的:涵盖不同的提供商和尺寸,并在同一模型存在推理和非推理变体时各选一个(Grok 4.20 提供了这种变体)。实际限制也塑造了这份名单,主要是 Kaggle 的每日 AI 配额以及其“添加模型”列表中提供的模型。每个模型都接受了相同的 40 个场景、提示词和设置(使用库默认值,温度设为 0,种子设为 0)。Sonnet 5 的输出上限为 8,000 个 token,这远远超过了一次回复所需的几百个 token。
发现
首先说明一下数据。下面的排行榜是 Kaggle 上的公开运行结果,每个模型仅运行一次。在开发过程中,我还在本地运行了相同的场景,其中一些是针对较早版本的场景集进行的。这些运行的结果差异高达六个场景(针对同一模型),而下方详细的表格来自另一次本地运行,因此其总数与排行榜并不完全一致。请将排行榜视为快照,而非排名。
最有趣的结果并非排行榜本身,而是得分相似的模型以截然不同的方式失败。
| 模型 | 得分(40 个场景) |
|---|---|
| Grok 4.20 Reasoning | 1.00 |
| Claude Sonnet 5 | 1.00 |
| Gemini 3.7 Flash | 1.00 |
| gpt-oss-20b | 0.90 |
| Claude Haiku 4.5 | 0.78 |
| GPT-5.4 mini | 0.75 |
| Grok 4.20 Non-Reasoning | 0.75 |
在我所有的运行中,gpt-oss-20b 的得分在 40 分中的 30 到 36 分之间,Haiku 为 29 到 31 分,GPT-5.4 mini 为 27 到 30 分,Grok 非推理版为 27 到 30 分。因此,这三个最低分模型之间的差距处于噪声范围内,而顶级组别则各运行一次。
难点在于历史,而非编辑
为了最后一次本地运行,我详细考察了四个模型。其中三个——Haiku、GPT-5.4 mini 和 Grok 非推理版——表现几乎完全相同:
| 类别(场景数) | Haiku 4.5 | GPT-5.4 mini | Grok non-reasoning | gpt-oss-20b |
|---|---|---|---|---|
| 撤销仍在生效的最早更改(6) | 0 | 0 | 0 | 6 |
| 撤销最后一次更改(4) | 4 | 4 | 4 | 3 |
| 通过间接描述撤销(6) | 6 | 6 | 6 | 5 |
| 重做(3) | 3 | 3 | 3 | 2 |
| 耦合规则(6) | 6 | 5 | 3 | 5 |
| 取消(4) | 3 | 3 | 3 | 4 |
| 干扰项(5) | 4 | 5 | 5 | 4 |
| 长链条(4) | 1 | 2 | 1 | 0 |
| 总计(40) | 29 | 30 | 27 | 30 |
这三个模型处理“撤销上次更改”、“撤销对 X 的更改”以及重做操作时毫无困难。它们无法做到的是“撤销仍在生效的最早更改”,因为目标必须从哪些更改仍然活跃中推导出来。在我的第一次完整运行中,GPT-5.4 mini 在所有包含该句子的十个场景中全部失败。通常它什么都不改,或者反而撤销了较晚的更改。Haiku 在早期的一次运行中完成了 6 个中的 2 个,而在最后一次运行中完成了 6 个中的 0 个。大多数长链条失败源于同一原因,因为四条长链条中有三条包含这一转折。
gpt-oss-20b 是异常值。它通过了所有六个此类场景,却丢掉了几个简单的场景。在这次运行中,它与 GPT-5.4 mini 均得 30/40 分,但表现模式几乎相反。
无论是否推理,同一模型
Grok 4.20 推理版得分为 1.00,非推理版为 0.75。在我的本地运行中,非推理版每次都在最早更改场景中得 0/6 分,而推理版的唯一遗漏是我后来因措辞模糊而修正的一个场景。这是一对模型和各自的一次运行,我是在看到早期结果后才选择这对模型的。因此,我将此视为一个有趣的线索,并据此不对推理模型得出普遍结论。
通过率掩盖了模型的失败方式
Haiku 在 40 个场景中失败了 11 个,但移动的不相关设置最少(平均变化半径为 0.075)。GPT-5.4 mini 失败了 10 个,但移动最多(0.325)。Grok 非推理版为 0.125,gpt-oss-20b 为 0.200。单一得分无法告诉你模型的错误是局部的还是散布在整个规范中的。
我的许多“模型失败”其实是我自己的措辞问题
在责怪任何模型之前,我阅读了转录记录,发现了四个措辞问题。它们都没有改变预言机、评分标准或提示词,我为每次修复都做了记录。
- “收回我之前关于每个文件可能需要多长时间的说法。”Gemini 3.7 Flash 将超时时间设置为未指定(unspecified),而不是恢复为 30 秒。我的初始规范也提到了超时时间,因此“关于它们的说法”这一解读是合理的。我将措辞改写为“撤销我对……所做的更改”。
- “删除标题行。”我的指示告诉模型,对于用户已放弃的要求应使用未指定值。相同的场景在完全相同的文本上通过了一次,失败了一次。事实证明,这是每次都会出现的模式:一个在通过和失败之间切换的场景通常具有歧义性。
- “我不再关心每个文件的时间限制。”Claude Haiku 和 Grok non-reasoning 在所有四次全新运行中都回答 timeout='none'。none 是一个合法值,表示无限制,因此两种解读都是合理的。
- “读取 JSON 文件而不是 CSV。”这是从头开始的故事。在 9 次重跑中的 5 次里,Sonnet 5、Grok reasoning 和 gpt-oss-20b 都将分隔符和标题行设置为未指定,而我的预言机只知道三个耦合关系,导致其将合理行为误判为附带损害。我从场景中移除了未声明的耦合关系,而不是添加第四条规则。
如果你构建一个带有确切答案基准的测试集,我的建议是:当一个模型似乎破坏了某些内容时,检查你的提示词的合理读者是否也会做出同样的反应。
最终状态评分掩盖了波动
某次 gpt-oss-20b 的运行在第 1 至 3 轮中,有 20 个设置中的 18 个返回了未指定值,然后在第 4 轮恢复了。最终状态是正确的,因此最终状态评分永远不会显示这一点。只有首次分歧轮次显示了这一点。
头部饱和
三个模型的得分为 1.00,因此 ButterflyBench 对中等强度模型的区分能力优于对强模型的区分能力。
接下来我想测量的内容
- 最早变更指令的释义。所有包含该指令的十个场景都使用相同的句子,因此我还无法区分是“无法跟踪历史”还是“这种措辞很难理解”。
- 对每个模型进行重复运行,以便在排行榜上添加误差棒。
- 更长的链式交互以及每轮重置状态的版本,以区分早期失败与晚期失败。
- 代码作为工件。ButterflyBench 跟踪结构化规范。它不测试生成的代码是否能经受住编辑。
局限性
它测试的是对结构化规范的跟踪,而非代码内部的附带损害。这是一个小型公开基准,因此模型可能已在其中训练过。此外,预言机仅编码了我陈述的三个耦合关系。
如果你正在基于 Kaggle Benchmarks 构建应用
有三件事耗费了我的时间。嵌套评估强制要求 max_attempts=1,因此你需要在自己的任务中进行重试,否则某个提供商的一次小故障会表现为整个模型的“错误”。一个昂贵的模型曾出现 403 错误(“预估成本超过您的可用配额”),直到我通过 extra_api_params 限制了其输出令牌数。此外,主任务的 docstring 会成为其描述,且必须少于 255 个字符。
我的基准
- Kaggle 上的基准:https://www.kaggle.com/benchmarks/emmimalalexander/butterflybench
- 任务和排行榜:https://www.kaggle.com/benchmarks/tasks/emmimalalexander/butterflybench
- Notebook(包含预言机、评分及全部 40 个场景):https://www.kaggle.com/code/emmimalalexander/butterflybench
译文已达到本站中文翻译的字数上限,剩余内容请查看原文。