前阵子团队把一个中型Flutter项目整体交给AI编程助手来维护,Claude Code和Codex轮着用,结果翻车翻得我一度怀疑人生。最离谱的一次,AI在一个下单页面里直接塞了三百行业务逻辑,连网络请求都写在build方法里,编译没问题,跑起来也不崩,但代码评审真的一眼都看不下去。折腾了大半个月,我总结出一条核心经验:想让AI写代码不翻车,关键不在换更强的模型,而是先给项目搭一套AI能读懂的工程规范。这篇文章就完整记录一下我是怎么用Claude Code和Codex给Flutter项目搭建工程规范的,包括规范文件怎么写、工具怎么配置、CI怎么把关,以及调试过程中踩过的那些坑。如果你也在用AI写Flutter,或者正准备让团队里的AI编程助手真正提升效率,这篇文章应该能帮你少走很多弯路。
1. AI写Flutter代码为什么会“翻车”:三个经典现场
1.1 现场一:目录结构完全失控
Flutter项目对目录结构的要求其实挺讲究的,至少团队内部要有统一约定。但AI在没有明确指引的时候,完全凭模型内部的代码分布做判断。我遇过的情况是这样的:它建议新增一个登录页面,结果在lib/pages/下建了login_page.dart,在lib/screens/下又建了一个LoginScreen.dart,两个文件功能几乎一样,只是命名风格不同。我追问它为什么这么建,它的原话是“为了保持结构清晰”。
更烦的是它会根据prompt里的一句话自动创建目录,比如lib/modules/、lib/features/、lib/ui/混着来。新文件摆在哪个目录,完全取决于对话历史里最后出现过的那个路径。这样的结果就是代码库结构被严重污染,手工合并的时候非常难受。
1.2 现场二:状态管理写法“大杂烩”
Flutter生态有个特点,状态管理方案非常多,provider、riverpod、bloc、getx、setState,各有各的适用场景。团队内部只要选定了方案,就应该在整个项目里保持一致。但AI不这么想,它在一个文件里用ChangeNotifier,换一个文件又用setState,再换一个场景直接上StreamBuilder。
我印象最深的是有一次它生成一个列表页,用了FutureBuilder加载数据,同时又在页面里自己维护了一个TextEditingController来管理搜索框,结果搜索状态根本同步不上,页面反复重建,性能问题一个接一个。我跟它说“项目统一用Riverpod”,它的确会改,但只要对话窗口长一点,它又忘了上下文,重新回到自己的“万能模板”。
说实话,单独来看这些写法都能跑,但混在一起就是代码库灾难。维护的人得同时懂三套状态管理模式才能改得动,这恰恰是工程规范要解决的最核心问题之一。
1.3 现场三:看似能跑,评审却要命
AI生成的代码最迷惑人的地方在于,它给出来的方案表面完整,函数命名合理,注释也写得像模像样,很容易蒙混过review。实际上里面埋了不少雷:把API返回的原始Map直接存到Model里,UI层到处硬编码图片路径和颜色值,错误处理被吞掉,甚至连路由都没注册就敢在代码里用Navigator.push。
我让AI写一个订单详情页,它自己发明了一个OrderRepository,但项目里根本没这个类。编译报错之后它马上又“编”了一次,这次居然在main.dart里快速生成了一整套伪实现。全程给人的感觉就是它一直在讲故事,而不是在写真实项目中的代码。它的依据不是项目里的真实结构,而是模型见过的“海量历史项目平均值”。
1.4 根因:AI不读文档,你也没给它文档
总结下来,这些问题最底层的共同点不是模型蠢,而是它真的不知道你的项目长什么样。AI编程助手本身具备很强的代码生成能力,但它对单个项目的理解完全依赖上下文。你给它多少有效信息,它就产出多少符合预期的代码。你什么都不给,它就只能“平均发挥”,而这往往跟团队规范背道而驰。
所以我们需要的不是让AI变得更聪明,而是建立一套机制,把项目的结构约定、写法规范、禁止事项、行业标准都塞进它的上下文里,让它每次动手前先看一遍规则。这也是这篇文章要讲的核心:工程规范不只是给人看的文档,更是给AI看的“项目宪法”。
2. 整体设计思路:给项目建立“AI可读的工程宪法”
2.1 规范体系的三个层级
我一开始犯过一个错误,把规范全写在一个超长的Markdown文件里,塞到项目根目录,指望AI每次都能完整读取。实测下来根本不现实,上下文窗口有限,规则文件一长,AI就开始选择性失明,只遵守前面几条,后面的全部当作不存在。
后来我把规范拆成三层:
- 第一层是全局规则,放在根目录的
AGENTS.md或CLAUDE.md里,内容只写团队最核心的公约,比如状态管理方案、目录结构总览、禁止在Model里写网络请求这类红线,控制在40行以内。 - 第二层是模块规则,放在每个feature目录下,比如
lib/features/login/里放一个README.md或SPEC.md,说明这个模块特有的约束、依赖关系、数据流方向。 - 第三层是单文件要求,通过代码模板和lint规则隐式约束,比如
analysis_options.yaml里的自定义规则、.editorconfig、以及统一的代码片段模板。
这样分层的好处是,AI在处理具体模块时只需要加载少量高相关的规则,而不是把所有内容一股脑读一遍。你让它改登录模块的代码,它先看登录目录下的SPEC,再翻全局规则,上下文占用小,执行力反而更强。
2.2 两个工具的加载机制差异
Claude Code和Codex虽然都是AI编程助手,但对项目上下文的管理方式有些区别。Claude Code对项目根目录下的CLAUDE.md非常敏感,每次对话开始时基本会自动加载,而且它对Markdown结构的理解相当好,标题层级、表格、列表都能正确解析。
Codex这边的习惯不太一样,它在项目根目录优先找AGENTS.md,这也是目前业界逐渐形成的通用约定。如果你两个工具混用,最好在项目里同时保留这两个文件,内容保持一致,避免AI“换个入口”就换了一套行为。
表格式对比:
| 配置项 | Claude Code | Codex |
|---|---|---|
| 默认读取的规则文件 | CLAUDE.md | AGENTS.md |
| 规则文件格式偏好 | Markdown层级清晰,支持较长列表 | 更倾向于简洁、条目式指令 |
| 对上下文相关文件的自动感知 | 较强,能做仓库级搜索 | 较依赖命令行指定的上下文 |
| 对增量修改的稳定性 | 对话较长时偶尔跑偏 | 独立任务模式下更稳定 |
| 适合的任务 | 大型重构、跨文件修改、方案讨论 | 局部功能实现、单文件代码生成 |
这个差异是我实际对比用了两三周才摸清楚的。现在的工作流是:大方向上的重构和代码库探索用Claude Code,单文件实现和小步迭代交给Codex,两者共享同一套规范文件。
2.3 为什么同时用Claude Code和Codex,而不是只选一个
有人问我,既然要建规范,为什么不锁定一个工具?我的真实体会是,现阶段没有任何一个AI编程助手能全覆盖所有场景。Claude Code在复杂语义理解上更强,给它一个模糊的“订单模块状态管理乱了,帮我梳理”,它给出的方案通常有全局视角;Codex则在代码补全和单文件生成上更快更直接,你给它一个明确的函数签名,它产出的实现质量很高。
同时用两个工具还有一个组织层面的好处:团队里不同人偏好不同,强制统一工具会降低大家的使用意愿。但规范文件是统一工具,只要规范一致,你用哪个工具产出都在同一套标准内。省下来的沟通成本相当可观。
3. Flutter工程规范具体怎么拆
3.1 目录结构:选用Feature-first为主、Layer为辅助
Flutter工程的目录组织方式大体分两派:按层组织(lib/models/、lib/views/、lib/services/)和按功能组织(lib/features/auth/、lib/features/order/)。我们团队最终选择主推Feature-first,因为它更适配AI生成代码的模式。AI在实现一个功能时,能自然地在一个模块目录内完成数据、页面、逻辑的闭环,减少跨目录定位文件的频率。
具体到每个feature内部,再固定一层细分:
lib/features/order/ data/ # 仓库、数据源 domain/ # 实体、用例 presentation/ # 页面、状态、widget order_spec.md # 模块规范这条规则我写在全局规范里,AI每次新建文件时都会参照这个结构。实测下来,新建文件的落点准确率从原来的三成左右提升到八成以上。
3.2 状态管理、路由、依赖注入的写法约束
状态管理方面我们锁定了Riverpod,原因在于它天然支持编译期安全,并且对AI生成代码的友好度非常高。它的Provider声明方式直白,依赖关系清晰,AI不容易写出“伪代码”。规范里我明确写了:页面级状态一律用StateNotifierProvider或FutureProvider,禁止在build方法里直接发请求,禁止在ConsumerWidget里实例化Controller。
路由这块,我们抛弃了在每个页面里直接Navigator.push(MaterialPageRoute(...))的写法,统一走go_router的路由表。规范里会给AI举一个例子,告诉它新增页面时必须先注册路由再使用。
依赖注入没有上太重量的框架,Riverpod本身就承担了这层职责。规范强调:所有可测试的外部依赖(API client、本地存储、时间函数)必须通过Provider注入,禁止在业务代码里直接import全局单例。这条规则极大提升了单元测试的可写性。
3.3 import、命名、错误处理,这些细节不能省
命名规范是AI最容易翻车的地方,尤其是文件命名。Flutter社区常用的下划线命名法(order_detail_page.dart)和Dart类名的大驼峰命名法(OrderDetailPage),AI有时会混着用。规范里我会给一个对照表格,让它照着办,比如“文件命名一律snake_case,类名一律PascalCase,常量一律kebab-case写法的枚举或static const”。
import方面最烦的是相对路径和包路径混用,AI因为见到过太多不同风格的代码,经常会生成import '../../order/order_entity.dart'然后又写import 'package:app/core/network/api_client.dart'。统一方案是:项目内部全部用package路径导入,禁止相对路径。这样重构时移动文件不会引发大范围import报错。
错误处理我专门加了一节:所有网络请求必须捕获异常并映射成业务异常;UI层不允许出现裸catch (e);打印错误一律用统一的logger封装,禁止print。
3.4 测试规范:AI最容易偷懒的地方
AI生成代码时,能不写测试就不写测试,这是所有AI助手的通病。所以测试规范必须写得像强制命令,而不能只是“建议”。我在规范里写明:新增或修改的每个领域模型必须配套单元测试;每个页面必须至少有一个widget smoke test;所有涉及异步数据的Provider必须测试loading和error两个分支。
另外,我要求AI在每次生成代码完成后,主动列出“你没有覆盖到的测试场景”,通过这种追问机制反向给它压力。虽然不能完全保证它每次照做,但比完全不管好得多,测试补齐率明显上升。
4. 从零实操:把规范真正砸进项目里
4.1 第一步:写全局规则文件AGENTS.md
这一步是整个体系的地基,内容不需要长,但每一条都必须可执行。我给出的初始版本是这样的:
# AGENTS.md ## 项目技术栈 - Flutter 3.x / Dart 3.x - 状态管理:Riverpod(flutter_riverpod / hooks_riverpod) - 路由:go_router - 网络:dio + 统一ApiClient ## 目录规范 - 功能代码按feature组织,禁止在lib根目录散落新文件 - feature内部固定分层:data/domain/presentation - 新文件必须放入对应feature对应层级,禁止随意创建新目录 ## 代码规范 - 文件命名:snake_case.dart - 类名:PascalCase - 项目内部import一律使用package路径 - 禁止在Widget build方法中执行网络请求 - 禁止在Model中存放API原始Map,必须有toJson/fromJson - 所有外部依赖通过Riverpod Provider注入 ## 测试要求 - 每个domain实体必配单元测试 - 每个页面必配Widget测试 - 异步Provider必须覆盖loading/error分支 ## 红线 - 严禁修改 pubspec.yaml 中已有依赖版本 - 严禁生成文件后不注册路由 - 严禁在业务代码中直接使用print别小看只有几十行,这套全局规范配上下面的analysis_options和CI检查,已经能拦住大部分原则性错误。规范文件写好后,一定要提交到git并让团队周知。
4.2 第二步:用analysis_options.yaml把规则变成强制检查
光有文档还不够,人都会选择性忽略文档,AI也一样。所以我用Dart的静态检查规则把一部分规范固化到analysis_options.yaml里。这样即使AI写错了,CI阶段也会直接报错,而不是靠人工review去抓。
include: package:flutter_lints/flutter.yaml linter: rules: always_declare_return_types: true avoid_print: true prefer_single_quotes: true sort_child_properties_last: true directives_ordering: true prefer_const_constructors: true avoid_relative_lib_imports: true analyzer: errors: avoid_print: warning sort_child_properties_last: warningavoid_relative_lib_imports这个规则特别重要,它直接把“禁止相对路径import”从口头规范变成了强制检查项。avoid_print和directives_ordering则分别堵住了print满天飞和import乱序的问题。
如果嫌内置规则不够细致,还可以引入custom_lint写自己的lint,比如检查Provider命名是否以Provider结尾、检查Model是否都实现了fromJson。这一步对老项目改造比较费劲,但新项目从一开始就把规则挂上,后面很轻松。
4.3 第三步:让Claude Code和Codex按规则干活
这一步的核心是把规范文件与AI工具的加载机制对齐。Claude Code会自动读取项目根目录下的CLAUDE.md,我在里面写一句简介,然后指向AGENTS.md作为完整规范:
# CLAUDE.md 项目工程规范请严格遵循根目录 AGENTS.md。 重点提醒: - 新增页面必须先注册 go_router 路由 - 禁止在 Widget build 中发起网络请求 - 状态管理统一使用 Riverpod,不要使用 setState 做全局状态 - 涉及多文件改动时,先查看对应 feature 目录下的 SPEC 文档Codex这边我们沿用AGENTS.md,因为现在很多项目都通用这个文件。它读取规范的能力不如Claude Code那么“懂格式”,所以我把AGENTS.md设计成短条目式、每条独立一句话,而不是长篇大论的段落。另外,在命令行里手动指定规则文件也是一个稳妥做法:
codex --project-rule-file AGENTS.md如果你用的是IDE插件版本,就在项目设置里把AGENTS.md和CLAUDE.md同时设为项目指令。这样无论队友在IDE里用哪个工具,都能加载同一套约定。
4.4 第四步:在CI里加一道“AI代码审查闸门”
规范本地执行得再好,也挡不住有人把AI生成的代码直接push。我在CI流水线里加了一个“代码规范检查”阶段,跑三件事:flutter analyze、dart format --set-exit-if-changed、关键目录结构校验脚本。
flutter analyze --fatal-infos dart format --output=none --set-exit-if-changed lib test同时用一个简单的shell脚本检查文件和目录约定,比如检测lib根目录下是否出现未经约定直接新建的Dart文件:
for file in lib/*.dart; do if [ -f "$file" ]; then echo "ERROR: 禁止在 lib 根目录直接创建文件:$file" exit 1 fi done这套检查不是要把AI的创造力锁死,而是保证就算AI跑偏了,也进不了主干。以前靠人工review提醒,现在机器直接拦。团队里有人私下说这个闸门有点“暴力”,但一个月后代码库的整洁度肉眼可见地提升,没人再反对。
5. 这轮实操踩过的坑和排查心得
5.1 规范文件太大会被AI无视
我第一次写的全局规范大概有150行,产品背景、开发流程、发布节点全写进去了,结果Claude Code确实读了,但每次只能记住前面30行,后面很多重要规则像没看见一样。后来我学到的做法是:把“背景故事”全部删掉,只留跟代码生成直接相关的指令。每一条规则都尽量控制在20字以内,用祈使句。
如果规则确实多,就按主题拆成多个小文件,比如AGENTS.md只写高频约束,CONVENTIONS_DETAIL.md放低频细节,AI在需要的时候再让它去查。少即是多,这句话在给AI写规范时格外适用。
5.2 规则之间互相矛盾,AI选择困难
有一版规范里既写了“所有Repository必须实现接口”,又在另一个文件里说“简化设计,不要过度抽象”,结果AI生成代码时开始自行取舍,有时生成接口包装,有时直接裸用实现类,两套风格并存。问题出在规则之间没有优先级。
排查后我加了一条总原则:“当本文件规则与其他文件冲突时,以AGENTS.md为准;AGENTS.md内部冲突时,以靠后的条款为准。”并刻意删掉了一些弹性表达,把“不要过度抽象”改成“禁止创建单实现接口,除非你有两个以上实现类”。规则一旦有歧义,AI就会按它自己的偏好理解,所以规范化语言必须像法律条文一样不留余地。
5.3 AI不遵守测试规范怎么办
这个问题纠缠了我好久。明明AGENTS.md里写了“必须补测试”,Codex生成的代码还是常常不带测试文件。后来我换了一种问法,不直接让它“写测试”,而是给它演示一个任务分解模板:
任务:新增订单列表页 1. 先写 order_entity.dart,并配套 order_entity_test.dart 2. 再写 order_list_provider.dart,并配套 provider 测试(loading/error/success) 3. 最后写 order_list_page.dart,并配套 widget test 输出前自查:是否每个文件都有对应测试?把测试步骤前置到任务拆解里,比在规范末尾写一句“记得测试”有效得多。因为AI是按任务一步步执行的,它在第一步就看到了测试文件要求,后面自然会把测试当“任务的一部分”。另外我也写了一个lint脚本,检查lib/features/xxx/domain/下新增的.dart文件在test/目录里是否存在对应的_test.dart,不存在就直接CI失败。这条之后,测试缺失率大幅下降。
5.4 规范应该跟着项目演进,而不是一成不变
规范文件最忌讳写完就再也不动。项目从单一模块扩展到十几个feature之后,原本适合的规范可能就不合适了。比如早期我们禁止使用BuildContext跨异步操作,后来为了配合某个特定场景,又不得不允许在受控条件下使用。如果不更新规范,AI就会持续生成与当前架构冲突的代码。
我们现在的做法是:每次代码评审中,如果发现某条规则被违反三次以上,就停下来开会讨论,看是规则本身不合理,还是执行机制有问题。规则本身的问题就改规则,执行机制的问题就加lint或CI约束。规范文件也要版本化管理,每次修改都写入commit message,让整个团队知道AI的行为基线在随着项目一起进化。
最后分享一点实操体会
整套方案跑通之后,最明显的变化不是AI生成的代码质量瞬间拉满,而是“稳定性”上来了。以前它今天生成90分的代码、明天可能生成50分的代码,全看对话状态和上下文运气。现在规范固定、工具固定、CI检查固定,它的产出基本稳定在75到80分,再由人补上最后的架构判断和业务细节,整体效率是真的高。
如果你也想这么搭,我建议不要一上来就把规范做得特别大,先写一份二十行的AGENTS.md,配一个强制flutter analyze的CI,跑两周,看看AI还犯哪些错,再针对性地补充规则。规范工程是一个持续迭代的过程,不是一次性交付物。最后再提醒一句:所有规范文件都放进git仓库,跟代码一起走,否则AI换一台机器就“失忆”了,而工程规范恰恰是最不该失忆的东西。