关于我
AI 与开发
harness 实践:AI 让编码变快,真正要交出去的却是验证

harness 实践:AI 让编码变快,真正要交出去的却是验证

AI 把编码提速了,但改完还得验证——一旦验证压在人身上,省下的时间又被抵消掉。本文以一个 300+ 包的 Rush monorepo 从 TypeScript 5.9 升到 7.0 为背景,讲怎么搭一套 harness,把「编码 + 验证」的整个闭环交给 AI 自主推进。

很多人谈 AI 提效,默认它就是"代码敲得更快":让 AI 帮你写函数、补类型、改配置,一分钟顶过去半小时的手工。这个印象只对了一半。代码敲得再快,改完还得验证,而验证这道工序一旦压回人身上,AI 省下的时间又被验证这头抵消掉。这篇文章记录的,就是笔者把一个 300+ 包的 Rush monorepo 从 TypeScript 5.9 升到 7.0 的过程里,怎么把验证这道工序也一并交给 AI。下文结合笔者的工程经验给出推荐步骤与 Prompt,不含完整的逐包迁移记录或耗时实测。

这里的 harness 指围绕 AI agent 组织的提示、工具、脚本、验证与工作流——它为长任务保存推进状态,并约束 agent 在什么条件下继续、修复或停止。

一、逐包核对可能成为迁移的瓶颈

TypeScript 7.0 采用 Go 重写的原生实现,微软在 TypeScript Native Port 中介绍了构建性能与内存占用方面的收益。对一个 300+ 包的 monorepo 来说,这些收益值得评估——但工具本身跑得快,和"把 300 个包全迁完"要付出多少,是两回事。

一次性切换所有包,出问题时排查范围就是整个仓库。为此我个人更倾向于渐进式迁移:每次改一个包,检查它及其下游的兼容性,再往下推。这样便于把失败关联到具体改动,代价是慢——改一个包 AI 几分钟就切完,但每轮改完都要反复跑检查、分析结果、处理回归,验证这头把整体拖住了。

于是问题的重心就挪了位:这场迁移真正卡住的是"验",不是"改"。 当 AI 缩短了配置与代码修改的时间,而每轮结果仍要工程师逐项核对,验证就成了新的瓶颈。以我的经验,很多团队迟迟不敢启动一些明摆着该做的升级,顾虑的也正是后续这段核对与回归处理的人工投入。自动化能减少这部分重复投入,但要说清楚:检查的执行时间、异常处理和规则维护仍得单独算账,不能据此就推断总体耗时会按同样的比例掉下来。

二、一次改动需要验证依赖它的下游包

在这个 monorepo 中,包之间通过仓库内依赖协同开发。相较于依赖独立发版来传递变更的多仓流程,仓库内的依赖图让我们可以直接追踪一个包的改动会影响哪些调用方。

这里需要区分两个范围:迁移清单是全仓需要迁移的包,用于记录整体进度;**受影响集(affected set)**是某次改动需要复验的包,按依赖图计算为"本次直接改动的包 ∪ 它们的所有传递下游"。迁移清单回答"哪些包要迁",受影响集回答"这次修改后哪些包要重新验证"。

例如,修改底层 @myorg/utils 的类型签名,就需要检查直接或间接引用它的下游包。下图仅用示意包名展示依赖传播,不对应真实逐包迁移记录;箭头从被依赖的包指向依赖它的包。

@myorg/utils(改动点) @myorg/core @myorg/http @myorg/ui @myorg/form @myorg/dashboard @myorg/admin-app @myorg/portal-app

图里的颜色按依赖层次区分:粉红是直接改动的 @myorg/utils,其余颜色标出沿依赖关系受到影响的包。直接改动的包只有一个,受影响集却包含图中所有包。

依赖图也有边界。共享配置、锁文件等改动可能影响未被包依赖边完整表达的范围,需要在验证规则中补充。再者,批量编译只能提供类型检查与编译结果,运行时行为仍需由相应的测试和人工验收检查。

这类迁移的特点很清楚:单个包的改法往往相似,可包有几百个,每改一个又牵连着一串下游要重新验证。改是局部的机械动作,验却要沿依赖边成百上千次地核对——所以既得安排好改的顺序,也得圈清每轮验的范围。

三、harness 把修改、检查与修复组织成闭环

按前两节的分析,迁移的效率卡点主要在"验证"这道工序上——改是局部动作,验却要沿依赖图反复核对,而且验证越是压在人身上,编码那头省下的时间就越会被抵消。而这道工序恰好适合交给 AI:检查跑挂之后,读日志、按报错定位原因、改代码、再重新验证,这一串机械动作过去只能人来,现在 AI 完全能在约定范围内自动跑下去,省的正是人反复分析和操作的次数。

要让这个循环连续跑起来,AI 得知道当前处理哪个包、检查结果如何决定下一步,以及失败后允许改什么。harness 据此撑起三根支柱:推进机制管理迁移清单与处理顺序;多维验证信号分别记录类型、规范、构建和测试结果;门禁自愈约束失败后的修复、重验与人工介入。这三支柱对应后面几节。

不过在展开这三根支柱之前,得先划清这套 harness 的定位,免得把它误解成"让 AI 常驻后台无限找事做"。它服务的是一项有明确目标、有终态、由人发起并监督的迁移任务——工程师圈定迁移范围、定下验收标准,AI 在这些约束内逐包推进,完成最终验收就结束。方向和边界始终攥在人手里,机器只负责把定好的事沿拓扑推完。

四、本次以包为单位,沿依赖拓扑推进

本次以 package(包) 为单位管理迁移状态,这不是随手定的粒度:这个仓库的 TypeScript 配置、构建入口、依赖关系,本就都是按包组织的——迁移状态记在包上,才能跟每轮的修改与检查结果一一对上。未处理的包继续用稳定版 tsc,已切换的包用 tsgo,渐进迁移期间两者都得接受兼容性检查。

PS:我在渐进收紧 lint 规则时也用过类似做法:先让存量违规保持 warn,清完一个包,再把这个包切成 error。按包记录状态,比较容易知道哪些已经处理、哪些还欠着——这是一条经验佐证,具体粒度仍要看仓库的配置和构建边界。

迁移清单中的状态需要区分三个阶段,不能在类型检查通过后就直接写成"已迁移":

状态含义与后续动作
完成修改已按确认的变更模式修改代码与配置,等待检查。
通过当前阶段检查本轮受影响集通过类型、lint、构建、单测及按约定触发的 E2E,可继续处理下一个包;这一状态仅对应当前代码版本。
完成最终验收迁移清单全部处理后,在待合入版本上通过整体复验与约定的人工确认,此时才统一标记为"已迁移"。

推进顺序来自迁移清单的依赖拓扑:被依赖的包先处理,依赖它的包后处理。这有助于减少上层包因底层后续切换而重复调整,但不能免除每轮对下游的复验。

受影响集则需要随改动重新计算。Rushrush list --impacted-by git:origin/main 可以选出相对 origin/main 发生改动的包及其传递下游(来源:Rush 项目子集选择文档)。这个命令得到的是相对所选基线的受影响集,不能用来代替全仓迁移清单;逐包检查时应记录本轮修改前的基线,最终验收时再以整次迁移的改动范围计算。依赖图未覆盖的共享配置影响也要补入验证范围。

下面的 Prompt 用于组织推进。执行前,需要先确认第六节列出的变更模式描述和验证清单;第五节给出对应的门禁流程。

我要把当前 monorepo 从 TypeScript 5.9 迁移到 7.0(@typescript/native-preview)。
请分两步进行:

第一步,先整理迁移清单并给出迁移顺序:
- 从全仓包列表中识别所有需要迁移的包,不要用某次改动的受影响集代替;
- 按依赖拓扑排序,被依赖的包排在依赖它的包前面;
- 输出包名、直接下游包与迁移状态,供我 review。

第二步,等我确认迁移清单、变更模式描述和验证清单后,按序一次处理一个包:
- 记录本轮修改前的基线,按确认的变更模式调整该包的配置与代码;
- 标记为"完成修改",重新计算本轮受影响集;
- 执行约定的门禁:类型、lint、构建、单测,以及按约定触发的 E2E;
  已切换的包用 tsgo 检查类型,未处理的包继续使用稳定版 tsc;
- 通过门禁后标记为"通过当前阶段检查",再处理下一个包;
  若失败,先按门禁流程修复和重验;需人工判断时停止推进;
- 迁移清单全部处理后,执行最终验收,通过后才统一标记为"已迁移"。

一次处理一个包,可以为每轮检查提供明确的改动范围,也便于把失败日志关联到最近的修改。

五、自验证按预先确认的标准决定是否继续

先把"自验证"这个词界定清楚,免得被它误导:它指的是 AI 执行工程师预先确认好的检查,再依据结果决定继续、修复还是停下,不是 AI 自己发明标准来判自己合格。验收标准由负责迁移的工程师会同相关模块负责人定,AI 至多帮着整理,绝不能为了让改动过关而自行放宽。

我一直坚持的原则是:分别保留多维度的检查结果,便于分析失败原因。类型检查报告类型错误、lint 报告规则违规、build 报告构建失败、单测报告已有用例中的断言失败,每一类提供不同的定位线索。例如,类型与构建检查通过、单测失败时,可以先查看失败用例及其上下文。信号数量不等于覆盖充分,检查通过也不构成改动正确的保证。 多个检查可能重复覆盖同一类问题,也可能共同遗漏某种运行时行为。

E2E 通过启动应用并执行用户旅程,补充运行时与集成层面的检查,但它只能检查已有用例覆盖到的路径及断言。未覆盖的运行环境、用户路径和业务语义,需要在验证清单中明确记录;关键行为缺少用例、结果含义不明确或需要调整预期时,仍由工程师补充验证或人工判断。

与第四节的状态对应,推荐把门禁分成逐包检查和最终验收。逐包检查覆盖本轮受影响集,运行类型、lint、构建与单测;涉及模块解析、emit 或导出契约变更,或触及验证清单指定的用户路径时,当轮必须运行相关 E2E。缺少这些用例时转人工确认,不能把未执行记成通过。

最终验收在待合入版本上,对迁移清单及整次迁移的受影响集执行约定的全部检查与全部 E2E,并通过验证清单要求的人工确认。如果缓存条件完整(检查输入、工具版本都没变),之前的检查结果仍然有效,可以直接复用;但复用不能缩水:只重跑直接改动的包是不够的,最终验收要求的是对整个受影响集复验。

检查失败后,门禁允许 AI 根据日志做最小范围修复,再重新计算受影响集、运行该阶段的全部检查,包括修复前已通过的项目。断路器负责限制连续修复次数,达到阈值仍失败就交给人工。该规则用于发现修复引入的回归,不能保证每次修复都只产生正向效果。

把推进和验证合起来,就是这套 harness 的运转循环:

全部通过 没有 全部通过 检查失败 检查失败 逐包检查 最终复验 缺少必需用例或需解释标准 缺少必需用例或需解释标准 已确认迁移清单与验收标准 按拓扑序取下一个包 AI 修改该包标记为完成修改 本轮受影响集检查类型 / lint / build / test解析、emit、导出或指定路径变更触发 E2E 标记为通过当前阶段检查 迁移清单还有待处理包? 待合入版本最终复验迁移清单及整次迁移受影响集全部检查与约定的全部 E2E 约定的人工确认是否通过? 完成最终验收统一标记为已迁移 交人工判断 连续修复已达 3 次? 按日志定点修复重新计算受影响集 返回失败所在阶段

下面的 Prompt 将同一套门禁写成执行约束,其中连续修复 3 次是示例阈值,实际使用时由工程师确认:

使用我已确认的验证清单,按以下门禁处理当前迁移,不要跳步或自行降低标准:

1. 每个包完成修改后,计算本轮受影响集,补入共享配置等额外影响范围。
   在这个范围内依次执行类型、lint、构建、单测,分别记录命令、结果和代码版本。
   已切换的包用 tsgo 检查类型,未处理的包继续使用稳定版 tsc。
2. 涉及模块解析、emit、导出契约,或验证清单指定的用户路径变更时,
   当轮运行相关 E2E。缺少必需用例或需要解释验收标准时,停止推进,交给我判断;
   不要把未执行的检查记成通过。
3. 当前阶段的检查全部通过后,标记为"通过当前阶段检查",
   按拓扑序修改下一个包,再回到第 1 步。此时不要标记为"已迁移"。
4. 迁移清单全部处理后,在待合入版本上对迁移清单及整次迁移的受影响集,
   执行验证清单约定的全部检查与全部 E2E,再提交约定的人工确认项。
   全部检查与人工确认通过后记录"完成最终验收",统一标记为"已迁移"。
5. 逐包检查或最终复验失败时,依据具体结果和日志定位原因,只做相关的最小修复。
   修复后重新计算受影响集,重跑失败所在阶段的全部检查,包括此前已通过的项目。
   不要擅自修改测试预期或关闭规则。发现标准需要调整时,交给我判断。
6. 对每个包分别计数;进入最终复验阶段后单独计数。任一阶段连续修复 3 次仍未通过,
   停止自动修复,输出失败项目、报错原文和已尝试的修复,交给我判断。

六、前期建设需要靠后续复用收回投入

这套流程有前期成本:得先花人工写变更模式描述验证清单,再把执行检查、记录结果、恢复任务的工具接起来。变更模式描述规定哪些改法可以重复套用、哪些情况得单独处理;验证清单把验收标准落到具体的命令、范围、E2E 触发条件和人工确认项。这些准备都得赶在第四节的批量推进开跑之前铺好。

说到底,验证的代价并没有消失,只是从"事后逐个人肉核对"搬到了"事前建模式与清单"。后续省下的,是逐包核对那部分反复的人工;可构建和测试照样要跑,AI 的推理和重试照样烧资源,异常处理与规则维护还会持续占着工程师的时间。所以评估这笔账时,得把三样分开记:人工投入、检查与推理的执行成本、以及被等待和重试拖出来的总体耗时。

只有迁移规模足够大、规则能够反复复用,累计节省的人工投入才有机会覆盖前期建设与后续维护。 包的数量本身不足以判断是否划算:若每个包都需要新增规则或人工处理异常,复用收益就会减弱;若只有少量包需要迁移,直接运行已有检查并人工核对可能更合适。本文没有实测数据,因而不估算回本所需的包数或收益比例。

前期准备也可以先由 AI 起草,再由工程师确认:

在开始批量迁移之前,先不要改任何代码。请基于当前项目产出两份文档供我 review:

1. 变更模式描述:列出 TS7 迁移要调整的配置与代码、适用条件和已知例外,
   根据各包的构建与运行方式说明配置选择,不要默认所有包使用同一套配置。
   标出需要人工决定的行为变化。
2. 验证清单:列出逐包检查与最终验收各自的命令、范围和通过条件,
   包括 tsgo/tsc 的适用范围、lint、构建、单测、E2E 与人工确认项。
   写明受影响集的计算基线、共享配置等额外影响范围;
   模块解析、emit、导出契约或指定用户路径变更,当轮必须触发相关 E2E;
   最终验收运行约定的全部检查与全部 E2E。
   列出未覆盖的行为、缺少的用例、需要谁确认,以及自动修复的停止条件。

我会与相关模块负责人确认这两份文档,再让你整理迁移清单并逐包推进。

PS:若把逐包串行扩展为多任务并行,还需要隔离工作目录,协调公共配置与锁文件的写入,并保持依赖顺序。分支落后于主干时可以考虑自动 rebase,但发生冲突仍需停止并确认处理方式,rebase 后也要重算受影响集、重新验证。这些属于并行扩展的额外成本。

七、自动推进的程度取决于规则与验收依据

同一份迁移清单里,不是每个包都该放手让 AI 连续跑。给一个包定自动化的档位,我个人只看两件事:变更模式能不能讲清楚、能不能重复套用;以及已有检查能不能替这次改动的主要风险扛住验收。这两条都硬,就放开;缺一条,就往回收。

规则明确、主要风险有可靠检查覆盖的部分,可以连续自动推进,直到约定的人工确认点。若代码改法明确、但某些行为还没被检查覆盖,就让 AI 把改动和已有检查做完,继续之前先交人工确认——比如模块职责调整,类型和测试能验一部分兼容性,可职责划分合不合理,终究得负责人拍板。

至于那种目标只写了句"更优雅"、各包的改法和验收依据都还没落定的,当前阶段就别硬推,让 AI 辅助分析更合适——梳理依赖、归纳问题、给候选方案,等人把标准定下来再谈动手。这时强行连续推进,只会放大偏离预期和返工的风险;但这不妨碍把其中规则已经明确的局部,单独切出来交给自动化。

八、其他批量改造可以复用流程,验收标准需要重定

这套逐包推进、检查、修复的流程,并不专属于 TS7 迁移——规范统一、依赖升级、批量代码清理,同样能套。但复用时有个前提不能省:变更模式和验收标准得按新场景重新定,不能把"类型和测试过了就行"原样搬过去。

编码规范与契约统一往往有较明确的修改规则,但验收范围并不相同。收紧一条 lint 规则,可以用该规则检查违规是否清除;给所有包补 exports 字段、统一 tsconfig 或调整公共接口类型,还要检查调用方与运行时兼容性。

批量依赖与框架升级可以沿依赖拓扑安排,例如 React 大版本升级、Node 版本升级带来的 API 迁移、基础库的不兼容变更。已有迁移规则可以复用,行为变化则需要对应的测试和必要的人工确认。

批量清理代码问题需要更细地划分规则适用范围。消除 any、统一错误处理、清理废弃 API 调用,都可能涉及业务语义。同样是收窄类型,目标类型也要由具体场景决定,不能只以类型检查通过作为验收依据。

九、总结

这次迁移的经验,收成几条:

  1. AI 让编码变快之后,真正卡住迁移的是验证——要优化的是"编码 + 验证"这整个闭环,而不只是编码那一端。
  2. 让人退出验证的办法,是把验证也交给 AI 沿依赖拓扑逐包推进、失败即定点修。但**"检查通过"不等于"改动正确"**,验收标准得由工程师预先定好,AI 只负责执行,不能自行放宽。
  3. 这套做法不消灭验证的代价,只是把它从"事后逐个人肉核对"前移到"事前建模式与清单"。要靠后续足够多、改法能反复复用的包才收得回来,包太少就未必划算。
  4. 同一套流程能复用到规范统一、依赖升级、批量清理等批量攻坚,但验收标准必须按新场景重定,不能照搬"类型和测试过了就行"。

回到最初的问题:真要落地,我个人建议先挑迁移清单里的一部分包试行这套流程,记录人工介入、检查执行与异常处理,再决定要不要扩大范围——这套 harness 在你的仓库里到底值不值得投入,试一小段就有答案。

On this page