# AI开发最大的坑，不是代码写错

URL: https://caijiao.org/posts/194
Source: docs/posts/194.md
Description: 第一次写错容易改，难的是第十次改动悄悄冲掉了第八次定下的约定。把决策固化进 schema、迁移脚本和 CI 检查，让约定不依赖任何人的记忆，这是 AI 辅助开发里最省事的一道工序。

代码写错是最好处理的那一类问题。它有明确症状，能复现，能被测试抓住，改完就过去了。真正拖垮项目的是另一种：**每一版单独看都对，放在一起就互相矛盾。**

我踩过一次。一个订单状态机，第一版定义了五种状态和六条转换规则，跑得很顺。之后每隔几天让它加一个小需求，前后改了十来次。第十一次的时候，某个状态在 A 文件里还叫 `pending_refund`，在 B 文件里已经被改成 `refunding` 了，两边各自能编译、各自有测试覆盖，直到一个用户同时触发退款和发货才暴露。

JetBrains 的调研里 90% 的专业开发者每周至少用一次 AI 编码 agent。高频迭代加上无状态的生成过程，让这类"渐进式漂移"变成了这个时代最典型的故障模式。

## 为什么 AI 特别容易制造漂移

模型的每一次对话都是无状态的，而你的项目是有状态的。

你告诉它"用 `refunding` 这个状态"，它知道了。下一轮对话你不提这件事，它就会按当下最合理的写法重新起一个名字，或者沿用仓库里另一处更常见的写法。它不会记得这是你上周特意统一的命名，因为那次对话没有出现在这一次的上下文里。

人也有这个问题，但人有两条缓冲：一是人会翻历史 PR 找当初为什么这么定，二是同一个人对同一件事的判断是连续的。模型两条都没有。它每次都在做局部最优决策，而局部最优加起来不等于全局一致。

于是项目里会慢慢长出一堆"各自正确"的碎片：同一张表在两个模块里有不同的字段命名，同一个错误码在前端有两种处理方式，同一个配置项在三个地方被读，默认值还不一样。

## 把约定从记忆里搬出来

解决办法不是更仔细地 review，是把约定放到代码之外也生效的地方。

第一层是数据层。表结构、字段类型、枚举值、非空约束，全部写进 schema 和迁移脚本，让数据库替你挡住不一致。SQLite 这种轻量存储在这方面意外地好用，因为 schema 就是仓库里的一个文件，可以被 diff、被 review、被 CI 校验，[SQLite 教程](/sqlite/) 里的约束和迁移部分值得认真过一遍。应用代码里再怎么漂，漂不过 `CHECK` 约束和唯一索引。

第二层是类型层。跨模块的共享概念必须有唯一定义，不允许在两个文件里各写一个字面量联合类型。这一层能挡住命名漂移，因为一旦改名，所有引用处都会红。

第三层是 CI。把一致性检查写成脚本：扫描代码里有没有硬编码的状态字符串、有没有绕过统一错误处理的地方、有没有新增未登记的环境变量。这些规则写一次，之后每一版产出都会被自动卡一道。

这三层加起来，本质上是同一件事：**让约定不依赖任何人的记忆，也不依赖任何一次对话的上下文。**

## 一个可以直接抄的思路

Omni Calculator 的做法在架构上很值得借鉴。它不把每个计算器当成独立页面手写，而是把计算器的公式、单位、输入输出定义抽象成一份配置，页面由模板根据配置生成。新增一个计算器，写的是配置而不是页面。

![Omni Calculator 官网首页截图](/images/2026-09/omnicalculator.com.png)

*Omni Calculator 首页的分类卡直接标着数量：Conversion 329 个，Everyday life 393 个。*

这个思路搬到 AI 协作里同样成立：把业务里那些"容易漂"的东西——状态机、错误码、字段映射、权限矩阵——变成单一数据源的配置，让代码从配置生成，而不是让 agent 每次重新写一遍。配置改错了一眼能看见，代码漂了你得靠测试才能发现。

配置化的另一个好处是它天然适合被校验。你可以写一段脚本遍历所有配置，检查状态机的每条边都有对应处理、每个错误码都有文案、每个字段都有类型。这些检查在开发阶段就能跑，不需要等到运行时。

## 前端也一样

组件层面最容易漂的是"状态放在哪一层"。同一个数据，有的组件自己 useState，有的提到父组件，有的进了全局 store。每一处单独看都合理，合起来就会出现两份不同步的副本。

约定清楚之后把它固化：什么数据属于服务端状态（交给请求库的缓存），什么属于 UI 局部状态（留在组件内），什么属于跨页面共享（进 store）。[React 教程](/react/) 里关于状态提升和状态归类的部分讲的就是这件事，但光"知道"没用，得写成团队能执行的规则，并且最好能有一条 lint 规则来提醒。

## 我在项目里保留的三条硬规矩

共享概念只允许有一个定义处，改名必须全局改完再提交，不允许留兼容别名——留别名就等于正式承认漂移合法。

数据库 schema 的改动必须走迁移脚本，不允许在应用代码里靠判断字段是否存在来兼容两种结构。

每次让 agent 改动跨模块的东西之前，先让它列出"这次改动会影响到哪些已有约定"，我去核对这个列表全不全。它列漏的部分，通常就是它会改坏的地方。

前两条是给代码加约束，第三条是给协作流程加约束。三条都不复杂，难在坚持——尤其是当 agent 给的那一版看起来已经能跑的时候。

---

*JetBrains 数据引自其开发者生态调研报告；Omni Calculator 的架构描述来自其公开的技术分享。*
