AI 能在几分钟里改完一个组件、补齐测试、顺手整理目录。真正让人卡住的,往往不是它写得够不够快,而是那份 diff 摆到评审里以后:我们究竟在确认什么?
碰到维护多年的前端模块,代码评审最容易变成"这段看起来没问题"和"我不敢合"的拉扯。把争论拆开看,核心只有两个问题:这次改动有没有守住原来的业务语义;出了问题,谁能把它说清楚、兜得住。
第一个问题:代码能跑,还是团队还能看懂
老模块最麻烦的地方,不在于代码写得旧,而在于里面藏着很多没有写进文档的默认值。比如列表筛选里,status=all 和不传 status 在接口层可能不是一回事;首页的 page=1 不传,也许是为了让缓存键稳定。
原来的实现看起来很朴素:
ts
type ListFilters = {
keyword: string;
status: "all" | "open" | "done";
page: number;
};
export function toQuery(filters: ListFilters) {
const query = new URLSearchParams();
if (filters.keyword.trim()) query.set("q", filters.keyword.trim());
if (filters.status !== "all") query.set("status", filters.status);
if (filters.page > 1) query.set("page", String(filters.page));
return query;
}
让 Claude Code、Codex 这类 agent 做"清理"时,很容易拿到一版更通用的写法:
ts
export function toQuery(filters: ListFilters) {
return new URLSearchParams(
Object.entries(filters)
.filter(([, value]) => Boolean(value))
.map(([key, value]) => [key, String(value)]),
);
}
类型没报错,页面也能请求成功。但新实现会把 status=all、page=1 都带出去。它是不是 bug,取决于后端约定、缓存策略和历史兼容逻辑,而不是取决于代码是否"更优雅"。
这正是第一场争论的来源。一边会说:既然行为看起来正常,就别被旧写法绑住;另一边会问:谁能证明这几个省略条件没有业务含义?两边都不是在反对 AI,而是在用不同标准判断风险。
评审时不妨先把"看懂"翻译成三个可验证的问题:
| 要确认什么 | 评审里该看什么 |
|---|---|
| 原有语义有没有保留 | 默认值、空值、边界页、权限态是否仍然一致 |
| 差异有没有被说出来 | PR 描述能否列出"原来不传什么,现在为什么传" |
| 下次能否快速定位 | 关键分支是否有用例,而不是只留一段生成后的代码 |
如果作者只能说"AI 跑过测试了",这份改动还没有达到可评审的状态。测试通过只说明覆盖到的行为没坏,不能替团队解释没被覆盖的约定。
第二个问题:出了问题,谁负责解释和回滚
另一种常见的争论更直接:既然改动是 AI 写的,出了线上问题算谁的?
这个问题问错了对象。AI 不是提交人,也不会参与线上排障。它可以生成一个跨多个文件的方案,却不能替任何人理解埋在老模块里的取舍。责任不会因为代码的产生方式改变,只会因为改动范围变大而更需要被写清楚。
一个能落地的分工应该是这样的:
| 角色 | 合并前要承担的事 |
|---|---|
| 改动发起人 | 解释目标、列出不允许变化的行为、补上回归用例 |
| Reviewer | 判断风险是否被覆盖,必要时要求拆小或补证据 |
| AI 工具 | 提供实现候选、定位影响范围、协助生成测试,不拥有合并决定 |
这里最容易走向两个极端。一个极端是"它生成得快,先合再说";另一个极端是"祖传模块谁都别动,AI 更不能碰"。前者把合并当成试运行,后者把历史包袱当成不可改变的规则。
更稳妥的做法是给每次 AI 参与的老模块改动设一道很具体的门:没有说明不变量、没有最小回归测试、没有回滚方式,就不合。它和人工写的代码一样适用,只是 AI 往往一次触及更多文件,所以这道门更重要。
不要在大 diff 里讨论"AI 好不好用"
当一个 PR 同时改了请求参数、状态管理和 UI 组件,评审很容易失焦。有人盯命名,有人讨论模型能力,真正高风险的行为变化反而被淹没。
我更建议按下面的顺序拆:
- 先锁行为。 先写现有模块最不能变的 3 到 5 条规则,例如"默认筛选不发参数""切页时保留关键词"。
- 再让 agent 出方案。 在 Claude Code 或 Codex 中先要求它列出会影响哪些状态、接口和测试,再让它改代码;不要一上来就接受整份重写。
- 把改动拆成可回退的 PR。 表征现状的测试、逻辑重构、视觉调整分开提交。出了问题,回滚就不需要猜。
- 最后才看代码风格。 行为和边界没有共识时,争论
reduce还是for...of没有意义。
前面的例子可以先补一组表征测试,把历史规则钉住:
ts
import { expect, it } from "vitest";
import { toQuery } from "./toQuery";
it("首页默认筛选不写入查询参数", () => {
const query = toQuery({ keyword: "", status: "all", page: 1 });
expect(query.toString()).toBe("");
});
it("只在非默认状态和翻页时携带参数", () => {
const query = toQuery({ keyword: "react", status: "open", page: 2 });
expect(query.toString()).toBe("q=react&status=open&page=2");
});
这不是为了限制工具,而是为了让它的输出有边界。生成速度越快,越不能把"我大概看过了"当作评审结论。
评审里值得留下的四句话
下次碰到 AI 改老模块,可以直接把下面四句话写进 PR 描述或评审意见:
- 这次明确不改变哪些用户可见行为?
- 哪些默认值、空值和异常分支已经有回归测试?
- 如果这个改动要撤回,最小回滚单元是什么?
- 合并后第一个看告警和问题反馈的人是谁?
这四句话会把"AI 写得行不行"的空泛争论,拉回到团队真正能决策的地方。
AI 接手祖传模块不是不能做。不能接受的是,把一份更大的 diff 当成更高的效率,然后把理解成本留给下一个排障的人。
如果是你,会把底线设在"必须补齐回归测试",还是"必须把改动拆成小 PR"?评论区说说你的做法。