测试通过了,功能却从未生效
让 AI 助手实现一个带退避的重试行为,几分钟后,类出现了,针对它的测试出现了,展示它的示例出现了,解释它的文档页面也出现了。每一部分看起来都在证明:这个功能可用,而且有人需要它。
一年多以前,从第一次提交开始,我就在 AI 助手的协助下构建 Agenor——一个面向 JVM 的多智能体框架。它已以九个模块发布在 Maven Central 上。我犯的错误都是每个 Java 开发者熟悉的错误。新的是,围绕这些错误堆积起来的 API 表面的速度。
两个从未运行过的行为
测试一个类,与测试用户使用它时所走的路径,是两回事。Agenor 仍有两个功能落在这道缝隙里。用户通过 @Behavior(type = CYCLIC) 这样的注解选择一种行为,而从不需要写出它背后的类。在 0.28.0 之前提供的十五个常量中,EVENT_DRIVEN 和 WAKER 从未通过该注解生效,尽管它们各自对应的类都有通过的单元测试。两者都没有到达用户手中。0.28.0 弃用了它们,0.30.0 移除了它们,而这一切都发生在 Agenor 首次发布到 Maven Central 之前。
它们为何能蒙混过关,正是本文的主题。
同一来源的四个见证者
每个常量都配有自己的类和测试,有些还配有自己的示例。同一个过程写出了它们全部。测试使用这个类,是因为它本来就是为测试这个类而写的;示例写出它的名字,是因为它本来就是为展示它而写的。仓库就这样长出了自己“被使用”的证据。
关于一个功能,有两个问题值得问:它能否通过用户的入口点生效?以及,是否有人需要它?与功能一同写下的证据,无法单独回答其中任何一个问题。因此,其余证据必须来自产生该功能的过程之外。所谓“之外”,是指为了展示该功能之外的其他目的而写,而且它仍然可以由 AI 来写。
更多智能体,同一个仓库
常见的答案是增加结构:一个智能体写测试,另一个智能体审查测试。第二个智能体可以发现第一个智能体漏掉的实现错误;如果测试者从用户写下的内容出发,而不是从 diff 出发,它本可以抓住这两个问题。至于是否有人需要这个功能,则是另一回事。每个角色读的都是同一个仓库,也没有任何一个角色知道谁会使用这些代码。
从用户一侧测试
第一项对策是老建议。它值得重复,因为 AI 助手会让跳过它这件事变得不易察觉。类的测试会自动出现,而路径的测试必须主动要求。对 WAKER 来说,这就是一个声明了 @Behavior(type = WAKER) 并期望被唤醒的智能体。配置键或 HTTP 端点也是如此。
在没有见证者的情况下计数
在有人使用这个库之前,你无法知道谁需要某个功能。可以统计的是:你自己的仓库里有哪些代码写出了每个名字。这是一个更窄的问题。在 Agenor 中,一个脚本负责这项统计,它遵循三条适用于任何语言的规则。
测试不算数,在任何模块里都不算。一个只被自己的测试使用的类型,看起来像是被使用了,而这正是值得找出来的情况。
专用示例也不算。直到 0.29.0,唯一写出 RetryBehavior 这个名字的示例就是 RetryExample,而它的全部职责就是展示它。
一个类型只有在它的名字被写出来的地方才计入。PingPongExample 调用 runtime.getAgentDirectory().findAgents(...),因此它使用了一个 AgentDirectory,却从未写出这个名字,所以不计入。这里衡量的是:一个新来者必须学会写出多少个类型的名字。
对于一种行为,用户写出的名字是常量,而不是类,所以脚本也统计常量。按这种方式统计,行为常量显得很突出。在 90 个示例文件中,CYCLIC 被 18 个文件写出,ONE_SHOT 和 FSM 各被两个文件写出,其余十二个则只被它们自己的演示写出,或者根本没有被任何示例写出。那两个坏掉的行为,就是在从这次统计开始的清理中发现的。
在 v0.36.0 中,Agenor 有 264 个公共顶层类型,而这项统计没有将其中任何一个标记为应弃用。大约三分之一在文档中被写出名字,但没有出现在任何示例里。这些就是需要逐个检查的类型。代码可以通过 getter 访问某个类型而不写出它的名字,所以仅仅计数低,并不意味着没有人需要它。
趁代价还低时修剪
0.30.0 移除了全部十二个已弃用常量,以及配置它们的十一个注解元素、它们背后的七个行为类,还有其他七个没有任何引用的类型。这一切都是破坏性变更,而其中没有任何一部分到达过 Maven Central,所以唯一受影响的人是我。哪怕只有一个用户,同样的清理也会需要为每个常量经历一段弃用期。
答案来自哪里
从用户一侧写的测试会告诉你一个功能是否生效。计数会告诉你该去哪里寻找没人需要的功能,但它无法给出定论。那个答案来自第一个在自己的代码中导入这些类型的人。
该脚本位于标签 v0.36.0 的 tools/api-census.sh。即使不用它,这三条规则也成立。