生成式软件工程
搜索

让检查持续发生

亮红灯不等于拦住。有检查和检查有约束力,是两件事。

这一章讲什么
  • 让助手反复尝试的循环,以及它的边界在哪里;
  • 为什么「工作区是状态、助手是状态迁移」这个视角有用;
  • 持续集成与持续交付的差别,检查为什么要真正拦住;
  • 三个真实项目里的做法,与三个失败案例的教训。

一、一个五行代码的循环

让助手变强的朴素办法是让它反复尝试。最简形式只有五行:

while true; do
    cat PROMPT.md | assistant -p
done

每一轮把同一份任务说明交给它,让它看当前成果、挑下一件事、改完、验证。一轮结束,下一轮重新开始。 提示词没变,但工作区变了:上一轮实现了功能、留下了测试结果、记录了失败原因,下一轮可以接着这些状态往下走。

这就是循环的全部魔法。它没有更玄的机制。

循环本身不会判断 while true 不会自己识别「已经完成」或「无法改进」,也不能保证下一轮比上一轮好。 必须另外定义三样东西:验收条件(什么样算完成)、预算(最多跑多少轮、花多少)、 停止条件(连续几轮没有进展就停)。缺了这三样,这个循环只是在制造开销。

二、工作区是状态,助手是状态迁移

理解这类循环,有一个很好用的视角:把整个工作区看成状态。

工作区里不只有代码,还有任务说明、设计笔记、测试、运行结果、日志、待办清单。助手读入这些内容, 执行一轮工作,把它们变成下一份状态。一次开发就是一次状态迁移:从「这个功能还没做」到「有实现但测试不过」, 再到「失败原因已定位,修完通过检查」。

这个视角解释了一件重要的事:长期任务不能只靠一段越来越长的聊天记录维持。 新一轮的助手必须能从留下来的材料里判断:目标是什么、已经做了什么、哪些结论有证据、哪里还没完成。 如果关键经验只存在于上一轮的对话里,下一轮就得重新踩一遍坑。

生活类比 社团办活动时,交接文档写「场地已定」没有用;写「场地是周三下午三点在体育场东门,联系人张老师, 押金已交、收据在共享盘」,接手的人才能继续办事。工作区里的记录要写到这种程度, 才叫可以交接的状态。

三、重新发现持续集成

循环里有一条关键要求:每一轮都要验证并评估结果。理由可以说得很不客气:开发者是不可信的。 「我只改了一点点,不会有事」这句话,正是很多事故的开场白。

既然开发会不断改变工作区,而进入主线的状态都必须是好的,就应该在集成这些改变时执行检查。 把「好」变成具体条件:能构建、测试通过、没有破坏已有功能、满足项目的关键约束。这些条件成立,才允许合入。

做到这一步,你重新发明了持续集成。持续集成强调频繁把小改动集成到共同主线,并自动构建和验证。 它应对的是「各写各的、最后才发现互相冲突」这个局面。改动越小、反馈越及时,出问题时需要怀疑的范围就越小。

发布环节还有两个容易混淆的说法:

说法含义什么时候上线
持续交付软件始终处于可发布状态由人决定,随时可以按一下发布
持续部署通过检查的变更自动发布给用户不需要人按,自动完成
flowchart LR
  A["提交改动"] --> B["自动检查
构建 测试 格式"] B -->|"失败"| C["阻止合并
报告失败位置"] C --> D["按报告修正"] D --> A B -->|"通过"| E["允许合并"] E --> F["构建产物"] F --> G["发布"]

四、检查要有约束力

这是本章最重要的一条。有检查和检查有约束力,是两回事。

如果测试失败之后仍然能照常合并,那这套检查只是在旁边亮了一盏红灯,起不到拦截作用。 要让检查真正起作用,需要三个动作:

  1. 把检查设成合并的必要条件。检查没过,合并按钮就是灰的。
  2. 保护主线分支。不允许跳过检查直接推送。
  3. 检查组合结果。两个改动单独都通过、合起来可能出问题。合并队列可以检查待合并的改动与主线、与其他待合并改动的组合。
关键结论 检查的价值不在于它能发现错误,而在于它能否决不合格的变更。 只能报告不能否决的检查,会逐渐被忽略;能被忽略的规则,等于不存在。

五、把发布流程也写成规格

持续集成真正琐碎的部分往往是业务规则,配置文件怎么写只是其中一小块。以下这些情况都要有明确答案:

一个课程网站可以这样约定:合并请求有新提交就重跑检查;合并前检查与最新主线的组合,失败则禁止合并; 主线更新后对这次提交跑检查并构建,全部成功才部署,失败保留上一版;多次发布重叠时,不允许过时产物覆盖新版本; 例外放行需要明确权限与记录。

还有一种更省事的分工:检查阻止、助手修正。检查负责给出明确结论与位置, 助手负责根据结论修改,再由同一套标准验证。这个循环能跑起来的前提是检查的输出足够具体: 只说「有一个错误」会让助手花时间猜;指出违规位置与允许的写法,下一轮才有明确方向。

必须守住的边界 负责实现功能的助手,不能因为过不了检查就单方面把标准调低。删掉失败的断言得到的通过, 与修好程序得到的通过,颜色一样、含义完全不同。规则确实可能过时,但修改规则要回到需求与设计理由。

六、三个真实项目的做法

下面三个案例来自公开的工程分享,各自解决一个具体问题。

把确定步骤写进流程,只把需要判断的交给模型

一家支付公司把工作流拆成两步:实现任务、修复失败这类需要理解与选择的步骤交给助手; 运行格式检查、跑测试这类每次都必须执行且方式固定的步骤,由普通程序保证执行。 他们还限制了检查失败后的重试轮数,不让一个任务无限循环。

这个案例给出一个直接的设计思路:每一步都必须执行、且执行方式已经明确的工序,就不要交给模型判断。 模型适合处理需要理解的问题,固定工序由程序可靠完成。重试也有成本,连续失败时要判断反馈是否带来了新信息。

改善任务的可分性,胜过增加人手

一个团队用十六个助手协作编译一个大型操作系统,结果大家撞上同一个错误,重复修复、互相覆盖。 后来他们改变做法:大部分文件用成熟的编译器编译,只让新编译器负责一部分,再通过切换文件的编译方式缩小失败范围。

关键不在用了多少个助手,而在于验证方式改变了任务能否拆分。 原来只知道「整个项目编译失败」,所有人只能涌向同一个入口;有了可信的参照与可缩小的失败集合, 大问题就能拆成可以分别推进的小问题。增加并行之前,先改善任务表示与反馈。

用行为替身把难以复现的场景变成可反复演练

另一个团队发现,助手可能用过于简单的实现应付狭窄的测试,也可能直接改测试去迁就实现。 他们的对策是把端到端用户故事组织成场景,放在代码库之外作为独立验收依据; 对包含助手的产品,还观察多次执行轨迹中是否满足用户意图,而不只看一次测试是否全绿。

他们还为外部服务建立行为替身,模拟接口与边界情况,让大量场景可以反复运行。 迁移到选课系统,可以设计「最后一个名额刚被别人占走」「请求已提交但响应丢失」这样的练习环境。 开发时不必等真实用户踩到这些问题,才知道失败长什么样。

替身与真实系统的差异也必须验证,否则只是造了一个特别容易过关的考场。

七、三个失败案例的教训

案例发生了什么教训
社交网站的主库加完整副本用户量增长后,增加读副本无法解除写入瓶颈,因为每个副本都要承担全部更新。有工程师提出按用户拆分数据,但方案没能推进。看出架构该怎样改,和组织真的能改掉,是两件事。及时重构需要技术判断,也需要组织能力。
网上买菜配送中心建成时,业务量不到原设计容量的四成,订单量也低于盈利所需水平。如果需求密度没有得到验证,庞大的配送设施会把一次判断错误变得极其昂贵。
移动端短视频服务上线数月后结束业务。管理层给出的解释是:这个想法不足以支撑独立的订阅服务,或推出时机不合适。有内容、有技术、有产品形态,不等于用户需要一个独立服务。实现便宜了,这个判断依然要做。

三个案例的共同点是:它们都不是「代码写错了」。前两个是架构与规模判断,第三个是需求与时机判断。 实现变便宜之后,这三类判断没有变便宜。

现在就做 给你的项目加一条有约束力的检查,四步:
① 选一条容易机械判断的规则(例如某个文件不超过多少行、不允许提交含密码的文件、提交说明必须符合格式)。
② 写成一个脚本,能返回成功或失败。
③ 把它设成合入前必须通过的条件(本地钩子或平台设置都可以)。
④ 故意违反一次,确认它真的拦住了你。第四步最重要,没试过就不算装上。

自测

1. 那个五行循环缺了哪三样东西就会变成浪费?
验收条件(什么样算完成)、预算(最多跑多少轮或花多少)、停止条件(连续几轮没有进展就停)。缺了它们,循环不会自己识别完成或无法改进,只是在持续消耗。
2. 「有检查」与「检查有约束力」的区别是什么?
前者只是执行检查并报告结果;后者让检查能否决不合格的变更,例如设为合并的必要条件、保护主线不允许跳过、检查与他人的改动组合。不能被否决的检查会逐渐被忽略。
3. 为什么说「先改善任务表示,再加人手」?
十六个助手撞同一个错误的案例说明:任务不可分时,增加并行只会增加重复劳动与互相覆盖。改善了验证方式,失败集合可以缩小、任务可以拆分,并行才有意义。
4. 助手改测试去迁就实现,为什么危险?怎么防?
因为测试从验收依据变成了它可以修改的对象,绿灯就不再代表功能正确。防范方式是把验收依据外置:端到端场景放在代码库之外、独立维护;关键测试的修改需要人工确认;观察多次执行轨迹而不只看一次通过。
5. 三个失败案例里,哪一个属于架构判断、哪一个属于需求密度判断?
社交网站的主库加副本、以及按用户拆分数据的方案推不动,属于架构与组织判断;网上买菜属于需求密度判断(配送设施容量远超实际订单)。移动端短视频属于需求与时机判断。