Skip to content

Vibe Coding 方法论:AI 写,你读 ​

读完这章,你应该能回答:

  • 把"给 jackdaw 加个命令,列出配置里的模型"这样的一句话需求,当场扩写成目标、约束、验收三要素齐全的规格,并说出每个要素防的是哪种翻车;
  • AI 说"改好了"之后,按什么顺序让哪四个 cargo 命令说话,为什么"模型说它对了"不算数;
  • 拿到一份几百行的 diff,哪些块可以放过,哪四处必须你逐行读懂,命令为什么替你挡不住那四处。

分工契约:它管怎么写,你管什么算对 ​

第 2 章说过,agent 是一个循环:消息进、模型答、结果回填、继续。你和 AI 的协作是同一个循环:你发提示词,它回一个 diff,你把命令输出贴回去,它继续修——你的需求是 user 消息,diff 是 assistant 消息,你贴回的报错是回填的结果。有一处关键失配:agent 循环里由模型决定调什么工具;这里决定跑什么命令、何时收工的,是你。所以这个循环的瓶颈不在模型,在你的每一次裁决。

契约三句话:AI 对"怎么写"负责;你对"写什么、什么算对"负责;编译器和测试对"谁对"负责。 vibe coding 的翻车几乎都走同一条路:模糊需求 → 自信但错误的代码 → 你看不懂 → 只能在"信"与"不信"里二选一。契约做的事,是把"信不信"换成"验不验"。

规格先行:目标、约束、验收 ​

主线例子:给 jackdaw 这个网关加一个小功能——列出配置里的所有模型。坏规格一句话就够:

给 jackdaw 加个命令,列出模型。

AI 会交回一段能编译的代码;是不是你要的,全凭运气。规格先行就是把运气换成条款。三要素:

  • 目标:行为变化,一句话说给用户听——"运行 jackdaw models list,打印配置中所有模型,格式 provider/model";
  • 约束:不许碰什么、必须守什么——"只读配置文件;不新增依赖;输出按字母排序";
  • 验收:怎么算完成——"对仓库里的示例配置运行,输出恰好 5 行;cargo test 全绿"。

验收里埋一个可数的事实(几行输出、什么退出码),你不用读懂全部代码,也能当场抓住"明显不对"。

小步走:一次一个可验证的改动 ​

规格别整包丢给 AI 要成品。拆:

  1. 加子命令骨架,什么都不做,先让编译通过;
  2. 从配置收集模型名;
  3. 排序、打印,补上测试。

每步走完,跑一遍下一节的仲裁,绿了才进下一步。为什么不肯一步到位?大 diff 你审不动,错了回滚也疼;三步里错一步,重做的只是三分之一。步子多小合适?经验值:一条验收,对应一个改动、一次提交。

四个仲裁者:check、test、clippy、fmt ​

AI 说"已完成"没有效力,Cargo 的四个子命令才有:

bash
cargo check  # 编译得过吗:类型、名字都对吗
cargo test   # 行为对吗:验收条款成立吗
cargo clippy # 写法地道吗:有更惯用的路吗
cargo fmt    # 格式统一吗

读法:四条命令各只回答一个问题,谁也不越界——clippy 不管业务对错,test 不管格式。仲裁的意思是:你和 AI 争"输出有没有排好序",不比谁口才好——补一条断言排序的测试,让 test 出结论;争"这写法地不地道",让 clippy 出结论。你可以把报错原样贴回给 AI——暂时读不懂每个报错,不妨碍用它仲裁;但"这一步过了"必须出自命令输出,不能出自模型的口头承诺。(各命令细节第 8 章展开)

向 AI 要可读代码:注释、命名、最小 diff ​

可读性不靠乞求,写进规格。三条常用条款:

  • 注释密度:每 10–15 行代码一条"为什么"注释——写意图,不翻译代码;
  • 命名:函数名用动宾结构,list_models 好过 process;
  • 最小 diff:只改与目标相关的行,禁止顺手重构无关代码。

最后一条最常被违反。下面是一段示意 diff(教学示意,不是 jackdaw 源码;语法你多半还不认识,第一次读只看结构和行为,细节第 4–7 章逐步展开):

diff
 fn print_all(models: &[String]) {
-    for m in models {
-        println!("{}", m);
+    for model in models {
+        println!("{}", model);
     }
 }
+
+fn lookup(models: &Vec<String>, name: &str) -> Option<usize> {
+    let owned: Vec<String> = models.clone();
+    owned.iter().position(|m| m == name)
+}

读法:先看开头成对的删行与增行——只是把 m 改名成 model,跟"排序打印"无关,属于该退回的夹带;再看新增的 lookup:拿到的是只读的列表,第一行却整表复制一份再查找——复制了一张表,只为读它。那一行就是下一节"必须自己看懂"的地方;而改名这种夹带不产生任何价值,却占掉你的审查注意力。

哪些时刻必须自己看懂 ​

四个仲裁者挡住大部分风险,但有四类地方命令管不了,只能你逐行读:

  1. 所有权转移:数据被谁拿走、哪里发生了复制。.clone()永远是显式决定——这个决定值不值,编译器不评判。(第 5 章展开)
  2. 并发:两段代码同时碰同一份数据,交错顺序会改变行为,而测试往往恰好抓不住。(第 7 章展开)
  3. unsafe:显式绕过 Rust 内存安全保证的代码块,编译器已放弃担保,人审是最后一道闸。jackdaw 全项目一行 unsafe 都没有——见到它,停下来。
  4. 错误被吞掉:
rust
let _ = write_line(&mut out, line);

读法:let _ = 把 write_line 的返回值整个丢弃。如果那是一个错误,这里既不处理也不上报,上层还以为一切正常。错误应该向上传播(第 6 章展开),而不是就地蒸发;在 diff 里搜 let _ = 和 .ok() 这两类吞错写法,是审 AI 代码的固定动作。

除这四处,第一次读 diff 允许放过细节:看函数签名和"为什么"注释、对上验收条款,就可以走下一步。你不需要此刻读懂一切,只需要知道哪里以后必须回来。

小结 ​

规格先行把模糊需求变成可仲裁的条款;小步走让每个改动都验得起;check、test、clippy、fmt 是不讲情面的仲裁者。所有权转移、并发、unsafe、吞掉错误四处,仲裁者管不了,只能你亲自读。下一章开始读 Rust 本身:变量、函数与表达式。

自检 ​

  1. 把"给网关加故障转移"扩写成三要素规格,每个要素至少写一条。
  2. AI 一次提交同时:改了路由逻辑、重命名 20 个变量、把一个函数改成 unsafe。按本章方法分几步处置?依据各是什么?
  3. cargo test 全绿,为什么不能替代你亲自读 let _ = write_line(...) 这一行?
参考答案
  1. 目标:上游请求失败时,网关自动改用列表中的下一个模型重试,对调用方透明;约束:只换上游,不改请求体,不新增依赖;验收:第一个上游拒绝时返回第二个上游的成功响应,且"全列表失败"分支有测试覆盖。
  2. 三步:先要求拆成三份独立 diff——路由逻辑按验收条款审;重命名属无关夹带,按"最小 diff"条款退回;unsafe 单独逐行人审,因为命令仲裁覆盖不了 unsafe,它是四处必读之一。
  3. test 只验证被执行到的路径。let _ = 把错误丢弃后,程序带着错误状态"正常"走完,断言成功路径的测试照样全绿——吞错的证据不在测试输出里,只在那行代码里。