最近几个礼拜,我反复在干同一件很憋屈的事:把 AI 写好的 Flutter 页面整个删掉重写。真不是 AI 能力不行,而是它每次都按自己“脑内模板”来,和我这个项目的目录分层、状态管理风格、命名习惯完全对不上。直到我花了差不多两天,用 Claude Code + Codex 给一个内部 Flutter 工程搭了一套“AI 可读的工程规范”,返工率才肉眼可见地降下来。这篇文章想完整分享这套方法:规范文件怎么写、两个 AI 工具怎么分工、自检闸门怎么设,以及我一路踩过的坑。
如果你也在用 AI 写 Flutter 或改 Flutter 项目,请先记住一句话:AI 不是一个全知的老程序员,它更像一个阅读理解能力极强、但完全不懂你项目历史的新实习生。你不把“本项目的规矩”写清楚,它就只能靠猜。而工程规范,就是把这家公司、这个团队、这个仓库里所有“隐形规矩”翻译成 AI 能读懂、能执行、能自查的说明书。
1. 翻车现场:AI 写 Flutter 代码的四个典型症状
1.1 目录与命名反复横跳
我见过最频繁的问题,是 AI 生成的文件位置和命名规则一塌糊涂。一个产品详情页,第一次生成在lib/pages/product_detail.dart,第二次让它改个字段,它又把相关 widget 丢进了lib/widgets/,第三次换个需求,它甚至直接在lib/main.dart里堆了 300 行。
为什么会这样?因为主流 Flutter 教程、开源项目和教学 Demo 用的目录结构五花八门。有些喜欢lib/pages,有些喜欢lib/screens,还有些按 feature 分目录。模型训练数据里这些风格都有,概率上谁也不占绝对优势,于是它每次都在“猜”。这个猜测没有恶意,但结果就是:同一个项目里,列表页可能在pages,详情页可能在features/product/presentation,你每天都要做“垃圾分类”。
Dart 本身不强迫你用什么目录结构,flutter create生成的默认工程也不复杂。可真实项目一旦超过几个页面,目录和命名就是最基础的“骨骼”。AI 没有意识到,这个仓库的命名一旦混起来,日后定位文件全靠全局搜索,维护成本直接暴涨。
1.2 状态管理三周换三种流派
Flutter 的状态管理是另一个重灾区。今天生成订单页,AI 用了ChangeNotifier;明天生成用户中心,它换成了Provider;后天写登录流程,它又给你整出flutter_bloc。同一套代码里三套状态管理方案并存,互相没有关联、到处都能 setState,项目看起来像个“流派博览会”。
根源在于:Flutter 官方没有钦定唯一的状态管理方案,生态里 Provider、Riverpod、Bloc、GetX 都有大量拥趸。AI 从训练数据里学到的不是“你这个项目用什么”,而是“这个问题通常有人怎么解”。你只给一句“做一个登录页面”,它内部概率分布上可能刚好选中 Bloc,下次再问,上下文变了,它又选了 Riverpod。它不是失忆,是根本没有“项目风格基线”这个概念。
这种翻车最隐蔽。因为代码能运行、页面能跳转、逻辑能跑通,但团队里每个人接手时都要先问一句:这个功能到底走哪套机制?跨 AI 生成的功能模块之间更是难以复用,改一个状态,另外三个页面跟着遭殃。
1.3 增量活干成重构大手术
还有个让我血压飙升的毛病:AI 非常热衷于“大面积修改”。你只想让它给某个页面加一个空状态提示,它顺手把整个页面组件重写了,还“顺手”清理了旁边两个文件的命名,甚至还“贴心”地帮你把http换成了dio。
这种行为的本质,是 agent 类工具在长上下文里追求“整体最优解”。它觉得既然要改,不如把相关的都理顺。但对人来说,代码评审最怕的不是代码差,而是不知道你到底改了什么。一次 PR 里混着“新增功能 + 重构 + 换依赖 + 调样式”,出了问题根本没法定位。
更刺激的是,AI 偶尔会引入根本不存在的工具包版本,或者为了修复一个 lint 警告,把另一个模块的公共接口悄悄改了。这些都属于“超越任务范围”的隐性破坏,比命名混乱更难察觉。
1.4 依赖扩展毫不收敛
Flutter 项目里 package 的选择非常影响架构。可 AI 对“项目现在用了什么依赖”往往不敏感。同是网络请求,你项目里已经在用http,它却给你dio;你要格式化日期,你已经有intl,它还去 pubspec 里搜罗一个新库;最离谱的是,有次它为了做一个图片缓存,直接给我引了个需要原生配置的第三方包,而项目原来根本不打算接入那套原生方案。
不是说新依赖不能加,而是“加依赖”这件事在工程里应该有特权和成本意识。新增一个 package,意味着要维护它的版本、兼容 API、关注安全通告,还可能引入原生插件,导致打包体积和平台配置变化。AI 没有这个意识,它只知道“这个库能解决当前问题”。所以依赖约束必须放进规范里,而且要写得非常死。
2. 核心思路:把工程规范变成 AI 能执行的文件
2.1 什么是“AI 可读的工程规范”
既然 AI 的问题在于“不知道项目的隐形约定”,那解法就很直接:把约定写进它每次开工前必须读的文件里。这个文件通常可以叫CLAUDE.md,也可以叫AGENTS.md,本质上都是给 AI 看的“项目说明书”,很多主流编码代理工具都支持在上下文中读这类文档。
但它和传统 README 不一样。README 面向人,目标是“讲清楚项目是什么、怎么跑”;AI 规范面向模型,目标是“约束模型别乱来”。所以它不能是抒情散文,不能是“请尽量保持整洁”这样的模糊劝告,而应当是“禁止什么、必须什么、违反后怎么检查”的命令式文本。
我习惯把规范文件分成三大块:目录与命名约定、代码风格与状态管理约定、AI 工作流约定。目录和命名负责管骨架,代码风格负责管血肉,工作流约定负责管流程。三者缺一不可。
2.2 规范文件应该覆盖哪些具体主题
根据我遭遇过的翻车现场,下面是 Flutter 项目规范文件里最值得写的主题:
| 主题 | 覆盖的问题 |
|---|---|
| 目录结构与文件命名 | AI 把页面乱放、命名风格混乱 |
| 状态管理策略 | 多种状态管理方案混用 |
| 依赖管理 | 随意新增 package、版本不受控 |
| 路由与页面跳转 | 路由散落各处、跳转靠硬编码 |
| 数据层与网络请求 | UI 直接发请求、DTO 与模型不分 |
| 异步与错误处理 | 未处理 Future、异常吞掉 |
| 完成定义(Definition of Done) | AI 不跑测试、不检查代码格式 |
有些团队还会把国际化、主题色、本地存储策略写进去,这个可以根据仓库现状补充,但没必要一口气堆一百条。规范文件不是越全越好,而是“每一条都能被验证”最好。
2.3 按“命令式 + 示例”的方式写规则
规范文本写得好不好,直接决定 AI 守不守规矩。我踩过很深的坑是:用“尽量”“建议”“可以”这类软词写规则,AI 大概率当成空气。后来我把所有规则改成“禁止/必须/唯一”的硬语气,并附带正反例,效果立竿见影。
举个例子,同样一句话,两种写法完全不同:
- 反例:请保持项目整洁,减少硬编码。
- 正例:业务文案禁止直接出现在 widget 中,必须统一放入
lib/core/constants/text_strings.dart;页面只能通过常量引用文案。
反例里的“整洁”“减少”都是模糊词,AI 不知道阈值,也不知道检查方式。正例里有对象、有位置、有动作,AI 至少知道该往哪儿放代码。再复杂一点的规则,我会写成“规则 + 示例 + 例外”三行,示例放一小段可编译的 Dart 代码,让模型照着抄。
拿状态管理举例子:
// 合规:异步加载使用 BlocBuilder 订阅状态 class ProductListPage extends StatelessWidget { const ProductListPage({super.key}); @override Widget build(BuildContext context) { return BlocBuilder<ProductBloc, ProductState>( builder: (context, state) { if (state is ProductLoading) { return const Center(child: CircularProgressIndicator()); } return ListView.builder( itemCount: state.products.length, itemBuilder: (context, index) => ProductCard(product: state.products[index]), ); }, ); } }规范文档里有这样一段“范本代码”,AI 就不容易走偏。给它看一百次“应该这样”,不如给它看一个可以直接模仿的片段。
2.4 给 AI 立工作流规矩
工程规范不仅约束代码长什么样,还要约束 AI 的工作过程。这一步是我后来才补上的,但作用非常大。
你可以在规范文件里直接写:开工之前必须阅读本文件;生成任何改动前,先列出“受影响的文件清单”;完成改动后,必须运行flutter analyze和dart format --output=none --set-exit-if-changed .。这些条款不针对代码风格,针对的是模型的行为模式。
为什么要这么写?因为 Claude Code 这类 agent 型工具有很强的“长链路自治”能力。它会自己翻目录、自己决定改哪些文件、自己往前冲。如果没有强制拆解步骤,它很容易一口气改出二十个文件,然后把检查结果抛给你。所以工作流规矩实际上是把“大任务拆小、小任务确认、完成后自检”这套人类协作方式,变成模型必须遵守的路径。
3. 实操一:让 Claude Code 按规范生成 Flutter 代码
3.1 开工前先做上下文“对表”
我在项目根目录进入 Claude Code 的交互界面后,第一件事不是让它写需求,而是让它做上下文对齐。一般我会先发一段类似这样的话:
请你先阅读根目录的 CLAUDE.md,并总结这个项目的关键约定。先不急着写代码,回答我几个问题:项目的目录结构按什么划分?状态管理用哪一套?网络请求封装在哪一层?依赖管理有什么限制?
这个动作看起来浪费时间,但极其重要。它确保模型在生成代码前已经“加载”了规范,而不是等到写了一半才想起来去看。模型如果回答得不对,或者回答得很空,说明它没有真正理解规范,这时候再叮嘱它重新阅读。对表通过后,我才会把下一个真实任务交给它。
3.2 用“任务卡”替代一句话需求
直接跟 AI 说“给我写个商品列表页”,它一定会自由发挥。我后来养成的习惯,是把需求写成一个任务卡,包含背景、目标、范围、约束、验收条件五部分。格式不复杂,但每一项都有实际意义。
实际我常用的模板是这样:
任务:新增商品列表页 背景:目前在商品分类页进入列表,需要展示商品名称、价格、缩略图 范围:只允许新增 lib/features/product/ 下的文件,不允许改动现有分类页与数据层 约束:
- 遵循根目录 CLAUDE.md
- 状态管理使用 flutter_bloc,禁止在 build 方法里直接 setState
- 商品接口调用必须走 product_repository,不直接在 widget 里发请求 验收:
- flutter analyze 无新增错误
- dart format 通过
- 不修改与任务无关的任何文件
这种任务卡对 AI 的约束效果非常明显。范围字段告诉它别越界,约束字段替代口头叮嘱,验收字段是它可以自行判断的标准。我还遇到过几次它无视规范的情况,但只要验收里写明“不得修改无关文件”,它就会收敛很多。
3.3 强制“自检三步”循环
很多 AI 生成的代码初看没问题,一跑flutter analyze就全露馅了。所以我会在任务卡里要求模型在生成结束后自己运行三步检查:
flutter analyze dart format --output=none --set-exit-if-changed . flutter test其中最容易被忽视的是dart format --output=none --set-exit-if-changed .。这个命令只检查格式、不改动文件,一旦有格式化差异就返回非零退出码。用它来做“格式门禁”,比让 AI 自己宣称“代码已经格式化”更可靠。
实测下来,让模型自己跑检查,而不是我们拿到代码后再给反馈,能节省好几轮往返。因为模型会一边跑一边看报错,然后自己修掉明显问题。它写出的第一版质量不一定高,但加上这个自检循环后,很多低级错误在交到你手里之前就被消掉了。
3.4 用 git diff 限制修改面
Claude Code 生成完代码后,我从不直接看文件本身,先看git diff --stat。这个命令会告诉我它到底动了多少文件。如果任务卡里写的是“只新增一个页面”,而 diff 里出现 6 个无关文件被改动,我会直接要求它还原无关改动,而不是手动修改。
还有一个习惯:每轮任务开始前,我会基于 main 分支起一个新分支,例如feature/ai-product-list。这样生成的所有改动都留在分支上出不了大事,一旦发现 AI 跑偏得太离谱,直接git checkout丢弃分支,再重新开一轮就是。把 AI 当成 PR 提交者,而不是直接往主分支上推代码的双手,这个心理模型非常重要。
3.5 依赖变更必须亮红灯
在规范里我会明确写明:禁止在任务执行过程中随意新增 pubspec.yaml 依赖;如果确实需要新依赖,必须先停下来向人类说明原因,并使用flutter pub add明确安装。为什么要走这一道流程?因为 AI 新增依赖往往不是从产品需求出发,而是从“某个包里有个函数能解决眼前问题”出发。
有一次它为了展示一个 loading 动画,引了一个动画库,结果那个库需要修改 Android 原生配置。这在纯 Dart 环境下根本谈不上必要。所以现在凡是 diff 里出现pubspec.yaml或pubspec.lock的变化,我都会第一个看,而且通常会让 Claude Code 先把这个改动单独说明清楚,再决定要不要保留。
4. 实操二:让 Codex 当独立评审,把 AI 写坏的内容拦下来
4.1 为什么要“第二个模型”而不是自评
Claude Code 写完代码后,我还会把它交给 Codex 做一轮独立审查。很多人会问:让同一个模型自己检查自己不行吗?答案是效果很差。因为模型生成代码和检查代码时共享同一套内部表征,它在生成阶段形成的错误认知,在自评阶段大概率会原样路透出来。就像一个人写完文章自我欣赏,常常发现不了笔误,换一个陌生编辑却一眼能看出来。
Codex 作为另一个模型,对代码的“审美”和“盲区”都不完全一样。让它独立看一遍 git diff,更容易指出“这个命名和项目风格不像”“这里空安全处理有问题”“这里并发场景会崩”等 Claude Code 可能忽略的点。我把它当作第二个 reviewer,而不是 20 条规则的复读机。
4.2 Codex 审查的四个维度
我给 Codex 的审查提示词一般包含四个固定维度。整理成表格方便你直接用:
| 维度 | 核心检查点 | 期望输出 |
|---|---|---|
| 架构一致性 | 是否违反目录分层、依赖倒置、命名规则 | 列出违规文件和具体位置 |
| 正确性 | 空安全、异步生命周期、异常处理 | 指出风险点与原因 |
| 可维护性 | 重复代码、函数过长、命名语义混乱 | 给出建议修改方向 |
| 性能与资源 | build 方法中有无重复计算、有无不必要 setState | 说明性能隐患 |
这四个维度基本覆盖了我翻车最多的场景。性能这条一开始我没加,后来发现 AI 很容易在一个大列表项里堆昂贵的构建逻辑,导致页面滑动卡顿,所以现在固定把它列进审查清单。
4.3 审查 prompt 怎么写才有效
实际操作时,我先把 Claude Code 改动的内容交给 Codex,而不是让它自己去翻整个仓库。抓取 diff 的命令很简单:
git diff main...HEAD > /tmp/ai_code_review.diff然后把 diff 内容放进 prompt。我的提示词框架大致如下:
你是资深 Flutter 代码审查者。请阅读下面这段 git diff,对照仓库根目录的 CLAUDE.md 检查:
- 是否违反目录约定与命名约定;
- 是否存在空安全、异步生命周期、异常处理问题;
- 是否存在重复代码或过度设计;
- 是否有明显性能隐患;
- 依赖是否出现不该有的新增。 请按严重程度从高到低输出,问题必须包含文件路径和行号,最后给出一个结论:可以合并 / 需要修改 / 不建议合并。不要直接改代码。
这里有一个关键点:不要让它直接改代码。一旦你把代码修改权交给它,它会忍不住重写整个表达式,审查就失去了独立意义。我只把 Codex 当“找茬的”,修复任务仍然交给 Claude Code,或者由我自己决定怎么改。
4.4 评审结论如何闭环处理
Codex 给出意见后,我不会逐条照单全收。先拿规范文档做裁判:如果意见和 CLAUDE.md 冲突,以规范为准;如果规范没有覆盖,再人工判断。判断完要修改的项,我会把审查意见整理后发给 Claude Code,告诉它“请根据这些意见修复,但依然遵循任务范围,不要扩大改动”。
遇到 Claude Code 和 Codex 对某条规则理解不一致时,我会把双方的意见对比放出来,人工裁决后把结论补写进规范文件。这其实就是把团队里的“架构评审记录”变成了 AI 可读的经验沉淀,一边用一边养规范,越用越顺手。
5. 一套可直接抄走的工程规范模板与自检闸门
5.1 顶层规范文档的完整模板
下面这个模板可以直接保存为CLAUDE.md,放到 Flutter 项目根目录,然后根据仓库现状裁剪。我尽量写得通用,但每一条都有对应的“防翻车”目的。
# 工程规范(AI 行为约束) ## 1. 目录与命名 - 所有业务功能放在 `lib/features/<feature_name>/` 下,禁止在 `lib/pages` 或 `lib/widgets` 下堆功能页面。 - 每个 feature 内部只允许出现: - `pages/`:页面组件 - `widgets/`:该功能私有的小部件 - `models/`:实体模型 - `repositories/`:数据访问 - 文件名使用 `snake_case`,类名使用 `UpperCamelCase`,widget 文件以对应组件名命名。 ## 2. 状态管理 - 异步状态管理统一使用 flutter_bloc,禁止在 widget 的 build 方法里直接 setState 处理异步数据。 - 一个业务页面对应一个 Bloc,禁止跨页面共用同一个 Bloc 实例来维护页面级状态。 ## 3. 数据层与网络 - 所有网络请求必须封装在 `repositories/` 中,UI 层禁止直接出现 `http` 或 `dio` 客户端。 - 网络返回原始 JSON 先用 `fromJson` 转成模型,再交给 Bloc 或页面,禁止把 `Map<String, dynamic>` 直接裸露在 UI 层。 - 所有异步操作必须显式处理错误,禁止空 catch 和未监听 Future。 ## 4. 依赖管理 - 禁止在功能开发中随意新增 pubspec 依赖。 - 如确实需要新依赖,必须先说明用途、替代方案,并经过人工同意后再执行 `flutter pub add`。 - 示例代码只能使用当前 pubspec 已存在的版本 API,禁止凭空使用仓库中不存在版本的函数。 ## 5. 完成定义 - 每次任务结束前,必须运行: - `flutter analyze` - `dart format --output=none --set-exit-if-changed .` - `flutter test` - 只有当上述命令全部通过后,任务才算完成。 - 禁止修改与任务无关的文件;涉及无关文件时必须提前显式说明。这个模板不包含路由写法、不包含主题定制,那些要按你项目的实际情况补。但就是这几条,已经能挡掉我前面提到的大多数翻车。
5.2 落地一个可执行的预检脚本
规范文档写得再好,如果每次都要人肉去跑命令,总有漏网之鱼。所以我给项目做了一个预检脚本,可以放在tool/ci.sh里,本地和持续集成共用同一套:
#!/usr/bin/env bash set -euo pipefail echo "==> flutter analyze" flutter analyze echo "==> dart format check" dart format --output=none --set-exit-if-changed . echo "==> unit tests" flutter test --no-pub这个脚本核心就是让机器守住底线。flutter analyze检查静态问题,dart format卡格式,flutter test保行为。哪个项目接入 AI 辅助开发,我建议都先把这样的脚本跑通,否则 AI 给你的“完成”根本不可信。
5.3 把预检脚本接入本地与 CI
本地接入我一般用 git hooks,命令式地设置执行路径:
mkdir -p .githooks cp tool/ci.sh .githooks/pre-push chmod +x .githooks/pre-push git config core.hooksPath .githooks这样每次执行git push前,都会自动跑一遍完整检查。AI 生成完代码后,它自己也会尝试跑检查。但双保险是必须的:模型可能跑过检查后又在修 lint 时改出新问题,而 pre-push 这个位置是最后一道物理闸门。
持续集成里同样跑这个脚本,作用不是给团队上眼药,而是给“AI 改出来的代码”和“人改出来的代码”同一个质量标准。任何绕过检查的内容,在合并前就会被拦下来。
5.4 人看 diff 时的四个重点
脚本只能拦掉“机器能判断的问题”,架构、设计、业务正确性永远需要人来看。但人看 AI 的 diff 不需要逐行读,我通常只看四个地方:
一是git diff --stat,确认改动面是否超出预期;二是pubspec.yaml和pubspec.lock,确认没有偷偷加依赖;三是新增文件里有没有违反命名或目录约定;四是关键业务逻辑有没有被“看起来能跑但语义不对”的代码替换。如果你的团队用了 AI 大量提交代码,这个精简版 review 流程能让 Code Review 时间缩短非常多。
6. 常见问题与排障技巧实录
6.1 规范写了,AI 还是无视,怎么办
谁都会先遇到这个情况。最常见的原因不是模型坏,而是它上下文中根本没读进规范,或者上下文太长把规范挤掉了。碰到这种情况,我会先让它在当前会话里重新读一次 CLAUDE.md,并要求它用自己的话复述三条核心约束。如果它复述得准确,再让它先只改一个小文件作为“试运行”。
另一个技巧,是把一个任务的 prompt 开头直接写上“请先阅读根目录 CLAUDE.md,再开始动手”。模型每次聚焦当前 prompt 前部的能力比中间更好,把这个指令放在开头,命中率高得多。规范很长时,还可以只摘重点三行带到当前 prompt,但注意要声明“完整规范见 CLAUDE.md”。
6.2 Claude Code 和 Codex 意见冲突,听谁的
两个模型意见打架很正常,毕竟它们的训练效率和知识侧重不同。我的处理原则是:机器规则优先于模型风格,显式规范优先于个体偏好,人工决策最后接管。具体来说,flutter analyze报的错误必须改,这是底线;CLAUDE.md 明确禁止的必须遵守;两个模型都提不出明确依据的风格争论,由人来定。
千万不要让模型自己“讨论”出结论,费时间还可能带偏。最有效的做法是:双方意见摆出来,人按仓库实际情况拍板,然后把结论回写给 CLAUDE.md。久而久之,模型之间的分歧会越来越少,因为规范补充得越来越细。
6.3 规范越写越长,AI 反而糊涂
定期扩充规则是必要的,但一次性堆 100 条只会适得其反。我建议走“问题驱动”的路线:碰到一次翻车,提炼一条可验证的规则;录进去后,专治这一类问题。同时每季度清理一遍,如果某条规则连续三周没有被任何工具或 diff 触发,就考虑删除。
另一个避免糊涂的办法,是把规范按文件拆开。根目录CLAUDE.md只放全局规则,子目录里的README.md或局部规范放模块专属规则。模型按上下文需要读取范围更小的文档,比一股脑塞给它几十条规则有效得多。
6.4 升级 Flutter 版本后 AI 全部乱套
AI 知识的截止时间通常比最新稳定版慢半拍。升级 Flutter 或关键依赖后,AI 很可能因为不认识新 API 而“幻觉”出老旧写法,甚至伪造不存在的参数。我建议在规范里写死当前 Dart SDK 版本范围,并约定:遇到 API 不确定时,先在仓库里用grep搜同类现有代码,按项目内已有用法来写,不自行发明。
如果发生大规模 API 迁移,别指望 AI 一次搞定。先把 upgrade 任务单独拆给 AI,让它只负责“适配 diff”,并跑完整套自检脚本。出问题后,把报错信息喂回去,让它修正。千万不要让它顺手做重构,否则新版本兼容问题会跟老架构问题混在一起,无从查起。
7. 写在最后:这两周实测下来的体会
试过 Claude Code + Codex + 工程规范这套组合后,我最大的感受是:AI 写代码靠不靠谱,很大程度取决于你把工程决策写得多清楚。Flutter 本身有一套默认工程结构,但真正项目里那些“我们一直这么写”的经验,并没有默认值。以前要靠新人慢慢看代码悟,现在你只写进文档还不行,还得考虑机器读不读得懂。
我个人比较推荐“三条规则开工法”:每次会话开始前,从 CLAUDE.md 里挑出当前任务最相关的三条规则,放到 prompt 开头。一次会话只盯住三个重点,模型往往能稳定执行。规则放多了,它反而容易平均用力,最后一条都记不住。
最后再分享一个小技巧:每次 AI 违反规范后,不要急着生气,也不要只专门修正代码。把这条违规行为记下来,看它属于哪类问题,然后补进规范。坚持两三周后,这份规范文件会变成你这套架构里最值钱的资产。以后不管是换一个大模型、扩容团队,还是接新项目,这份“AI 可读的工程规范”都能让新成员和新模型快速对齐。