过去一年半,我把业余时间几乎全部投给了一个叫 TrailSnap 的开源项目——一个 AI 驱动、可自托管的照片管理应用。它有五个子包:Vue 3 前端、FastAPI 主后端、可选 GPU 的 AI 微服务、文档站、发布到 npm 的 CLI,外加桌面端和移动端的构建流水线。如今仓库里业务代码约 11 万行,测试代码约 7 万行。
这些代码 95% 以上不是我手写的——是 AI 写的。我的角色更像产品经理 + 架构师 + 测试负责人:拆需求、审方案、定约定,然后把实现交给 AI。
vibecoding 的幸福感是真实的:一个周末就能做完过去一个月的功能量。但恐惧也是真实的,而且随着代码量增长与日俱增:
AI 最大的问题从来不是写不出新功能,而是在加新功能的时候,悄悄把旧功能改坏。
传统软件工程靠三样东西兜住这类问题:code review、自动化测试、CI 流程。但独立开发者没有同事帮你 review。我一度以为这意味着质量体系缺了一角,后来才想明白:这一角本来就不该由"人"来补。
AI 写代码有三个特点:快、多、健忘。前两个是优点,第三个是万恶之源。
AI 每次会话都像一位"新员工上班":它不知道两周前某个函数已经被三处依赖,不知道某个看似冗余的判断是为了绕过一个线上 bug,更不知道它重构"优化"掉的那行代码是整个支付链路的护栏。它写的新功能大概率是对的——但改动波及面里有没有碰坏别的东西,它不知道,你也不知道。
所以 vibecoding 时代的可靠性问题,定义变了:
不是「AI 能不能写出正确的代码」(绝大多数时候能),而是「AI 的改动破坏了什么,我多快能知道」。
唯一能让"改坏了"自动暴露的机制,是自动化测试 + CI。这不是什么新鲜事。新鲜的是角色转换:在传统团队里,测试是 QA 的活、CI 是平台的活;在 vibecoding 里,测试体系就是你的"同事",CI 就是你的"reviewer"。它们不睡觉、不吃人情、不会被 AI 写的花哨代码迷惑,只看结果。
而独立开发者有一个隐藏优势:没有历史包袱,整个工程流程可以为"AI 高频提交"这个前提重新设计。
下面是我实践中的三道防线。
TrailSnap 的 CI(.github/workflows/tests.yml)有六个 job,按反馈速度分三层:
| 层 | 内容 | 作用 |
|---|---|---|
| 秒级反馈 | CLI / 后端 / AI 三组单元测试 | 挡掉 80% 的低级错误:import 挂了、签名变了、边界忘了 |
| 分钟级 | 集成测试 | 起 pgvector 容器 + 真实 uvicorn,测试用 HTTP 打真接口 |
| 十分钟级 | 全量 E2E | docker compose 起全栈,Playwright 跑约 340 个场景 |
比"分层"更重要的是几条专门为 AI 高频提交设计的原则:
1. 本地与 CI 走同一个脚本。 我的仓库只有一条测试入口 run-tests.ps1,本地和 CI 完全同路径,唯一区别是 -Mode dev(本地进程)还是 -Mode docker(容器栈)。传统团队最常见的甩锅现场——"我本地是好的"——在 AI 时代换了台词:AI 会说"我测过了"。 所以我立了一条规矩:AI 声称测过不算数,CI 绿了才算数。因为本地和 CI 是同一个脚本、同一份环境变量、同一套夹具,"AI 本地验证通过"和"CI 会绿"几乎是同一个事件。
2. paths 过滤 + 并发取消。 AI 一天提交几十次,CI 资源就是真金白银。改文档、改官网不触发测试;同一个 PR 新推送自动取消旧 run。还有个容易忽略的细节:concurrency 的 group 里要带上 event_name,否则一次普通的 push 会把正在跑的夜间全量 E2E 打断——AI 可不会替你想到这个。
3. 该缓存的都缓存。 AI 模型权重几百 MB,用 actions/cache 跨 run 复用;测试照片放在独立仓库按需 checkout;AI 服务镜像直接拉已发布的版本,省掉 torch 的重度编译。这些优化单独看都是小事,叠起来决定了 CI 能不能塞进免费额度。
当前数字:pytest 用例 2754 个(240 个测试文件),Playwright E2E 67 个 spec、约 340 个场景。测试代码约占仓库四成。
这套体系里我认为最值得抄的,是三条"名义上写给人、实际上写给 AI"的约定:
1. 统一响应格式 = 统一断言方式。 所有 API 返回 BaseResponse({code, msg, data}),测试永远先断言 code == 0 再校验 data。这条约定让 AI 写新测试时天然不会断言错位——它只需要模仿旁边已有的用例。约定越统一,AI 的模仿越准,幻觉越少。
2. marker 体系 = 止损速度。 每个用例打三组标记:覆盖度(smoke/regression/slow)、资源(postgres/model)、模块(module_photo/module_album…)。改了相册模块?先跑 -m "smoke and module_album",半分钟知道伤没伤到核心。这不为省 CI 时间,为的是让"改坏了"的信号以最快速度、带着定位信息到达。
3. E2E 分档 = 按需全量。 smoke(页面能打开)/ p0(核心路径)/ p1(业务深测)/ full 四档。日常迭代先跑 p0 几分钟收口,睡前挂 full 兜底。凌晨三点还有一条定时任务,全量再扫一遍——那是第三道防线的一部分。
还有一个容易被忽视的细节:测试夹具是独立 git 仓库。测试照片不进主仓库,按套件组织,CI 上自动 checkout。任何人(包括 AI)跑全量 E2E 的门槛,降到了一条命令。门槛这件事很关键——门槛每高一寸,AI 和你自己偷懒的概率就多一分。
上面的测试体系不是一天建成的,也没法全靠"写功能时顺手写测试"攒出来。功能开发时 AI 只会覆盖它刚写的路径;几千行遗留代码没人爱碰,覆盖率债越积越多。
这种活枯燥、机械、收益不即时,人类不爱干——但恰恰适合扔给 AI。
我的答案是:再雇一个 AI,值夜班,用最便宜的模型。
tests/scripts/NIGHTLY-TEST-WATCH.md 是一份任务清单,但更准确的定位是单一事实源。文档开头写着:任何 AI agent(Codex / Claude / GPT 等)或人拿到这份清单,都能自主完成一轮「测试 → 失败修复 / 盲区补测 → 提交」,执行时不需要回头询问任何前置决策,遇到模糊点按文档默认策略处理。
每轮流程:
nightly/test-watch-日期 分支。ModuleNotFoundError → A;AssertionError 且明显行为回归 → B;Connection refused → C(重启重试,不算业务改动)。核心是一条禁令:不允许跳过判断直接改代码。 A 类动手前必须先读被测函数源码和最近的 git log -p,确认真是测试错了才许改测试。pytest --cov,解析 Cobertura XML,按"未覆盖行数 / 覆盖率"排序产出候选清单;前端不追行覆盖率(主力是 E2E 没有单测),改用"哪些 view 从没被任何 spec 引用过"来判定。另有一个基线文件登记已覆盖的盲区,避免每轮重复补测。第二天早上,我看到的不是一堆散乱的改动,而是一个干净的 PR 和一份结构化的战报。
AI 自主执行最怕的不是干不了活,是失控地干活。所以那份文档里,围栏写得比流程还细:
tests/ 和指定业务目录。diff 里出现白名单外的文件,退出码 5,中止本轮。这套机制跑了七周,30 轮值守、累计 61 个 commit 直接落进主干。三个真实收获:
抓到一个用户永远报不出来的 bug。 实况照片解析里,Apple 分支用大写 .HEIC/.MOV 做大小写敏感的替换,而 is_supported 判断是大小写不敏感的。Windows 上一切正常;NAS/Linux 同步后文件名归一为小写,实况图配对逻辑里的 cid1 == cid2 永不成立——Apple 实况图配对成功率 0%。这个 bug 的触发条件是"大小写不敏感文件系统 + 同步归一化",用户报不出来,盲区测试覆盖之前我也不知道。是夜间值守在给实况图模块补测试时,顺手挖出来修掉的。
还清了一笔挂了五轮的技术债。 有一条测试挂起问题连续五轮值守都没解决,某轮值守追根因,发现是一个 asyncio.create_task 的补丁没有 close 传入的 coroutine,三组未 await 的协程把事件循环卡死了——五轮悬案的部分根因就此定位修掉。
自动跟上重构节奏。 某次主线重构改了 select_model 的返回类型,第二轮值守就发现旧测试断言没跟上,读源码确认属于 A 类,自动适配了新旧两种返回形态。
这些活的共同点:枯燥、机械、需要耐心和全局视野,但不需要顶尖智力。 所以夜间值守全程用便宜的中端模型跑,旗舰模型只在白天写新功能时上。每轮 5–10 个模块,一晚一晚跑下来,覆盖率是复利式上涨的——七周,后端核心模块基本覆盖完。
白天的 AI 造债,夜里的 AI 还债,而决定造不造的始终是我。
以前独立开发者死于带宽:一个人一天只有十几个小时。现在带宽被 AI 买断了,新瓶颈变成了判断力:你得知道 AI 做得对不对。
我的答案是把"知道对不对"这件事也尽量交给机器——CI 判断每次提交,测试锁住每个功能,夜里的 AI 检查白天的 AI。而我负责的,是那个最不可替代的角色:决定做什么,以及每天早上按下 merge 键之前,认真读完那几行 diff。
本文作者:司小远
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!