最近几个月,我陆续把 Claude Code 和 Codex 拉进了 Flutter 项目的日常开发流程里。一开始确实挺爽,一句自然语言就能生成一坨 widget,测试也能给你补上。但爽了没几天,问题就来了:AI 写出来的代码能编译、能跑,但跟项目里已有的风格、命名、分层逻辑完全脱节,甚至把页面原本的状态管理方式都给换了。说白了就是“翻车”——不是编译不过,而是埋了一堆后面要还的债。
后来我花了不少时间,围绕这个项目搭了一套工程规范,把 AI 的“自由发挥”关进笼子里。等到这套做法跑通,再回头看,AI 的产出质量稳定了不止一个档次。这篇文章就把整个过程拆开讲:AI 为什么会翻车、Claude Code 和 Codex 分别适合干什么、工程规范具体怎么落到文件里、以及实操过程中踩过的坑。
不管你是刚接触 AI 编程的 Flutter 开发者,还是已经在用但被 AI 坑过的老手,这篇内容应该都能给你一些可直接抄作业的思路。
1. 为什么 AI 写 Flutter 代码容易“翻车”
1.1 我遇到过的几种“翻车”现场
先举几个实际遇到过的典型场景,你大概率也会有共鸣。
第一次比较明显的翻车,是我让 AI 给一个列表页加“点击跳转详情”。它接手之后,不仅加了跳转逻辑,还顺手把整个页面的ConsumerStatefulWidget换成了StatelessWidget+FutureBuilder,理由是“这样更简洁”。逻辑上确实能跑,测试也能过,但它完全绕过了项目里统一的加载状态组件。原来我们有一个AppLoading用来统一展示 loading 和错误重试,它压根儿没用上,等于页面交互风格直接和其他页面分岔了。
第二次是命名问题。我们项目文件一直用snake_case,类名用PascalCase,方法和变量用camelCase。结果它在某个页面里生成的文件名是userProfileModal.dart,类名却是User_profile_modal,让我恍惚以为来了个 Python 风格混搭选手。这种错误虽然能靠flutter analyze拦住,但它已经说明:AI 默认情况下并不会把你的项目约定放在心上。
第三次更让人头疼,是“自作主张”引入依赖。我想让它实现一个地图选点能力,它二话不说引了一个封装得很漂亮的三方地图包。但其实项目里早就有基于现有地图 SDK 封装好的MapPicker组件,只是它没有主动去翻shared/widgets目录。结果一个本来 10 分钟能完成的需求,最后花了大半天在删依赖、重写交互上。
这些“翻车”很少在编译期暴露,更多是藏在代码评审和后续迭代里。特别考验人的是:AI 生成的代码可能看起来非常“标准”,但它能不能融入现有项目,完全是另一回事。
1.2 翻车的根因:AI 缺的不是能力,是上下文
为什么 AI 会反复“翻车”?我的理解是,AI 代码生成模型本质上是在做“基于统计的预测”。它的训练数据来自大量公开代码仓库,里面什么风格都有:有人用 Bloc,有人用 Provider,有人一个文件堆两千行。当你不给它任何约束时,它会选一个“统计上最可能被接受”的写法,而不是“最符合你们项目约定”的写法。
你可以把 AI 想象成一个技术很强的新同事。这个新同事入职第一天,不看你们团队的 README、不看现有代码、也不问你任何问题,直接上手写代码。那他产出的东西大概率会偏离团队风格。AI 比这位新人还“自信”,因为它很少主动承认“我不确定”,而是会直接假设所有条件,然后生成一段看起来合理的代码。
所以问题的出路不是“禁止 AI 写代码”,而是“把工程规范变成 AI 的上下文”。如果能在动手之前就让它知道这些信息:
- 项目的技术栈和选型,哪些库是“钦定”的;
- 目录结构以及业务代码的组织方式;
- 命名、格式化、lint 规则;
- 哪些 API 是推荐用法,哪些是禁止使用的;
- 什么情况下要多问一句,而不是直接动手。
你会发现,AI 的“翻车率”会肉眼可见地下降。这也是我后来坚决推动“工程规范先行”的最核心理由——规范不只是给人看的,更是给 AI 看的。
2. 双工具怎么搭:Claude Code 和 Codex 的分工
2.1 Claude Code 和 Codex,真实手感差异在哪
先说一个比较容易有的误解:别把 Claude Code 和 Codex 当成“二选一”的关系,它们其实适合不同的活。我用了挺长时间之后,对这两个工具形成了这样的判断:
| 维度 | Claude Code | Codex |
|---|---|---|
| 擅长形态 | 长对话、整体重构、方案讨论 | 单点任务、批量修复、生成 diff |
| 上下文利用 | 对项目整体结构的理解较强 | 更像按指令执行的“执行器” |
| 适合任务 | 梳理旧模块、跨多文件重构、方案评审 | 改 lint、补测试、实现单个功能 |
| 需要的前置信息 | 希望它先读文档、先规划 | 任务描述必须写得极其明确 |
用生活化的比喻来说,Claude Code 更像一个“可以反复讨论的结对者”,而 Codex 像一个“接到任务就执行的流水线工人”。这不代表 Claude Code 不能执行小任务,或者 Codex 不能进行多文件修改,而是说它们各自最顺手的工作模式有明显差异。
所以我在项目里的用法是:需要“想清楚再改”的活给 Claude Code,需要“说清楚就改”的活给 Codex。比如“把登录页完全重构成我们新的加载模式”这种涉及多个文件、需要权衡的任务,我交给 Claude Code;而“把排行榜页面上的 5 个 lint 错误修掉”这种边界清晰的任务,我直接甩给 Codex。
2.2 为什么必须“先搭规范再开工具”
很多人第一次接触 AI 编程工具时,习惯的做法是“先把工具接入,然后边写边发现规范问题”。我一开始也这样,结果发现一个很尴尬的问题:AI 会把仓库里已经存在的混乱当成“风格模板”来学习。
举个例子。当时仓库里有一部分老代码用Provider.of(context),另一部分新代码已经迁到了 Riverpod。我没有明确告诉 AI 该用哪种,结果它在两个文件里分别用了两种写法,理由是“跟随原有风格”。它确实“聪明”,但这种“聪明”恰恰说明了:如果仓库本身没有统一约定,AI 只会放大这种分裂。
所以我强烈建议一个固定的顺序:
- 先把项目里最重要的约定定下来,比如目录结构、命名规范、状态管理选型、lint 开关。
- 把约定写成一份 AI 能读取的文档,放在仓库根目录。
- 配好分析器和 CI 门禁,让规则有强制力。
- 最后再把 Claude Code 和 Codex 接入日常开发。
这样做的好处是:AI 第一次读项目时,映入眼帘的就是一个“规矩井然”的代码库,它的模仿对象是健康的,产出的代码也就更接近团队风格。顺序如果反了,后面你就要花大量时间去纠正 AI 已经“学歪”的习惯,比一开始就规范要累得多。
3. 工程规范重点覆盖的 4 个维度
3.1 目录结构:给 AI 一张“分区地图”
Flutter 项目的目录结构比很多后端框架更自由,这也是 AI 最容易“自由发挥”的地方。如果你不给它一张明确的“分区地图”,它会按照自己的通用习惯,把页面全部塞进lib/pages,把工具函数全部塞进lib/utils,最后整个仓库变成一个巨大的“堆”。
我在项目里用的是 feature-first 的分层方式。大致结构长这样:
lib/ app/ # 应用入口、路由、全局配置 core/ # 跨 feature 的基础设施 network/ storage/ theme/ features/ # 按业务模块聚合 auth/ data/ domain/ presentation/ home/ data/ domain/ presentation/ shared/ # 真正跨模块复用的通用组件和工具 widgets/ utils/这个结构对小团队来说看起来可能“重”,但对 AI 协作来说非常值。为什么?因为 AI 在改一个功能时,可以顺着路径走:需要改数据源就去data层,需要改业务规则就去domain层,需要改界面就去presentation层,不需要在几百个平铺文件里大海捞针。
我会在规范文档里明确写死一条:业务代码一律放在features/<模块名>/之下,禁止在features之间直接跨层引用彼此的 data 实现;如果两个 feature 需要共享数据访问,把共享部分下沉到core/或shared/。有了这条硬约束,AI 再想“随手建目录、随手乱放文件”就难了。
3.2 命名与代码风格:把团队“味儿”写进规则
Dart 本身有一套官方风格指南,但团队想稳定运行项目,光靠官方指南不够。AI 能记住的规则太多了,多到它总会选一个“看起来最像正确答案”的。我们需要把团队自己的约定显式写出来。建议至少覆盖这些:
- 文件名一律
snake_case.dart,禁止出现loginPage.dart这种混搭。 - 类名用
PascalCase,方法、变量、参数用camelCase。 - 常量用
k前缀开头,比如kDefaultAvatarSize。 - 回调参数使用明确的类型(
ValueChanged<T>、VoidCallback),不要直接用裸Function。 - 样式对象不要在
build方法里反复创建,能提为 const 就提为 const。 - 颜色不写硬编码,统一从
Theme.of(context)或主题文件里取。 - 字符串不硬编码,统一走翻译资源。
尤其是颜色和字符串这两条,AI 特别容易踩。你让它画一个按钮,它给你的代码里往往会直接写Color(0xFF2196F3);你让它显示一段提示文案,它往往会直接写中文原文到 widget 里。如果不提前约束,后续做深色模式、多语言时,就得满仓库找这些魔法值。
提示:这类规范最适合用“正面清单 + 负面清单”的方式书写。正面告诉 AI 该怎么做,负面告诉它哪些是雷区。比如“不硬编码颜色,统一使用主题色”比单纯说“注意代码风格”有效得多。
3.3 analysis_options.yaml:让 lint 变成强制门禁
如果说规范文档是“劝告”,那analysis_options.yaml就是“强制”。我建议把 Flutter 项目的flutter analyze当作 AI 产出的第一道自动化闸门。一个比较实用的起步配置大概长这样:
include: package:flutter_lints/flutter.yaml linter: rules: - prefer_const_constructors - avoid_print - sort_child_properties_last - require_trailing_commas - always_declare_return_types - avoid_dynamic_calls - unnecessary_lambdas - prefer_single_quotes analyzer: errors: unused_import: error missing_required_param: error dead_code: error这里的关键动作是把关键规则设置成error,而不是默认的warning。配合 CI 里跑flutter analyze --fatal-infos,任何带 error 的代码都无法合入主分支。AI 再怎么能说会道,也不可能绕过编译器的检查。
另外,还可以把dart format放进保存时和提交前脚本里。代码格式的分歧,很多地方不需要人评,交给格式化器就能解决。团队 review 的精力应该留给“业务逻辑是否正确”,而不是“这个逗号要不要换行”。
3.4 状态管理与依赖:AI 最容易翻车的高发区
Flutter 生态里状态管理方案非常多,AI 的“知识库”里什么样的写法都有。如果没有明确约束,今天它会给你setState,明天给你Provider,后天给你Bloc,甚至同一个文件里都可能混着多种。
我们的约定是:
- 全局共享状态统一用 Riverpod。
- 页面内部局部状态允许
StatefulWidget+setState,但禁止把“需要跨页面共享”的状态也塞进setState。 - 新代码禁止引入新的状态管理依赖。
- 异步数据加载统一走
FutureProvider/AsyncNotifier,不在build里直接发请求。
依赖规则就更关键了。AI 生成代码时碰到“需要一个功能”的第一反应,往往是“引入一个第三方包”。这在 AI 场景下会造成严重的依赖膨胀——本来项目里已经封装好的能力,它偏要再从 pub.dev 拉一个轮子回来。
我在规范里写了一条硬性规则:新增第三方依赖前,必须检查shared/utils和现有代码中是否已有等价实现;确需新增时,必须列出理由,并经过人工确认。这一条看似简单,但实际帮项目避免了很多不必要的“数学库”“时间处理库”“字符串工具库”。
4. 实操:把规范真正喂给 AI
4.1 核心:一份让 AI 看懂项目的 AGENTS.md
规范文档如果只写在团队 Wiki 里,AI 是读不到的。要让 Claude Code 和 Codex 都能自动读取,我把核心规范放在仓库根目录的AGENTS.md里,这等于给了 AI 一份“入职手册”。
实际使用的模板,核心部分大概是这样的:
# 项目工程规范 ## 技术栈 - Flutter 3.x / Dart 3.x - 状态管理:Riverpod - 路由:go_router - 网络:core/network 中的统一封装 ## 目录结构 - 业务代码按 feature 划分,放在 lib/features/<feature>/ - feature 内部分层:data / domain / presentation - 公共组件放 lib/shared/widgets/,工具函数放 lib/shared/utils/ ## 编码约定 1. 文件命名 snake_case,类名 PascalCase,方法/变量 camelCase 2. 不硬编码颜色,统一使用主题色 3. 不硬编码字符串,统一走翻译资源 4. 不在 build 方法里反复创建样式常量,能提 const 就提 5. 状态管理统一 Riverpod,新代码不要引入其他方案 6. 引入第三方依赖前先检查 shared/utils 是否有现成实现 ## 禁止事项 - 禁止修改 core/ 下的基础设施文件,除非任务明确要求 - 禁止在 features 之间直接引用彼此的 data 层实现 - 禁止在 build 方法内触发网络请求 - 禁止为了提高“现代感”而擅自重构与任务无关的代码 ## AI 使用提醒 - 如果需求描述不够具体,先提出澄清问题,不要假设 - 生成代码必须同步更新相关测试 - 代码必须通过 flutter analyze 且保持零 error - 涉及公共 API 的修改,必须说明影响范围想强调一点:这份文件不是给 AI“背”的,而是给 AI“查”的。真正实操时,我会让 Claude Code 在每次开始时主动“读一遍并复述要点”,确认它真的把规范纳入了上下文,而不只是“扫了一眼文件名”。
4.2 让 Claude Code 进入“先规划再动手”模式
Claude Code 最适合做“需要理解全局的任务”。我在项目里使用它时,通常会走一个固定套路。
第一步,先让它建立上下文。我会输入类似这样的话:
“请先阅读根目录的 AGENTS.md,再浏览 lib/features 下的主要模块,然后简要告诉我这个项目的目录约定和状态管理方式。”
这一步看似多问了一句,其实非常关键。它会主动把 AGENTS.md 里的内容加载进自己的对话上下文,之后给出的建议和改动都会受到这些规范的约束。
第二步,让它先给方案。在真正写代码之前,我会要求它“先列出你要改动的文件清单和改动思路”。如果方案里出现“引入新依赖”这种触碰底线的事,我会立刻纠正;如果方案要动的文件超出任务范围,我也会喊停。
第三步,才让它进入执行模式。执行时我一般还会叮嘱一句:
“严格按照 AGENTS.md 的目录和命名约定,不要改动与任务无关的文件。如果遇到歧义,先暂停并问我。”
多写这一句的代价很小,但能让它收敛很多。毕竟 AI 的“主动性”有时会变成“多管闲事”——把旁边的代码顺手也重构了。
4.3 给 Codex 的任务拆分模板
Codex 更适合“边界清晰、可验证”的任务。它的产出质量,很大程度上取决于任务描述的质量。我后来发现,把大功能拆成小任务,效果远好于丢一个整包需求给它。
举例来说,假设要做“修改昵称”这个功能,我不会直接说“帮我实现修改昵称”。我会拆成三条指令,逐条丢给 Codex:
domain 层任务:“在 features/user/domain 下新增 UpdateNicknameUseCase,输入为 userId 和 nickname,复用现有的 Result 类型,失败时返回统一的业务错误码。不要修改其他文件。”
data 层任务:“在 features/user/data 下实现 UpdateNicknameApi,使用 core/network 的 ApiClient 发起 PATCH 请求,不要自己创建新的 HttpClient。接口地址和鉴权方式参考现有代码里的其他接口。”
presentation 层任务:“在 features/user/presentation 新增修改昵称对话框,复用 shared/widgets/ 下的 LoadingButton 和 AppTextInput,表单校验规则参考注册页面的实现。禁止硬编码颜色和文案。”
你看,每条指令都包含四个要素:目标文件的位置、要复用或不要动的模块、禁止事项、参考对象。Codex 拿到这种任务,基本能稳定产出接近团队风格的代码。
另一个特别实用的场景,是用 Codex 来处理flutter analyze报出的批量化问题。比如 CI 报了一堆 lint error,我可以提取成一句指令:“请修复 lib/features/auth 下所有文件的 lint 错误,规则见 analysis_options.yaml,不要改动业务逻辑。”它就能批量产出修复 diff,再由我 review 合并。
4.4 CI 门禁:AI 合入代码前的最后一道闸
规范说到底是“软约束”,模型未必每次都能 100% 遵守,所以必须配上“硬约束”——自动化检查和门禁。我的建议是最低配置包含三个:
dart format --set-exit-if-changed .:检查代码是否被格式化器统一处理。这一步能拦截大量“风格漂移”。flutter analyze --fatal-infos:把分析器的 info 和 warning 也变成失败条件,确保零 lint 问题。flutter test:跑全量测试,确保 AI 的改动没有破坏已有行为。
如果是合入主分支的合并请求,再加一条人工确认:PR 描述里必须写明“本次改动是否新增了依赖、是否修改了 core 层文件、是否更新了测试”。这条要求听起来是对人的,但它实际上也会变成 AI 的提示——当 AI 在生成 PR 描述时,它会潜意识地避免触碰这些“需要额外解释”的红线。
注意:
flutter analyze的规则不能只是“开着”,而要让关键规则在 CI 里变成阻断性错误。否则 AI 产出的代码照样能合进来,规范就形同虚设了。
5. 常见问题与排查经验
5.1 规范写了 AI 还是违规,怎么排查
我经常被问到:规范明明写了,为什么 AI 还是违规?最典型的就是“不要引入新依赖”,但它还是引入了。
遇到这种情况,先别急着怪 AI。很可能是你把规则写在了文档末尾,模型在超长上下文里的注意力分布并不均匀,它根本没有“看到”那条规则。我的应对方式是:把最高优的“红线规则”同时放到 AGENTS.md 的顶部,并且在每次下达任务时也明确写一遍。比如“本次任务禁止新增任何第三方依赖”。
如果依然违规,我会把违规案例存成一个“常见违规记录”,在下一次任务描述里明确引用:“上次任务你引入了某个包,本次请避免。项目已有等价实现位于 shared/utils/ 下,请优先使用。”AI 代码模型对“最近一次修正”往往有较强的服从倾向,这种反馈式约束比空泛说“你要遵守规范”有用得多。
5.2 规范文档多长合适,太长反而不管用
我一开始把 AGENTS.md 写得非常详细,甚至包含各种基础示例。结果发现 Claude Code 在长上下文里会“抓不住重点”,经常高概率地遵循“长尾规则”而不是顶部规则,而且每次对话的加载开销也更明显。
后来我把规范精简到“3 屏以内能读完”的长度,用编号列清楚核心规则,用负面清单明确禁止事项。效果反而更好。团队成员也更容易记住——规范文档最后变成了大家嘴里常说的“那几条红线”。
一个可以参考的经验是:不要试图用一篇文档覆盖所有场景,只覆盖“最容易翻车”和“最影响一致性”的那 20% 规则,剩下 80% 交给analysis_options.yaml和 code review 兜底。
5.3 多个工具读不同规范文件,怎么保持一致
同时用 Claude Code 和 Codex 时,会碰到一个现实问题:Claude Code 默认会读CLAUDE.md,Codex 则更多依赖AGENTS.md或者任务描述。如果两套文档各自维护,很容易出现“一个改了另一个没改”的漂移。
我的办法是只维护一份权威文档AGENTS.md,同时建一个极简的CLAUDE.md,里面只写一句话:
“本项目的所有工程规范请阅读仓库根目录的 AGENTS.md,并严格遵守其中的编码约定、禁止事项和 AI 使用提醒。”
这样无论哪个工具最终读到的是哪份文件,它都会指向同一个信息源。代码库里的“规范事实”永远不会分裂。
5.4 用数据判断 AI 产出是不是真的变好了
只有感觉“好像变好了”不够,我还会用几个硬指标持续跟踪:
- 每次 PR 里
flutter analyze挂掉的次数。目标:趋于 0。 - 评审时因为“命名不规范”“颜色硬编码”“引入多余依赖”被打回的占比。
- 一次 AI 任务从发起到产出可用代码的迭代轮数。规范配得好的话,这个数字会明显下降。
- 合并后一周内出现与 AI 改动相关的回归 bug 数量。
这些指标不需要做得很复杂,记在项目文档的一个表格里就行。我每个月会扫一遍,如果某项指标回升,就去检查是不是 AGENTS.md 该更新了——比如团队换了一种加载态写法,但规范文档里没有同步写清楚,导致 AI 继续按老写法输出。
最后说一点个人体会。以前一说到“AI 写代码”,身边总是两种声音:要么觉得神器降临,要么觉得项目要完蛋。我现在的看法有点不一样——AI 本身不是问题,问题在于你有没有把“团队约定”讲清楚。把工程规范当作 AI 的入职手册来写,它就能成为真正提效的同事,而不是那个“看起来很厉害、实则总要返工”的新人。
如果你打算在自己项目里试,我的建议是不要一上来就追求完美规范。先挑最容易翻车的三个点:命名风格、状态管理、硬编码。写进 AGENTS.md,配好flutter analyze门禁,跑一周,然后根据实际翻车情况迭代补充。你可能会发现,原来让 AI“不翻车”的关键,并不是换一个更聪明的模型,而是先想明白“你自己希望代码长成什么样”。