跳到主要内容
那六条活不过 CI

那六条活不过 CI

原石
原石

· 阅读约 6 分钟

dev.to 九月一号那篇讲"不懂架构怎么用 AI 构建"的文章,我读了两遍。第一遍读那六条生存技能,第二遍读那七十八条评论。值钱的东西在下半场。

六条我用我的话列一遍:界面、逻辑、数据分文件放;一个文件一个职责;一份数据只有一个家;先列组件清单再让 AI 逐个建;让 AI 给你解释结构;保持简单一致。

全对。但一条都跑不起来。你没法在 CI 里 assert 一句"保持简单一致",也没法 grep 一个"这算不算单一事实来源"。它们描述的是状态,不是检查。描述状态的话谁都会说,包括说出问题的那个模型自己。

原文讲了一个现象,我熟:AI 帮你把应用跑起来,加功能,加到第五个左右开始塌——修一处坏一处,而且你已经说不清自己在改什么。第五这个数字挺准。前四个功能基本各干各的,从第五个开始,功能之间要碰同一份数据。

生成出来的代码通常长这样:

// Header.jsx
const [cart, setCart] = useState(loadCart());

// Checkout.jsx
const [cart, setCart] = useState(loadCart());

两个组件各读一份,各改各的。用户在 Header 里加了商品,进结算页看不见;刷新一下又有了,因为两边都从 localStorage 读。评论区有个人说,他因为两个组件存了同一份状态的两个副本,赔进去一个周末。就是这种周末。

"一份数据只有一个家"这条在说这件事。但真正拦得住 bug 的不是那句话,是这个:

# 提交前跑一下,期望输出:1
grep -rl "loadCart()" src/ | wc -l

或者干脆把状态收进一个东西,物理上只可能有一份:

struct app_state {
    int cart_count;
    /* ... */
};
static struct app_state S;   /* 整个进程就这一个 */

一个 static。没有第二个地方能 new 出它来。这就轮不到"记得要单一事实来源"了——编译器不给你机会。规矩写成人话,靠自觉;写成 static,靠编译器。我信后者。

成本搬了家

评论区一个做隐私工具的哥们说,AI 让每次迭代变便宜了,但确认这次改动到底动了什么,变难了。作者回了一句我完全同意的话:生成一次编辑几乎免费,理解这次编辑的成本一点没降。瓶颈从"能不能做出这个修改"挪到了"能不能确认这个修改只做了预期的事"。

这句是地基。

AI 把人从"写"这条腿上松了绑,"确认"那条腿没跟着长。而确认的成本和改动速度成正比。改得越快,欠的确认债越多,还债的日子就是第五个功能。

另一个评论说得更狠,那位自称做 AI 基础设施的:AI 不是架构师,是浇筑工,而且被上下文窗口锁死,它一次只能看见你递给它的那几个文件。

这就解释了状态漂移。那位土木转过来的评论者讲,两边数据会慢慢不一致,然后 AI 在已经不一致的数据上,自信地给出一个让漂移更严重的修复。它不笨。它只读了 A 文件,没读 B 文件,而 B 里那份副本三天前就改过了。所以别指望换个更强的模型解决这个,这不是模型问题。

把边界做成它越不过去的形状

能解决的是边界。让边界成为 AI 跨不过去的东西,而不是请求它遵守的约定。那位基础设施工程师的方案我基本同意:模块化单体,加物理契约。接口文件只读,SDK 从契约生成,CI 里跑契约检查。

// 只读。改这个文件走单独的 review。
service Cart {
  rpc Add(AddReq) returns (Cart);
  rpc Get(GetReq) returns (Cart);
}
# 生成 SDK,跑契约检查
make contracts && make check-contracts

生成出来的 SDK 谁都不许手改——AI 不许,我也不许。改需求的唯一入口是改契约。约束到位之后,模型强弱就不太关键了;这里我甚至同意那位工程师说的,一个便宜的小模型也够用。瓶颈从来不在模型那头。

但别把这话读成"该上微服务了"。一个进程、一个 make、几个模块加一份接口文件,就叫模块化单体,够用。真撑不住再拆。提前拆的代价是你多出一整套网络边界要维护,还以为自己在换"灵活性"。那玩意儿不该存在。

细节上还有个说法我觉得准:下个功能开工之前,先画一页模块地图,再列一份"这次 AI 不许碰的文件清单"。前者是结构切割,管东西在哪;后者是操作切割,管这次只动哪。新手通常只有一个,或者两个都没有。两种是两种动作,都得有。

往回砍才是活

原文那第四条——先列组件再逐个建——我理解它为什么有效,但理由跟它写的不太一样。不是"逐个建更省事",是模型默认给"什么都覆盖"的方案,因为它的训练数据里长这样。

它给你一个能跑的模块,同时给你三个扩展点、一个可选回调、一个用不上的配置项。你得往回砍。砍完它才属于你。

我前段时间让模型写个按范围删元素的函数,它回了四十多行,带回调注册、可选锁、一个"如果传了 logger 就记日志"的分支。我那个东西是单线程的,没 logger,那回调这辈子不会有第二个实现。二十分钟砍成一行签名。AI 写代码可以,但它给的方案默认偏复杂——往回砍是工程师的活,不是往上加。

原文六条里,我唯一不买账的是第五条,让 AI 解释代码结构。让刚写完这段代码的东西来解释它,而且它只能看到一部分——你问它"为什么这么组织",它会给你一段自洽的、听起来很对的结构叙述。流利是这工具的长项。但让生成者解释生成物,等于让它给自己当裁判。确认必须来自别的方向:断言、契约、编译器。这三样比解释靠谱得多。

说句题外话。我们好像特别怕让一个东西只做一件事——一个文件,一个函数,一个模型。可能是因为一旦只让它做一件事,你就得先回答"这件事到底是什么"。混成一坨反而可以永远不回答。返回键就是那个"再想想"。

扯回来。

那六条里,有五条是形容词。唯一能写成断言的是"单一事实来源",而写成断言之后,它就不再是一条"技能"了——它就是一行 grep,或者一个 static,或者一份生成出来的 SDK。

所以规则一句:凡是写不进 CI 的架构规矩,先别写进 README。

三个月后的你不会读 README。三个月后的你只会跑 make。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →