摘要
1) 一句话总结
Spaghetti Bench 评估了 AI 智能体修复并发漏洞的能力,指出传统单元测试容易导致智能体产生“伪修复”,而引入受控并发测试工具(如 Fray)能显著提升简单并发漏洞的修复率,但智能体在应对复杂现实项目时仍存在根本性的推理短板。
2) 关键要点
- 评估背景:主流基准测试(如 SWE-bench)严重缺乏并发漏洞用例。由于线程调度的非确定性,标准单元测试在验证并发漏洞时往往会因为“碰巧通过”而失效。
- 实验设置:基于 OpenHands 框架,评估了 6 款最先进的 AI 模型(如 Claude 3.5 系列、GPT-5.2、Gemini 3 系列等)。
- 数据集划分:共包含 39 个 Java 并发漏洞,分为 Spaghetti-Easy(28 个单文件独立并发谜题)和 Spaghetti-Hard(11 个来自 Apache Kafka 的复杂现实世界漏洞)。
- 对比配置:对比了智能体在“仅使用默认开发工具”与“可调用 Fray(受控并发测试工具)”两种情况下的表现,超时限制为 20 分钟。
- 智能体默认缺陷:在无专用工具时,智能体倾向于通过多次重复运行标准测试来验证修复。这会产生虚假的安全感,导致智能体过早提交表面正确但实际无效的补丁(例如 Claude Sonnet 4.5 仅修改单个字符的无效修复)。
- 简单任务表现(Spaghetti-Easy):引入 Fray 后,所有模型的修复成功率均获得显著提升。Fray 能够跨数千种线程交错进行验证,帮助智能体捕获假阳性并持续迭代出更完善的补丁。
- 复杂任务表现(Spaghetti-Hard):无论是否使用 Fray,模型在现实世界复杂漏洞上的表现都极低。例如 GPT-5.2 在 Kafka 漏洞中仅做出了治标不治本的去重修复,未能发现需要加锁的根本原因。
- 未来计划:Spaghetti Bench 已开源,未来计划增加更多现实世界并发漏洞,并扩展对其他语言及其并发测试工具(如 Rust 和 Shuttle)的支持。
3) 风险与能力缺口
- 验证工具局限性风险:仅依赖标准单元测试无法有效评估 AI 智能体在非确定性领域(如并发、分布式系统、日期/时间处理)的真实编码能力。
- 复杂系统推理缺口:AI 智能体在面对大型代码库时,缺乏对系统架构、状态管理模式及多组件交互的深入推理能力,难以诊断复杂并发漏洞的根本原因。
- 反馈机制缺口:现有的测试工具仅能提供“测试失败”的反馈,智能体缺乏交互式探索线程交错的工具(如 Fray Debugger)以及关于特定线程交错和不变量违规的针对性诊断信息。
- 训练数据缺口:由于并发漏洞在开源 Issue 中相对罕见,LLM 缺乏针对性的并发数据集来提升其固有的并发推理能力。
正文
太长不看(TL;DR): 在修复竞态条件(race-condition)漏洞方面,AI 智能体的表现不如在传统软件工程任务中那么成功。但是,如果为它们提供用于并发测试和确定性重放的工具,其修复成功率将得到显著提升!
软件工程智能体在软件开发中正变得越来越普及。随着这些由 AI 驱动的工具承担起更复杂的编程任务,了解它们在各种具有挑战性的领域中的表现变得至关重要。像 SWE-bench [1] 这样的基准测试已经成为衡量编码智能体解决现实软件问题能力的标杆,它通过数百个来自流行开源项目的 GitHub Issue 对智能体进行评估。
然而,并发(Concurrency)作为现代软件工程的一个关键方面,在 SWE-bench 中却明显代表性不足。在数百个任务中,只有一个竞态条件的例子。这一空白非常关键,因为并发编程是性能关键型系统(从 Web 服务器、数据库到分布式系统和移动应用)的基础。
评估并发漏洞修复的挑战不仅在于数据集的覆盖率。SWE-bench 通过运行特定的测试来验证提交的补丁:那些在包含漏洞的代码上会失败的测试,在修复后必须通过,而现有的其他测试应继续保持通过状态。这种方法对确定性漏洞很有效,但在面对并发问题时就会失效。针对并发漏洞的测试可能会因为线程调度的不同而时过时不过——这意味着即使底层漏洞根本没有被修复,测试也可能因为在特定的测试运行期间没有发生有问题的线程交错(thread interleaving)而侥幸通过。
我们评估了领先的 AI 编码智能体在修复并发漏洞时的表现,并得出了一个重要结论:如果没有合适的测试工具,即使是最强大的模型生成的补丁,表面上看起来是正确的,但在特定的线程交错下仍会默默失效。 本文将探讨 Fray [2] 的受控并发测试如何揭示这些局限性,以及为什么它对于可靠的并发漏洞验证是必不可少的。
实验设置
我们使用 OpenHands(一个用于软件工程任务的智能体框架)作为基础脚手架,评估了六个最先进的模型:
- Claude Opus 4.5
- Claude Sonnet 4.5
- OpenAI GPT-5.2
- Qwen 3 Coder 480B
- Google Gemini 3 Pro Preview
- Google Gemini 3 Flash Preview
每个模型都在我们数据集中的 39 个 Java 并发漏洞上进行了测试,这些漏洞分为两组:
- Spaghetti-Easy(28 个程序): 收集自 SCTBench [3],每个程序都是一个大约 100-200 行代码的单文件。它们是独立的并发谜题,隔离了特定的漏洞模式。
- Spaghetti-Hard(11 个程序): 收集自开源项目 Apache Kafka 的并发漏洞。这些是嵌入在庞大、复杂的代码库中的现实世界漏洞,诊断和修复的难度要大得多。
所有这些漏洞都有一个共同点:即使运行数千次测试,也无法可靠地触发漏洞——竞态条件太微妙了,且依赖于特定的线程交错。然而,Fray 能够在几秒钟内稳定地发现这些漏洞。
我们在两种配置下运行了每个模型:
- 不使用 Fray(Without Fray): 智能体只能访问默认的开发工具(bash、文件编辑器、测试运行器)。
- 使用 Fray(With Fray): 智能体可以额外调用 Fray 来在不同的线程调度下测试它们的补丁。
在这两种配置中,我们都使用 Fray 作为最终的仲裁者,来验证智能体提出的解决方案是否真正修复了漏洞。这意味着,即使在“不使用 Fray”的配置中,智能体可能会生成它们自认为正确(基于它们自己的测试)的补丁,但我们仍会使用 Fray 的交错探索来验证其正确性。如果智能体在 20 分钟内未能生成补丁,我们将其视为失败案例。
智能体的默认行为模式
在没有任何额外工具的情况下,AI 智能体通常通过运行 bash 命令重复执行测试来验证并发修复:
这种逻辑很直观:如果测试连续通过了 10 次、20 次甚至 100 次,那么修复肯定是正确的。不幸的是,这种方法带来了一种虚假的安全感。测试通过可能仅仅是因为在这些运行期间从未发生过有问题的线程交错。这通常导致智能体在提出第一个解决方案后就草草结束任务,因为它从测试结果中错误地假设修复已经成功。
案例分析:WorkStealQueue
让我们看一个基准测试中的具体例子:WorkStealQueue。
这是一个无锁的工作窃取队列(lock-free work-stealing queue)——一种并发数据结构,单个所有者线程可以从一端(队尾)推入和弹出项目,而多个“窃取者”线程可以从另一端(队头)获取工作。测试会创建项目,将它们推入队列,然后让所有者和窃取者共同处理它们。每个项目应该被精确处理一次,通过检查 field == 1 来验证。
漏洞: 原始代码在 pop() 方法中存在竞态条件。当队列中只有一个元素时,所有者递减队尾并检查 if (head <= tail)。如果此时一个窃取者并发地递增了队头,两个线程可能都会认为自己成功获取了该项目;或者所有者可能会从 elems 数组中读取到一个过期的值,因为该数组不是 volatile 的。这会导致项目被处理多次或根本不被处理。
Claude Sonnet 4.5 在两种配置下都尝试了这个任务。在不使用 Fray 的情况下,智能体生成了一个最小化的修复:
这个单字符的修改(将 <= 改为 <)试图修复边界条件。然而,这并没有真正解决问题。问题不仅仅在于比较操作——而在于“检查后执行(check-then-act)”的序列不是原子的。即使使用了 <,窃取者仍然可以在检查和数组访问之间递增队头,并且所有者仍然从一个可能包含过期值的非 volatile 数组中读取数据。智能体测试了这个修复并将其标记为完成。但是,当我们进行最终验证时:
Fray 在 2,616 次迭代后仍然发现了漏洞,这证明了这些竞态条件有多么微妙。由于没有 Fray 来指导其解决方案,智能体过早地停止在了一个不完整的修复上。
当智能体可以将 Fray 作为工具调用时,它们可以运行它来验证跨数千种不同线程交错的修复,并利用它来迭代那些无法被重复单元测试捕获的解决方案。
我们可以对比同一个模型在有 Fray 访问权限时的表现。在通过 Fray 对其解决方案进行了 5 次改进后,智能体生成了一个全面的修复方案,解决了多个竞态条件:
- 将
elems数组和mask设为volatile,以确保跨线程的可见性。 - 在本地捕获
mask,以防止在数组调整大小时出现不一致的索引。 - 完全重写了
pop(),通过加锁和双重检查正确处理单元素情况。 - 在
push()中添加了稳定性检查,以检测数组是否在操作中途被调整大小。 - 修复了第一次尝试中的边界条件(
<=改为<)。
在这个补丁上,Fray 验证通过了 100,000 次迭代,没有出现错误。通过提供更强大的验证工具,智能体能够提出更复杂的补丁,而不是默认采用第一个(通常也是最偷懒的)补丁。
案例分析:KAFKA-17402
来自 Apache Kafka 的一个真实案例展示了智能体如何生成看似合理,但在受控并发测试下仍然失败的修复。该漏洞发生在 DefaultStateUpdater 中,它负责管理跨多个线程的 Kafka Streams 任务的状态恢复。当调用 getTasks() 时,任务在状态转换期间可能会短暂地出现在多个内部队列中,从而可能导致返回重复的任务。
GPT-5.2(在不使用 Fray 的情况下)分析了显示“测试期望得到 2 个任务但得到了 3 个”的错误信息。智能体进行了详细的根本原因分析:
智能体的诊断:
DefaultStateUpdater#getTasks() 通过流式处理多个内部集合(待添加任务、updatingTasks、restoredActiveTasks 等)来构建其结果,并将每个元素包装在一个新的 ReadOnlyTask 中,然后收集到一个 Set 中。在正常的并发操作期间,一个 Task 可能会暂时存在于多个集合中(例如,在被添加到 restoredActiveTasks 之后,但在从 updatingTasks 中移除之前)。
因为每个包装器都是一个不同的对象(ReadOnlyTask 没有 equals/hashCode 方法),最终的 Set 可能包含重复项,从而导致大小膨胀(表现为期望 2 个,得到 3 个)。
修复方案: 先对底层的 Task 对象进行去重,然后再包装它们。
基于此分析,智能体修改了 getTasks(),首先将任务收集到一个 Set<Task> 中(消除了同一任务对象短暂的重复可见性),然后再映射到 ReadOnlyTask。
智能体的推理似乎很合逻辑——它正确地识别出任务并发地出现在多个集合中,并诊断出 ReadOnlyTask 包装器身份的问题。运行现有的 gradle 单元测试通过了,因此该修复被认为是正确的。
然而,当 Fray 验证这个补丁时:
在 15 次迭代后,Fray 发现了一个测试仍然失败的场景。去重的方法只是治标不治本。
将其与 Kafka 开发者合并的实际修复进行对比:
真正的问题在于 updatingTasks.remove() 是在锁外部调用的,从而产生了一个竞态条件。正确的修复是将这单行代码移到锁内部,以确保原子性的状态转换。这与智能体的去重策略在根本上是不同的。由于目前工具的局限性,智能体无从得知其修复是不充分的。这表明智能体需要更好的工具和推理能力,才能正确诊断这些现实世界并发问题的根本原因。
评估总结
我们在 39 个并发漏洞任务上评估了多个最先进的模型,分别在有和没有 Fray 的情况下进行了对比。结果揭示了 Spaghetti-Easy 和 Spaghetti-Hard 之间不同的模式。
Spaghetti-Easy 表现(28 个任务)
在 Spaghetti-Easy 基准测试中,Fray 为所有模型带来了显著的提升。这种提升是巨大的,特别是对于顶级模型而言。Fray 帮助捕获了假阳性(即智能体生成的补丁看似正确,但在系统测试下失败的情况)。WorkStealQueue 的例子清晰地展示了这种模式。
Spaghetti-Hard 表现(11 个任务)
在 Spaghetti-Hard 基准测试中,情况则截然不同:
无论是否使用 Fray,在现实世界问题上的表现都很低,这表明单靠 Fray 不足以解决这些更复杂的漏洞。像 Kafka 这样的大型代码库中的现实世界并发问题,需要对系统架构、状态管理模式以及多个组件之间的交互进行更深入的推理——这些挑战超出了改进验证工具所能解决的范畴。这指出了一个关键的差距:虽然 Fray 帮助智能体迭代和验证它们的修复,但智能体在面对复杂的现实世界并发漏洞时,仍然难以完成初始的诊断和推理。
核心收获
我们的发现揭示了验证工具对于 AI 编码智能体的价值和局限性:
1. 需要更强大的验证工具来扩展 SWE-Agent 的评估范围
单靠单元测试不足以评估软件工程智能体在许多领域中的表现。在并发领域,线程交错的非确定性意味着即使漏洞未被修复,标准测试套件也可能通过。我们的结果表明,像 Fray 这样的受控并发测试工具对于可靠的验证是必不可少的。
并发不太可能是唯一一个标准测试失效的领域。我们预计在其他涉及固有非确定性的领域(如分布式系统和日期/时间处理)也会遇到类似的挑战。为了有意义地将 SWE-agent 的评估范围扩展到确定性漏洞修复之外,我们可能需要找到为每个问题领域的独特挑战量身定制的创造性验证解决方案。
2. SWE-Agent 的并发推理能力需要根本性的提升
无论是否使用 Fray,在现实世界中的低迷表现揭示了一个关键差距:智能体难以找到复杂并发漏洞的根本原因。仅仅通读代码和运行单元测试不足以提供必要的反馈。提高并发推理能力可能需要比现有工具更好的调试工具和反馈机制。我们可能需要为智能体提供以下方式:
- 交互式探索线程交错: 允许智能体决定在各个断点运行哪些线程,以了解竞态条件是如何表现的工具。我们为此创建了 Fray Debugger;确定如何将其适当地暴露给智能体是一个充满希望的下一步。
- 获取针对性反馈: 不仅仅是“测试失败”,而是提供关于哪种线程交错触发了失败以及违反了什么不变量的诊断信息。
- 改善固有的并发推理: 由于并发漏洞在开源 Issue 中相对罕见,针对性的并发数据集可能会提高 LLM 在诊断这些并发漏洞根本原因方面的表现。
亲自尝试
Spaghetti Bench 是开源的,并在 GitHub 上可用。你可以:
- 在你自己的模型上运行基准测试(必须支持 LiteLLM)
- 探索智能体交互的完整追踪记录
- 贡献新的并发漏洞示例
未来工作
我们计划通过向数据集中添加更多现实世界的并发漏洞来扩展 Spaghetti Bench,并扩展对其他具有成熟并发测试工具的语言的支持,例如结合 Shuttle 的 Rust。
结论
并发漏洞对 AI 编码智能体提出了独特的挑战,因为标准的验证方法(多次运行测试)在大多数情况下是不够的。我们的评估表明,像 Fray 这样的受控并发测试工具对于扩展软件工程智能体评估的范围至关重要,它提供了更可靠的验证,并能改进智能体最初提出的不充分的补丁。
随着软件工程智能体越来越有能力快速、廉价地生成整个软件仓库,为它们配备合适的专用工具以验证各种问题领域中的解决方案变得越来越重要。并发只是一个例子,但我们预计在其他标准测试不足的领域也会出现类似的模式。
参考文献
- Jimenez, Carlos E., et al. “Swe-bench: Can language models resolve real-world github issues?” arXiv preprint arXiv:2310.06770 (2023).
- Li, Ao, et al. “Fray: An Efficient General-Purpose Concurrency Testing Platform for the JVM.” Proceedings of the ACM on Programming Languages 9.OOPSLA2 (2025): 4035-4063.
- Thomson, Paul, Alastair F. Donaldson, and Adam Betts. “Concurrency testing using schedule bounding: An empirical study.” Proceedings of the 19th ACM SIGPLAN symposium on Principles and practice of parallel programming (2014).