工具:Claude Code、Codex 项目:两个中大型 Flutter 项目(移动端 + 桌面端),其中桌面端由我一个人借助 AI 从 0 开发,900+ 次提交、1600+ 个测试用例。
用 AI 写代码一段时间后,我最大的感受是:AI 的问题不是不会写,而是不知道"这个项目里什么不能做"。
- 它会用
malloc分配一个原生结构体,代码能跑,画面却悄悄变了色; - 它会直接改
lib/l10n/app_localizations.dart,下次生成就被覆盖; - 它会手写一个
Duration(milliseconds: 300),绕过项目统一的动效规范; - 它会说"已修复",但
flutter analyze里多了 3 个 error。
这些问题靠每次在对话里提醒是没用的,下一个会话它又忘了。我的做法是把约束沉淀到仓库里,分成四层:
objectivec
CLAUDE.md / AGENTS.md ------ 项目长什么样(架构、命令、约定)
rules/ ------ 什么不许做(硬规则)
hooks ------ 自动执行的检查(不靠 AI 自觉)
agents / skills ------ 固定流程(审查员、修 bug、加功能、发版)
一、CLAUDE.md / AGENTS.md:给 AI 的项目说明书
Claude Code 启动时会读仓库根目录的 CLAUDE.md,Codex 读的是 AGENTS.md。两边都用的话,不要维护两份 ,AGENTS.md 里只写一句话指过去:
markdown
# AGENTS.md
本项目的工作指引请阅读 [./CLAUDE.md](./CLAUDE.md),所有规则、命令、发版流程均以该文件为准。
CLAUDE.md 写什么?我的经验是只写AI 从代码里不容易看出来、但做错了代价很大的东西:
- 环境与命令 :必须用
fvm、Flutter 版本、测试命令、打包命令; - 架构地图 :分层、入口、状态管理用法、哪些文件是"门面"(比如统一导出的
base.dart); - 关键约定 :例如"首页控制器由 24 个 mixin 组合而成,
with列表的顺序就是依赖顺序,每个 mixin 的onInit必须调super,漏调会静默断掉后面所有 mixin 的初始化"。
不要写什么?代码里一眼就能看到的东西、git 历史里有的东西。写多了 AI 抓不住重点,而且文档会过期,代码不会 。我们就吃过亏:CLAUDE.md 里长期写着某个结构体用 malloc 分配,实际代码早就改成 calloc 了。所以在修 bug 的流程里专门加了一条:不许拿文档当依据下结论,必须在当前代码里证实。
二、rules/:每条规则都要有"执行手段"
CLAUDE.md 讲架构,rules/ 讲禁令。我给 rules/ 定了一条元规则:
AI 犯一次错,要么加一条规则,要么加一个检查。没有执行手段的规则不算规则。
踩坑后按成本从低到高自问:
- 能不能加一条 lint?(最便宜)
- 加不了 lint,能不能加一个 hook 检查?(grep 级,亚秒)
- 都加不了,才写成"AI 自觉遵守"的文字规则。
每条规则行尾都标上执行方式,比如:
markdown
- **`ViewerProcessConfig` 必须 `calloc` 分配**(零初始化),不许改回 `malloc`。
ABI 升级会在结构体末尾追加字段,这些字段"非 0 即开",malloc 的堆垃圾几乎必然非 0,
等于悄悄打开了某个图像处理开关,画面变淡掉色还没有任何报错。〔执行:ffi-safety-reviewer〕
- `viewer_bindings_generated.dart` 是 ffigen 产物,**禁止手改**。〔执行:pre_write_guard〕
- **静态成员不跨 isolate**:每个 isolate 各自加载原生库。〔执行:自觉〕
规则按主题拆文件(工作流、原生层、代码风格、生成文件),rules/README.md 放一张索引表,AI 动手前先扫一眼命中了哪几条。
三、hooks:把检查变成自动的
Claude Code 的 hooks 配置在 .claude/settings.json 里,可以在特定时机执行脚本,不依赖 AI 记不记得:
json
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session_check.sh\"" }] }],
"PreToolUse": [{ "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/pre_write_guard.sh\"" }] }],
"PostToolUse": [{ "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/post_edit.sh\"" }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/stop_gate.sh\"" }] }]
}
}
1. PreToolUse:拦截编辑生成文件
AI 每次写文件之前触发。脚本从 stdin 读到要写的路径,命中黑名单就 exit 2 拦下,并告诉它正确做法:
bash
f=$(jq -r '.tool_input.file_path // empty')
if printf '%s' "$f" | grep -qE '(/lib/l10n/app_localizations|/lib/generated/|/pubspec\.lock$|/build/)'; then
echo "禁止编辑生成文件: $f" >&2
echo "依据: rules/30-generated-files.md(里面有重新生成的命令)" >&2
exit 2 # exit 2 = 阻断,stderr 会反馈给 AI
fi
同一个脚本还做了"自保护":拦截 AI 修改 hooks 和 settings 自身,防止它为了绕过检查把检查删了。
2. PostToolUse:只做亚秒级的事
编辑后触发。这里千万不要跑 flutter analyze:单文件 analyze 每次要几秒,AI 一轮改 30 次就是几分钟纯等待,而且单文件模式看不到跨文件的破坏。
这里只做:
- 格式化(
dart format,只格式化新建的文件:老代码是旧版格式风格,用新版 formatter 一碰就整个文件重排,diff 没法看); - 记录本轮改了哪些文件;
- grep 级的提醒(比如新代码里出现了绕过主题的硬编码颜色),只提醒不阻断。
3. Stop:AI 说"完成了"之前,跑一次 analyze
Stop hook 在 AI 准备结束这一轮时触发,对本轮所有改动的文件 跑一次 flutter analyze,有 error 就阻断,让 AI 继续修:
bash
# 防止无限循环:已经因为本 hook 阻断过一次,就放行
[ "$(jq -r '.stop_hook_active')" = "true" ] && exit 0
out=$(flutter analyze $changed_files)
if echo "$out" | grep -q ' error '; then
echo "$out" >&2
exit 2
fi
几个设计取舍:
- 只对 error 阻断,warning 只回显。项目里有存量 warning,如果拿 warning 阻断,AI 很快就学会"无视 analyze 输出";
- 不在 Stop 里跑测试。全量测试要 40 多秒,每轮都跑太贵,留给提交前;
- 补上 Bash 通道的盲区 。PostToolUse 只能感知 Write/Edit 工具,AI 用
sed -i或 Python 脚本批量改文件时完全绕过了它。我们就遇到过 AI 用脚本删了 26 个 import,analyze 被直接跳过。解决办法是 Stop 时用git diff+ 文件哈希做快照对比,把"这一轮新变化的 dart 文件"全找出来。
另一个项目里,我们还在 Stop hook 里检查:"本轮改了业务代码,但没有写技术文档条目"就拦一次,要求 AI 补上需求的技术文档。
四、守卫测试:把"规范"写成单元测试
有些规范 lint 表达不了,hook 的 grep 又太粗,就写成守卫测试 :扫描 lib/ 源码,违反规范就让测试失败。
比如"动画时长和曲线必须用统一的 AppMotion,不许手写":
dart
final _duration = RegExp(r'\b(duration|reverseDuration|transitionDuration)\s*:\s*(const\s+)?Duration\(\s*(milliseconds|seconds)\s*:');
final _curve = RegExp(r'\b(curve|reverseCurve)\s*:\s*(const\s+)?Curves\.(?!linear\b)');
/// 例外只进白名单,并写清理由
const _allowed = {
'lib/base/motion.dart': '预设本身',
'lib/base/component/listview.dart': '加载转圈的匀速循环',
};
void main() {
test('动画参数不许手写 Duration / Curves', () {
final violations = <String>[];
for (final f in Directory('lib').listSync(recursive: true).whereType<File>()) {
if (!f.path.endsWith('.dart') || _allowed.keys.any(f.path.startsWith)) continue;
final lines = f.readAsLinesSync();
for (var i = 0; i < lines.length; i++) {
if (_duration.hasMatch(lines[i]) || _curve.hasMatch(lines[i])) {
violations.add('${f.path}:${i + 1} ${lines[i].trim()}');
}
}
}
expect(violations, isEmpty, reason: '请改用 AppMotion:\n${violations.join('\n')}');
});
}
项目里类似的守卫测试有好几个:颜色只能用设计 token、毛玻璃只能用统一组件、字重只能用统一常量、原生内存的分配方式......它们对人和 AI 一视同仁,谁写错了跑测试时都会报出来。
五、agents 与 skills:把"流程"固化下来
agents:专职审查员
在 .claude/agents/ 下定义子代理,例如 ffi-safety-reviewer.md:
markdown
---
name: ffi-safety-reviewer
description: 审原生层的内存安全:结构体是否 calloc + 逐字段赋值、释放器是否配对、
asTypedList 是否拷贝后才 free。触发:改动落在 lib/base/uniview/ 或 native/;
画面莫名变色、直方图空白、解码后偶发崩溃。
tools: Bash, Read, Grep, Glob
---
你是原生层内存审查员。**只出报告、不改代码。**
这层的失效方式是静默的:不崩、不报错,只是画面悄悄变样。
所以"看起来没问题"不是结论,逐条比对才是。
...
还有"影响面分析"(改了公共组件,列出所有受影响的页面)和"代码定位"两个代理。
skills:固定工作流
在 .claude/skills/ 下按场景写流程:bug-fix(修 bug)、feature-dev(开发需求)、add-panel(新增一个图像处理面板的 7 个步骤)、release(发版)、language-sync(多语言同步)、packaging-debug(打包排查)等。
以 bug-fix 为例,核心是先分档、再决定谁来验证:
| 档位 | 判据 | 谁验证 |
|---|---|---|
| 视觉 | 显示不对、样式问题 | 人看截图 |
| 逻辑 | 状态、数据错误 | 单元测试 |
| 性能时序 | 卡顿、竞态 | 实测 + 日志 |
| 平台集成 | 只在某个系统上出问题 | 对应系统真机 |
AI 第一次回复就要给出分档结论,收尾必须做影响面回归。
六、一个需求的完整流程:从复述到提交
前面讲的是"约束",这一节讲"流程"。来了一个需求,AI 不是直接开写,而是按下面的顺序走。这套流程也写成了 feature-dev skill,AI 每次接到需求都会按它执行。
css
复述需求 → 定档 → 方案设计 → 需求文档 + 计划文档 → 【A:检验计划】→ 测试文档(业务 / 性能)
→ 影响面分析 → 逐个任务实现 →【B:检验任务】→ 验证到一轮 clean → 性能分析
→ 交叉审查 → 总结文档 → 归档 → 提交前全面检查(含影响面回归)→ 提交
第 0 步:先复述需求
AI 的第一次回复必须先用自己的话把需求说清楚,不许直接开改,哪怕是一行的小改动(小改动最容易把需求理解反)。复述要落到四项:
| 项 | 写什么 |
|---|---|
| 场景 | 什么状态下、哪个界面、什么前置条件会碰到 |
| 现状 | 现在的行为(读过代码就给出文件和行号,不许凭印象) |
| 要做成什么 | 改完之后用户会看到什么 |
| 落点 | 预计改哪几个文件,为什么是这里 |
复述完如果还有会改变做法的分歧点,一次性问清楚再动手;命名、缩进这类有惯例的,AI 自己定,回复里说一句就行。
第 1 步:定档
| 档 | 判据 | 流程 |
|---|---|---|
| 简单 | 单模块、不动原生层和接口、≤ 3 个文件 | 直接实现 → 验证 → 影响面检查,只写总结文档 |
| 复杂 | 新功能面板、动原生层、跨 3 个以上模块、有设计图 | 走完整流程,三份文档齐全 |
拿不准就按复杂走。
第 2 步:方案设计与文档
复杂需求先让 AI 出 2~3 个方案 并对比取舍,我拍板后再写文档。每个需求一个目录 docs/features/<需求名>/:
requirement.md:业务规则、数据来源、验收标准;plan.md:任务拆分。每个任务写清楚改哪些文件、怎么验证,粒度以"一个任务完成后可以独立验证"为准;summary.md:做完后写,见第 8 步。
文件头必须写实现状态 :未开始 / 方案待评审 / 部分落地(哪几条 + 日期) / 已落地(日期 + 分支),并把"已定"和"待定"分开列。AI 读文档时判断新鲜度全靠这一行。
A:检验计划------计划写完先"测"一遍
计划写错了,后面执行得越认真,偏得越远。所以 plan.md 写完不直接开干,先做一轮检验。我一般让另一个模型(Codex)或者一个新会话来审,避免"自己写、自己审":
| 检查项 | 怎么查 |
|---|---|
| 覆盖需求 | requirement.md 里的每一条验收标准,在计划里都能找到对应的任务 |
| 可以验证 | 每个任务都写了"做完怎么证明它对":跑哪个测试、看哪个现象 |
| 顺序正确 | 任务之间的依赖关系没有颠倒,前一项做完后一项才能开始的,顺序要对 |
| 落点真实 | 计划里提到的文件、类、方法在当前代码里真实存在(AI 很容易凭印象写一个不存在的方法名) |
| 风险标出 | 动原生层、动公共组件、动接口的任务单独标出,提前安排专项审查 |
检验发现的问题改回 plan.md,计划改到"没有意见"才进入实现阶段。
目录名用英文。中文目录名在
git ls-files里会被转义成八进制,grep、diff、脚本里全是乱码。
第 3 步:测试文档(业务 + 性能两份)
测试用例让 AI 逐文件通读源码后产出,分成两份,编号互不冲突、内容互不重复:
| 业务测试用例集 | 性能测试用例集 | |
|---|---|---|
| 关心什么 | 对不对、全不全、状态串不串 | 快不快、内存涨不涨、崩不崩 |
| 判定依据 | 界面和数据的期望值,不设时间阈值 | 毫秒 / MB / 帧数阈值 |
| 构建模式 | debug / profile / release 均可 | 必须 release |
| 用例编号 | TC-F-<模块>-nn |
TC-<模块>-nn |
每条用例有优先级(P0 / P1 / P2)和自动化状态(全自动 / 部分自动 / 手工),一眼能看出哪些已经有测试兜底。目前业务用例 188 条,其中 83 条有自动化覆盖;性能用例 149 条。
性能用例里挑出 24 条作为发版红线,比如:
| 用例 | 场景 | 红线 |
|---|---|---|
| 连续换图 200 张 | 原生内存泄漏 | 静置后内存增量 ≤ 250MB,每 50 次增量不得单调递增 |
| 单张大图解码 | 内存峰值与回落 | 峰值增量 ≤ 900MB,切走后残留 ≤ 60MB |
| 拖动直方图手柄 | 不得触发重解码 | 拖动期间解码次数 = 0,掉帧 ≤ 5 帧 |
没有历史数据的阈值标为"基线型":第一轮执行只负责采集基线,从第二轮起才作为门禁,避免拍脑袋定阈值。
第 4 步:影响面分析
AI 改代码最常见的翻车方式不是"改错了",而是"改对了这里,弄坏了那里"。所以专门有一个 impact-analyzer 代理,只分析、不改代码,输出"必须回归的清单 + 建议跑的测试"。
什么时候必须做:
- 改动落在公共组件、全局服务、原生解码层这几个目录;
- 改了组件的参数、函数签名、控制器的响应式字段;
- 单次改动超过 20 行。
用两次:写代码之前 做一次,摸清引用面再动手;提交之前再做一次,作为回归清单。
怎么分析: 不是泛泛地"全局搜一下",而是把这个项目特有的传播路径写进代理的说明里,让它逐条过:
| 改了什么 | 会影响到哪 |
|---|---|
| 公共组件的参数 / 签名 | 搜组件名,所有用到它的面板都要回归;如果它从统一导出文件(base.dart)导出,波及面更大 |
| 全局服务 | 服务注册顺序;所有 XxxService.to 的取用点;夜间模式、色盲模式、语言这三个会穿透到全部界面 |
| 原生解码层 | 所有走原生解码的格式;直方图、星点分析、去星、标注、导出都吃同一份解码结果 |
控制器 mixin 的 with 顺序 |
顺序就是依赖顺序,改动会静默影响后面所有 mixin |
| 面板状态字段 | 本地 / 设备两个 Tab 的状态是成对写的,改一个必须同步另一个 |
输出格式固定:
markdown
## 改动面
<文件:行数,一句话说改了什么>
## 必须回归(人工)
- [ ] <具体到哪个面板 / 哪个操作 / 期望看到什么>
## 建议跑的测试
fvm flutter test test/xxx_test.dart test/yyy_test.dart
## 判定
影响面:小 / 中 / 大 ------ 一句话理由(按实际引用面判断,不按 bug 严重程度)
还有一条反"形式主义"的规定:引用面确实很窄(直接引用的文件 ≤ 3 个、单模块、改动 ≤ 20 行)时,直接写"影响面:小,可免人工回归",不许为了凑格式编回归项。只改注释、删无用 import 这类"删掉这次 diff 程序行为完全不变"的改动,可以免做影响面分析,但静态检查照样要跑。
第 5 步:实现
- 按
plan.md逐项做,每项都要通过下面的 B 检验才能勾掉(这也是换工具、换会话时的交接依据); - 逻辑类改动先写测试再实现;
- 按落点走对应 skill:新增图像面板走
add-panel(带脚手架脚本),动原生层走uniview-ffi; - 新增文案先硬编码并留
// TODO: i18n,最后统一走多语言同步流程,避免中途反复改 10 种语言。
B:检验任务------做完一项测一项
每个任务做完,马上验证这一项,通过了才勾掉、才开始下一项,而不是全部写完再统一测:
- 对照任务自己的验收方式:就是 A 阶段要求每个任务都写好的"做完怎么证明它对";
- 跑相关测试:逻辑类跑对应的单测,并确认新写的测试在改动前是失败的(证明测试真的测到了东西);
- 静态检查:本次改动的文件 analyze 0 error;
- 对照计划检查有没有做多、做少:没要求的"顺手优化"不做,计划里的要求一条不漏。
这样做的好处是:问题在哪一项引入的一清二楚,不会出现全部写完一跑测试一片红、不知道从哪查起的情况。中途额度用完、换工具接手时,已勾掉的任务也都是确认过的,可以放心往下接。
第 6 步:验证到"一轮 clean"
bash
fvm flutter analyze # 0 error
fvm flutter test # 全量 1600+ 用例全绿
一轮 clean = 两条命令连着跑一遍全过,中间没有任何手工干预。 没到就继续改,不许报"基本完成"。
验证按类型分工:
| 改动类型 | 谁验证 |
|---|---|
| 视觉 / 布局 / 文案 | 我来验收,AI 只说明改了什么 |
| 纯逻辑 / 数据处理 | AI 补单测并跑绿 |
| 性能 / 时序 / 并发 / 内存 | AI 自己起 App 实测(见下一步) |
第 7 步:性能分析
性能问题不能靠"感觉快了",要有数字,而且要改动前后对比:
- 用 profile / release 模式测 。debug 模式下数据会失真,比如
Isolate.run冷启动在 debug 下约 600ms,是调试器造成的假象; - AI 自己驱动 App 取数:通过 Dart VM Service 连上正在运行的 App,脚本化地重复操作(比如连续换图 200 次),采集耗时时间线和内存曲线,不需要我手动去点;
- 改动前先跑一遍拿基线,改完再跑一遍,两组数字一起写进总结;
- 对照性能用例集里的红线判定是否达标。
大图内存优化就是这么做的:先测出"并发解码导致峰值翻倍""decodeImageFromPixels 让 Dart 堆多占 100~250MB"等问题和具体数字,逐项优化后再用同一套用例复测。
第 8 步:审查与收尾
- 交叉审查 :用 Codex 审一遍本次改动(下一节详细讲);动了原生层的,再派
ffi-safety-reviewer逐条核对内存规则; - 写
summary.md:实测结果写真实数字(不许写"性能良好"),再附一份"待人工验证清单"(直接用影响面分析的输出); - 归档 :需求闭环后,整个目录移进
docs/archive/plans/,同时修正引用它的路径; - 不主动提交:我说提交才提交,提交前走下面的全面检查。
第 9 步:提交前全面检查
提交不是简单的 git commit,而是一个固定的检查命令 /commit(make commit 调的也是它),把"提交前该做的事"一次做全:
① 收集改动,一个都不漏
git status -uall和git diff HEAD一起看:前者能展开未跟踪目录里的每个新文件(新增的图片、字体、资源),后者只看得到已跟踪文件的改动,只看一个就会漏;- 再跑一次
git status --ignored:有本该提交却被.gitignore误忽略的文件,单独git add -f; - 默认只提交本次会话改动的内容,不把工作区里别的半成品一起带进去。
② 代码检查三连
- 静态检查
flutter analyze:和基线(0 error)对比,新增 error 判为严重;warning 数量比基线涨了判为警告; - 全量测试
flutter test:和基线(1600+ 全绿)对比,本次改动引入的失败判为严重;与本次无关的存量失败单独列出,不阻断提交; - 人工审查 diff (由 AI 逐项过):逻辑错误、空安全、资源未释放(控制器的
onClose、原生内存、文件句柄、监听器)、误删代码、调试残留(print、临时配置)、硬编码、未翻译的文案;- 动了原生层的,按内存规则逐条核对;
- 动了公共组件、全局服务或超过 20 行的,再跑一次影响面分析。
③ 分级处理
| 级别 | 含义 | 处理 |
|---|---|---|
| 🔴 严重 | 崩溃、逻辑错误、编译不过 | 列出 文件:行号 和改法,问我是否先修;我说改就停下等改完重跑,我说不改才提交 |
| 🟡 警告 | 潜在隐患 | 提交后列出 |
| 🔵 建议 | 可优化 | 提交后列出 |
没有严重问题才提交。提交信息按 Conventional Commits,描述用中文,scope 用功能域,例如 fix(histogram): 默认对数轴改回来、feat(psf): 倾斜分析四角补圆度数值。push 单独问我一次,不主动推送。
④ 提交后兜底
再跑一次 git status,确认工作区干净。还有残留,说明第 ① 步漏加了或者被 .gitignore 吃掉了,必须查清楚,不许默默放过。
⑤ 失败不许假装成功
任何一步失败,打印原因并停下,不许吞掉错误、不许假装提交已完成。这一条是专门写给 AI 的:它在遇到失败时,有时会倾向于"换个说法"把结果报成成功。
七、Codex 怎么用:额度兜底 + 交叉审查
我的主力是 Claude Code,Codex 主要用在两个场景:Claude 额度用完时接手继续干活 ,以及审查代码。两个工具共用一套规范,切换几乎没有成本。
1. 让两个工具读同一份上下文
Codex 读的是 AGENTS.md,而且是分层读取 的:全局的 ~/.codex/AGENTS.md、仓库根目录、当前子目录,越具体的越优先。前面说过,仓库根目录的 AGENTS.md 只写一句话指向 CLAUDE.md,这样两边看到的是同一份说明,不会出现"改了一边忘了另一边"。
需要注意的是,hooks 是 Claude Code 专有的 ,Codex 不会触发 .claude/settings.json 里的检查。所以规范要分两类看:
| 约束 | Claude Code | Codex |
|---|---|---|
CLAUDE.md / AGENTS.md、rules/ |
✅ | ✅(通过 AGENTS.md 指过去) |
lint(analysis_options.yaml) |
✅ | ✅ |
| 守卫测试 | ✅ | ✅ |
| hooks(自动格式化、拦截生成文件、Stop 时 analyze) | ✅ | ❌ |
这也是我越来越倾向把规则写成 lint 和守卫测试 的原因:它们与工具无关 ,换任何 AI、甚至人来写,都一样生效。只有 hook 能做的事才放 hook。用 Codex 时,我会在 AGENTS.md 里补一句收尾要求:"结束前必须跑 fvm flutter analyze 和相关测试",把 hook 自动做的事改成显式要求。
2. 额度兜底:用"需求文档"做交接
Claude 额度用完往往发生在任务进行到一半。如果交接信息只存在对话里,换工具就得重新解释一遍。
我的做法是进度落盘 :就是上一节的三份需求文档,plan.md 里每完成一项就勾掉。
切到 Codex 时只需要一句话:
bash
读 docs/features/xxx/plan.md,从第一个未完成的任务继续,规则见 AGENTS.md。
上下文全在仓库里,Codex 接手后的产出和 Claude 写的风格基本一致;等 Claude 额度恢复,再切回来也一样。
3. 交叉审查:让另一个模型来挑错
这是我用 Codex 最多的场景。同一个模型写的代码,让它自己审,很容易"自己说服自己":写的时候漏掉的边界条件,审的时候往往也会漏。换一个模型来审,盲区不同,能发现更多问题。
常用的几种方式:
(1)审查未提交的改动
Claude 写完一个功能后,在 Codex 里用 /review 审当前工作区的改动,或者用非交互模式直接出报告:
bash
codex exec "审查当前 git diff:重点看空安全、异步竞态、资源释放(Timer/StreamSubscription/原生内存)、是否违反 rules/ 下的规则。只输出问题清单,按严重程度排序,每条给出文件和行号,不要改代码。"
(2)审查整个分支
提测前审一次整个分支相对主分支的改动,重点看跨文件的影响:
bash
codex exec "对比 develop 分支,审查本分支所有改动。重点:公共组件的改动影响了哪些页面;新增的异步逻辑有没有竞态;多语言文案是否 10 种语言都补齐。"
(3)专项审查
针对高风险的模块,把对应的规则文件直接喂给它,让它逐条比对:
bash
codex exec "按 rules/10-native-ffi.md 逐条审查 lib/base/uniview/ 下的改动,每条规则给出"符合/违反/无关",违反的给出代码位置。"
(4)审完回到 Claude 修
Codex 只出报告,不改代码(审查时我用只读沙盒)。报告交回给 Claude Code,让它逐条核实后再修:审查意见不一定都对,有的是误报,要求它先在代码里证实问题存在,再动手。
一个经验:审查的提示词里一定要写清楚**"只输出问题,不要改代码"和"按严重程度排序"**,否则容易得到一大堆格式、命名之类的建议,真正的 bug 反而被淹没。
4. 其他用法
- 第二意见:遇到方案选择(比如状态放在哪一层、缓存怎么设计),让两个模型各出一个方案,对比取舍;
- 排查疑难 bug:Claude 卡住、反复修不好的问题,换 Codex 从头看一遍,经常能换个角度找到原因;
- 批量机械改动:比如 API 迁移、废弃用法替换这类规则明确的改动,交给 Codex 跑,Claude 的额度留给需要设计的任务。
八、实际成果
这套方式最完整的一次实践,是一个 Flutter 桌面端看图软件(macOS + Windows),由我一个人借助 AI 从 0 开发:
- 900+ 次提交 ,业务代码 5 万多行 Dart;
- 测试代码 3.4 万行 ,1600+ 个测试用例,本地全量跑一遍约 50 秒;
- 覆盖原生库集成(C/C++、Rust)、图像处理、局域网设备连接、macOS Quick Look 扩展、Windows 资源管理器扩展、签名公证与安装包发布;
- 已发布多个正式版本。
一个人能把这样体量的项目做下来、并且质量可控,靠的不是 AI 写代码有多快,而是规范 + 自动检查 + 交叉审查让 AI 的产出可以被信任。
九、体会
- 先复述、再定档、再动手,流程比提示词更重要;
- 计划要检验,任务也要检验:计划先审再做,任务做一项测一项;
- 提交前全面检查:改动收集不漏、analyze + 全量测试对比基线、影响面回归,严重问题不清零不提交;
- 规则要有执行手段,否则就是一纸空文;
- 优先写成 lint 和守卫测试,它们与工具无关,Claude、Codex、人都适用;hook 只做 hook 才能做的事;
- hook 要便宜:编辑后亚秒级,重活放到 Stop;
- 阻断要克制:只拦真正的错误,否则 AI(和人)都会学会无视;
- 文档会腐坏,让 AI 以代码为准;
- 进度落盘:需求文档就是交接文档,换工具、换会话都能无缝继续;
- 写和审用不同的模型,交叉审查比自审靠谱得多。
AI 编程工具更新很快,但"把经验沉淀成可执行的约束"这件事,不管换什么工具都有价值。