在灯火通明下打造你的软件工厂
暗工厂将一个请求转化为已合并的代码,并在过程中自主做出所有决策。亮灯工厂则在每项操作之前实施治理。每一项变更都有明确的负责人,在各个检查点接受质询,审计日志将每项决策与该负责人关联起来。
我搭建了一个这样的系统,目前已投入生产使用一周,因此尚无确切的数据。它刻意运行得较慢,因为将判断力、理解力和责任担当置于原始速度之上。
监督力度是一个可调节的旋钮,因此一旦某个环节经验证可靠,便可转为“暗”模式,但仍会记录在案。
暗工厂的理由
这一术语源自制造业。熄灯工厂在车间内无人值守的情况下运行,因此无需开灯。软件版本的暗工厂接收工单并输出已合并的代码,其间无人审批任何内容。
其优势显而易见:自动化代理编写代码的速度远超人类阅读的速度,而人工介入则决定了整条流水线的节奏。如果测试充分且影响范围有限,人工检查点只会增加等待时间,却几乎无法发现任何问题。
有些工作确实适合在“暗”中完成:依赖项升级、格式化扫描,以及对上百个文件统一应用的 lint 规则。
然而,一旦所有流程都以这种方式运行,就会出现两个问题。
第一个是理解债务:生产环境中存在团队无人理解的代码。每一次“暗”合并都会让这种债务累积一点。变更上线当天看似一切正常,因此债务始终隐而不现,直到发生事故时才会暴露。
这时,值班人员第一次打开某个模块,却发现无人曾阅读过它。你只能在最糟糕的时刻偿还这笔“利息”。
第二个问题是责任归属。优秀的暗工厂会记录一切,而且理应如此。两种模式都需要同样的可观测性。但暗工厂的日志仅记录工厂的决策,而不会标注具体执行人:选择何种方案、什么才算完成、失败的测试是否属于偶发问题等。
当其中一项决策出错时,往往是由客户反馈才发现,而日志中并未指明责任人。
亮灯带来的价值
我的工厂接收书面需求,并返回带有实时预览的拉取请求。其间依次经历以下阶段:需求分析、方案制定、验收测试、实现、各项检查、评审、端到端验证,以及业务层面的简报。
各阶段的工作均由自动化代理完成。亮灯意味着工厂会在八个环节暂停,向人工发出询问:
- 需求分析阶段提出的问题,在任何计划制定之前
- 方案本身的审批
- 验收测试的选择——从方案建议的选项中挑选
- 被拒绝的调用——当代理试图执行的操作被防护机制拦截时
- 重构、评审和 UAT(用户验收测试)
- 合并
每个检查点都有明确的负责人。目前,由需求提出者回答问题、审批方案并选定验收测试。UAT 的规则最为严格:只有提出变更的人才能签字确认。
工厂会拒绝其他任何人,因为验收权归需求方所有。
这种责任归属也决定了系统的扩展方式。每位开发人员只需审批自己负责的变更方案,因此审批量随分支数量增长,而不会集中到某个人身上。
主管仅需审批针对框架及工厂本身的变更,其中部分职责归属于平台团队。
责任归属还影响变更日后的可读性。当我打开一条旧的拉取请求时,看到的并非凭空出现的差异;而是谁提出了这项需求、他们批准了怎样的方案、选择了哪些测试,以及他们为何否决了评审意见及其原因。
这些信息来自审计日志,而审计日志正是区分两种工厂的关键。两者都应记录每一步操作。但在亮灯工厂中,日志还会将每项决策与具体负责人绑定,因此每一个答复和每一次被拒绝的工具调用都会标注执行者。
日志采用追加式的 JSON 格式,且不可修改,包括工厂自身也无法改写。六个月后,当你需要回答“为什么这个接口返回 409”时,只需查证是谁批准了该变更即可。
分支保护、代码所有者和必需评审者机制在一定程度上也能实现类似效果。但相关信息分散于多个工具中,难以完整串联。一旦代理成为提交者,这些工具便将变更与代理而非具体人关联起来。
而工厂的审计日志首先是为审计人员设计的,同时也能作为证据,用于收紧约束或扩大代理的权限范围。
亮灯模式的另一大价值在于培养阅读习惯。审批一份方案只需几分钟,在这短短时间内就能预先了解变更将触及的范围。随后在最后阶段再阅读一次简报。
久而久之,团队成员便能熟悉工厂所生成的代码,从而有效防范理解债务。
审批疲劳确实存在,检查点也可能沦为简单的点击操作。我们的设计通过两种方式应对:工厂提出的每个问题都是产品级别的——这是正确的方案吗?
这些是合适的验收测试吗?变更在预览中是否正常工作?这些问题都不需要人工逐行审查差异。此外,当某个检查点逐渐失去应有的关注时,可以调整其监督策略,而不是假装仍有人在认真阅读。
监督力度可调节
亮灯模式下,每项操作都受到管控,但并非每个检查点都需要永久由人工响应。每个检查点有三种设置:
- “询问”模式会暂停并等待人工回复。
- “保留”模式会等待一段固定时间,若无人及时回复,则自动采用默认答案。
- “自动”模式则立即采用默认答案。
我们预设了四种配置,可一次性为全部八个检查点设定相应模式;同时也可以针对特定仓库或所有仓库,单独覆盖任意检查点的设置。
# Oversight: how much a person is asked, checkpoint by checkpoint.
# high everything asks
# medium answers, checks, refactor and review hold; plan, refused calls, UAT and merge ask
# low most go to auto; the plan, refused calls, UAT and the merge hold
# off everything auto, except the merge, which never goes below hold
preset: high
checkpoints: {}
# questions: {level: hold, window_minutes: 30}
# merge: {level: hold, window_minutes: 240}
拨盘控制的是由谁来响应,而不是门是否保持开启。测试、护栏、质量门以及持续集成会在每个层级上运行。即使设置为关闭,流水线仍然会执行所有检查,并且不再询问检查结果。
某些检查点还设有下限。合并永远不会低于“保持”状态,因此即便在关闭状态下,也会在代码合入主分支之前通知相关人员,并留出一段“保持”窗口期以阻止其合入。
你可以让整个流水线几乎处于“黑暗”模式,但绝不可能在不告知任何人的情况下让代码落地。
黑暗并不意味着静默
当某个检查点切换到自动模式时,决策依然会发生,并且会记录在审计日志中。发生变化的是决策的归属。日志会将其记为监督设置所作出的决定,操作者为监督角色;它绝不会被记录为某个人的决定,因此事后没有人会将自动批准误认为是人工批准。
从严格意义上讲,该检查点已经进入“黑暗”状态:它自主做出决策,日志也如实记录。你可以对其进行审计,并将其重新调回开启状态。
关于该设置的归属问题,我尚未最终确定。目前,我将每一步的监督视为衡量该步骤受信任程度的指标,并且可以逐级进行配置。而贯穿整个流水线的监督设置,则更像是一个平台层面的决策。
什么条件能让检查点进入“黑暗”状态
那么,何时应该将其调低呢?当证据表明该检查点已不再捕获任何问题时。我的流水线会统计逃逸缺陷,即那些绕过了所有检查的缺陷。
逃逸可能表现为用户验收测试失败、流水线对工厂提交的构建失败、流水线对合并提交的构建失败、合并被回滚,或者某个缺陷可追溯至某次变更。
每一次逃逸都会记录在对应的任务和审计日志中,并成为增加新检查的依据。
流水线还会逐个排查,确认评审是否发现了自动化门未覆盖的问题。如果评审始终只发现已被自动化门捕获的内容,那么评审就是多余的,该检查点就可以转入“保持”状态。
在降低任何检查的强度之前,请先确定可接受的逃逸率。之后,就由数据说话,而不是由你在繁忙周中的耐心来决定。
这项度量方式仍存在一个盲区,我仍在完善中。一旦某个检查点进入“黑暗”状态,你就再也无法看到它本应捕获的内容,只能在缺陷上线后才知晓它的存在。
我目前采用高频率运行的策略,每个检查点都会参与判断。这是一项选择,而非需要熬过的一个阶段。我负责的产品变化迅速,本月流水线触及的代码,很可能自上月以来就无人审阅。
知识欠债在那里积累得最快,而检查点正是团队跟上节奏的方式。无论如何,我都在收集逃逸缺陷的数据。等到某个检查点确实不再捕获任何问题的那一天,相关证据早已存在。
你的流程即配置
检查点和拨盘并不依赖于我的特定流程。凡是描述团队工作方式的内容,都应纳入声明式配置之中。这包括请求的类型、每种类型所遵循的工作流,以及每个工作流中的阶段。
同时也包括每个检查点的归属、哪些门会被触发,以及每个拨盘的最大旋转范围。流水线会读取并执行该配置文件。流程存在于配置中,而代码仅知道如何执行既定流程。
我使用 YAML 格式,尽管这种格式未来可能会改变,但其结构不可动摇:一个人只需阅读一次各阶段,就能了解整个流程。
我的实践尚未完全达到这一目标。目前,每个阶段的设置都以 YAML 形式存储:其模型、旋转上限,以及不得触碰的工具。每个代码库的门控和监督拨盘也是如此。
此外,每个请求还标明其类型(功能、缺陷,或对现有功能的变更)。不过,所有类型的请求均按代码中设定的顺序执行相同的阶段。下一步,我计划将每种类型拆分为各自的声明式工作流,届时整个流水线的配置将集中在一个文件中,大致如下:
workflows:
feature:
stages: [discover, plan, define, implement, checks, review, prove, comprehend, uat, merge]
bug:
# The acceptance test is already known: the reported symptom.
stages: [reproduce, plan, implement, checks, review, prove, uat, merge]
change:
stages: [discover, plan, implement, checks, review, prove, uat, merge]
stages:
plan:
model: claude-opus-5–5
max_turns: 40
tools: read-only
checkpoint: {owner: requester}
reproduce:
model: claude-sonnet-5–5
max_turns: 30
# Done when a test for the symptom exists and fails.
prove:
model: claude-sonnet-5–5
max_turns: 50
tools: read-only
uat:
manual: true
where: preview
checkpoint: {owner: requester}
gates:
- name: tests
run: make test
oversight:
preset: high
一项新功能和一个缺陷修复需要不同的流程。新功能始于需求发现阶段,因为此时尚不清楚变更会触及哪些部分;在开始开发之前,还需要先定义验收测试。
而缺陷则是在报告中已附带验收测试的情况下出现的:针对原始症状的测试在修复前应失败,修复后应通过。因此,缺陷的工作流从重现缺陷开始,跳过了定义测试的环节。
如果无法重现缺陷,则暂时无法修复,需退回至需求发现阶段。若将两者混用同一条流水线,难免顾此失彼:要么让缺陷为本不需要的需求发现付费,要么让新功能省略了应有的工作。
在更稳健的模型下,计划阶段以只读模式运行,因为错误的计划会产生最大的连锁反应。验证阶段同样以只读模式进行,其机制在于禁用编辑功能。
负责检查的阶段无法修复其所检查的内容,因此也就无从争辩。
门控机制直接调用仓库自身的命令。门控执行“make test”命令,且不关心其后的具体内容,因此即使某个仓库使用了不同的技术栈,也只需修改一行配置即可,无需其他改动。
手动步骤也是阶段
并非每个阶段都需要自动化代理。在我的流水线中,用户验收测试(UAT)由专人负责,并在合并前执行。每项变更都会部署到独立的临时环境,拉取请求中会附带该环境的预览链接。
验证阶段会记录其执行的步骤,请求方打开预览、按步骤操作,并签字确认或退回待改进。
在流水线视角下,这仍是一个带有“manual: true”标记的阶段,拥有负责人,并与其他阶段一样设有检查点。它会等待,并在审计日志中记录响应者;同时,它也强制执行该阶段所代表的规则:只有请求方才能签署通过UAT;若验证未通过,无论设置如何,UAT都无法自动通过。
目前尚未处理请求方不在或已离职的情况,暂由管理员代为管理。
团队中任何手工完成的环节均可采用相同的模式。安全评审、设计签核、合规性检查、产品经理对文案的审阅——每一项都是一个有负责人的阶段。
声明这一阶段只需在配置文件中添加一段代码;若将其遗漏,则这些环节将在旁路执行,审计日志也无法记录。
工具应适配团队
大多数工具都内置了一套流程,团队往往被迫迁就。一旦遇到与现有工作方式不符的环节,采纳便会受阻,人们就会绕过工具自行处理。
基于配置构建的流水线则相反:核心组件在各处保持一致——阶段执行器、门控、审计日志、控制旋钮。真正体现团队特色的,是配置内容:哪些阶段运行、每个检查点由谁负责、每个旋钮的调节范围、以及必须启用哪些门控。
两支团队即便使用同一套流水线,也能执行截然不同的流程,且都能获得相同的审计轨迹。
在这份自由之上,有一条关键的约束:操作者是实际运行流水线的人。仓库可以要求比操作者所需更多的检查,但不能关闭自身必须遵守的检查。
若允许仓库自行禁用门控,便等同于自己批改自己的作业。
刻意放慢节奏
亮灯模式的流水线比暗灯模式的运行得更慢,因为它是同步的。计划需要等待审批,UAT需要等待请求方响应。每次等待的时间取决于人员到达所需时长,而人手又常有会议安排。
我尚未精确测量过具体差异,但每增加一个需人工确认的检查点,都会为整个流程额外增加一个人的响应时间。
那么,为何要接受这种等待?我们交付的是价值和成果,这一点从未改变。单纯的速率提升只有在产出的代码确实有价值时才有意义,而判断这一点的正是那些检查点。
决策权始终保留在最需要的地方:范围界定、完成标准的定义,以及某项变更是否值得实施。理解力也始终掌握在团队手中,因为有人阅读过计划和简报。
责任也有明确的归属:请求的负责人必须对结果负责。
慢即是稳,稳即是快。暗灯模式虽然能在前期节省时间,却可能在后期付出代价——比如回滚无人理解的变更,或在事故现场首次研读某个模块。
与其如此,不如在前期以小额且可预期的成本承担这些风险。甚至有人认为暗灯模式有害,我怀疑这正是因为它会累积技术债务。因此,亮灯模式旨在及早应对债务,而非事后埋单。
您可以从将流程拆解为各个阶段开始。先选定一类请求——缺陷修复或新功能——并列出变更当前经历的每一个步骤,包括手工环节。为每一步指定负责人,并设定当该负责人未响应时的默认处理方式。
这份清单即为您的首个工作流。将其写成 YAML 格式,然后对另一类请求重复相同的操作。运行时,将所有检查点均设为需人工确认;仅当逃逸指标显示某检查点已不再有效时,方可将其关闭。