freeCodeCamp.org

How to Stop Letting AI Agents Fake Their Own Tests

8.5内容质量

TL;DR · AI 摘要

spec-verify工具通过结构化验证防止AI生成的测试失效,揭示AI测试验证中的两大陷阱:空测试与不可验证测试。

核心要点

  • spec-verify能检测AI生成测试是否真正验证了逻辑,而不仅是语法正确
  • 7%的验证失败率暴露单次测试运行不足以保证可靠性
  • vacuous测试(空测试)与unvalidatable测试(不可验证测试)是两种不同错误类型

结构提纲

按章节快速跳转。

  1. 展示spec-verify工具在验证AI生成测试中的关键作用与发现的验证陷阱。

  2. ·spec-writer的局限性

    说明spec-writer无法确保AI生成的测试真正验证了逻辑假设。

  3. 空测试陷阱

    解释AI可能生成仅验证语法正确但无实际验证效果的测试用例。

  4. 介绍spec-verify通过突变检测生成可验证测试的实现机制。

  5. 演示spec-verify如何发现会话ID去重逻辑的隐藏缺陷。

  6. 提供spec-verify的安装、运行及自定义验证流程说明。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AI测试验证
    • spec-verify工具
      • 结构化验证机制
      • 突变检测生成测试
      • 验证两大陷阱
    • 验证陷阱
      • vacuous测试(空测试)
      • unvalidatable测试(不可验证测试)
    • 使用场景
      • AI生成测试验证
      • 代码逻辑缺陷检测

金句 / Highlights

值得收藏与分享的关键句。

#AI测试#spec-verify#代码验证#Claude#软件工程
打开原文

如何防止AI代理伪造自己的测试

2026年8月26日

/

#claude.ai

Daniel Nwaneri

我已确认一个幻觉修复措施为已验证。现在五次测试中没有出现任何伪造响应,运行结果干净,完成。

然后我在更严格的条件下重新运行测试,结果为66.7%。我曾足够信任这个数字,以至于在旁边标注了“HUMAN-VERIFIED”。但这个数字是错误的。这并非因为我撒谎,而是因为一次干净的运行和“已验证”并不等同。我还开发了一个工具,其核心目的就是拒绝让这种区别被忽视,但自己差点就让它被忽视了。

这种讽刺恰恰值得单独制定规范。

这个工具名为spec-verify:一个Claude Code技能,它将spec-writer已经生成的Given/When/Then验收标准规范转换为可检查是否真正测试了任何内容的测试用例。

本教程将展示如何安装spec-verify,如何将其应用于真实代码,以及如何解读它设计用于捕获的两种失败模式。这两种模式是完全相反的,将它们视为相同会违背开发这个工具的初衷。

目录

  • spec-writer无法解决的问题
  • 陷阱:空洞的测试
  • 基于变异检测的测试生成
  • 实际案例
  • VACUOUS和UNVALIDATABLE是相反概念而非变体
  • 结构检查并非真实性检查
  • 如何安装spec-verify
  • 自行运行验证
  • 如何在自己的规范中使用它
  • 这对三部曲系列意味着什么

spec-writer无法解决的问题

spec-writer是一个Claude Code技能,它能将模糊的功能需求转化为结构化规范,并在未被要求时自动标记所有决策为[ASSUMPTION: ...]。当输入一个12个词的会话捕获功能需求时,它会生成如下内容:

code
3. 从.jsonl文件名中提取的会话ID是去重键
   影响程度:中等
   需要修正的情况:如果您的架构中存储会话ID的方式不同

这12个词的提示中隐藏着一个真实决策:通过文件名识别会话,当其中一个会话被重命名时,两个相同会话的副本会立即被视为两个不同会话。(如需完整了解spec-writer如何得出这个假设,请参阅《如何防止AI代理猜测您的需求》。)

spec-writer的核心价值在于在编写代码前捕获这个假设。但假设您发现并修正了这个假设。您告诉代理"不,应该对内容进行哈希处理,而不是文件名"。代理会写出修复方案,同时也会写出测试用例,因为您要求了,或者因为现在代理本就该这么做。

这里存在无人检查的关键问题:这个测试真的验证了修复方案吗?还是仅仅调用去重函数,得到一个列表,断言这个列表是列表,然后就通过了,而不管底层的bug是否仍然存在?

Given/When/Then是人类阅读的文本。它本身并不是机器运行的测试。这两个事物之间的差距,就是修正后的假设可能悄然退化为真实bug的温床,直到生产环境中出现会话重命名时才会被发现。

陷阱:空洞的测试

明确的解决方案是让代理从每个 Given/When/Then 块生成一个测试。许多工具正是这样做的。但陷阱在于:"代理编写了测试"和"代理编写了具有实际意义的测试"并不是同一个说法。不幸的是,由大语言模型生成的测试往往更容易陷入前者:一个测试可以运行、断言一个显而易见的真值,并且无论代码实际如何执行都会通过。这看起来像是覆盖率。绿色对勾与红色对勾没有本质区别,因为两者从未真正可能失败。

更糟糕的是:这些测试不仅无法发现原始错误,当它们本应防护的行为因其他无关原因中断时,它们还会继续通过。没有任何内容被验证。测试看起来与其他所有通过的测试完全一样。

因此,解决方案不能止步于"生成测试"。它必须验证测试是否能在被保护的内容实际失效时真正发现问题。

经变异检测的测试生成

对于每个 Given/When/Then 块,spec-verify 会执行以下四步操作:

  • 生成测试:Then 子句会转化为对返回值、状态或副作用的具体断言。永远不会出现"无异常运行"的情况。
  • 生成一个针对性变异:这不是一般的变异测试扫描,而是基于[ASSUMPTION: ...]标签的特定、明确的破坏。如果假设指明了风险,变异会精确地重新引入这个风险。
  • 将测试运行在变异体上:如果测试仍然通过,说明这个测试从未真正验证过它声称要检查的内容。这是空洞的测试,会被标记出来而非被信任。
  • 闭合失败:如果无法通过这种方式验证测试,该准则会通过名称明确地阻止"完成"操作,并说明原因,而不是默许通过审查。

实际案例

取自上述规范编写者示例的精确假设:去重键是基于文件名而非文件内容生成的。以下是看起来正确的测试:

python
def test_dedup_sessions_runs(tmp_path):
    result = dedup.dedup_sessions([str(f)])
    assert result is not None

它调用函数,得到一个列表并通过。它也会通过一个使用文件名作为去重键的 dedup_sessions 版本(正是假设指出的错误),因为它从未检查哪些会话在去重后幸存,只检查是否有内容返回。

以下是真正验证准则的测试:

python
def test_renamed_session_still_deduped(tmp_path):
    original = tmp_path / "session_abc123.jsonl"
    original.write_bytes(content)
    renamed = tmp_path / "session_abc123_renamed_by_sync_tool.jsonl"
    renamed.write_bytes(content)  # 内容相同,文件名不同

    result = dedup.dedup_sessions([str(original), str(renamed)])

    assert result == [str(original)]

分别在正确实现和变异体(将去重键改回文件名)上运行这两个测试:

python
test                             baseline   mutant     verdict
test_dedup_verified.py           pass       FAIL       VERIFIED
test_dedup_vacuous.py            pass       pass       VACUOUS

PROOF PASSED: 变异检测正确区分了 VERIFIED 和 VACUOUS。

当错误重现时,一个测试立即失败。另一个测试则毫无察觉。在仔细查看前,两者都显示绿色对勾。但提供的保护程度却完全不同。

VACUOUS 与 UNVALIDATABLE 是对立关系,而非变体关系

并非所有准则都能进行突变测试。我文章开头提到的修复方案(一个实体关联规则,告诉模型不要以其他支付服务商的名义伪造支付服务商的文档)首次发布时,完全存在于系统提示中。这是对LLM的通俗语言指令。没有可供调用和断言的功能边界。唯一能验证的方法是向模型提出陷阱问题并阅读其回答。突变测试无法触及这一领域。

spec-verify将这种情况标记为UNVALIDATABLE,人们很容易将其与空洞测试(vacuous test)同等对待:认为这是需要修复的不完善测试。但事实并非如此。空洞测试是缺陷:测试本身存在缺陷,修复方式永远只有一种——编写更好的测试。UNVALIDATABLE意味着该准则完全超出当前技术的验证范围,通常是因为行为具有非确定性,而非因为任何人犯了错误。

若对两者采取相同处理方式,将导致两种不良后果:要么因"有些事情根本无法测试"而放任空洞测试(实际上可以测试,只是未被正确编写),要么永远卡在非确定性问题上(在真实代码库中这种情况非常常见)。这两种方式都不正确。

因此验证机制设有两条路径:

  • VACUOUS和BROKEN-TEST:永不豁免。唯一突破缺陷测试的方式是编写更好的测试。
  • UNVALIDATABLE:仅通过明确的书面人工确认才能通过,需由人员书面说明实际验证方式。

这让我回到文章开头。我针对实体关联准则签署了带有具体数字的说明:5次尝试中0次伪造响应。该说明通过了spec-verify的结构检查:内容不为空、不是单字的橡胶印章,并且指明了具体方法。

后来发现,这实际上是一次乐观的单次运行。在固定温度下进行独立复测(不固定种子)发现实际干净拒绝率为66.7%,而非100%。某些服务商组合检索到几乎相同的文本块,导致自检失败频率比首次运行显示的更高。

诚实的修复方案不是更好的签署说明,而是替换被签署的内容:为已知问题场景设置一个确定性的代码级验证门禁,该验证可以通过常规方式测试,与基于提示的其他验证并行存在。UNVALIDATABLE不是永久状态,而是表明需要人工介入,理想情况下应最终消除对人工的依赖。

结构检查并非真实性验证

这是空洞测试问题的温和版本:HUMAN-VERIFIED注释可以诚实但仍然错误,就像我的注释那样。spec-verify对签署说明的结构检查(拒绝空注释、拒绝字数不足一整句的文本、拒绝"看起来没问题"或"lgtm"等黑名单短语)无法验证人类是否真的执行了声称的操作。它仅能提高最懒惰的橡胶印章成本。有决心的人仍可通过伪造叙事绕过检查。

对于团队协作场景,有更强的方案:从git提交的实际作者和时间戳中解析签署人及时间,而非依赖JSON文件中的自由文本字段。此时伪造签署需要真实提交记录:在历史中可见,而非无人关注文件中的编辑。这对单人项目来说过于繁琐,但当多人可能有合理动机伪造时,这就是正确的选择。

如何安装spec-verify

与 spec-writer 类似,spec-verify 也是 Claude Code 的一项技能:一个 markdown 文件加上几个可运行的示例目录,无需安装任何软件包,也无需 API 密钥。

code
mkdir -p ~/.claude/skills/spec-verify
git clone https://github.com/dannwaneri/spec-verify.git ~/.claude/skills/spec-verify

在 Windows PowerShell 中:

code
New-Item -ItemType Directory -Force -Path "$HOME\.claude\skills"
git clone https://github.com/dannwaneri/spec-verify.git "$HOME\.claude\skills\spec-verify"

自行运行验证

不要盲目相信上方的 VERIFIED / VACUOUS 表格:该仓库包含了本文中的所有示例作为可运行代码:

code
cd ~/.claude/skills/spec-verify/example
python run_proof.py

这将完全复现去重表格。对于更严格的签核层级:

code
cd git_attributed_signoff
python build_demo_repo.py
python check_signoff.py demo_repo entity_grounding
python tamper_demo.py

tamper_demo.py 值得亲自运行,而不仅仅是阅读说明:它会在工作树中修改一个签核的备注但不提交,然后再次检查。检查会完全拒绝信任该文件,甚至在读取被篡改的备注之前就拒绝,因为工作树已不再匹配任何提交。

如何在自己的规范中使用

安装完成后,在你完成通过 spec-writer 编写的特性实现后、在标记任务完成前调用它。它需要 spec-writer 的输出(Given/When/Then 块及其 [ASSUMPTION: ...] 标签)和待验证的实现代码。它不会自动生成待验证的验收标准。如果没有可供插入的 spec-writer 输出,它就无事可做。

阅读报告的方式应与阅读 spec-writer 的假设摘要一致:优先扫描所有未标记为 VERIFIED 的内容。出现 VACUOUS 或 BROKEN-TEST 时,需要修复测试。任何版本的此类发现都不应被接受为可发布状态。遇到 UNVALIDATABLE 时,必须明确决定:是有人正在手动检查并记录该情况,还是需要将底层行为迁移至可测试的位置(如 entity-grounding 修复最终所做的那样)。

这对三部曲意味着什么

spec-writer 会捕获你为错误理由构建的功能。spec-verify 会捕获本应告知你却未告知的测试。两者结合:假设会被标记并修正,且在代码发布前完成。现在存在一个测试,如果六个月后有人未阅读原始规范就撤销了修正,该测试会实际失败。

这些工具无法替代判断。规范可能格式正确却仍可能错误地指导构建内容。签核可能诚实却仍可能遗漏更严格复测会发现的问题。这两项工具的作用是确保"看起来完成"与"真正完成"之间的差距必须有意识地跨越,并记录原因,而非因无人检查而跳过。

spec-verify 仓库位于 github.com/dannwaneri/spec-verify。在你标记下一个通过 spec-writer 生成的功能为完成前,尝试用它进行验证。如果其中某个测试是空洞的,你应通过变异检测发现,而非在生产环境中发现。

来自尼日利亚哈科特港的全栈开发者,专注于边缘计算和AI集成。我根据真实项目经验撰写全面的生产级教程——不仅分享成功案例,更剖析失败原因及背后的原理。我的文章包含可直接运行的完整代码、实际性能指标以及大规模系统运行的实践经验。热衷于Cloudflare Workers、RAG系统、无服务器架构,以及构建可全球扩展的高性价比解决方案。https://dannwaneri.com/

如果你读到了这里,请感谢作者以表达你的支持。说声谢谢

免费学习编程。freeCodeCamp的开源课程已帮助超过40,000人成功获得开发工作。立即开始

ADVERTISEMENT