最近我被 AI 写出来的 Flutter 代码折腾得够呛。
我手上这个跨平台项目用了 AI 辅助编码之后,功能是出得快了,但“翻车”的频率也上来了:某个页面还是老的状态管理写法,另一个页面已经换成新的;同一个网络请求在不同文件里被封装了三遍;更有一次,AI 顺手把一个公共组件从“按需加载”改成了“全量引入”,热重载看着没事,一打 release 包体积直接涨了 10%。队友开玩笑说,这不是在写代码,是在给 AI 当保姆。
后来我想明白一件事:AI 写代码翻车,多半不是因为模型不行,而是因为你没给它立规矩。我把 Claude Code 和 Codex 这两个工具组合起来,给我的 Flutter 项目搭了一套工程规范,核心就三件事:把项目约定写成机器能读的规则文件,把代码检查变成 AI 提交劳动成果之前的硬门槛,把每一次“翻车”沉淀成新的规则补丁。这套东西跑下来,AI 代码的返工率肉眼可见地降了,原本最耗精力的“给 AI 擦屁股”环节也基本消失了。
这篇东西就是把这套方法论完整拆一遍,包括规则文件怎么写、守门命令怎么配、CI 怎么闭环、出了问题怎么排查。适合正在用 AI 写 Flutter 但总被烂摊子缠身的同学,也适合想带团队引入 AI 编码规范但不知道怎么下手的同学。
1. 为什么 AI 写 Flutter 代码容易“翻车”
1.1 先盘点一下 AI 代码的典型“翻车现场”
AI 生成代码的问题,和新人刚入职写出来的问题高度重合。我把这两三个月里遇到的典型事故整理成了下面这张表,你可以对照一下自己项目里有没有类似症状:
| 现象 | 具体表现 | 严重程度 |
|---|---|---|
| 目录结构混乱 | 同一种功能代码今天放 pages、明天放 views、后天放 modules | 高,后续维护直接迷路 |
| 类型约束薄弱 | 到处都是 dynamic、as 强转,空安全形同虚设 | 高,运行期容易炸 |
| 状态管理混用 | 一个页面用 setState,另一个页面用 Bloc,风格不统一 | 中,代码 review 成本剧增 |
| 依赖随意变动 | AI 为了“省事”直接改 pubspec.yaml,升级了一堆无关依赖 | 高,极易引发版本冲突 |
| 资源文件引用错误 | 图片、字体、多语言 key 大小写不对,编译期才发现 | 中,白白浪费构建时间 |
| 测试基本为零 | 生成的代码不补测试,或者只写能跑通的 happy path | 中,回归全靠人肉 |
你会发现,这些问题几乎都不是“语法错误”,而是“约定错误”。语法错误编译器能兜住,约定错误只能靠人的经验兜。
1.2 根因不是“模型笨”,而是“约束缺位”
AI 写 Flutter 代码和资深工程师写 Flutter 代码,最大的差别不在编码能力,而在对项目语境的感知。
资深工程师知道这个项目里数据层必须走统一的数据源封装,知道公共组件不能直接改对外接口,知道新增页面要同步补多语言配置。这些知识来自项目文档、团队约定和走过的坑。但 AI 每次接手任务时,默认只有一堆代码文件和一句用户指令,它不知道你的架构决策、不知道你的目录边界、不知道哪些东西是雷区。
换句话说,你让一个很聪明但完全不了解项目背景的工程师直接开始写代码,他大概率也会“翻车”。AI 只是把这个过程放大了十倍。
1.3 “翻车”的本质:缺少三个闭环
复盘下来,AI 代码不受控,本质上是三个环节断了:
- 上下文闭环:AI 没有稳定、完整地获取项目规范,每次都是“盲写”。
- 验证闭环:AI 写完代码后,没有一套自动化的硬检查去拦截问题,全靠人肉 review。
- 反馈闭环:这次犯过的错,没有沉淀成下次不再犯的规则,AI 永远在同一个地方跌倒。
所以解决方案也很清晰:把“项目规范”变成 AI 可读取的上下文,把“代码检查”变成 AI 提交前必须通过的硬门槛,把“翻车记录”变成规范的增量补丁。Claude Code 和 Codex 在这个体系里各管一摊:Claude Code 负责在编码过程中理解和遵守规则,Codex 负责批量化的检查与修复。下面我从地基开始一步步讲。
2. 先把地基打牢:搭一套能约束 AI 的工程基线
2.1 工程规范管四件事
在动手给 AI 立规矩之前,你得先有规矩本身。很多 Flutter 项目所谓的“工程规范”就是一份没人看的 Notion 文档,这种规范对 AI 没有约束力,因为 AI 根本不会去点开你的文档链接。
真正能约束 AI 的工程规范,至少要覆盖四个层面:
- 命名与格式:文件命名、类命名、常量命名、代码格式化标准。
- 架构与依赖:目录分层规则、状态管理选型、依赖引用边界。
- 测试与验证:哪些场景必须补测试、测试文件怎么命名、最低覆盖率门槛。
- 流程与协作:提交粒度、分支策略、代码 review 要求。
规范不用一开始就追求大而全,先把最痛的点管住,后面再迭代。
2.2 Dart 侧的硬约束:从 analysis_options.yaml 说起
Flutter 项目里最容易被忽略的“硬约束”其实是 analysis_options.yaml。这个文件是 Dart 静态分析的配置中心,天然就是给机器看的规范。我建议把下面这套配置作为项目基线:
include: package:flutter_lints/flutter.yaml analyzer: language: strict-casts: true strict-inference: true strict-raw-types: true errors: invalid_annotation_target: ignore linter: rules: avoid_print: true prefer_const_constructors: true prefer_const_declarations: true prefer_final_locals: true unnecessary_lambdas: true unnecessary_string_interpolations: true use_key_in_widget_constructors: true我解释几个关键项:
strict-casts、strict-inference、strict-raw-types:这三兄弟一开,AI 想写dynamic糊弄事就难了,类型不明确直接编译期报错。avoid_print:强制 AI 使用统一的日志封装,而不是到处撒print。prefer_const_constructors:对 Flutter 性能优化很重要,这条规则会逼着 AI 在能加const的地方加上const。
这套配置意味着“规范”不再是一句口号,而是每次flutter analyze都会执行的机器检查。AI 写完代码,跑一遍分析器,不符合规则的地方会直接列出来。这就是后面 Codex 自动修复的基础。
2.3 目录与架构约定:给 AI 划好“格子”
如果说 analysis_options.yaml 管的是“代码怎么写”,目录结构管的就是“代码往哪儿放”。AI 生成代码时如果没有目录边界,它就会按自己的理解随便放,第一次放pages,第二次放views,第三次放modules,项目结构很快就变成一锅粥。
我给项目定的目录规范是 feature-first,也就是先按业务功能分目录,再在功能内部按层级分文件。示意如下:
lib/ core/ constants/ utils/ network/ theme/ features/ auth/ data/ domain/ presentation/ profile/ data/ domain/ presentation/ shared/ widgets/ extensions/这个结构的核心逻辑是:AI 接到一个“做登录功能”的任务时,它应该去features/auth/下面找对应的data、domain、presentation,而不是在lib根目录下随手新建目录。
你可能觉得这有点死板,但正是这种“格子”能大幅降低 AI 代码的随机性。把这句话写进规则文件,AI 每次动手前都会被提醒一次(后面第 3 节会详细说怎么写)。
2.4 测试基线:让 AI 不敢裸奔
让 AI 写的每个功能都配测试,靠自觉是不可能的,必须靠基线约束。我在项目里定了一个三层测试策略:
- 单元测试:覆盖 data 层和 domain 层的核心逻辑,特别是网络解析、数据转换这类容易出错的代码。
- Widget 测试:覆盖关键页面的渲染和交互,至少保证页面能正常 build,按钮点击有响应。
- Golden 测试:对核心 UI 组件做快照比对,防止 AI 无意中改动视觉样式。
对应到命令就是flutter test,我会在 CI 里强制要求 AI 提交的代码必须保证测试全绿。这一点放到第 4 节“守门员”部分展开,这里只先立好规矩。
3. 给 Claude Code 立规矩:项目规则文件怎么写
3.1 为什么规矩必须写成文件,而不是放在对话里
很多人用 Claude Code 的时候,习惯在每次对话开头写上一大段“你要遵守 XXX 规范”。这个做法问题很大:对话一长,上下文窗口一挤,前面的约束会被慢慢遗忘。而且每次开新会话都要重新写一遍,迟早会漏写。
正确做法是把规范写进项目根目录的规则文件里,我用的文件名是CLAUDE.md,Claude Code 会自动读取这个文件作为长期记忆;也有团队叫AGENTS.md。这个文件本质上就是“AI 的项目入职手册”,每次会话启动时都会被加载,比你在对话里反复强调要可靠得多。
关键点是:规则文件必须“机器可读、可执行、可自查”。它不是给人看的文档,而是给 AI 看的行为准则。
3.2 一份可直接落地的规则文件骨架
下面这份是我实际在用的CLAUDE.md精简版,你可以直接复制到项目里按需修改:
# 项目规则 ## 项目概述 这是一个基于 Flutter 的跨平台移动应用,使用 Dart 语言开发。 核心业务模块包含用户登录、个人中心、消息通知等。 ## 技术栈约束 - 状态管理统一使用 Riverpod,禁止混用其他方案。 - 网络层统一走 lib/core/network 下的封装,禁止在业务代码中直接使用 HTTP 客户端。 - 所有字符串必须走多语言资源文件,禁止硬编码中文文案。 - 所有依赖版本由 pubspec.yaml 统一管理,禁止 AI 自行修改依赖范围。 ## 目录结构 - 业务代码必须放在 lib/features/<feature_name>/ 下。 - 每个 feature 内部按 data/domain/presentation 分层。 - 公共组件放在 lib/shared/widgets/,禁止在业务代码里复制粘贴公共组件。 ## 编码风格 - 类型必须显式声明,禁止使用 dynamic 绕过类型检查。 - 能加 const 的地方必须加 const。 - 禁止使用 print,统一走日志封装。 - 文件命名使用 snake_case,类命名使用 UpperCamelCase。 ## 测试要求 - 新增或修改核心逻辑时必须同步补单元测试。 - 修改 Widget 时必须确保相关 Widget 测试通过。 - 禁止删除已有测试用例。 ## 提交流程 1. 动手前先阅读相关模块的已有代码,保持风格一致。 2. 任务拆解为小粒度,每完成一个子任务后运行 flutter analyze 和 flutter test。 3. 提交前检查是否有无关文件的改动,禁止混入重构类修改。 4. 如果遇到不确定的设计取舍,先输出方案让用户确认,再继续编码。 ## 禁止事项 - 禁止大范围重构不相关的代码。 - 禁止修改 pubspec.yaml 中的依赖版本。 - 禁止在 initState 里直接发起网络请求。 - 禁止复制粘贴生成重复代码,遇到复用场景先检查 shared/widgets 下是否已有组件。你可能注意到了,这份文件写得非常“霸道”。这是故意的。AI 不是人,它不会“领会精神”,只认明确边界。你把边界划得越清晰,AI 的自由发挥空间就越小,“翻车”概率自然越低。
3.3 开工提示词:把“先看规则”变成习惯
光有规则文件还不够,Claude Code 每开一个新会话时,虽然会自动加载规则文件,但它未必会真正“重视”里面的内容。我的做法是在每次开工提示词里再强调一遍:
先阅读项目根目录的 CLAUDE.md,严格按照其中的规范和禁止事项执行。 本次任务:实现用户个人中心页面,包含头像上传和个人信息编辑。 动手前先列出你的实现计划,确认后再开始编码。为什么要多这一步?因为 Claude Code 处理长上下文时,规则文件会被其他信息冲淡。开头的显式提醒等于给它定了一个“高优先级锚点”,让它先从规则文件里提炼约束,再开始思考任务。
另外有个小技巧:复杂任务一定要拆解。不要一句“帮我实现个人中心”丢过去,然后等结果。我会先让它做数据层,跑一遍验证;再做 UI 层,再跑一遍验证。每多一步拆解,AI 的出错空间就小一分。
3.4 “禁止事项”比“应该做的事”更重要
在写规则文件时,我最深的体会是:“禁止事项”的价值远大于“应该做的事”。
“应该做的事”是方向性建议,AI 可能做了但做得不彻底;而“禁止事项”是红线,踩了就违规,可以被检查工具直接拦截。比如禁止修改 pubspec.yaml 中的依赖版本这条,配合 Git 变更审查,几乎可以做到 100% 拦截。
我在项目里积累了一批高价值的禁止项,这里分享几个:
- 禁止修改与本任务无关的文件:这一条能挡住 AI“顺手”改代码的毛病。
- 禁止删除测试用例:AI 在测试跑不过时会倾向于“删测试”而不是“改代码”,这是最恶劣的行为。
- 禁止在构建产物和缓存目录下乱建文件:避免污染项目。
- 禁止自己造轮子:优先使用项目已有封装和第三方库,禁止另起炉灶。
黑名单式的规范,配合机器检查,才能真正做到“有法必依”。
4. 用 Codex 当自动守门员:把规范变成“机器说了算”
4.1 生成与审查分离:两个工具分工为什么更稳
Claude Code 的强项是“生成”,它能结合项目上下文写出结构合理、风格统一的代码。但让生成代码的工具自己去检查自己,有一个天然盲区:它会为了“证明自己写的没问题”而放过一些潜在缺陷。
所以我把“审查”和“修复”交给了 Codex。Claude Code 负责按规范生成代码,Codex 负责用更机械、更冷酷的方式检查代码是否符合规范,并且可以批量执行修复任务。生成与审查分离,这套路的可靠性比单工具自查高很多。
4.2 三条“守门命令”:机器说了算的第一步
我不太信任“AI 说自己检查过了”这种话,项目里所有 AI 提交的代码都必须过下面三条命令,一条不过就不允许进入 review 环节:
dart format --set-exit-if-changed . flutter analyze --fatal-infos flutter test三条命令各有各的脾气:
dart format --set-exit-if-changed .:只要工作区里有未格式化的代码,退出码就是 1,直接判失败。这一步完全消除格式争议。flutter analyze --fatal-infos:把检查级别拉到最严,连 info 级别的问题都按错误处理,也就是说动态类型、不必要的表达式这类“小毛病”也逃不掉了。flutter test:所有测试必须全绿,不允许任何跳过和忽略。
这三条命令是站在 AI 代码和主干分支之间的一道闸门。AI 生成的代码,要么符合规范,要么被挡在外面。
4.3 给 Codex 的自动修复任务单
守门命令发现的问题,不会直接丢回给 Claude Code 重新生成——那样可能引入新问题。我的做法是把报错清单交给 Codex,让它做定向修复。
举个例子,flutter analyze输出了一堆 lint 告警,我会用类似下面的方式写一个修复任务单(实际以你自己使用的任务描述方式为准):
flutter analyze --fatal-infos > analyze_report.txt # 将 analyze_report.txt 内容及修复要求发送给 Codex修复指令模板:
以下是当前项目 flutter analyze 的输出报告。 请逐条修复其中列出的问题,要求如下: 1. 只修复报告中指出的问题,禁止改动任何无关代码。 2. 修改类型声明时,保持原有业务逻辑不变。 3. 修复完成后重新运行 flutter analyze,确认错误消失。 4. 如果某条问题无法在不改变业务逻辑的前提下修复,单独列出并说明原因。这份任务单的核心价值在于“限制边界”。只修报告里点名的文件,只改跟问题相关的代码。Codex 批量修复 lint 告警这种重复劳动,效率和稳定性都远高于人肉处理。
4.4 在 CI 里形成硬闭环
本地跑守门命令全靠自觉,总有漏网之鱼。我后来把整套检查流程搬进了 CI,只要 AI 发起合并请求,流水线就会自动跑一遍:
- 拉取分支代码。
- 安装依赖,锁定 pubspec.lock。
- 运行
dart format --set-exit-if-changed . - 运行
flutter analyze --fatal-infos - 运行
flutter test - 执行自定义规则检查(比如检查是否有人改动过 pubspec.yaml 里的依赖版本、是否在禁止目录下新增了文件)。
- 上述步骤全部通过后,才允许进行人工 review。
第 6 步听起来比较“野路子”,但特别管用。它的本质是把规则文件里的黑名单写成脚本,每次提交都自动比对。比如检查 Git 变更里有没有出现pubspec.yaml,如果出现了并且不是人工确认的依赖升级,就直接拦下来。这一步能堵住 AI 很多墙角的尝试。
我不打算在这里推荐具体的 CI 平台,核心思想是:所有能用机器做的检查,都不要留给人工判断。你也别觉得这一套配置起来费劲,真跑起来之后,它每天帮你省下的 review 时间远远超过搭建成本。
5. 让规范跟着项目长:用“翻车记录”反向迭代
5.1 把每一次“翻车”变成规则补丁
工程规范不是一次性写完就完事的,它得跟着项目一起长。我养成了一个习惯:每次 AI 代码搞出事,不管是线上问题还是 review 时发现的隐患,我会先在项目根目录维护一个故障记录文档(我用的是docs/ai-failures.md),把事件经过还原一遍,再提炼规则补丁。
举个例子,有一次 AI 实现某功能时,在initState里直接拉取了用户信息,页面一启动就并行发了好几个请求,搞得接口压力很大。事后我沉淀出来的规则补丁就是两条:
- 禁止在
initState里直接发起网络请求,初始数据加载必须走统一的数据层方法。 - 新增页面默认需要说明数据加载的时机和方式。
补丁沉淀完成后,我会同步更新CLAUDE.md里的对应章节,让以后所有 AI 会话都继承这条规则。这样每翻一次车,规则系统就强大一分。
5.2 定期给规范做“体检”
规则文件也会积累“僵尸条款”。有些规则一开始有用,后来随着项目演化已经不再适用;有些规则太死,把 AI 的正常发挥空间压得太小,反而阻碍了效率。
我大概每两周会做一次规则体检,检查点包括:
- 过去两周内,这条规则有没有实际拦截过问题?如果完全没有,考虑放宽或删除。
- 有没有 AI 频繁反映“规则互相矛盾”的情况?比如一条说“必须复用公共组件”,另一条说“禁止修改公共组件”,就会让 AI 陷入死锁。
- 规则文件的篇幅是不是过长了?如果超过 150 行,AI 在上下文中很难每次都完整吸收,需要精简合并。
另外可以给规范建立量化指标。我关注三个数:AI 代码一次 review 通过率、AI 代码平均返工轮次、提交前 lint 和测试失败次数。这三个数如果持续向好,说明规范在起作用;如果长期不变,说明规范可能只是摆设。
5.3 团队协作里谁负责什么
这套体系能不能跑起来,还取决于团队分工。以我和同事们的实际分工为例:
- 规则维护人:负责
CLAUDE.md和故障记录文档的更新,通常由对架构理解最深的人担任。规则变更要走 review,不能谁想改就改。 - 批量修复负责人:负责把守门命令抛出的问题整理成 Codex 任务单,并验证修复结果。这个角色不需要很资深的工程师,但对命令和工具要非常熟悉。
- 最终 review 人:机器检查和 AI 修复都通过之后,最后由人工做代码评审,重点看业务逻辑对不对,而不是纠结格式和风格。
每周安排一次简短的规范复盘会,过一遍故障记录、规则变更、量化指标数据。整个团队都对“AI 代码是怎么被约束的”有共识,而不是把规范当成某个人的自嗨。
6. 常见问题与避坑实录,拿去就能用
6.1 问题速查表
把这几个月实操中遇到的高频问题整理成了速查表,照着排查就行:
| 问题现象 | 可能原因 | 处理办法 |
|---|---|---|
| AI 改了 A 文件,还顺手改了 B 文件 | 规则文件缺少“禁止无关改动”项 | 在 CLAUDE.md 补充黑名单,并在 CI 检查变更文件范围 |
| flutter analyze 通过,但运行期崩溃 | 类型检查挡不住逻辑错误,测试覆盖不足 | 补 Widget 测试和边界用例,强制核心路径测试 |
| AI 不遵守“禁止事项” | 规则文件太冗长,AI 上下文中没吸收 | 精简规则,把最关键禁止项提到文件最前面 |
| Codex 修完 lint 又引入了新问题 | 修复任务单边界不够清晰 | 任务单里强制要求“只修指定范围,禁止动无关代码” |
| 格式化检查和 AI 的格式化习惯冲突 | 没有统一格式命令 | 所有 AI 生成的代码统一跑dart format --set-exit-if-changed |
| 测试跑不过,AI 倾向于删测试 | 规则文件漏掉了“禁止删测试” | 明确写进禁止事项,并在 Git 变更检查里拦截 |
| 规则文件太长,AI 每次加载很慢 | 规范沉淀过多冗杂内容 | 定期体检,删僵尸条款,子模块规范拆到独立文件 |
6.2 几条用真金白银换来的心得
最后聊几条我在实战里总结出的非共识经验,这些是文档里不会写的:
第一,别让 AI 一口气实现整个功能模块。看上去很爽,但生成代码的排查成本极高。把它拆成“数据层 → 状态层 → UI 层 → 联调细节”四步,每步跑完守门命令再进入下一步,返工率能降一半。
第二,AI 的自检结果不能盲目信任。它会因为“已经对齐过规则”而在自查时产生路径依赖。必须让独立的 Codex 检查和 CI 流水线去把关,不依赖生成者自证清白。
第三,规则文件本身也要版本化。别放在个人笔记里,放进代码仓库,每次修改走 commit,谁在什么时候、因为什么案例改了哪条规则,全部可追溯。这对团队信任和规则迭代太重要了。
第四,review 不能完全省掉。机器检查能挡住格式、类型、测试问题,但挡不住“需求理解偏了”。AI 代码最终必须经过人工 review,只是这个 review 的负担会大幅减轻,人可以把精力放在真正的业务逻辑上。
我自己实际跑下来,最明显的变化是:以前每次让 AI 开发一个功能,光检查和返工就要耗掉半天;现在规则闭环建立之后,大部分提交能一次性通过机器检查,人工 review 基本只看逻辑、提提意见就够了。AI 写 Flutter 代码翻不翻车,说到底不是运气问题,而是你有没有给它修好护栏。这套规范系统,就是护栏本身。后续我打算再把规则文件按业务模块拆细,针对不同 feature 定制不同的约束,让 AI 在不同领域都能“戴着镣铐跳舞”,而且跳得稳稳当当。