← 返回蜂巢洞察

如何防止人工智能代理伪造自己的测试结果

我确认,那些被标记为“已验证”的结果实际上都是虚假的。这样一来,原本五个伪造的响应中现在一个也没有了,测试过程进行得非常顺利,任务已经完成。 随后我在更严格的条件下再次进行了测试,得到的准确率为66.7%。然而,这个数值并不符合我的预期;我本来还打算在它旁边标注“人工验证”呢。出现这种结果的原因是:仅仅一次成功的测试并不能证明其结果就是正确的,“已验证”这个标签其实并不代表这一点。而我之前开发的那个工具,本来就是为了防止人们忽视这种区别,但这次我却差点忽略了它。 这种讽刺意味确实值得专门加以说明。 我开发的这个工具叫做“spec-verify”:它属于Claude Code系列技能之一。该工具

我确认,那些被标记为“已验证”的结果实际上都是虚假的。这样一来,原本五个伪造的响应中现在一个也没有了,测试过程进行得非常顺利,任务已经完成。

随后我在更严格的条件下再次进行了测试,得到的准确率为66.7%。然而,这个数值并不符合我的预期;我本来还打算在它旁边标注“人工验证”呢。出现这种结果的原因是:仅仅一次成功的测试并不能证明其结果就是正确的,“已验证”这个标签其实并不代表这一点。而我之前开发的那个工具,本来就是为了防止人们忽视这种区别,但这次我却差点忽略了它。

这种讽刺意味确实值得专门加以说明。

我开发的这个工具叫做“spec-verify”:它属于Claude Code系列技能之一。该工具会利用那些已经由spec-writer生成的需求规范,将这些规范转化为测试用例,然后检查这些测试用例是否真正能够检测出问题所在。

这个教程会教你如何安装“spec-verify”,如何使用它来检测实际代码,以及如何理解它所设计的两种错误检测机制。这两种错误类型其实是相互对立的,如果把它们当作同一种东西来处理,那就完全违背了开发这个工具的初衷了。

目录

  1. spec-writer无法解决的问题

  2. 陷阱:空洞的测试用例

  3. 基于变异检测的测试用例生成机制

  4. 一个真实的例子

  5. 空洞的测试用例与无法验证的测试用例是相互对立的,而不是同一概念

  6. 结构检查并不等同于真实性检验

  7. 如何安装spec-verify

  8. 亲自验证其效果吧

  9. 如何在自己的需求规范中使用它

  10. 这一部分内容在整部三部曲中的位置

spec-writer无法解决的问题

“spec-writer”这个工具确实可以将一个模糊的需求描述转化为结构化清晰的需求规范,但它也会在未经明确要求的情况下,将自己做出的每一个决策都以[ASSUMPTION: ...]的形式标出来。例如,当你向它提交一个关于“会话捕获功能”的请求时,它会生成如下这样的内容:

3. 从.jsonl文件中获取的会话ID就是去重键
   影响程度:中等
   如果你的数据结构中会话ID的存储方式不同,请修改此处

这个决定其实隐藏在短短12个字的需求描述中:通过文件名来识别会话,而一旦某个会话的名称被更改,那么同一个会话就会被视为两个不同的会话。(想要了解spec-writer是如何得出这个假设的,可以阅读:如何防止AI代理猜测你的需求。)

规范编写工具的真正价值在于在代码被编写之前就发现这些错误假设。但假设你发现了这些问题并进行了修正,然后告诉该工具“不,应该对文件内容进行去重处理,而不是文件名”,那么这个工具就会根据你的要求编写相应的修复代码,并且还会生成测试用例——因为你是这样要求的,或者因为现在这类工具默认都会这么做。

但这里有一个问题:这些测试用例是否真的能验证这些修复措施是否有效?还是说它们只是简单地调用了去重函数,得到了一个结果列表,然后就认为测试通过了,而根本不去关心底层的问题是否仍然存在?

“给定条件、操作行为、预期结果”这种描述方式是人类用来阅读的文本,并不是机器可以执行的测试指令。正是这种差异导致了那些被修正的错误假设会悄悄地重新变成真正的漏洞,而直到生产环境中的某个功能被重新命名时,人们才会注意到这个问题。

陷阱:空洞的测试用例

显而易见的解决办法是让工具为每一个“给定条件、操作行为、预期结果”的组合生成相应的测试用例。很多工具确实能够做到这一点。但问题在于,“工具生成了测试用例”和“生成的测试用例具有实际意义”这两者并不是一回事。不幸的是,由大型语言模型生成的测试用例往往属于前者:这类测试用例虽然能够运行并通过验证,但它们实际上并没有检查代码是否真的实现了预期的功能;它们看起来像是有效的测试用例,但绿色标记与红色标记并没有什么区别,因为这两种标记本质上都毫无意义。

更糟糕的是,这些测试用例不仅无法捕捉到最初的错误,而且即使在那些它们本应用来检测的问题发生之后,这些测试用例仍然会正常通过——哪怕这些问题是由于完全无关的原因导致的。没有任何东西真正得到了验证,这些测试用例看起来和其他所有通过的测试用例没有区别。

因此,修复措施不能仅仅停留在“生成测试用例”这一步上,还必须确保这些测试用例确实能够检测到那些它们本应检测的问题是否真的发生了。

基于变异检测的测试用例生成

对于每一个“给定条件、操作行为、预期结果”的组合,规范验证工具会执行以下四项操作:

  1. 生成测试用例: “预期结果”这部分代码会转化为针对返回值、状态或副作用的具体验证逻辑。这类测试用例绝对不会“在没有任何错误的情况下正常运行”。

  2. 生成一个针对性的变异版本:这不是进行全面的变异测试,而是根据规范中指定的[ASSUMPTION: ...]标签,有针对性地创建一个会导致问题发生的变异版本。

  3. 用这个变异版本运行测试:如果测试仍然通过,那就说明这个测试用例根本没有真正检测到它应该检测的内容。这样的测试用例就是空洞的、毫无意义的,因此会被标记为无效。

  4. 如果测试失败,则终止整个流程:对于那些无法通过这种验证方法的测试用例,系统会明确标出其名称,并说明失败的原因,而不会让它们默默地通过了审查流程。

一个具体的例子

以上面提到的那个错误假设为例:假设去重键是根据文件名而不是文件内容生成的。那么,下面这个测试用例才是正确的:

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

它调用该函数,得到一个列表作为结果,然后继续执行后续操作。即使使用那种以文件名为键的dedup_sessions版本,这个测试也能通过——因为该版本根本不会检查哪些会话在去重过程中被保留下来,而只会确认最终是否有结果返回。

下面是那个真正会检查相关标准的测试代码:

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)]

先使用正确的实现版本运行这两个测试,然后再使用那个将去重键重新设置为文件名的变体版本进行测试:

测试                          基线版本      变体版本     测试结果
test_dedupVerified.py           通过         失败        通过验证
test_dedup_vacuous.py            通过         通过         未通过验证

一旦那个缺陷重新出现,其中一个测试就会失败;而另一个测试则完全察觉不到任何异常。在表面上看,这两个测试使用的是相同的标准,但它们所提供的保护效果却截然不同。

“未通过验证”与“无效测试”是对立的概念,而非简单的变体

并不是所有的标准都适合进行变异测试。当我在本文开头提到的那个修复方案首次被应用时(该方案是一条规则,用于告诉模型不要用另一个提供者的名称来生成支付提供商的相关文档),这条规则完全存在于系统的提示信息中——也就是以自然语言形式提供给大型语言模型的指令。对于这样的规则,根本不存在可以调用并对其进行验证的函数边界。因此,检查这类规则的唯一方法就是向模型提出一些刁钻的问题,然后分析它的回答。而变异测试是无法达到这一目的的。

spec-verify将这类规则归类为“无效测试”,人们很容易认为它们和那些“未通过验证”的测试属于同一类别:都是需要改进的、不够完善的测试。但实际上并非如此。“无效测试”是一种真正的缺陷——这种测试本身存在问题,而解决它的方法也总是相同的:编写一个更好的测试用例而已。而“无法进行变异测试”意味着某种标准超出了当前技术所能检测的范围,通常是因为相关行为具有非确定性,而不是因为有人犯了错误。

如果把它们当作同一类来处理,就会导致两种不良后果:要么你选择忽略那些“无效测试”,认为“有些东西就是无法被验证的”;要么你会在面对所有具有非确定性的情况时都陷入停滞——而在实际的代码库中,这种情况其实很常见。这两种做法都是错误的。

因此,这个检测机制包含两条审核路径:

  • VACUOUS和BROKEN-TEST:这两种状态永远不允许被忽略。对于存在缺陷的测试结果,唯一的解决办法就是进行更全面的重新测试。

  • UNVALIDATABLE:这种状态只有通过明确、有记录的人工审核才能被解除。审核人员需要以书面形式说明他们实际是如何进行检查的。

这让我回到了开头提到的那个例子。我在确认实体验证标准时,附上了一条注释,其中提到了一个具体的数字:在五份伪造的测试报告中,有零份是合格的。这条注释通过了spec-verify的工具结构检查——它既不是空内容,也不只是简单的敷衍了事,而且确实指出了具体的检查方法。

然而后来发现,这次检查结果其实并不代表实际情况。在固定温度下进行的独立复测显示,合格率仅为66.7%,而不是100%。有些测试对中,某些数据对几乎完全相同,但它们通过自动审核机制的频率却比初次测试时预期的要高。

所谓的“诚实的修复”其实并不是什么更好的解决方案。它只是替换了原本需要人工审核的内容:对于那些已知存在问题的情况,仍然使用传统的代码级检测方法进行测试;而对于其他所有情况,则继续采用基于提示的自动审核机制。UNVALIDATABLE这种状态并不代表永久性的失败结果,而只是一个标志,表明某些环节还需要人工干预——或者理想情况下,应该彻底摆脱对人工审核的依赖。

结构检查并非真实性检验

这种情况其实与“vacuous-test”问题属于同一类:即使HUMAN-VERIFIED这样的标注是真实的,也仍然有可能是错误的。spec-verify工具对审核注释的结构检查(会拒绝空内容、少于一句话的简短注释,以及像“看起来没问题”这类千篇一律的表述)并不能真正验证审核人员是否确实履行了他们的职责。这种检查机制只会让最敷衍了事的操作也能通过审核而已。一个有心的人仍然可以编造虚假的内容来蒙混过关。

在团队协作环境中,还有更严格的解决方案:可以通过git提交记录中的实际作者信息和时间戳来确定是谁在何时进行了审核,而不是依赖JSON文件中的自由文本字段。这样一来,伪造的审核记录就必须通过真实用户的git提交才能生效——这样的记录会显示在版本历史中,而不会是某个没人关注到的文件修改记录。对于个人项目来说,这种做法可能有些过度了;但当有多人有可能有动机伪造审核记录时,采取这种措施才是必要的。

如何安装spec-verify

与spec-writer一样,spec-verify也是一个基于Claude Code技术的工具:只需要一个markdown格式的配置文件以及几个包含示例代码的目录即可使用,无需安装任何软件包,也不需要API密钥。

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

在Windows PowerShell环境中,可以使用以下命令进行安装:

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状态:该仓库实际上提供了本文中提到的两个示例,并将其以可运行的代码形式提供出来:

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

这样就可以准确地重现那些验证过程。而对于更高级的验证步骤,可以执行以下命令:

cd gitatributed_signoff
python build_demo_repo.py
python check_signoff.py demo_repo entity_grounding
python tamper_demo.py

建议亲自运行tamper_demo.py,而不仅仅是阅读其说明:该脚本会先修改工作树中的验证信息,但不会提交这些更改,随后会再次进行验证。由于工作树的状态已经与任何已提交的代码都不匹配,因此系统会拒绝接受这些被篡改的文件。

如何在自己的需求规范中使用它

安装完成后,在实现某个功能并通过需求规范编写工具的检查之后,但在最终标记该任务为“完成”之前,请运行这个验证工具。它需要需求规范编写工具生成的输出内容(包括“Given/When/Then”结构以及其中的[ASSUMPTION: ...]标签),以及实际的代码实现,以便进行对比检查。该工具并不会自行生成验收标准来进行验证;如果没有这些输入数据,它就无法执行任何操作。

阅读报告时,请像阅读需求规范中的假设总结部分那样来查看:首先关注那些标记为VERIFIED的项。VACUOUS或BROKEN-TEST表示需要修复相应的测试用例;任何标记为UNVALIDATABLE的项都意味着你需要明确决定是由人工手动检查这些内容,还是需要将相关代码修改到可以被测试的环境中才能进行验证——就像最终对“entity-grounding”功能所做的那样。

这个工具在整套流程中的位置

需求规范编写工具能帮助你发现那些因为设计原因而存在问题的功能,而spec-verify工具则能检测出那些本应被发现的测试用例但实际并未被检测到的问题。通过这两者的结合,假设错误可以在代码发布之前就被纠正;同时,现在也有了能够在未来某人无意中撤销这些修改时立即触发失败的测试用例。

不过,这些工具并不能完全替代人类的判断力。一个编写得很好的需求规范仍然可能包含错误的实现要求;一份真实的验收报告也可能遗漏一些通过更严格的重新测试才能发现的问题的。这两款工具的作用在于确保“看起来已完成”与“真正完成”之间的差距必须经过有意识的努力才能跨越,并且所有修改的原因都必须被记录下来,而不能因为没有检测机制而被忽略。

spec-verify仓库的地址是github.com/dannwaneri/spec-verify。在为你生成需求规范之后,请先使用这个工具对其进行验证,然后再将其标记为“完成”。如果其中某些测试用例被标记为VACUOUS,你应该通过代码修改来发现这些问题,而不是等到生产环境中才发现。

相关文章

技术实践

智能代理的可靠性:为工具的使用、安全机制、循环流程以及工作流程的输出结果构建相应的测试框架 [完整书籍]

一个完美的最终结果并不能证明某个具有自主行为能力的人工智能系统的工作流程是正确的。 本书提出了一种基于代码的方法,用于验证系统执行过程及所产生的业务成果,并利用这些证据来决定是否允许该系统正式投入使用。 书中还提供了一个可运行的离线测试工具,用以演示各种核心技术。第19章专门区分了那些已经实现的功能与那些仍处于开发阶段、尚未投入实际应用的扩展功能。 目录 引言 第一部分:可靠性差距 第二部分:基础原理 第三部分:核心结论 第四部分:行为特征 第五部分:跨领域问题 第六部分:决策过程 第七部分:诚实性与普及度 结论 术语表 参考文献 生产就绪性检查清单 引言 那个“虚假的演示” 许多具有自主行为

阅读全文
技术实践

Cloudflare新收购的Vite+版本在加入了GitLab持续集成支持、相关钩子功能以及tsup迁移工具后,其性能已经提升到了1.0级。

VoidZero推出了Vite+ 1.0,这是其JavaScript工具链的第一个稳定版本。该版本配备了一个命令行工具,用户可以通过一个配置文件来管理Node.js项目、进行包管理以及进行前端开发。此次更新还包括与GitLab的集成,以及增加了对Docker的支持。未来还会继续进行升级,重点提升系统的稳定性及诊断功能。 作者:Daniel Curtis

阅读全文
技术实践

如何利用“政策即代码”与开放隐私框架来管理由人工智能生成的基础设施【完整手册】

现代模型在近100%的情况下都能生成语法正确的代码。Veracode在2026年发布的报告明确指出:“语法问题已经基本得到解决。” 这听起来像是一个里程碑,但实际上正是这个原因导致了当前存在的问题。 同一份报告测试了上百种模型,发现它们的平均安全通过率仅为56%,“与第一份报告中的55%几乎没有变化”;大约44%的代码生成任务会引入安全隐患。 功能正确性与安全性其实是两个不同的问题,而目前只有其中一个问题接近得到解决。 这个结果既不是个特例,也不是什么新鲜事。2022年,在IEEE安全与隐私会议上,纽约大学坦登分校的一个团队让GitHub Copilot在89种与安全相关的场景中运行,生成了1

阅读全文
技术实践

在旧系统迁移过程中如何使用差异测试方法

在旧系统迁移过程中,最危险的时刻并不一定是在开始编写新实现代码的时候,而是在新实现看起来已经完成的时候。 代码编译通过了,测试也通过了,架构也更加清晰了,新的服务响应速度也更快了…… 然后所有人都会开始问同一个问题: 我们现在可以把流量切换过来吗? 这时,信心就会变得难以建立。 一个新的实现虽然能够通过自己的测试套件,但其行为仍可能与它所要替代的系统有所不同。 也许数值的舍入规则发生了变化,或者对空值的处理方式不同了,又或许某个错误现在被当作成功的响应来处理了…… 也许记录的排序方式改变了,某些副作用出现的顺序也变了,又或许在迁移过程中,一些从未被记录下来的业务规则丢失了…… 正因如此,在进行

阅读全文