从测试自动化 Workflow 到 Agent Harness

32 分钟,6/6 PASS:我的测试 Agent 到底验证了什么?

一次从软件需求到 Simulink 单元测试的实验。Agent 找到了需求冲突,也完成了失败诊断,但最终全部通过的测试,仍不能证明软件满足需求。

32 分钟,6 份测试需求,6 条可运行的测试脚本,最后是 6/6 PASS。

任务总结蹦出来时,这确实是个很漂亮的结果。但报告里还附着一份核查建议:模型实现与软件需求不一致。

这个错误是我故意留下的。既然模型有错,为什么测试最后还能全部通过?

真正让我重新审视这套设计的,就是这 6 个 PASS 是怎么来的。

写在开始:出于保密要求,我对文中的需求、接口和对话做了脱敏与简化,只保留与工程方法有关的信息,不代表原始工程的全部复杂度。

1. 我交给它的,是从需求到测试的一整段工作

我的起点只有一句口语描述:

我想测试模块 A 在某个工作场景下有关某项计算值的逻辑。你帮我编写测试需求和可用的测试文件。

这里的 Agent,是一个能够调用工具、读取工程信息,并根据运行结果继续行动的 AI 系统。我希望它接手的工作,要一直做到测试真正运行起来。

先交代一点背景。在这次采用的模型开发流程中,工程师用 MATLAB / Simulink 模型表达软件逻辑。测试需要检查模型的实际行为是否符合软件需求。

这中间有三样容易混淆的东西:

工程产物 在这次任务中回答什么问题
软件需求 软件应该做什么?
测试需求 给什么输入、经过哪些步骤、应该看到什么结果?
测试脚本 怎样在工程环境中执行这些步骤并检查结果?

Agent 需要先找到相关软件需求,遇到矛盾时向我确认,再设计测试步骤、准备运行环境、生成脚本,最后执行测试并处理失败。

所以我真正交给它的,并不是“根据需求写几个测试用例”。需求探索、测试设计、工程创建,连同失败以后怎么办,我都想交给它试一试。

2. 它怎么知道我要测的功能是什么?

Agent 首先需要知道,我说的“某个工作场景”究竟对应哪些需求。

这个项目的需求放在需求管理系统(ALM)里。工程师打开它时,看到的像是一份有目录、有前后文的文档;工具读到的,却是一个个独立的需求条目。

读到条目 A,不等于同时看到了人眼中紧挨着它的条目 B。

为此,我给 Agent 提供了两种能力:先读文档目录,建立整体索引;再按索引读取具体条目。它需要自己判断哪些需求相关,把分散的信息连起来。

在这次任务中,相关的平级需求没有显式链接,Agent 需要依靠文字之间的语义关系理解功能。

但我还想知道,它究竟是在逐条读取,还是已经把这些需求联系起来了?

为了验证这一点,我提前埋了一个错误。原本代表两种不同状态的 State A 和 State B,应该对应不同的软件行为。我故意把一条需求中的 State A 写成了 State B,让两条需求变成“同一种状态下,要求两种不同的输出”。

结果,它在梳理完需求后主动停了下来,向我提问。下面是对话的中文简化摘要,并非逐字日志:

text
Agent:两条需求都描述 State B,但要求的输出逻辑不同。
       这里是否存在需求冲突?

我:这里是笔误,其中一条应更正为 State A。

得到确认后,它才继续设计测试。

3. 黑盒测试,不能直接把状态改到目标位置

读懂需求后,Agent 要把它转成测试需求:写清测试目的、对应的软件需求、可控制的输入、操作步骤和预期结果。

最难的地方不在格式,而在一个限制:负责设计和执行测试的主 Agent,不能查看模型内部的具体实现。 后文把它称为主 Agent,原设计中的名字是 Main Loop。

我希望它按黑盒方式设计测试:从外部输入出发,检查外部输出。

测试目标位于状态机的一个中间运行状态。这里可以把状态机理解为软件经历的一组阶段:只有满足条件,才会从当前阶段进入下一阶段。

主 Agent 不能直接把内部状态改到目标位置,也不能修改中间变量来“抄近路”。它只能操作外部输入接口和允许调整的标定参数。

因此,测试步骤必须包含到达目标状态的过程:先给什么输入,软件经过几个调度周期,再给什么输入。这里的调度周期,是软件被操作系统安排执行的轮次。

这次任务中,Agent 回头读取了其他需求,梳理状态跳转条件,按调度轮次推演路径,然后才生成完整的测试步骤。

所以,这一步我真正想看的是:不让它直接改内部状态,它还能不能像软件实际运行那样,一步一步走到我要测的位置?

4. 写完测试需求,还没有真正开始测试

测试需求写完,还只是完成了文字设计。接下来,Agent 要把它变成 MATLAB / Simulink 中可执行的测试脚本。

一个测试能否运行,依赖的不只有被测模型。项目路径、初始化脚本、参数与信号定义、接口数据类型和采样时间,都可能影响执行。

还需要 Test Harness,也就是围绕被测对象搭建的测试环境,用来提供输入并观察输出。

这一阶段,我没有提前把完整的环境信息整理给它。

我只给了它一个项目根目录。

接下来,初始化工程、读取必要的模型信息、搭建测试环境,再到生成和运行脚本,都需要它自己完成。当然,运行所需的接口信息可以读,被测模型内部怎么实现,仍然不在主 Agent 可以查看的范围内。

这次我采用了基于脚本的函数式测试,没有使用更结构化的 Test Sequence。具体取舍留到后续文章展开。

到这里,前面的需求理解和测试设计都还只是准备工作。我最想看的那一刻,要等测试真正跑起来以后才会出现。

5. 第 21 分 43 秒,我等到了第一例 FAIL

任务运行到第 1303 秒,也就是 21 分 43 秒时,出现了第一例 FAIL。

这是我在这次 Demo 中最期望看见的一刻。

如果构造出来的测试工程一次就通过,我其实很难判断:它到底有失败后继续处理的能力,还是我恰好给它铺了一条没有障碍的路?

所以,除了前面的需求笔误,我还故意修改了模型实现,让它不再符合软件需求,并且没有把这个修改告诉 Agent。我想让它遇到的就是这个问题:“明明用例是按照需求写的,为什么就是 FAIL?”然后看看,它会怎么办。

不过,我并不希望主 Agent 直接去看模型实现。它可以继续调试,但我前面设下的黑盒边界,还得保留。

6. 为什么我不让主 Agent 自己查实现

其实,最简单的方法就是让主 Agent 自己查看模型,定位问题究竟出在测试工程、测试需求,还是被测模型上。

但我没有这么做。在这个 Demo 中,我给它下了一个禁令:禁止获取被测模型的具体实现。因为我只希望一件事:被测模型对主 Agent 始终保持黑盒。

那谁来诊断失败?

我还需要一个能看到实现、又不会把实现细节直接带回来的角色。于是,我设计了一个专门的诊断子 Agent,叫作 Failure Diagnosis Agent。

它的权限比主 Agent 更大:除了软件需求、测试需求和测试脚本,还可以查看模型内部实现。它需要判断,失败究竟来自模型与需求不一致、测试设计遗漏、脚本实现偏差,还是运行环境问题。

我给它设置的返回规则是:只告诉主 Agent 诊断是否完成、问题在哪一层,以及建议如何修改,不返回模型实现的具体细节。

两者的分工可以这样看:

角色 可以查看的信息 承担的工作
主 Agent 需求、测试产物、运行所需接口信息和结果 设计测试、执行测试、处理反馈
诊断子 Agent 上述信息,以及模型内部实现 定位失败原因,返回诊断与修改建议

所以在我原本的预期中,主 Agent 不会接触实现细节,整个系统又保留了失败后恢复的能力。

至少,看起来是这样的。

7. 6/6 PASS,但不是 6/6 需求满足

于是,就有了开头那个漂亮的结果:6/6 PASS,报告里也指出了“模型实现与软件需求不匹配”。

但随之而来的,是一股异样感:模型是我故意改错的,问题也找到了,为什么最后还是全部通过?

仔细检查日志后,我发现了问题所在。主 Agent 获取诊断建议后,直接修改了测试工程脚本,同时保留了那份不一致问题的核查建议。修改后的脚本,得到了 6/6 PASS。

回头看,我给这个任务隐含了两个没有明确拆开的成功目标:

  • 产出可以执行、并且全部通过的测试资产。
  • 保持测试忠于软件需求,检查软件是否满足要求。

当模型符合需求时,两者可以同时成立。但这次我故意放入了错误:如果测试确实检出了这个错误,保留失败本来就可能是正确结果。

这里涉及测试的判定依据,技术上常称为 test oracle:依据什么判断结果是对是错。在这次任务中,这个依据应该来自软件需求。

而测试是否仍忠于需求,也不只取决于有没有修改预期结果。输入条件和执行步骤一旦改变,测到的就可能不再是原来的场景。

这层冲突,是我设计之初没有注意到的。而 Agent 在这里做了一个很有意思的选择:保住可运行的测试资产,同时在任务总结中把模型与需求不匹配的问题暴露出来。

所以,我得把这个结果重新拆开看:

检查项 本次结果 应该怎样理解
脚本能否执行 6/6 可执行 工程链路已跑通
最终脚本是否通过 6/6 PASS 修改后的脚本检查通过
原测试目标的需求符合性 6 个测试项中,3 项涉及需求不一致问题 不能据此宣告原需求验证通过

显而易见的结论是:测试资产跑通,不等于需求验证通过。

Agent 暴露了模型问题,也交付了可运行脚本。但将两者一起交回来,并不能让修改后的 PASS 代替对原需求的验证。

8. 没看到实现,为什么仍然受了实现的影响?

还有一个更棘手的问题:主 Agent 没有看到模型内部实现,为什么测试最终仍然向模型的实际行为靠拢?

影响来自诊断建议。

诊断子 Agent 看到了实现,再据此提出修改测试输入的建议。主 Agent 虽然不知道建议背后的实现细节,却执行了这个修改。

下面用抽象的 A、B 表示修改前后的测试输入,只解释信息如何传递,不对应真实接口值:

  1. 诊断子 Agent 看到了模型的实际实现。
  2. 它据此建议:把测试输入从 A 改为 B。
  3. 主 Agent 只收到了“改为 B”的建议。
  4. 主 Agent 修改测试,模型实现因此间接影响了测试。

实现细节没有被直接转述,并不意味着这些细节没有参与决定下一步行动。

隔离了实现细节,不等于隔离了实现影响。

这是这次实验里,最让我重新审视原设计的地方。我盯住了“它有没有把实现细节带回来”,却漏掉了另一个问题:它带回来的建议,本身是不是已经受了实现的影响?

主 Agent 确实没看到实现,但测试已经因为实现而改变了。这个问题,比我最初设想的隔离边界更难处理。

9. Demo 跑通了,但问题刚刚开始

至此,这个 Demo 算是跑完了。总共约 32 分钟,记录中的上下文规模约为 170k。这个数字只是本次长任务的运行记录,不能单独说明任务复杂度或效率,也不应混同于累计调用消耗。

按我过去完成同类任务的人工耗时粗略比较,这次节省了约 80% 的时间。这是个人经验估算,不是严格对照实验;尤其是测试的正确性仍需要复核,省时不能替代这部分判断。

它确实完成了一段跨越需求理解、测试设计、工程创建和失败诊断的工作。过程中也包含我的人工澄清:Agent 发现需求冲突后,由我确认了笔误。

这条路径至少跑通了一次。但真要继续往工程化走,我还有很多问题想弄清楚:

  • 换个任务还能否完成? 换模块、换需求,或者连续运行十次,成功率和人工介入会怎样变化?
  • 任务更长时能否继续? 上下文压缩、工具报错、状态更复杂时,它能否保留关键信息并恢复工作?
  • 完成的仍是不是原任务? 如何分别评价脚本执行、需求验证,以及失败后修改的合理性?

这种约束能不能真正阻断实现信息通过诊断建议间接影响测试,还需要后续实验验证。

回到这次实验,Agent 的自主性让它有机会从故障中恢复,也带来了我没有预料到的路径:即使没有直接违反那条“不许看实现”的禁令,它仍然可能改变任务最终验证的内容。

所以我也在想,对于一个要在专业领域里执行长任务的 Agent,究竟怎样的自由度才算“合适”?

在这套 Agent 之前,我已经做过另一套自动生成 Simulink 单元测试工程的系统,并在实际工作中使用。它依靠明确的状态校验和流程控制,留给系统的自由度更小。

我曾考虑让 Agent 直接调用那套 Workflow,也就是预先规定步骤的自动化流程,最终没有这样做。

既然已经有一套更确定的流程,为什么还要选择一个更开放、更难约束的 Agent?

这个问题,留到下一篇。