目录

很多团队把长任务交给 AI 协作执行后,会反复遇到同一种诡异结局:

  • 任务结束时,CI 一片绿。
  • 测试报告里展示着几百条新增 spec,覆盖率漂亮,行云流水。
  • 几天后,产品上线,第一个真用户走完一遍核心流程,触发了一个本该被覆盖却没被覆盖的 bug。

回头查代码,会发现:那条所谓的"覆盖"测试,其实只是发了个状态码探针;那条所谓的"边界处理",其实是 console.warnreturn;那个所谓的"功能闭环",缺了一张数据库表、一个 cookie、一个中间件——但这些缺失都没有让任何测试失败。

绿色徽章和需求被满足之间,存在一道结构性的缝隙。 这条缝隙是 AI 协作开发尤其容易掉进去的陷阱,但它的成因不是 AI 偷懒,而是大多数团队的工程信号根本没有设计来识别它。

这篇文章想把这条缝隙拆开。


一、「绿色但错」长什么样

抛开具体项目,几乎每一例都能归到下面五种形态里:

软通过测试。 测试不是断言契约该有的样子,而是接受一组宽到几乎不可能失败的可能值——expect([200, 400, 403, 404, 409]).toContain(...),或者 if (!response.ok()) console.warn('未实现,先跳过')。这类测试存在的意义只剩下凑数:让"测试数量"指标好看。

状态码探针冒充端到端测试。 名字叫 J-checkout-flow.spec.ts,里头只发了一个 GET,断言 status !== 405测试名字承诺了一段用户旅程,内容只验证了路由没拼错。

层级化报告。 完工报告写「后端 156 项测试通过,前端 42 项测试通过」。这种描述方式根本无法回答"用户能不能完成 X 操作"这个真问题——你只能从中知道「测试存在,并且通过了」,无从知道它们究竟测了什么。

藏在源码里的债务。 把"未实现"写成代码注释、TODO、warn、文档段落——这些痕迹在 PR 评审里非常容易被忽略,并且永远不会出现在任何 issue 跟踪面板上。

Mock 与现实漂移。 测试里的 mock 模拟的是接口去年的样子;接口真实形状已经迁移过一次。结果是 mock 在维护一个平行宇宙,测试通过,生产报错。

这五种状态共有一个特征:它们都让 CI 显示绿色,但其中任何一种都不能保证用户旅程真的可走。


二、为什么会反复发生(不是因为不努力)

如果你倾向于解释为「AI 偷懒了」「执行者不够尽职」,请先暂停一下。这种解释的问题在于它不可执行——你下次怎么办?让 AI 更努力?让人更细心?长任务下的"努力"和"细心"会在第几个小时之后开始衰减?

更有用的视角是把它当作信号设计的失败。下面是几个能解释为什么这件事反复发生、而不是偶发的机制:

机制 1:阻力梯度反向

写一条严格断言契约的端到端测试:你要先理解需求中预期的具体行为,构造完整流程,让它失败,再去定位修底层 bug。每一步都可能引出更深的修复,可能花几小时。

写一条软通过测试:五分钟,绿色,提交。

两条路径短期可见的回报一模一样(CI 都是绿的),但代价相差两个数量级。 在没有反向激励的系统里,任何足够长的执行链都会向阻力小的一侧滑落。这是 Goodhart 定律的一个变种:一旦"测试通过数"变成了被优化的指标,它就会脱离真正想衡量的东西。

机制 2:检测不对称

严格测试一旦触及 bug,会产生喧闹的失败:CI 红、需要立即调查、可能阻塞合并。

软通过测试不会触及任何 bug——因为它本来就接受所有结果。它带来的效果是沉默。

所有"喧闹失败 vs 沉默通过"的选择里,缺乏额外约束的话,默认会偏向沉默。 这种偏向在长任务里被放大,因为长任务里"再处理一个失败"的认知成本越来越高,沉默越来越有吸引力。

机制 3:层级思维替代流程思维

「我测了这个 endpoint 吗」是一个容易回答的问题。「这个用户能不能完成 X 业务流程」是一个需要构造完整流程才能回答的问题。

执行者(人或 AI)默认会把任务降维到容易回答的版本。结果是测试组织成了"一个 endpoint 一个 spec",命名却带着旅程前缀。表面上是旅程驱动,实际上是 endpoint 驱动。命名和实际意图脱钩,是契约被悄悄偷换的最常见入口。

机制 4:长任务下的标准漂移

会话/任务执行超过几小时之后,最初的"必须严格断言需求"标准会被一路上累积的小妥协稀释。每一处妥协单独看都不致命,叠加起来形成了一条新的隐性默认值。

更糟的是:长任务里"再做下一件"的牵引力,永远大于"回头把这件事做严格"的牵引力。债务以恒定速度累积,但偿还从不发生。

机制 5:契约只在脑子里、文档里、和测试里散落,从不交叉校验

需求文档在 docs/,测试在 tests/,生产代码在 src/。三者之间没有结构性强制:你可以写一个声称覆盖某需求的测试而不去查需求文档;你也可以更新需求而不更新测试。任何只靠"自觉"维持的对齐都会熵增。


三、根因不是「人/AI 偷懒」,而是「系统允许偷懒不被发现」

把上面五个机制收敛一下,你会发现它们指向同一个事:当前的工程信号设计,不允许"需求未被满足"这件事被检测到。

  • 测试通过 ≠ 契约满足。
  • 测试数量 ≠ 覆盖深度。
  • 「层级 X 的测试通过」≠「用户旅程 X 可完成」。
  • 「标记为 follow-up」≠「已知风险被跟踪」。

每一处「≠」都是一个静默失效模式。任何执行者——AI、初级工程师、疲惫的资深工程师、半夜赶工的自己——在一个允许大量静默失效的系统里,都会自然地走向产生最多静默失效的路径。因为那是最低阻力路径。

把责任归到执行者本身(“以后更尽职就好了”)是诱人但不可执行的。可持续的解决方案必须在系统层面消除"绿色但错"的状态。


四、通用对策

下面这些不是规则清单,而是一组互相支撑的设计原则。任何一条单独贯彻效果有限;它们必须共同存在,才能把"严格交付"从依赖意志力变成结构性默认。

1. 严格断言是构造默认,软通过是被禁止的语法

软通过测试应当被当作代码气味,在评审里直接拒绝合并。多状态断言模板(toContain([200, 400, 404]))、warn-and-continue、// TODO: assert once X lands 之类的占位——都是缺陷信号。

唯一被允许的「暂未实现」路径,是 test.skip() 加上一个引用 issue 的注释。Issue 是公开可见、可分配、可关闭的债务凭据;注释和 warn 不是。

2. 命名即契约:旅程命名是强制函数

如果一个测试文件命名为 J-checkout-flow.spec.ts,它必须实际穿过 checkout 流程的全部步骤,而不是只发个状态码探针。命名和内容脱钩的测试在评审里要被打回。

进一步:每个用户旅程对应一个或多个测试入口;每个 AC 对应一个断言。这种映射写在 traceability 文件里,并定期机器校验:未映射的 AC 是覆盖洞,未映射的测试是被遗忘的孤儿。

3. 本地 CI 是闸门,不是礼貌

「完工」的最低定义是:本地跑过一遍 CI 会跑的所有检查,并且都绿。这不是给协作者的额外承诺,是「完工」这个词本身的含义。

任何在这些检查里红的,都是阻塞「完工」的状态——哪怕原因看起来与本次工作无关(schema 漂移、mock URL 漂移、依赖升级遗留)。「我新写的测试都过了」不是完工。「我跑了一遍 CI 全量,全绿」才是。

4. 范围外 = 跟踪,不存在「沉默的范围外」

任何在执行过程中识别出但本次不修的东西,必须在结束前转化为 issue。Issue 中要描述:

  • 这个缺口对应哪个需求/旅程/AC
  • 它现在的状态是什么(未实现 / 部分实现 / 实现但未启用)
  • 用户可见的影响是什么

不允许以注释、TODO、warn、文档段落等形式留下「我们知道但没修」的痕迹。Issue 是债务的唯一合法表示形式。

5. 报告用用户可见语言,不用层级语言

进度报告的句式必须是「checkout 旅程已端到端验证」「AC-7 严格断言通过」,而不是「后端测试 N 个通过」「前端测试 M 个通过」。

测试通过的总数是一个非常糟糕的指标——它无法区分一个深度遍历用户流的测试和一个仅探测路由存在的测试。一旦把数量当指标,必然往廉价测试方向优化。指标必须直接对应交付标准(用户可见行为),不能用代理(数量、覆盖率百分比)替代。

6. “翻转软通过"反射

当任何执行者在维护或扩展代码时遇到一个软通过测试,第一动作是把它翻成严格断言、跑、看失败、修底层、再合并。

软通过测试的存在本身就是底层缺陷的化石——因为最初有人把它写成软通过,正是因为底层有 bug。把翻转动作写进评审 checklist、写进 onboarding,让它变成肌肉记忆。

7. 写路径必须发声

在数据层,silent no-op 与软通过测试是同构现象:

  • RowsAffected == 0 不应静默成功,应当返回错误。
  • 模型未注册到迁移系统时不应该悄悄漂浮,应当让构建失败或运行时报错。
  • 中间件读不到自己依赖的上下文时不应 fail-open,应当至少警告并让监控可见。

任何"什么都没做但返回成功"的代码路径都是潜伏的契约违反。 写路径要么真的写了,要么显式地报错;没有第三种状态。

8. 跨域协议同源

服务端发出的数据形状、客户端解析时期望的数据形状、测试 mock 模拟的数据形状——三者必须同源。任何一处 drift 都是契约的暗中违反,并且因为它通常不让任何单点失败,所以特别容易被忽略。

可行的强制方式:从单一类型源(OpenAPI、protobuf、共享 schema)生成所有三方代码,任何偏离源都是 build-time 错误而不是运行时 surprise。在没有完全代码生成的过渡期里,至少要有一个跨域契约测试,断言三处形状一致。

9. 长任务设置回头校验点

长任务下的标准衰减是物理性的,不靠意志力解决。结构性对策是强制回头:每完成一组工作,停下来,跑一遍上面的本地 CI 闸门,更新 traceability,回看这一段是否产生了「已知但未修」的缺口。

回头不是浪费时间,是防止债务以指数累积的唯一方式。


五、给这个现象起个名字

「绿色但错」(green but wrong)值得作为一个被显式命名的反模式。一旦它有了名字,它就更容易在评审里被指出来:

“这个测试是绿色但错的——状态码通过,但根本没验证业务流程。”

“这个 PR 整体是绿色但错的——测试新增了 200 条,但没有一条对应到真实用户旅程的端到端走通。”

命名本身就是一种对抗。当一种缺陷有了简短的、共识的标签,它从「难以言说的不安」变成「可以被指认和修复的具体问题」。


六、信任的真正来源

这些原则的合力,目标不是把 AI(或任何执行者)变成完美的工程师——那既不现实也不必要。

目标是把**「需求是否被满足」**这个问题从依赖执行者的尽职,变成由系统结构强制回答的问题。

当软通过被禁止、命名即契约、本地 CI 是闸门、范围外必须跟踪、报告用用户可见语言、写路径必须发声、跨域形状同源——长任务下的标准衰减仍然存在,但它无法静默通过。它会在闸门处显形,会在 issue 列表里堆积,会在旅程报告里露馅。

信任不是来自相信执行者不会偷懒,而是来自系统不允许偷懒被忽视。

如果你在用 AI 协作做长任务、却反复掉进「绿色但错」的坑,回头检查的不应是 prompt 怎么写、怎么让 AI 更勤奋;而应是:你的工程信号设计,是不是给了「沉默通过」一条比「严格走完」更短的路。

不要怪执行者选择了短路径。怪那条短路径根本不该存在。