Skip to content

第 12 章 质量门禁

第12章隐喻:一条通道上几道检查关口,逐道把关,没有谁能不经检验就通过

CI、测试、监控闭环——这些东西不性感,但"能跑的 demo"和"可靠的生产系统"之间,差的恰恰就是它们。这一章讲我以前怎么忽略它们,后来又怎么补上、补的时候修正了哪些认知。

开篇:一个尴尬的事实

先说一个让我有点不好意思的事实。

我做了好几个 Agent 项目,代码量加起来好几万行。但在很长一段时间里,没有一个是配了 CI 的。

没有自动跑测试,没有 lint 强制检查,没有提交时自动验证。测试要靠人肉想起来才跑一次,代码风格靠自觉。我知道这不对,但就是这么过来的。

为什么会这样?因为在做 Agent 项目的过程中,兴奋点永远在"让 Agent 做新事情"上。每次有了新想法,我打开编辑器就开始写,写完在本地跑一遍——"诶,能用了"——就提交了。配 CI?下次再说吧。加测试?先让功能跑起来再说。

这个"下次再说"一拖就是好几个项目。回头看,这是 Agent 开发中最容易掉进去的坑——新功能的正反馈太即时了(跑一遍就能看到效果),而质量基础设施的正反馈太延迟了(不出事你感觉不到它的价值)。

这一章,就是讲我后来终于把这些补上、补的过程中又踩了哪些坑、修正了哪些想当然的认知。

第一关:配 CI——以及它给我上的几课

终于下定决心配 CI,是在一个项目里。我打算用最务实的方式:先跑一遍 lint 和测试,看现状如何。

结果第一次跑 lint,炸了。四百多个错误。 四百多个。

我当时有点懵——这项目我一直在用,功能也跑得好好的,怎么 lint 一跑出来这么多问题?因为我从来没真正跑过 lint。工具一直在配置文件里声明着,但从没在环境里装过、从没跑过。这就是"配了不跑"的真实代价:你以为你有 lint,其实你没有。

第一课:CI 的真实成本,不只是"配半天"

清这四百多个错误,花了我远超半天的时间。自动能修的(比如没用的 import、格式问题)让工具自动修,修了一百多处;剩下得人工判断的一个个改。

但真正值钱的,是清债过程中翻出来的东西。其中有几个错误是"引用了未定义的变量"——这种错误在普通代码里早就崩了,但这几个偏偏藏在一个 except Exception: pass 里。什么意思呢?代码本来想用一个变量,结果那个变量没定义,抛了 NameError,然后被宽泛的 except 一口吞掉,程序静默地继续跑,什么错都没报

这意味着一个功能——一个我以为是好的功能——其实从来没有真正工作过。它在每次调用时都安静地失败,因为异常被吞了,没人知道。我和任何 reviewer 都看不到这个问题,因为它不报错、不崩溃、只是默默不工作。只有静态分析能把它揪出来。

这件事给我上了一课,让我修正了一个想当然的认知。我以前以为 CI 最大的价值是"挡住有问题的提交"——这没错,但我低估了它另一个价值:它是唯一不带"我对这段代码的记忆和偏好"的客观反馈源。 人看自己的代码会带着盲区,reviewer(如果你有的话)也会累、会漏,但 lint 不会。它冷冰冰地把每一个违规都摆出来,包括那些被异常吞掉、肉眼永远看不见的静默 bug。

一个反复出现的模式

配 CI 这事,我也撞见了"统一原则 vs 分层落地"那个老问题——下面会讲,单人项目的 CI 和团队项目的 CI,最小可行形态根本不是一回事。

第二课:单人项目的 CI,最小可行形态是什么

我配 CI 之前,脑子里默认的 CI 长这样:代码 push 到远程仓库,触发一个云端流水线,跑测试、跑 lint,结果挂在网页上,红了就阻止合并。这是 GitHub Actions、GitLab CI 那一套——团队协作的标准配置。

但我这个项目是单人主干开发——没有 PR、没有 code review、直接推主干。团队 CI 的核心价值(挡住别人的代码、互相 review)对我来说根本不存在。那我的 CI 该长什么样?

我试了两种形态,对比之后有了结论:

  • 云端流水线(push 后触发):反馈是延迟的——你 push 完,过一会儿才看到结果。等你看到红的的时候,你可能已经在写下一段代码了。
  • 本地 pre-commit(commit 前触发):反馈是即时的——你想提交,它先拦住跑一遍,过了才让你提交。不过,提交都提交不上去。

我专门做了个行为测试验证后者:故意写一个必然失败的测试,git add 之后 git commit——结果被 pre-commit 拦住了,提交没进历史,零污染。这一刻我意识到,对单人项目来说,pre-commit 才是最小可行形态,而不是云端流水线。

不是说云端流水线没用——它是个"额外的客观环境"(干净的 CI 机器,没有你本地的各种配置),能抓出"在我电脑上能跑"这类问题。但它的核心价值(延迟的、远端的第二双眼睛)对单人项目是锦上添花,不是雪中送炭。真正能改变你开发习惯的,是那个在你指尖、commit 前就拦住你的 pre-commit。

这里还有一个意外收获。我把代码 push 到云端流水线后,原本预期它会红——因为 CI 机器上没有我本地的配置文件和密钥,我以为代码会因为这个隐式依赖而跑挂。结果它绿了。这反过来证明了一件事:我的代码并没有隐式依赖本地环境,它在没有配置时也有合理的默认行为。 CI 不只帮我抓 bug,还帮我证伪了一个悲观的假设,给了我"这代码比我想的更干净"的信心。

第二关:测试覆盖的失衡

配完 CI,下一个问题是:跑哪些测试?

这一关我没踩大坑,但它暴露了一个一直存在的失衡:我的测试精力,全花在了"好测"的部分上。

Agent 项目里,有些东西是好测的——核心算法、数据处理、各种边界和精度,用固定随机种子一钉,确定性测试写得又快又有成就感。但那些"难测"的部分——Agent 的行为、LLM 的输出质量、整条调用链——我基本没测。

为什么失衡?因为好测的部分有即时正反馈(写一个、过一个、爽一下),难测的部分连"怎么测"都要想半天(第三部分讲 eval 才解决了这个问题)。人自然会往有成就感的地方倾斜。

但没测的那一层,恰恰是用户直接打交道、最容易出问题的一层。你把最难测的东西留给了线上用户去测——这不是好策略。 第三部分(第 6、7 章)讲的 eval 方法论,本质上就是在补这一层:让"概率性的、难测的 Agent 行为"也变得可测。配 CI 之后,这套 eval 也应该接进流水线,每次提交自动跑——不只是跑确定性测试,也跑 eval 回归基线。

第三关:空壳闭环——以及一个比"空壳"更隐蔽的问题

第三关是个特别坑的问题:代码写了,但从来没被调用过。

我以前踩过这种坑:写了一个告警推送的功能,代码完整,本地调试也过了,但它从来没有被接进定时任务。也就是说,告警能检测到问题,但永远不会推送出来,因为没人触发它。还写过一个记录决策的函数,功能齐全,但业务代码从来没调用过它——表是空的,功能是空壳。

这种问题之所以坑,是因为它不报错。代码在那里,import 也正常,lint 也不报,测试(如果你写了的话)也是绿的——因为它自己内部是自洽的。唯一的"错"是它没被接进主干流程,而没有任何工具会主动告诉你这件事。

我以前发现这种问题,全靠偶然——哪天突然想起来"诶那个功能好像没接",去查一下,果然没接。靠偶然发现,显然不可靠。所以这次我试着提炼一套系统识别法

第一步:grep 调用点,抓"完全空壳"

最直接的方法——对每个功能,grep 一下它在整个项目里被调用的地方。如果一个功能 grep 出来的调用点是 0,那它就是"完全空壳",铁板钉钉。

这一步很有效,但也让我发现一个意外:我用这个方法查了一圈,一个完全空壳都没找到。

第二步:列举"该用的所有路径",抓"部分接通"

完全空壳没有,但问题真的不存在吗?我把每个功能"语义上应该在哪些路径出现"逐一列出来核实。这一查,查出来两个问题——它们不是"完全没人用",而是**"主路径用了,边缘路径漏了"**:

  • 一个功能在定时任务里用了,但在 Web 接口那条路径里没接——所以你从网页上操作,它不生效。
  • 另一个功能在后台生成报告时走了,但在前端查询报告时没走——所以同一份数据,两条路径行为不一致。

这种"部分接通"比"完全空壳"隐蔽得多。完全空壳,grep 一下调用点为 0 就抓到了;部分接通,调用点是大于 0 的,grep 根本分辨不出"这个调用点该有却没有"。它需要人去判断"这个功能语义上还该在哪些路径出现"——这一步 grep 替代不了。

这让我修正了对"空壳"的理解。我以前以为空壳是个二元的概念——"写了没接通"就是空壳。实际上它是个光谱:

  • 完全空壳:一个调用点都没有。grep 能抓,好发现。
  • 部分接通:主路径用了,边缘路径漏了。grep 抓不到,要靠人列举路径逐一核实,难发现。
  • 完全接通:该用的路径都用了。

真实项目里,最危险的不是完全空壳(它太明显,迟早被发现),而是那个"部分接通"——它在大部分时候工作正常,让你以为没问题,唯独在某个你没覆盖到的边缘场景里悄悄失效。

所以系统识别法是两步,缺一不可:

  1. grep 总调用点——抓完全空壳(容易)。
  2. 列举"该功能语义上该出现的所有路径",逐一核实——抓部分接通(难,要人判断)。
「空壳」不是二元的,是个光谱
「空壳」不是二元的,是个光谱 最危险的不是完全空壳(它太明显),而是中间那个「主路径用了、边缘路径漏了」的部分接通 调用点 = 0 该用的路径都用上 接通程度 → 完全空壳 一个调用点都没有 grep 能抓 ✓ 好发现,太明显 迟早被发现 部分接通 ⚠ 主路径用了,边缘路径漏了 grep 抓不到 ✗ 调用点 > 0,grep 分辨不出 「该有却没有」 最危险 · 隐蔽失效 完全接通 该用的路径都用了 目标状态 ✓ 功能真正在工作 不报错 = 自洽 系统识别法:两步缺一不可 第一步:grep 总调用点 抓完全空壳(容易,调用点=0) 第二步:列举「语义上该出现的路径」逐一核实 抓部分接通(难,要人判断)
图 12-1 | 光谱三段:完全空壳(grep 抓得到,好发现)→ 部分接通(grep 抓不到,最危险,靠人列举路径逐一核实)→ 完全接通。两步识别法缺一不可

该补什么,什么优先级

讲了这么多,如果现在要给一个 Agent 项目补质量基础设施,我会按这个顺序:

P0:本地 pre-commit(CI 的最小可行形态)。 对单人项目,这是性价比最高的一步。commit 前自动跑测试和 lint,把"想到才手动跑"变成"绕不过的自动跑"。半天配好(前提是项目没有历史债务),立刻改变后续每一次提交的安全感。

P1:接通空壳闭环。 用上面那两步识别法,把"写了但没接通"的功能揪出来接上。这一步往往不写新代码,只是把已有功能串进主干流程。

P2:补难测那一层的测试。 确定性部分早测了,现在该补 Agent 行为层——用 eval(第三部分讲的方法)。接进 pre-commit 或 CI,每次提交自动跑回归。

P3:云端 CI(可选)。 如果你需要"干净环境"的第二双眼睛,或者以后会有协作,再上云端流水线。单人不一定需要。

这个优先级的逻辑是:先解决"不知道改坏了什么"(pre-commit),再解决"写了没用"(闭环),再解决"漏测"(eval),最后才是"额外的客观环境"(云端 CI)。

一个更深的问题:为什么总是排在后面?

诚实地说,上面这些我都知道该做,但很长一段时间没做。为什么?

不是不知道怎么做,也不是做不了。是Agent 开发的节奏天然排斥防御性工程。

你用 Claude Code 写代码,让它配一个 CI——它可以配,但配完之后你没有"跑一遍看看"的即时快感。CI 的价值在"下次提交时自动验证",而那种延迟的、隐性的价值,和"让 Agent 学会一个新技能"的即时正反馈比起来,太弱了。

所以防御性工程总是排在后面。不是它不重要,是新功能的诱惑太强。这个心理陷阱,我自己到现在还在跟它搏斗。

我能给的建议只有一个:别等。 别等"功能做完了再补 CI"——功能永远做不完。在你写第三个功能之前,花半天把 pre-commit 配上。它会改变你后续所有开发的安全感——而且,像我这回清债时翻出那几个静默 bug 一样,它很可能会帮你发现一些你以为"一直好好的"功能,其实从来没真正工作过。

这一章的工具:质量门禁检查清单

🔧 质量门禁检查清单

对照你当前的 Agent 项目,过一遍:

CI(最基本)

  • [ ] 提交代码时,有没有自动跑测试?
  • [ ] 有没有自动跑 lint 和类型检查?
  • [ ] 你是单人项目吗?如果是——pre-commit 配了吗?(它比云端流水线更改变你的习惯)
  • [ ] 测试不通过时,能不能阻止提交?

测试覆盖

  • [ ] 你的测试覆盖了哪些层?(算法层?HTTP 层?数据层?Agent 行为层?)
  • [ ] 有没有"测了好测的部分,难测的 Agent 行为层完全没测"的失衡?
  • [ ] eval 用例跑完的结果,接进 CI 自动比对了,还是靠肉眼判断?

闭环

  • [ ] 有没有"代码写了但从没被调用"的功能?(告警、日志、审计……)
  • [ ] 第一步:grep 每个功能的调用点,0 个的就是完全空壳
  • [ ] 第二步:列举每个功能"语义上该出现的所有路径",逐一核实——抓部分接通
  • [ ] 你的告警能真的推送到你手里吗?还是写完了但没接通?

危险信号

  • [ ] 你做的所有项目都没有 CI → 你和上面那些坑之间只差一个提交
  • [ ] 你的测试全集中在某个层 → 未覆盖的层就是下一个 bug 的来源
  • [ ] 你有"写了但没接通"的功能 → 不接通等于没写
  • [ ] 你的项目从没跑过 lint,但配置文件里声明着 → 你以为你有 lint,其实你没有

小结

这一章的核心就一句话:"能跑的 demo"和"可靠的生产系统"之间,差的就是这些不性感但不能没有的东西。

补它们的过程,修正了我好几个想当然的认知:CI 的成本不只是配半天,还有清债(而且清债能翻出静默 bug);单人项目的 CI 最小可行形态是 pre-commit 不是云端流水线;"空壳"不是二元的,"部分接通"比"完全空壳"更危险也更难发现。

这些东西之所以总被忽略,不是不知道重要,是新功能的即时正反馈太强了,防御性工程永远排在后面。但只要开始补,你就会发现它带来的不只是"少出 bug",还有一种"我知道我的代码是什么状态"的踏实感——这种踏实感,是"跑一遍看看"永远给不了的。

下一章

第 13 章 · 驾驭曲线 —— 你和智能体的协作关系,经历了什么变化?四个阶段,每阶段的标志是什么?