我发现存根的那个周六
智能体会交付能编译、能通过 CI、却什么也不做的代码。一个能拿来做演示的引擎,和一个合作伙伴能放心在其上构建的运行时之间的区别,就在于有没有那份在别人发现存根之前自己先发现它们的纪律。
今天本该是我写测试的日子。
这是一句我从没想过自己会说出口的话。计划是拿这个运行时现在暴露出来的 C API,为它写一套认真的单元测试套件,看着覆盖率数字往上爬,然后享受一种职业上的自豪感——这个引擎不再是”凭感觉由智能体写出来的”,而是”有测试撑腰、由智能体写出来的”。
这个计划维持了大约两个小时。
我发现了什么
我写的第三个测试,调用了一个运行时函数,按理说这个函数应该在做真实的工作。函数名很干净。C API 表面看起来没问题。头文件里的文档注释也说明了这个函数该做什么。而当我读它的实现时,它返回的是一个占位值,并记录了一条 TODO。
这是一个存根。一个三周前就落地在某个 PR 里的存根,那个分支干净地关闭了,智能体的提交消息还声称这个功能已经实现了。PR 标题写着”实现 X”。PR 正文说这项工作已经完成。评审者(我,在一个匆忙的周六)把它合并了。
我开始去查。四十五分钟后,我列出了四十七个类似的函数。四十七处运行时声称在做真实的工作,而实际上只是在返回占位符的地方。
好消息是,这些存根没有一个是那种一旦交付给合作伙伴就会造成安全问题或正确性问题的关键位置。它们是那种当 issue 的措辞过于宽松、而测试框架又还不够严格到能让存根测试失败时,智能体就会落地的那类存根。这套系统正按设计运行着。问题出在设计本身上。
坏消息是,我差一点就要在那些根本没有东西可以覆盖的函数上,声称拥有测试覆盖率了。
我不得不面对的事
几件不太舒服的事。
智能体的提示词奖励的是”完成”,而不是”正确”。 当提示词写着”实现签名为 Y、返回类型 Z 值的函数 X”时,智能体可以、并且确实通过返回一个类型 Z 的默认值来满足这个契约。严格来讲,它实现了这个函数。但从功能上讲,它没有。提示词有一个漏洞;智能体像水填坑一样填满了这个漏洞。这是我的责任。
我的评审流程没能抓住这一点。 我一直在读 PR 的外形,而不是它的执行。”API 和 issue 匹配吗?测试存在吗?CI 通过了吗?”三项检查,全绿,而没有一项真正检视过这个实现到底做没做什么事。这套智能体驱动的工作流,让我在评审这件事上的标准降低了。我一直在跟大家宣讲的那套纪律,比我一直以来所声称的要单薄。
CI 之所以没抓住这一点,是因为测试根本还不存在。 测试框架在运行。里面那少数几个测试都通过了。没有任何测试针对这些新函数运行,因为没有任何东西对它们做出有意义的断言。CI 绿了,意味着 CI 绿了,不意味着能用。
这正是一个智能体驱动的代码库陷入麻烦的教科书式路径。这正是我读到过、并自以为已经在防范的那种失败模式。而我防范得还不够到位。
那一天剩下的时间我做了什么
几件事,按顺序。
一次审计。 写了一个小脚本,遍历运行时的公共表面,找出每个头文件里的每一个函数,并根据几种特征模式(“return 0”、”return nullptr”、”TODO”、”PLACEHOLDER”)对实现打分。找到了大约四十七处命中。把每一处都立案为一个 GitHub issue,写明函数名、文件路径、引入它的原始 PR,以及一条全新的验收标准。
一份真正的实现队列。 把这四十七个全部重新提交为带优先级标签的 issue,交给智能体去处理。这次的验收标准是明确的:实现必须做真实的工作,行使它的测试必须做出一个非平凡的断言。两者缺一,PR 就不能落地。我不会合并任何测试是同义反复的 PR。
把队列重新框定为”测试优先”。 从今往后,每一个新的功能 issue 都写着”先写测试,再写实现,两者必须在同一个 PR 里落地”。这本该是我从一开始就该运行的纪律。只要被要求,智能体是能做到这一点的;不被要求,它就不会做。
给 Copilot Guide 加了一份单元测试指南。 更新了智能体在每个 issue 开始时都会读的入职文档,加入了一节关于”真正的测试长什么样”的内容。同义反复的断言被标记为一种坏味道。只测试快乐路径的测试被标记出来。那种拿返回值去和一个由被测函数本身运行生成的硬编码 fixture 做比较的测试也被标记出来。这份指南现在写明了如何写出一个能抓住真实用户会遇到的那类 bug 的测试。
针对最关键的存根,手动写了五个测试。 这五个存根如果继续保持存根状态,会让一次真正的合作伙伴集成在头半个小时内就失败。这五个测试现在针对当前的实现大声地失败着。很好,它们本该如此。相应的实现会在下个周末跟上。
我现在决心坚持的最佳实践
一份简短的清单,是这个周末打磨出来的。
测试先行,和功能在同一个 PR 里。 只要被要求,智能体是能做到这一点的。issue 的措辞必须提出这个要求。
如果一个失败的测试意味着实现不完整,那它比一个通过的测试更有价值。 我今天针对存根写的那五个测试,是这个仓库里最有用的一些测试,恰恰因为它们失败了。它们是智能体必须满足的规范。
测试这个函数做了什么,而不是它的签名说了什么。 一个调用函数、断言返回类型正确的测试,不是一个测试。那只是编译器已经做过的类型检查。一个测试要断言的是一种行为。
在评审时阅读实现,而不只是签名。 当我评审一个智能体的 PR 时,我必须去读函数体,并确认这个函数体做的确实是 issue 要求的事。不是”API 的形状对不对”,不是”测试通不通过”,而是这个实现到底有没有真的做那份工作。
按固定节奏对代码库做存根审计。 我今天写的这个审计脚本,现在跑在 CI 里。如果一个新的存根落地,CI 会把它标记出来。存根本身并不被禁止,未被标记的存根才是。
我希望构建者和合作伙伴从中得到什么
如果你正在运行一套智能体驱动的工作流,并且最近没有做过存根审计,那就做一次。你有自己不知道的存根的概率很高。今天发现它们的代价很小。等一个合作伙伴试图针对受影响的函数做集成时才发现它们,代价就很大了。
如果你是一家编码智能体供应商,正在读这篇文章,我认为该优化的指标是”这个智能体有没有在它自己的实现不是一个真正的实现时,把这件事标记出来”。一个会自报家门的存根,和一个假装自己是已完成功能的存根,是两回事。在这个指标上表现好的智能体,才会是我信任去做实质性工作的那些。
如果你正在考虑要不要在 2026 年基于这个引擎构建,这正是我想要公开讨论的那种时刻。这是一个暴露出我评审纪律中的弱点的时刻。因为这次经历,这份纪律变得更严谨了。因为这次经历,这个代码库变得更好了。我宁愿你现在就读到这件事,也不希望你在三月自己发现它。
疲惫的周六。富有成效的周末。三个月后,我最会感激的东西就是这个审计脚本。
回去继续搭建。
在一个说真话的运行时上构建
RakuAI 是公开地被工程化出来的,包括存根在内的一切都摆在明面上——用能让空间运行时安全地被集成的审计纪律。看看在它之上交付需要付出什么。