← 返回蜂巢洞察

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

我确认,那些被标记为“已验证”的结果实际上都是虚假的。这样一来,原本五个伪造的响应中现在一个也没有了,测试过程进行得非常顺利,任务已经完成。 随后我在更严格的条件下再次进行了测试,得到的准确率为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将这类规则归类为“无效测试”,人们很容易认为它们和那些“未通过验证”的测试属于同一类别:都是需要改进的、不够完善的测试。但实际上并非如此。“无效测试”是一种真正的缺陷——这种测试本身存在问题,而解决它的方法也总是相同的:编写一个更好的测试用例而已。而“无法进行变异测试”意味着某种标准超出了当前技术所能检测的范围,通常是因为相关行为具有非确定性,而不是因为有人犯了错误。

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

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

  • VACUOUSBROKEN-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的项。VACUOUSBROKEN-TEST表示需要修复相应的测试用例;任何标记为UNVALIDATABLE的项都意味着你需要明确决定是由人工手动检查这些内容,还是需要将相关代码修改到可以被测试的环境中才能进行验证——就像最终对“entity-grounding”功能所做的那样。

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

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

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

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

相关文章

技术实践

如何在重构旧代码之前设计相应的特性测试用例

许多工程师在继承遗留代码后,首先想要做的就是改进这些代码。 你会遇到一些难以理解的函数,或者看到重复的逻辑、深度嵌套的条件语句、混合了业务规则的数据库调用,以及那些使得测试几乎无法进行的依赖关系。 你知道这些代码本可以做得更好,于是开始对其进行优化。 然而,随后就会出现问题。并不是因为新的实现方式存在明显的错误,而是因为旧的实现方式在背后做了某些人们之前根本不知道它在做的事情。 这就是在遗留代码现代化过程中最常出现的风险之一。 在修改代码之前,你需要有一种方法来回答一个简单的问题: 我是否保留了那些原本就重要的功能行为? 这时,特性测试就派上了用场。 特性测试并不是从“软件应该做什么”这个角度

阅读全文
技术实践

如何测试Flutter应用程序:单元测试、组件测试、黄金标准测试以及集成测试详解

第一次在技术面试中被问到“你的测试覆盖范围是多少?”时,我并没有一个令人满意的答案。 那时我已经发布了几款真正的Flutter应用程序,它们可以正常运行,用户也在使用它们。但我的测试工作其实非常有限——仅仅是为某个定价功能编写了少量的单元测试而已,并没有其他测试内容。 几个月后,我对其中一个任务完成流程进行了重构,这个修改在代码差异对比中看起来完全没问题,但却破坏了用户真正关心的一个功能:当用户将某项任务标记为已完成时,该任务并不会从错误的列表中移除。虽然没有任何程序崩溃,也没有任何错误日志被记录下来,但用户却开始不再信任这款应用程序。而我直到有用户把这个问题告诉了一位也是测试人员的朋友,才发

阅读全文
技术实践

如何利用LangChain深度代理构建多智能体交易研究系统【完整手册】

交易研究代理可以编写策略代码,运行回测,检查结果,并不断修改策略。而更困难的任务在于确保这一循环不会演变成一种无序的、盲目追求理想回测结果的探索过程。 在这本手册中,我们将利用LangChain Deep Agents构建一个多代理交易研究系统。EODHD会提供历史市场数据,而一个基于Python的确定性层则会负责控制数据的分割方式、回测逻辑、基准测试的设置、实验记录的保存以及策略选择规则的实施。随后,协调员、策略工程师和研究评估人员将在这些规定的框架内共同开发并评估三种不同的策略版本。 我们的目标并不是证明人工智能代理能够可靠地发现盈利策略,而是建立一个研究工作流程,在这个流程中,各代理可以

阅读全文
技术实践

了解人工智能软件开发生命周期流程——构建智能代理功能的完整指南

也许你可以理解这样的场景:本周,你用了同样的说明四次向别人解释人工智能模型的使用方法。 你反复讲解过团队是如何构建演示文稿的框架的,哪些检查步骤需要在部署之前完成,以及为什么测试数据库并不是文档中提到的那个。 每次你都要把这些内容重新写一遍,每次智能助手也能完成得不错,但每次新的会话开始时,一切都得从零开始。 而这正是 智能助手技能 所要解决的问题。 技能 实际上就是一个文件夹,其中只包含一个名为 Skill.md 的文件。智能助手在启动时会阅读其中的一行总结内容,而只有当真正需要时才会打开完整的说明文件。你只需把解释内容编写一次,将其与代码一起提交,那么团队中的每个智能助手就能访问这些信息,

阅读全文