Coding Agent测试能力被高估一倍:AI代码合格率为何集体失真?
测试评测缺了什么?
眼下Coding Agent写代码越来越在行,写测试的水平也跟着往上走。补单元测试、给GitHub Issue写复现用例,甚至为整个仓库搭回归测试,Codex、Claude Code这类前沿Coding Agent都能干:翻仓库、改实现,再靠执行反馈反复修测试。
测试的角色因此变了。它不再只是质量保障,还越来越多被当作Agent自我验证的凭据、代码评审的证据,甚至强化学习里的奖励信号。
测试写得靠不靠谱,直接关系到这些下游环节敢不敢信它的判断。
那什么样的测试才算好?
现有主流测试生成Benchmark大多走单一参考评测(Single Reference Evaluation)的路子:生成的测试在原始代码上失败、在一份标准答案(Gold Solution)上通过,就算成功。
可同一个需求,正确的实现往往不止一种,看着合理、实则藏着缺陷的实现也不少。
有的测试恰好只「认得」标准答案,真正有Bug的代码反倒放过了;有的把标准答案的实现细节当成需求本身,其他完全正确的写法一概拒之门外。只拿一份参考答案来衡量,测试质量很容易被高估。
南京大学刘佳恒老师团队近日提出新的测试生成Benchmark TestPrism,含来自17个来源的300个测试任务和3000个候选实现,每个任务都配了经过验证的正确与错误实现面板,核心指标是Joint Success Function(JSF):测试必须在原始代码上失败、接受所有正确实现、拒绝所有错误实现。
结果显示,14种Coding Agent配置里,最强配置的JSF只有28.00%,而同一配置在单一参考评测下的成功率达到59.67%。换句话说,通过参考答案的测试有一多半换一批实现就靠不住了。
团队还对286条失败轨迹做了专家标注,归纳出3大类、7种测试生成错误机制,并提出TestHelix框架,靠异构合成、同伴交叉验证与递归自我改进,在相同推理预算下把JSF提升11.67至12.00个百分点。

测试评测缺了什么?
测试生成评测算不上新问题。
已有Benchmark覆盖的任务粒度各不相同:TestGenEval、CLOVER和MultiFileTest管的是函数、文件或多文件级别的测试编写,SWT Bench和TDD Bench Verified则把测试生成搬进真实代码仓库,要求测试复现GitHub Issue里描述的问题。
这些工作普遍沿用同一条成功标准:测试在修复前失败(Fail)、在参考修复后通过(Pass),也就是Fail-to-Pass。
标准简洁、可执行,但背后压着一个假设:只要测试能区分「修复前」和「参考修复后」两种状态,就足以判断任意实现对不对。现实里这个假设很难站住。
一种情况是测试漏了关键输入,或者断言做得太弱,参考答案能过,有缺陷的实现照样能过;
另一种情况是测试过度依赖参考答案的具体写法,比如固定的内部函数名、输出格式或中间状态,其他同样满足需求的实现就被误判成错误。
OmniCode、SWE Mutation等工作已经开始引入实现变体,但重点还是看测试能不能拒绝错误变体,「接受正确替代实现」这一侧仍缺系统评测。

表1|现有测试生成Benchmark的评测范围与评测目标对比(原论文Table 1)
论文由此提出核心问题:How robust is the evaluation of generated tests to variation across implementations?
说白了,实现方式一变,对测试质量的评价还站得住吗?可靠的测试该像棱镜那样,把同一需求下的不同实现区分开:对正确实现宽容,对错误实现敏感。
为每个任务配备一组正确与错误实现
为回答这个问题,团队构建了TestPrism。它不给每个测试任务只配一份参考答案,而是配一个候选实现面板(Candidate Panel):1份参考实现、4份正确的替代实现、5份错误实现。生成测试时,面板对Agent完全隐藏;评测时,测试要在原始代码和面板里每一份实现上分别跑一遍。

图1|TestPrism的构建与评测流程(原论文Fig.1)
搭建TestPrism共分四步。
第一步:筛选任务并做适配
研究团队先从17个公开Benchmark里收集任务,SWE-bench系列、Terminal-Bench系列都在其中,还有SWE PolyBench、SWE rebench、EvoEval、USACO、AlgoTune等,场景覆盖仓库维护、函数规格演化、竞赛编程和数值优化。入选的每个来源都得有可追溯的测试出处,验证流程也要有据可查。
接着由适配器把不同来源的任务统一转成Harbor的任务格式,原始需求、初始代码状态、验证器、参考实现、依赖、服务与执行限制都原样保留。
第二步:挑选任务
团队筛任务看三个指标:参考实现的有效代码行数(Solution Size)、测试函数或检查项的数量(Test Count),以及每个被修改文件对应的测试数量(Test Density)。
筛选阈值按各来源自身的分布来校准,再用来源配额平衡代表性。最后TestPrism共有2000个任务,1700个划入训练集,供后文TestHelix做策略优化;剩下300个当测试集,两边互不重叠。
第三步:生成候选实现并验证
生成候选实现之前,团队先逐个复核任务原有的验证器:它得在初始状态下失败,在参考实现上通过。
两位有开发经验的专家各自独立评估任务需求和验证器,看有没有对参考实现过拟合,或者覆盖上有缺口。意见不一致就讨论澄清,仍有疑问的任务直接剔除(Cohen's κ = 0.68)。
候选实现是让不同的Coding Agent、不同能力的模型实际去解题得来的。传统变异测试(Mutation Testing)靠人为注入缺陷,这种做法不一样,真实的替代解法能抓到,Agent解题过程中自然冒出来的错误也能一并抓到。
空提交、完全重复和无法应用的补丁先被移除,然后在干净环境里跑原有验证器:所有检查都过的标为正确实现,任意一项检查失败的标为错误实现。这些标签在后续评测中固定不变。
第四步:多样性过滤与任务转换
4份正确实现要是都只是参考答案的轻微改写,这个面板就没有意义了。
所以正确实现组要满足平均AST距离标准;错误实现组则看未通过的检查项集合,集合不同就算多样,集合相同还得再过一道AST Jaccard距离检查。面板反复生成与替换,直到两组都满足多样性要求。
最后,测试生成适配器把每个编程任务转成一份测试编写契约:Agent要依据原始需求,在指定目录中提交测试、Fixture和一个可执行的入口脚本。
数据概览
TestPrism测试集的300个任务涉及Python、JavaScript/TypeScript、Java、C/C++和Go等六种语言,来自67个代码仓库和项目,带标签的候选实现共3000个。
任务分成6大类、34个子类:Bug修复(167个)、程序与算法(53个)、性能优化(28个)、功能扩展(20个)、项目与工作流(19个),还有重构与迁移(13个)。

图2|TestPrism测试集的任务分类(原论文Fig.7)
评测指标:从单一参考到联合成功
据此,TestPrism定义了一组互补的评测指标:
Reference Acceptance Rate(RAR):测试在原始代码上失败、在参考实现上通过的任务比例,也就是传统的单一参考评测;
Valid Acceptance Rate(VAR):测试接受4份正确替代实现的平均比例;
Invalid Rejection Rate(IRR):测试拒绝5份错误实现的平均比例;
H指标由VAR与IRR取调和平均得来,用来判断这两方面是否均衡。
Change Coverage和Entry Coverage这两个指标,定义分别照搬SWT Bench与TestExplora,看的是测试覆盖了多少参考修改代码以及相关入口代码。
核心指标Joint Success Function(JSF)的门槛更高,要求测试同时满足三条:在原始代码上失败、接受面板中所有正确实现、拒绝面板中所有错误实现。若把面板缩到只剩参考实现,JSF就退化成RAR。
换句话说,只拿单一参考实现来评,只是JSF最宽松的特例。JSF不会超过RAR,两者之差,恰好对应那些能通过参考答案、却扛不住更多实现检验的测试所占的比例。
RQ1,前沿Coding Agent能写出可靠的测试吗?
第一个研究问题很直接:当前Coding Agent产出的测试,能不能稳定地把正确实现和错误实现分开?
团队评测了来自六个模型家族的14种Coding Agent配置。主表列出每个家族中JSF最高的那套配置,并优先选用各模型的原生Coding Agent,原因是模型训练与Agent框架之间耦合很紧。

表2|各模型家族最佳配置在TestPrism上的表现(原论文Table2)
通过参考答案,不等于测试可靠
结果说明,可靠的测试生成离解决还很远。成绩最好的Claude Fable 5.1,在单一参考评测下成功率59.67%,JSF却只有28.00%;KimiK3、Qwen3.8 Max和GPT 5.6 sol -max的RAR都落在54%至57%区间,JSF却只有23%到25%;DeepSeekV4 Pro和GLM 5.3的JSF更低,分别只有13.33%和11.33%。所有模型都存在同一个现象:不少在传统评测里被判成功的测试,一旦面对更多实现就露馅。
接受正确实现,往往比拒绝错误实现更难
拆开看更清楚:六个配置里有五个的IRR高于VAR。也就是说,当前Agent写测试更常犯的毛病是过严——把参考实现的某种具体写法当成硬性需求,结果误伤了别的合法实现。
GLM5.3就是典型:IRR高达86.33%,在全部配置中排第一,VAR却只有50.33%,JSF也垫底。
Qwen3.8 Max反过来,VAR最高(77.00%),IRR只有66.00%。只会拒绝错误实现显然不够,靠谱的测试得在宽容和敏感之间同时站稳。全部14种配置中,VAR与IRR的调和平均H和JSF一样呈正相关。
覆盖率很重要,但不是全部
覆盖率与JSF同样关系密切:14种配置里,Change Coverage、Entry Coverage与JSF的Pearson相关系数分别为0.91和0.96。不过,覆盖到相关代码只是测试能正确判断它的前提——测试跑过某段逻辑,不代表它的断言就能正确判断这段逻辑的行为。
RQ1的核心结论是:前沿模型离可靠的测试生成还差得远,接受正确实现和拒绝错误实现各自暴露出不同的短板,这两者的平衡加上覆盖率,一起决定联合成功率。
RQ2,候选实现越多、越多样,测试越难蒙混过关?
第二个研究问题盯的是候选面板本身:往面板里逐步塞进更多、更多样的实现,测试的成功率会怎么变?
团队随机挑了50个任务,构建流程不变,把每个任务的面板从10份实现扩到15份:1份参考实现、7份额外正确实现、7份额外错误实现。
测试保持不变,从只含参考实现的面板起步,按三种路径逐步加实现:只加正确实现、只加错误实现,以及两者交替加入。

图3|三种实现添加方式下JSF与候选多样性的变化(原论文Fig.3)
数据表明,JSF均值从仅有参考实现时的49.33%一路走低:补入5份正确实现后降到38.10%,补入5份错误实现后落到30.49%,9份实现交替补入后进一步跌至24.33%;等到全部实现都加进去,三种路径的终点分别是37.33%、29.33%和23.33%。
测试代码一个字没动,变的是面板向它提出的考题——正确实现越堆越多,考验的是测试有没有过严;错误实现越堆越多,考验的则是测试有没有过松。
换句话说,JSF并非一个静止的分数,它随面板构成而浮动,是一种动态的可靠性度量:面板里放了哪些实现、放了多少实现,分量一样重。单一参考评测相当于只有一份实现的特例,检验力度最弱,也是扩展面板成功率的顶部边界。RQ2的关键结论是:候选实现越多样,越能揪出小面板看不出的测试错误。
RQ3,Agent生成的测试究竟错在哪里?
前两个研究问题量出了测试「不可靠」的程度,却没有交代它们为何不可靠。
为回答这一点,研究团队从六个模型家族的最佳配置里各取50条JSF为零的失败轨迹,剔除14条超时轨迹,余下286条全部由两位专家独立标注(Cohen's κ= 0.81),意见不一致时请第三位专家一起讨论定夺。
标注过程中,专家逐项核对任务需求、提交的测试、测试在原始代码与参考实现上的执行结果、在各候选实现上的结果,以及对应的补丁证据,最终归纳出3大类、7种具体错误机制。

图4|各模型的指标画像(a)与286条失败轨迹中的错误机制分布(b)(原论文Fig.4)
覆盖缺口:测到了功能,却漏掉了关键输入
覆盖缺口(Coverage Gaps)在全部失败中占34.6%,其中遗漏输入(Missing Inputs)为30.8%,未测试的需求(Untested Requirements)为3.8%。遗漏输入是最常见的单项错误之一:测试确实执行了某个操作,但没能覆盖足以暴露缺陷的输入组合。
举个例子,有个扑克牌型判断任务,生成的测试始终没有比较A作最小牌的顺子与三条的大小,于是一个把这类顺子错判成「高牌」的实现就顺利过关了。
错误检查:执行到了,却判断错了
占比最高的类别是错误检查(Faulty Checks),达50.0%,细分为检查过弱(Weak Checks,7.7%)、判断标准错误(Wrong Criteria,26.9%)和过拟合(Overfitting,15.4%)。
这一类说明,测试就算跑到了相关行为,也未必能判对。过拟合恰好对应RQ1里看到的「过严」:测试把参考实现身上的偶然细节当成了需求本身。
比如一个括号解析任务,测试的确构造了括号不匹配的输入,可它只查输出结果的数量,不查具体内容,结果一个丢弃合法分组的错误实现被放行了。
扑克牌和括号解析这两个案例中的测试,都在原始代码上失败、在参考实现上通过——放到单一参考评测下,它们都会被算作成功。只有把额外的错误实现加进来,那些漏掉的输入组合和过弱的断言才浮出水面。
执行逻辑错误:测试自己先跑不通
执行逻辑错误(Execution Logic Errors)占15.4%,Fixture与API使用错误(7.7%)和命令执行与输出解析错误(7.7%)各占一半。
比如在一个EvoEval任务中,测试把被测函数挂在测试类上,再用self去调它,结果多绑定了一个参数,测试连参考实现都跑不过。这类毛病会直接拖低测试的整体可靠性,但跟前两类不同,单一参考评测同样能发现它们,算不上「盲区」。
RQ3的关键结论是:测试失败要么因为漏掉了相关行为,要么因为误判了实现正确性,要么因为测试自身执行失败;其中遗漏输入组合与过弱断言能骗过参考答案的检验,只有在更多错误实现面前才露馅。
RQ4,如何让测试在不同实现之间泛化?
针对这些问题,研究团队拿出了TestHelix。思路很直接:既然只对照一份实现发现不了测试的毛病,那就在生成阶段主动造出多份实现,让测试接受多方检验。

图5|TestHelix框架(原论文Fig.2)
异构合成:多个Agent分头写测试、各自修代码
TestHelix给多个彼此隔离的Coding Agent配上不同的探索策略,各Agent仅凭公开的任务需求,独立写出一份测试T和一份修复P。每对测试与修复都要过内部一致性这一关:测试得在原始代码上稳定地失败,换成自己的修复则要能通过。Agent靠本地执行反馈反复调整,直到达标或预算用尽。
同伴交叉验证:拿别人的修复来考自己的测试
只在自己的修复上跑通,也可能是自洽的错误。为此,TestHelix把每份测试放到其他Agent的修复上交叉执行,让过严的测试或不完整的修复露出来。
另一边,一个完全看不到修复代码的独立评审Agent会对照公开需求逐条核查断言,每条阻断性意见都得拿公开需求作依据。
评审确认下来的反例,用来修正测试与修复;当几份不同的合格修复都能通过已认可的测试时,系统还会借鉴差分测试的思路,主动去找能区分它们的输入。最后,相互兼容的测试被合并,并在全新环境中再验一遍。
递归自我改进:从训练任务里学出更好的测试生成策略
TestHelix从一份基础策略起步,靠AutoSaddler做反思式演化:挑一个父策略,诊断它的执行轨迹,提出修订,再在同一批任务上比较父子表现,把可复用的经验留存下来供后面几轮使用。
优化在TestPrism训练集的600个任务上展开,与测试集完全隔离,也从不接触隐藏的候选面板。优化目标选的是信号更密集的RAR,而不是更严格的JSF——后者往往难以奖励渐进式的策略改进;至于对多实现区分能力的要求,则交给异构合成与同伴交叉验证去承担。
优化出的策略冻结之后,直接用于GPT 5.6 sol和KimiK3两个模型。
实验结果
研究团队在GPT 5.6 sol和KimiK3上比了五种TestHelix配置,对照是各自的原生Coding Agent(Codex和KimiCode)。同一模型组内的所有配置共用相同的基础模型和工具,单任务推理预算上限也一致。

表3|TestHelix消融实验与原生Coding Agent对比(原论文Table3)
结果显示,只生成三组测试与修复、再随机交一份,相比单组生成最多提升1个百分点;同样三组的基础上开启完整的同伴交叉验证,JSF提升6个百分点以上,GPT5.6 sol从21.00%升到27.67%,KimiK3从22.67%升到29.00%。组数从3加到5,只多出0.67个百分点;在三组配置上引入递归自我改进,又带来4.33至4.67个百分点的提升。
最终配置在两个模型上分别拿到32.33%和33.33%的JSF,比单组生成高11.67至12.00个百分点,也比各自的原生Coding Agent高出8.67至9.00个百分点。推理预算固定时,改进「如何生成和检验测试」的收益,明显高过单纯把预算摊给更多Agent。
RQ4的核心发现是:异构合成让测试在不同实现之间泛化——同伴交叉验证借多样的修复来检验和修正测试,递归自我改进则在任务之间积累测试生成策略,两者在消融实验里都贡献了明确的收益。
下一步是什么?
Coding Agent越来越依赖测试来验证自己的工作,测试质量的评价标准也得跟着提高。
这项研究表明,单一参考评测会系统性地高估测试质量:一份测试能区分修复前后的两个状态,并不代表它能正确判断同一需求下的其他实现。即便最强的前沿模型,面对多份正确与错误实现时,联合成功率也不到三成。
研究同时揭出了问题的两面。测试可能过松,漏掉关键输入或只做表面检查,把有缺陷的实现放过去;也可能过严,把参考实现的偶然细节当成需求,误伤合法实现。
前一种在单一参考评测里容易藏住,后一种在单一参考评测里根本发现不了。只有评测同时看接受与拒绝两个方向,测试的真实可靠性才看得清。
评测层面,TestPrism给出的检验思路更接近真实开发场景:拿一批已经验证过、彼此差异明显的实现去考测试,而非盯着一份标准答案打分。
方法层面,TestHelix则说明,想把测试可靠性提上去,未必非得换更强的基础模型。让多个Agent互相抛出反例、互相核对结论,再把管用的经验固化成策略,收益同样可观。
论文也提到局限。TestPrism每个任务只配10份候选实现,正确与错误实现按等比例摆放,这和真实分布对不上;基于AST的多样性度量,也保证不了语义层面全覆盖。把评测推向不断变化的需求、更大的实现空间和更多执行环境,是后续可以探索的方向。