2026-09-08
项目
0

目录

独立开发者 vibecoding 实录:15 万行代码,我怎么防止 AI 把旧功能改坏
一、vibecoding 的幸福与恐惧
二、问题的本质变了:从「写对」到「没改坏」
三、第一道防线:CI,让每次提交都被审判
四、第二道防线:测试体系,给每个功能上锁
五、第三道防线:夜间 AI 值守——用便宜的 AI,还 AI 欠的债
一份"任何 AI 都能执行"的任务清单
防失控,比能干活更重要
七周战果
六、成本账
七、如果你也想复制这套体系
八、写在最后

独立开发者 vibecoding 实录:15 万行代码,我怎么防止 AI 把旧功能改坏

一、vibecoding 的幸福与恐惧

过去一年半,我把业余时间几乎全部投给了一个叫 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 高频提交"这个前提重新设计。

下面是我实践中的三道防线。

三、第一道防线:CI,让每次提交都被审判

TrailSnap 的 CI(.github/workflows/tests.yml)有六个 job,按反馈速度分三层:

内容作用
秒级反馈CLI / 后端 / AI 三组单元测试挡掉 80% 的低级错误:import 挂了、签名变了、边界忘了
分钟级集成测试起 pgvector 容器 + 真实 uvicorn,测试用 HTTP 打真接口
十分钟级全量 E2Edocker 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 欠的债

上面的测试体系不是一天建成的,也没法全靠"写功能时顺手写测试"攒出来。功能开发时 AI 只会覆盖它刚写的路径;几千行遗留代码没人爱碰,覆盖率债越积越多。

这种活枯燥、机械、收益不即时,人类不爱干——但恰恰适合扔给 AI。

我的答案是:再雇一个 AI,值夜班,用最便宜的模型。

一份"任何 AI 都能执行"的任务清单

tests/scripts/NIGHTLY-TEST-WATCH.md 是一份任务清单,但更准确的定位是单一事实源。文档开头写着:任何 AI agent(Codex / Claude / GPT 等)或人拿到这份清单,都能自主完成一轮「测试 → 失败修复 / 盲区补测 → 提交」,执行时不需要回头询问任何前置决策,遇到模糊点按文档默认策略处理。

每轮流程:

  1. 预检:工作区脏检查——有未提交的改动?是临时文件就清理,不是就立刻中止并写 ALERT。绝不污染人类未提交的工作。然后自动切出 nightly/test-watch-日期 分支。
  2. 跑全量 E2E,失败则进入分类。
  3. 失败三分类:A(测试代码错了)/ B(业务代码回归了)/ C(环境问题)。文档里是一张"现场关键字 → 类型"的映射表:ModuleNotFoundError → A;AssertionError 且明显行为回归 → B;Connection refused → C(重启重试,不算业务改动)。核心是一条禁令:不允许跳过判断直接改代码。 A 类动手前必须先读被测函数源码和最近的 git log -p,确认真是测试错了才许改测试。
  4. 盲区扫描。这里有个关键设计决策:盲区以真实覆盖率为准,不猜。后端和 AI 服务跑 pytest --cov,解析 Cobertura XML,按"未覆盖行数 / 覆盖率"排序产出候选清单;前端不追行覆盖率(主力是 E2E 没有单测),改用"哪些 view 从没被任何 spec 引用过"来判定。另有一个基线文件登记已覆盖的盲区,避免每轮重复补测。
  5. 按优先级补测:从未被触达的 view 开始,其次覆盖率最低的 router,再到 service 业务逻辑。每轮选 5–10 个模块,每模块写 1–3 个用例,覆盖 happy path / 边界 / 错误三种形态。
  6. 提交:本地 commit,不 push。写 summary 报告,追加一行全局日志,关掉所有服务。

第二天早上,我看到的不是一堆散乱的改动,而是一个干净的 PR 和一份结构化的战报。

防失控,比能干活更重要

AI 自主执行最怕的不是干不了活,是失控地干活。所以那份文档里,围栏写得比流程还细:

  • 改动白名单:只能动 tests/ 和指定业务目录。diff 里出现白名单外的文件,退出码 5,中止本轮。
  • 重试上限:单个用例修两次还失败?回滚相关改动,跳过本轮 commit。新测试连续 3 次失败?整轮放弃。
  • 时间预算:120 分钟软上限,超时放弃。宁可白干一晚,不许硬扛到天亮。
  • 六种 ALERT 退出码,每种对应一类"需要人类介入"的状态,每轮必落盘 summary。
  • 最重要的一条:不 push。 夜间 AI 的产出以 PR 形式存在,合并按钮在我手里。AI 没有最终写权限。

七周战果

这套机制跑了七周,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 还债,而决定造不造的始终是我。

六、成本账

  • CI:每个 PR 全量约 30 分钟(六个 job 并行),在 GitHub Actions 免费额度内。
  • 夜间值守:便宜模型 + 120 分钟软上限(超时自动放弃),一晚成本几块人民币以内。
  • 我的时间:每天花 15–30 分钟 review 两个 PR——一个是功能开发的,一个是夜间值守的。这是我每天唯一固定花在"质量"上的时间。

七、如果你也想复制这套体系

  1. 先有 CI,再有 AI。 哪怕只有一个测试文件、一条 lint,也要让"AI 声称完成"和"客观验证"之间隔一道机器关卡。
  2. 本地与 CI 同一脚本。 消灭两个环境,就消灭了"我本地是好的"。
  3. 分层止损:秒级单测挡 80% 的问题,分档 E2E 兜底,marker 让失败信号能定位到模块。
  4. 把还债写成 AI 可执行的文档。 每个决策预先写死,AI 不需要问你任何问题——你的犹豫,就是 AI 的幻觉空间。
  5. 围栏比能力重要:改动白名单、重试上限、时间预算、ALERT 退出码,四件套齐了再让 AI 自主跑。
  6. AI 只 commit,人 merge。 最终把关权永远在你手里。

八、写在最后

以前独立开发者死于带宽:一个人一天只有十几个小时。现在带宽被 AI 买断了,新瓶颈变成了判断力:你得知道 AI 做得对不对。

我的答案是把"知道对不对"这件事也尽量交给机器——CI 判断每次提交,测试锁住每个功能,夜里的 AI 检查白天的 AI。而我负责的,是那个最不可替代的角色:决定做什么,以及每天早上按下 merge 键之前,认真读完那几行 diff。

本文作者:司小远

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!