让 AI 改一行代码,diff 里却多了几处你没提的改动——用 AI 编程的人都遇到过。我整理了 5 种最常见的"暗改"模式,每种附真实代码和防范方法。
最近改一个状态更新的 bug,三行代码的事。让 AI 改完,习惯性看了一眼 diff——
改动不止三行。
翻了翻最近的 commit 记录,这种事不是第一次了。AI 改代码有个毛病:它不只改你让它改的地方,还会"顺手"动一些它觉得"应该优化"的代码。
有些改动无伤大雅,但有些能直接让项目炸掉。
下面是我总结的 5 种最常见的 AI 暗改模式,每种都在社区里反复出现过。
这是最危险的一种。
AI 在重构的时候,会扫描代码引用关系。如果一个文件没有被任何地方显式 import,它就认为是死代码,直接删掉。
问题是,前端框架有大量"约定式"文件,不需要手动 import。
# AI觉得这些文件"没人引用",删了
src/
├── middleware.ts ← Next.js自动加载,不需要import
├── app/error.tsx ← Next.js错误边界,约定文件名
├── plugins/analytics.ts ← Nuxt自动加载plugins目录
└── composables/useAuth.ts ← Nuxt自动导入composables目录
// middleware.ts — 没有任何文件import它,但Next.js自动执行
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
AI 看到没有任何引用,判定为死代码。删掉之后——所有页面的登录校验没了,未登录用户可以直接访问所有路由。上线之前跑测试大概率还能过,因为单测通常 mock 了 auth 逻辑。
同类情况还有:
import.meta.glob自动注册的路由/组件app.use(plugin)所在文件被删)require.context动态加载的模块(Webpack 项目常见)content配置引用的工具 class 文件防范: 看到 AI 删文件,先想一下这个文件是不是框架约定的。middleware、error、layout、loading、plugins/目录、composables/目录——这些名字自带魔法,删了就炸。
AI 特别喜欢把同步代码改成异步的。在它的训练数据里,"异步=好"几乎是共识。
// 改之前:同步读取配置(启动时必须阻塞等结果)
const config = fs.readFileSync('./config.json', 'utf-8');
const parsed = JSON.parse(config);
initDatabase(parsed.db);
initCache(parsed.cache);
startServer();
AI 觉得readFileSync不优雅,改成了:
// AI"优化"后:异步读取
const config = await fs.promises.readFile('./config.json', 'utf-8');
const parsed = JSON.parse(config);
initDatabase(parsed.db);
initCache(parsed.cache);
startServer();
看起来没问题?问题在于这段代码跑在一个没有顶层 await 支持的 CommonJS 模块里。或者更隐蔽的场景:initDatabase内部依赖同步的初始化时序,异步化之后其他模块在 config 还没读完时就开始初始化了。
这种 bug 极其难排查——启动流程偶尔成功偶尔失败,取决于异步操作的完成顺序。
同类情况:
localStorage.getItem 被改成异步的 IndexedDB 调用防范: AI 把同步改异步时,问自己一个问题:"这里用同步是不是有原因的?"启动流程、构造函数、校验逻辑——这三个地方的同步代码,通常都是故意的。
AI 在做代码搬移或重构的时候,偶尔会丢失控制流逻辑。最常见的是 switch 里的 break 和循环里的提前返回。
// 改之前
function processItems(items) {
for (const item of items) {
if (item.type === 'invalid') {
logError(item);
break; // 遇到无效项就停止处理
}
transform(item);
}
}
让 AI 重构这段代码,比如拆成更小的函数:
// AI重构后
function processItems(items) {
for (const item of items) {
handleItem(item);
}
}
function handleItem(item) {
if (item.type === 'invalid') {
logError(item);
return; // 这个return只退出handleItem,不退出循环!
}
transform(item);
}
break变成了return,但语义完全不同。原来遇到无效项整个循环停止,现在只是跳过这一项,继续处理后面的。如果无效项后面的数据依赖前面的校验结果,后续处理全是脏数据。
同类情况:
continue在提取函数后变成return,语义改变break(带 label 的)被 AI 忽略,直接删掉 labelthrow在 try-catch 重构后被 catch 吃掉,异常不再向上传播防范: 重构后的第一件事——diff 里搜break、continue、return、throw。这四个关键词的任何变动都要逐个核对原始逻辑。
你让 AI 写一个判断空字符串的工具函数,期望得到这个:
function isEmpty(str: string): boolean {
return str.trim().length === 0;
}
AI 给你的:
interface StringValidator {
validate(input: string): ValidationResult;
}
interface ValidationResult {
isValid: boolean;
reason?: string;
metadata?: Record<string, unknown>;
}
class EmptyStringValidator implements StringValidator {
private readonly trimBeforeCheck: boolean;
constructor(options?: { trimBeforeCheck?: boolean }) {
this.trimBeforeCheck = options?.trimBeforeCheck ?? true;
}
validate(input: string): ValidationResult {
const processed = this.trimBeforeCheck ? input.trim() : input;
return {
isValid: processed.length > 0,
reason: processed.length === 0 ? 'String is empty or whitespace' : undefined,
metadata: { originalLength: input.length, processedLength: processed.length }
};
}
}
export const isEmpty = (str: string): boolean => {
return !new EmptyStringValidator().validate(str).isValid;
};
两行逻辑变成了三十行。接口、类、配置项、元数据——全套。
这种暗改不会让项目崩溃,但它会让代码库快速膨胀。三个月后,团队里没人敢碰这些"AI 写的专业架构",因为看起来太正式了,改了怕出问题。
同类情况:
防范: 如果你让 AI 写的功能用一句话就能描述清楚,结果代码超过 20 行——大概率过度设计了。删掉,让它重写,prompt 里加一句"用最少的代码实现"。
这是最让人崩溃的一种。
你在 AI 生成的代码里发现了一个问题,手动改好了。过了一会儿让 AI 继续改另一个地方,它把你的修复给"优化"没了——因为它的上下文里记住了自己之前生成的版本,认为那才是"正确的"。
// AI第一次生成的
useEffect(() => {
fetchData();
}, []);
// 你手动加了依赖项
useEffect(() => {
fetchData();
}, [userId]); // ← 你手动加的
// 让AI改另一个bug,它"顺手"改回来了
useEffect(() => {
fetchData();
}, []); // ← AI改回空数组了,因为它觉得空数组"是对的"
AI 不理解你为什么改了它的代码。在它看来,空依赖数组是"标准写法",你加的[userId]是多余的。
这种情况在长对话中尤其频繁。对话越长,AI 越倾向于用自己早期生成的版本覆盖你的手动修改。
防范:
useEffect的依赖数组,那是我故意改的"| 暗改类型 | 危险等级 | 排查方法 | 预防措施 |
|---|---|---|---|
| 删"死代码" | 🔴致命 | diff 里搜删除的 class/文件 | 有框架注解的 class 不是死代码 |
| 同步改异步 | 🔴致命 | diff 里搜 async/await 新增 | 启动/构造/校验的同步是故意的 |
| 丢失 break/return | 🟡高危 | diff 里搜 break/continue/throw | 重构后逐个核对控制流 |
| 过度抽象 | 🟢低危 | 一句话需求超 20 行代码 | prompt 加"最少代码实现" |
| 覆盖手动修改 | 🔴致命 | 看完整 diff,不只看改动点 | 改完手动代码开新对话 |
一条通用原则:AI 每次改完代码,看完整 diff,不只看你让它改的那行。
说到底,这些"暗改"不是 AI 故意搞破坏。它在做它认为正确的事——清理死代码、优化性能、统一风格。问题是它没有你项目的完整上下文,不知道哪些"不规范"的代码是故意写成那样的。
AI 是一个能力很强但完全不懂你项目历史的新同事。你不会让一个刚入职的人直接 push 到 main,对 AI 也一样。
你遇到过 AI 最离谱的"暗改"是什么?评论区聊聊。