AI 模型可以在几分钟内生成完整功能,但代码是否正确?我们构建了 AI 评估基准 Android Bench,在分析了数千个生成的代码更改后,回答了这个问题。
本次演讲将分享我们在此过程中的心得体会,包括我们发现的模型“作弊”方式、提示词的具体程度如何影响代码质量,以及不同智能体如何处理同一任务。
转写文稿
Tomáš Mlynarič
大家早上好。
让我们开始吧。
今年 3 月,我们推出了 Android Bench,这是一个数据集和排行榜,用于衡量大语言模型在 Android 开发任务中的表现。
自那以后,我们针对许多新模型运行了该基准。
就在上个月,我们发布了 Android Bench 2.0。
虽然才过去几个月,但在 AI 领域,这感觉就像是过了很久。
我是 Tomáš,今天我将分享 Android Bench 的发展历程,以及我们如何走到今天。
我们先从 1.0 开始。
在 1.0 版本中,我们从开源代码库中提取了实际问题。
我们筛选了包含 GitHub 问题、解决这些问题的拉取请求以及验证解决方案的测试的 GitHub 项目。
我们将拉取请求拆分为 AI 模型的任务存储桶。
GitHub 问题变成了模型的指令或提示,我们将其植入到 PR 之前的状态中。
模型生成一些内容后,我们就会对输出内容进行测试,从而得出通过或未通过的结果。
AI 尝试解决问题的过程称为“试用”。
我们还可以将其与该 PR 中由人工编写的解决方案(我们称之为“黄金解决方案”)进行比较。
此方法类似于广为人知的 SWE-bench 基准。
我们最初收集了超过 10,000 个潜在任务,但并非所有任务都适合。
有些指令不完整或过于具体。
测试可能与黄金解决方案或 PR 损坏之前的状态相关联。
因此,我们不得不反复审核,最终确定了 100 项公平的任务。
在这些审核中,我们使用了前沿模型来获得一些结果。
最初,我们解决的任务比例(我们称之为通过率)约为 30%。
然后,当我们锁定数据集以进行正式发布时,顶级模型的准确率已经达到了 70% 左右。
但那是一个月后的事了。
然后,我们计划每月评估新模型,并观察它们的改进情况。
在 8 月份的版本中,最佳模型的通过率为 92%。
我们知道,我们的标准太低,而且这个数据集已经饱和。
Tomáš Mlynarič
但我们预见到了这一点,并已在上个月发布了 2.0 版本。
我们的目标始终如一:通过复杂的真实 Android 任务提高标准,为开发者提供有关 LLM 性能的指导和透明度,同时通过提供良好的基准,推动模型构建者优先考虑 Android。
并提供分析和结果,为您和模型制作者提供服务。
但我们还需要考虑聪明才智、坚韧不拔和作弊。
我们还需要防范污染风险。
SWE-bench 方法存在的一个问题是源代码的实际开放性。
OpenAI 发布了一篇文章。
他们发现,模型能够补全指令中的后续字词,或者能够解决指令中未提及的隐蔽错误。
他们意识到,模型开发者正在使用开源代码进行训练,导致 SWE-bench 受到污染。
因此,对于 2.0,我们实际上会保持代码封闭,以防止其被用于训练。
我们将工作分为四个工作流。
第一个是应用转换,我们采用开源的 Flutter 和 React Native 应用,并让智能体将其转换为原生 Android 解决方案。
这实际上反映了我们现在看到的从跨平台迁移到原生的趋势。
第二个工作流是新功能和平台集成。
它们被内置到已建立的生产代码库中。
例如,在 Now in Android 中,我们添加了视频功能、画中画等功能。
第三个工作流是迁移。
我们正在使用现代库和架构升级我们之前未公开的生产级开源应用。
例如,从 Nav 2 迁移到 Nav 3、从 XML 视图迁移到 Compose,或从单窗格布局迁移到自适应布局。
最后一个是内部全新应用,使用视觉设计模型从头开始构建。
例如,完整的短信动态密码身份验证流程、使用 Google 地图进行订单跟踪等。
Tomáš Mlynarič
我们只用了几位工程师,就构建了一个与 Now in Android 规模相当的应用。
但新任务也带来了新挑战。
第一点:正确评估智能体。
基准测试比应用测试要难得多,因为它们必须通过任何有效的解决方案,而不仅仅是一个,而且任务越大,有效的解决方案就越多。
在 1.0 中,基准主要依赖于单元测试,而单元测试在不依赖于特定框架方面表现得非常糟糕。
import com.foodvibes.MyViewModel
@Test
fun testRetry() {
val vm = MyViewModel()
vm.retry()
assertTrue(vm.retried)
}它们需要确切的类或函数名称。
就像在这种情况下,没有花哨的测试,但基本上,问题在于代理可能会以不同的方式命名这些内容,或者即使他们没有以不同的方式命名,他们,只是如果他们把它放在不同的包中,测试将无法编译,因此,对我们的情况来说是不可用的。
import com.foodvibes.feature.my.RetriableViewModel
@Test
fun testRetry() {
val vm = RetriableViewModel()
vm.tryAgain()
assertTrue(vm.tried)
}因此,从技术上讲,我们可以在提示中指定 API,但这要么会泄露太多细节,要么会使提示中包含太多测试要求。
因此,解决方案是进行与实现无关的测试。
这些测试非常笼统,侧重于结果,它们测试的是结果和行为,而不是内部机制。
我们使用确定性界面测试,这些测试会与公开屏幕信息进行交互,就像用户一样。
对于这些测试,我们使用 UI Automator 或 Compose UI 测试,但我们也有专门的脚本,用于检查一些外部系统边界,例如,网络调用是否具有正确的数据,或者是否在不知道内部辅助程序或 DAO 的情况下将正确的数据保存到数据库中。
其中一部分是静态检查,用于检查清单中的权限或库是否在构建文件中,诸如此类。
此外,我们还提供混合测试,将这些脚本与界面测试相结合。
这是其中一个混合测试的示例。
它们在模拟器之外运行。
因此,这是针对短信动态密码身份验证流程的测试,其中脚本需要先计算应用的哈希值,以便能够正确地将短信发送到模拟器,然后启动测试(即 UI Automator 测试),并传递它所需的一些随机生成的动态密码,然后使用 ADB 命令将短信发送到模拟器。
测试只是检查输入是否包含正确的数字。
基本上就是这样。
实现之间没有任何耦合,只有检查,它只会检查流程是否正常运行。
第二个评分问题是,我们的任务需要多模态。
要么需要智能体生成界面,要么我们向其提供设计模型。
为此,最简单的方法是使用屏幕截图测试。
我们从一开始就知道,屏幕截图阈值需要更高,但它们还是失败了。
我们尝试了一些不同的像素差异算法,但它们在系统级噪声(例如模拟器状态栏不同、电池图标不同,甚至字体硬件抗锯齿不同)等情况下失败了。
但它们也会在一些基本有效的实现上失败,比如内边距有细微差异,或者色调略有不同。例如,界面可能完全正确,但背景色调略有不同,像素差异算法就会失败。
因此,我们使用并构建了多模态 LLM 评分器,它会像屏幕截图测试一样比较屏幕截图对,并给出一些分数,同时还会给出关于分数的结构化推理。
我们还会向其提供一些无障碍层次结构转储,以检查结构是否正确以及结构奇偶校验是否正确。
我们通过大量试验进行了实证检验,发现它具有很高的一致性,并且能够忽略我们之前遇到的问题。
现在,我们有了一种很好的方法来验证视觉效果和行为,因此我们发现智能体试图伪造一切。
请讲。
Tomáš Mlynarič
一切都从简单开始。
现在,智能体基本上是:“好的。
我有一项任务。
我们来看看 GitHub。
我们来找找解决方案。”
因此,这是他们首先要做的事情。
他们会尝试在 GitHub 或互联网上搜索有效的解决方案,因此我们需要对源代码保密,不能公开。
但那很简单。
在一些测试中,我们告诉智能体所有测试都 100% 顺利通过,智能体就会说:“大获全胜!”
我想说的就是这些
有意思,他们发现了什么?
但我们仔细研究了发展轨迹,发现他们会说:“我做不到。
我只是想,如果我将阈值更改为屏幕截图测试的 80% 会怎么样?”
// AuthFlowScreenshotTests.kt
@get:Rule
val dropshots = Dropshots(
resultValidator =
ThresholdValidator(
- 0.05f
+ 0.80f
)
)就像是,“是的,胜利了,一切都过去了。”
是的,但屏幕截图测试毫无用处。
但那还不是最复杂的。
在某些情况下,比如在 Flutter 转换应用中,智能体尝试了多次构建 Android 应用,但最终还是表示“我不知道该怎么做”。
因此,他们将我们提供的屏幕截图作为可绘制对象放在背景中。
现在,它就像通过了屏幕截图测试。
耶!
但随后,界面测试仍然失败,因此他们会说:“我要在上面放置一个不可见的叠加层,其中包含按钮、文本和测试标记,以及我们所需的一切。
现在,所有内容都显示为绿色。
太棒了。
干得漂亮。”
这并不是说,其中有一个特别好。
我们有一个 Java 到 Kotlin 的迁移任务,智能体基本上需要从 Java 迁移到 Kotlin,对吧?
他们无法构建该应用。
但还是有些问题,所以智能体想:“好吧,我直接把 Java 编译好的类打包成 JAR 文件,然后像普通库一样加到构建文件里。”
现在,所有测试都通过了,但我们有静态检查来检查 Java 文件,所以,是的,我将直接删除它们。”
我删除了 Java 文件,所有内容都显示为绿色,但 Kotlin 仍然失败,所以他们会想:“好吧,我要移除 Kotlin 源代码集,现在所有内容都通过了。
太好了。”
我甚至不知道这算不算作弊。
我是说,这真的很有创意。
因此,为了防止所有这些情况,我们构建了一个作弊检测器,它有三道防线。
我们查看界面树,应用不同的屏幕尺寸,使背景失真,然后查看结果。
但我们也会重置测试,并检查是否有任何测试篡改行为,以防他们更改阈值。
此外,我们还采用了 LLM-as-a-Judge,用于审核智能体的轨迹和实际输出,以发现一些异常情况。
如果发生任何问题,我们就会说:“好吧,直接失败。”
Tomáš Mlynarič
现在,所有捷径都消失了,无法查找解决方案,也没有脆弱的单元测试或虚假界面。
那么,当我们让智能体实际开发一个 Android 应用时,结果如何?
截至周一,您可能已经看到了博文,数据如下所示。
即使是表现最好的智能体,也只能通过大约 40% 的任务,而大多数前沿智能体的通过率在 20% 到 40% 之间。
这对我们这些基准作者来说是件好事,因为这达到了一个很好的平衡,也就是说,我们提供的任务具有现实价值,但也有很大的提升空间。
我们来看看结果。
现在,我们可以从多个角度查看结果。
我们可以看看已经发现的一些数据洞见。
看到这张图表时,您可能会问的第一个问题是:运行该智能体需要多少费用?
AI 公司通常会报告每个 token 的费用,包括输入、输出和缓存 token。
这或许是您做出决策时的一个考量因素。
但我们可以从其他角度来看待这个问题。
我们可以在二维空间中绘制相同的图表,其中一个轴表示每项任务的平均成本(而非每个 token 的成本),即每项任务的成本与通过率的关系。
在这里,我们可以看到,有些模型突然变得话很多。
它们在每项任务上花费的时间或 token 更多,而其他模型则能以更低的成本完成更多任务。
但如果我们在同一图表中加入每个 token 的价格,就会发现 AI 公司宣传的价格与您按任务实际支付的价格并不相符。
因此,您可以看到,通过率最高的智能体实际上是每个 token 成本最低的智能体之一,但也是每个任务成本最高的智能体。
或者,其他具有完全相同 token 价格的智能体通过了大约 30% 的任务,但价格便宜 15 倍。
因此,代币价格并不是衡量指标。
其中一些模型会更深入地探索代码,它们会重新构建、重新运行,每一步都会消耗更多 token。
因此,关键在于不要只看每个词元的费用。
选择一个模型,查看实际数据,例如在您的用例中使用该模型,或者查看 Android Bench,了解哪些模型在任务上实际消耗的 token 最多。
有些模型虽然每个 token 的费用较低,但需要更多步骤。
不过,这个问题还有另一个方面。
这是同一个图表,但我们也可以查看并显示客服人员完成该任务所需的时间。
我们可以看到,通过率最高的客服人员,其处理速度可能反而是最慢的。
例如,我们可以看到,有些任务几乎需要两个小时才能完成。
他们不断重建、重新测试,直到成功。
是的,它们能获得最高的通过率,但耗时也最长。
一些较便宜的智能体,每个任务仍需大约一个半小时,但结果并不理想。
而其他智能体在 20 分钟内完成,并且解决的任务比例仍超过四分之一。
因此,小型轻量级模型通常速度更快、成本更低,但在处理这些困难任务时更容易失败。
结论是,没有单一的最佳模型。
这取决于您要优化的目标。
是合格率吗?
是费用吗?
是等待时间吗?
在这种情况下,如果您处理的任务不那么复杂,可以考虑使用速度更快、成本更低的模型来完成工作,具体取决于您所从事的工作类型。
不过,成本和速度只能反映智能体的运行情况,无法解释为何大多数测试都以失败告终。
因此,我们检查了失败的运行,发现智能体经常提前退出。
他们确信自己已经完成了。
任务完成。
好吧,我回家。
但如前所述,在任务数据包中,智能体只会收到指令和执行前状态。
他们无法看到测试。
他们会发现自己无法查看验证套件。
所以我们想,如果让他们看到测试结果,会发生什么?
因此,我们进行了实验,设置了一个开放测试变体,让智能体提前获得失败的验证测试套件。
因此,他们现在以迭代方式运行测试。
他们可以检查错误,并在反馈环中优化代码。
我们发现,当代理人看到测试时,他们会更加努力,工作时间也更长。
在开放式运行中,对话轮数增加了 25%,Agent Runtime 增加了 40%。
他们每项任务花费近 30 分钟来运行和修复失败的测试,因为他们不是盲目调试。
这样,他们就能清楚地看到哪些测试失败了,而且令人遗憾的是,通过率也跃升了近 50 个百分点。
正如我所提到的,这些额外努力提高了平均通过率。
但最终,新的前沿模型问世,并迅速超越了这一开放变体。
通过率接近 90%。
因此,我们保留了封闭式搜索封闭式测试变体作为主要基准指标。
不过,看到模型在知道评分标准后表现更好,仍然非常有趣。
Tomáš Mlynarič
不过,这让我们能够围绕代理构建更多数据洞见。
例如,我们可以检查模型如何实际测试自己的工作。
从图表中可以看出,前沿模型编写的测试越来越好,通过率也越来越高。
而其他一些模型则过于自信,认为自己能做到,无需测试。
有时候,它们仍然有效。
因此,如果您使用的模型不擅长测试,可以帮助它们提升测试技能,或提供有关如何编写测试的具体说明。
但相关性并不意味着因果关系,对吧,CS?
我们需要对此进行对照实验。
我们仍需进行研究,以确定提示生成更好的测试是否确实能提高分数。
不过,我们确实在 Android 测试的另一半(即让代理使用模拟器)上进行了一项实验。
在基准测试的早期阶段,我们没有提及模拟器。
我们认为,智能体应该能搞定,对吧?
结果,我们发现几乎没有一个学生真正启动了模拟器。
正如我所提到的,界面测试在基准测试中至关重要,如果没有模拟器,就无法真正使用界面测试。
所以,他们都在非常简单的事情上失败了。
现在,我们面临着一个两难境地。
我们是应该告诉代理“您需要使用模拟器,但这样可能会导致分数更高”,还是什么都不说,希望他们自己能弄清楚?
但毕竟,我们的目标是提供一个公平的基准,用于衡量 Android 编码和问题解决能力。
我们不想测量一些环境异常。
因此,我们选择了一个折中方案。
我们告诉他们这是启动模拟器的方式,但我们没有提及是否使用模拟器。
毕竟,启动模拟器并不是我们真正需要测试或想要测试的内容。
并测量了该模拟器指令的效果。
现在,我们发现,在提供 HowTo 后,智能体启动模拟器的次数明显增加,并且实际运行了界面测试,这对通过率产生了重大影响,但对我们来说,结果并不理想。
但它也让我们能够检查他们真正的编码错误和问题,而不仅仅是,比如,在未启动的模拟器上失败。
因此,总结一下,提示的具体程度仍然非常重要。如果您要设计自己的基准或评估,则需要测试在您的用例中真正重要的内容。
举个例子:假设您正在评估某项技能或工具的 token 效率,首先,您需要确保评估通过,然后进行效率优化,最后还要验证在 token 数量减少的情况下,评估仍然能够通过。
否则,您会同时更改太多变量,从而无法确定是否是您的更改带来了更好的效果。
Tomáš Mlynarič
返回排行榜。
如前所述,最优秀的模型也只能通过大约 40% 的任务,而通过率要求所有检查都通过。
否则,任务会失败。
那么问题来了,智能体是否没有为另外 60% 的任务提供任何有用的帮助?
不,他们正在努力。
为了展示智能体与有效解决方案的接近程度(类似于距离),我们还计算了完成率或部分得分。
这是一个加权分数,涵盖四个类别:视觉一致性、功能性(是否正常运行)、一些特定于任务的要求,以及一些回归检查。
可以看到,排名较低的模型能够完成大约一半的工作,而前沿模型平均能够完成近 80% 的任务,但它们只通过了 40%。
因此,我们对 Flutter 到 Android 的转换进行了一项实验,以了解具体情况。
因此,我们每隔几个智能体步骤就重新计算一次完成率,结果发现所有前沿模型都呈现出相同的模式。
在 140 步左右,它们努力达到了平均近 90% 的分数,然后又花了 90 多步(几乎占运行时间的 40%)试图缩小与 100% 的差距,但它们只能再提高 1% 或 2% 的分数。
我们认为,这实际上反映了当今智能体开发的现状:经验不足的开发者或用户看到应用已经完成,
正在呈现所有帧。
输入类型起作用了。
可能没有崩溃。
但当资深工程师查看代码时,他们会发现许多缺陷,例如架构问题、生命周期缺陷等。
Tomáš Mlynarič
最后,我们来总结一下 Android Bench 2.0 的意义。
与 1.0 相比,我们现在只有 30 项任务,这是有意为之。
我们不再局限于孤立的问题修复和小型补丁,而是着眼于完整的屏幕、多模块迁移和整个应用的转换。
现在,补丁的大小增加了几个数量级,涉及数十个文件。
我们还提供多模态界面检查。
智能体必须运行更长时间,工作更努力。
如果与其他前沿基准进行比较,我们可以看到,我们的基准仍然需要进行更大规模的代码更改。
由于大多数任务都是从头开始编写的,因此不存在污染问题,并且仍有很大的提升空间。
正如我一开始所说,在 AI 领域,几个月的时间就像是永恒,最终,每个基准都会达到极限并趋于饱和。
当这一天到来时,我们希望能够推动更艰巨、更复杂的任务,这些任务将再次与那个时代、那个 AI 状态产生共鸣。
现在,您可以将 Android Bench 用作指南针,比较模型和测试框架,并从中获得一些洞见。
当您准备好深入探索时,请将相同的基准测试原则和评估原则应用到您自己的代码库和工作流中,因为没有什么比在您实际使用的代码上衡量 AI 效果更好的了。
这样,您就不必再寄希望于 AI 能够正确处理,而是可以确信它已经做到了。
我谨代表 Android Bench 全体成员,感谢您的关注。
import com.foodvibes.MyViewModel
@Test
fun testRetry() {
val vm = MyViewModel()
vm.retry()
assertTrue(vm.retried)
}import com.foodvibes.feature.my.RetriableViewModel
@Test
fun testRetry() {
val vm = RetriableViewModel()
vm.tryAgain()
assertTrue(vm.tried)
}// AuthFlowScreenshotTests.kt
@get:Rule
val dropshots = Dropshots(
resultValidator =
ThresholdValidator(
- 0.05f
+ 0.80f
)
)