让AI改一行代码,diff里却多了几处你没提的改动------用AI编程的人都遇到过。我整理了5种最常见的"暗改"模式,每种附真实代码和防范方法。
从一个简单的bug说起
最近改一个状态更新的bug,三行代码的事。让AI改完,习惯性看了一眼diff------
改动不止三行。
翻了翻最近的commit记录,这种事不是第一次了。AI改代码有个毛病:它不只改你让它改的地方,还会"顺手"动一些它觉得"应该优化"的代码。
有些改动无伤大雅,但有些能直接让项目炸掉。
下面是我总结的5种最常见的AI暗改模式,每种都在社区里反复出现过。
暗改一:删掉"没人用"的文件------其实是框架自动加载的
这是最危险的一种。
AI在重构的时候,会扫描代码引用关系。如果一个文件没有被任何地方显式import,它就认为是死代码,直接删掉。
问题是,前端框架有大量"约定式"文件,不需要手动import。
bash
# AI觉得这些文件"没人引用",删了
src/
├── middleware.ts ← Next.js自动加载,不需要import
├── app/error.tsx ← Next.js错误边界,约定文件名
├── plugins/analytics.ts ← Nuxt自动加载plugins目录
└── composables/useAuth.ts ← Nuxt自动导入composables目录
typescript
// 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逻辑。
同类情况还有:
- Vite的
import.meta.glob自动注册的路由/组件 - Vue的全局插件注册(
app.use(plugin)所在文件被删) require.context动态加载的模块(Webpack项目常见)- Tailwind CSS的
content配置引用的工具class文件
防范: 看到AI删文件,先想一下这个文件是不是框架约定的。middleware、error、layout、loading、plugins/目录、composables/目录------这些名字自带魔法,删了就炸。
暗改二:把同步改成异步------"优雅"地炸了启动流程
AI特别喜欢把同步代码改成异步的。在它的训练数据里,"异步=好"几乎是共识。
javascript
// 改之前:同步读取配置(启动时必须阻塞等结果)
const config = fs.readFileSync('./config.json', 'utf-8');
const parsed = JSON.parse(config);
initDatabase(parsed.db);
initCache(parsed.cache);
startServer();
AI觉得readFileSync不优雅,改成了:
javascript
// 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调用- 构造函数里的同步初始化被改成async(构造函数不支持async)
- 同步的校验逻辑被改成异步,导致校验还没完成就放行了
防范: AI把同步改异步时,问自己一个问题:"这里用同步是不是有原因的?"启动流程、构造函数、校验逻辑------这三个地方的同步代码,通常都是故意的。
暗改三:重构时弄丢了一个break
AI在做代码搬移或重构的时候,偶尔会丢失控制流逻辑。最常见的是switch里的break和循环里的提前返回。
javascript
// 改之前
function processItems(items) {
for (const item of items) {
if (item.type === 'invalid') {
logError(item);
break; // 遇到无效项就停止处理
}
transform(item);
}
}
让AI重构这段代码,比如拆成更小的函数:
javascript
// 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忽略,直接删掉label throw在try-catch重构后被catch吃掉,异常不再向上传播
防范: 重构后的第一件事------diff里搜break、continue、return、throw。这四个关键词的任何变动都要逐个核对原始逻辑。
暗改四:给你两行代码套了三层抽象
你让AI写一个判断空字符串的工具函数,期望得到这个:
typescript
function isEmpty(str: string): boolean {
return str.trim().length === 0;
}
AI给你的:
typescript
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写的专业架构",因为看起来太正式了,改了怕出问题。
同类情况:
- 一个简单的fetch请求被包成了带重试、超时、拦截器的HTTP Client类
- 一个环境变量读取被封装成了配置中心+热更新+类型校验
- 一个数组过滤被改成了策略模式+责任链
防范: 如果你让AI写的功能用一句话就能描述清楚,结果代码超过20行------大概率过度设计了。删掉,让它重写,prompt里加一句"用最少的代码实现"。
暗改五:把你手动改好的代码"优化"回去
这是最让人崩溃的一种。
你在AI生成的代码里发现了一个问题,手动改好了。过了一会儿让AI继续改另一个地方,它把你的修复给"优化"没了------因为它的上下文里记住了自己之前生成的版本,认为那才是"正确的"。
jsx
// AI第一次生成的
useEffect(() => {
fetchData();
}, []);
// 你手动加了依赖项
useEffect(() => {
fetchData();
}, [userId]); // ← 你手动加的
// 让AI改另一个bug,它"顺手"改回来了
useEffect(() => {
fetchData();
}, []); // ← AI改回空数组了,因为它觉得空数组"是对的"
AI不理解你为什么改了它的代码。在它看来,空依赖数组是"标准写法",你加的[userId]是多余的。
这种情况在长对话中尤其频繁。对话越长,AI越倾向于用自己早期生成的版本覆盖你的手动修改。
防范:
- 手动改完AI的代码后,开一个新对话再继续后面的任务
- 或者在prompt里明确说"不要修改
useEffect的依赖数组,那是我故意改的" - 养成习惯:每次AI改完代码,完整看一遍diff,不只看它改的地方,看所有改动
AI暗改防范速查表
| 暗改类型 | 危险等级 | 排查方法 | 预防措施 |
|---|---|---|---|
| 删"死代码" | 🔴致命 | diff里搜删除的class/文件 | 有框架注解的class不是死代码 |
| 同步改异步 | 🔴致命 | diff里搜async/await新增 | 启动/构造/校验的同步是故意的 |
| 丢失break/return | 🟡高危 | diff里搜break/continue/throw | 重构后逐个核对控制流 |
| 过度抽象 | 🟢低危 | 一句话需求超20行代码 | prompt加"最少代码实现" |
| 覆盖手动修改 | 🔴致命 | 看完整diff,不只看改动点 | 改完手动代码开新对话 |
一条通用原则:AI每次改完代码,看完整diff,不只看你让它改的那行。
这不是AI的错
说到底,这些"暗改"不是AI故意搞破坏。它在做它认为正确的事------清理死代码、优化性能、统一风格。问题是它没有你项目的完整上下文,不知道哪些"不规范"的代码是故意写成那样的。
AI是一个能力很强但完全不懂你项目历史的新同事。你不会让一个刚入职的人直接push到main,对AI也一样。
你遇到过AI最离谱的"暗改"是什么?评论区聊聊。