一个团队引入 AI 编码后,spotless 与 checkstyle 全部通过——格式完全统一。而半年后评审变得很慢:同一个需求,五个人的 AI 给出五种"结构"(有的用 Stream、有的用 for 循环、有的拆成三个小方法、有的写成一个长方法),而格式检查对此一无所知。
代码风格有两层:格式(缩进、命名、括号)与写法(结构、抽象、错误处理)。工具能管前者,这个分层解释了"为什么格式全绿却难读":格式只影响"看第一眼",写法影响"读懂一段要走多少步"——而后者才是评审耗时的主要来源。
而后者才是"读起来累不累"的主要来源。
1. 两层的可自动化程度
| 层 | 能否自动化 |
|---|---|
| 格式(缩进/换行/命名) | 能(formatter + linter) |
| 写法(结构/抽象层级) | 难(需"约定 + 评审") |
| 错误处理风格 | 中(可用检查拦一部分) |
| 注释风格 | 中(可检查"有无 doxygen") |
第二项是"AI 放大了差异"的地方:格式工具把它压平了,而"结构"的差异被保留并放大第二项的"放大机制"是"AI 一次生成一整块":人工写代码时"结构"随讨论逐步收敛,而 AI 一次给出"成品",于是"它选的抽象层级"直接成为结果。
——因为 AI 会"按自己的偏好"选结构。
2. 三种"写法差异"的具体表现
| 差异 | 例子 |
|---|---|
| 控制流 | Stream vs for |
| 抽象层级 | 一个长方法 vs 三个小方法 |
| 错误处理 | 抛异常 vs 返回 Optional |
| 数据形状 | DTO vs Map |
第四项的"隐藏成本"在"后续修改":Map 的键没有类型,改字段名时编译器不报错——于是"改名"变成了"运行时才发现"(与"安全重命名"的坑同源)。
第四项最容易被忽略:"传 Map"与"传 DTO"在编译期都不报错,而它们"后续怎么改"完全不同。
3. "写法约定"该怎么写才有效
| 写法 | 效果 |
|---|---|
| "尽量用 Stream" | 弱("尽量"无边界) |
| "集合转换用 Stream;含副作用用 for" | 强(有条件) |
| "方法不超过 30 行" | 中(可检查) |
| 给出"反例 + 正例" | 最强 |
第二项是"可判定"的写法:它给出"什么时候用哪个"的判据第二项的"检验方式"很实用:把约定给两个不同的人看,问"这段代码该用哪个"——答案一致,说明约定可判定;不一致,说明它还是"偏好"。
,于是"选择"变成"按条件判断",而不是"按偏好"。
第四项的"原因"是"示例是"具体的",规则是"抽象的"":具体的东西可以被"照抄",抽象的东西要"解释"——而解释会引入歧义。
第四项对 AI 特别有效:AI 对"示例"的遵循度高于"规则描述"——于是"一个反例 + 一个正例"比"三条规则"更能统一写法。
// 一条有效的写法约定(含条件与示例)
规则:集合的"纯转换"用 Stream;"含副作用(日志/IO/状态变更)"用 for。
正例:list.stream().map(User::name).toList() // 纯转换
反例:list.forEach(u -> { log.info(...); repo.save(u); }) // 副作用
判据:循环体里有没有"不只是算值"的动作
4. AI 为什么会"各有偏好"
| 原因 | 机制 |
|---|---|
| 训练语料多样 | 见过多种写法 |
| 提示未约束 | "写一个方法"没有结构要求 |
| 上下文不同 | 参考了文件里已有的写法 |
| 模型版本不同 | 偏好随版本变 |
第三项是"可利用"的:**如果文件里已有"统一写法",AI 会"照着写"**第三项的"实践"很直接:把"参考实现"的文件一起放进上下文,并说明"照这个文件的写法"——于是"风格"通过"示例"传递,而不是通过"规则"。
——于是"让第一个文件写对"比"事后统一"便宜得多。
第四项的"量化"可以很轻:每月抽 20 个 AI 生成的 PR,统计"用了 Stream / 用了 for / 拆成几个方法"——分布变化就是"漂移"。
第四项说明"统一"是"持续动作":换模型版本会带来写法漂移,所以要"定期抽样对比"。
"写法约定"应当只写"高频、可判定"的几条,否则没人记得住:
// 一份"够用"的写法约定(五条,都带判据)
1 集合纯转换 → Stream;含副作用 → for
2 一个方法只做一件事;超过 30 行拆(含判据:能否用一句话命名)
3 对外返回值用 Optional/DTO,不返回 null/Map
4 异常只抛"业务异常 + 参数",不抛裸 RuntimeException
5 IO 不在循环里(收集后批量)
// 判据:每条都能用"一段代码该不该这样写"来回答
5. 三种"统一写法"的手段
| 手段 | 成本 |
|---|---|
| 约定文档(含示例) | 低(但不执行) |
| 静态检查(自定义规则) | 中(能拦一部分) |
| 代码模板/骨架(脚手架) | 中高(最有效) |
第三项最有效:"新代码从一个骨架开始"第三项的"落地"是"模板 + 生成器":new-service 命令生成"controller/service/repo + 测试"的骨架,AI 只填"业务部分"——于是"结构"不由它决定。
,于是"结构"被固定,AI 只能"填内容"。
第二项能拦"可判定的部分":"方法超过 30 行"、"catch 块为空"、"循环里有 IO" 这类规则可以做。
// 自定义静态规则(示例:拦"循环里的 IO")
- id: io-in-loop
pattern: for (...) { ... repo.save(...) ... }
message: "循环内做 IO:应收集后批量写入"
severity: warning
// 收益:把"写法约定"变成"可执行检查";代价:规则要维护、可能误报
"评审意见"可以按"是否可判定"分类,从而判断"标准够不够清":
// 三类评审意见(比例决定"标准质量")
A 逻辑/边界错(应当占多数) → 说明评审在看"对错"
B 写法偏好("我觉得用 for 更好") → 说明标准不清
C 格式(缩进/命名) → 应当为 0(工具管)
// 判据:B 类占比 > 30% → 先补"判据",而不是"多评审"
6. 排错:五种"风格统一了但评审变慢"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 格式全绿但读着累 | 写法差异 | 加"写法约定 + 示例" |
| 同一需求五种实现 | 无结构约束 | 模板/骨架 |
| 换模型后风格变了 | 版本漂移 | 定期抽样 |
| 约定没人遵守 | 无检查 | 静态规则 |
| 评审都是"风格意见" | 无共同标准 | 先定判据 |
第五项的"修法"是"把争议点写成判据":每次争论后记一句"这种情况用 X",于是"下一次同类争论"有依据——而这份记录就是"团队约定"的雏形。
第五项是"评审质量"问题:评审里一半意见是"我觉得这样更好",说明"标准不清"——而它会让"该关注逻辑的评审"变成"风格争论"。
最后一个"粒度"建议:把"生成粒度"缩小到"一个方法/一个类",写法差异会自然收敛。
// 三种生成粒度与"写法一致性"
整块功能("实现订单模块"):结构由模型决定 → 差异最大
一个类("实现 OrderService"):结构半定 → 差异中
一个方法("实现这个方法体"):结构已定 → 差异最小
// 判据:团队是否已能"用一句话说清结构"
7. 边界:什么情况不必统一写法
| 场景 | 为什么 | 做法 |
|---|---|---|
| 单人项目 | 无协作成本 | 不统一 |
| 一次性脚本 | 不长期 | 不统一 |
| 强性能代码(手写优化) | 结构由性能决定 | 例外 |
| 已有成熟规范(大厂风格) | 跟随即可 | 跟随 |
判断口径一句话:"同一个需求,不同人写出来的结构差异,会不会让评审与维护变慢"。最后一条与"AI 使用方式"有关的边界:"让 AI 一次生成整块"与"让它填一个函数",对写法统一的影响差别很大。所以"统一写法"的一条隐含实践是"缩小生成粒度"。
会(多人协作、长期维护),就必须统一"写法";不会(单人、短期),格式统一就够了。
8. 自测
- 代码风格的两层里,哪一层工具能管、哪一层管不了?为什么 AI 会"放大"后者?
- "尽量用 Stream"为什么是弱约定?"含副作用用 for"为什么强?
- 为什么"反例 + 正例"对 AI 特别有效?
- "上下文不同"为什么是"可利用"的原因?它带来什么实践建议?
- 三种统一手段里,哪一种最有效?为什么"骨架"比"约定文档"有效?
- 判断"是否需要统一写法"的那个问题是什么?