聊天讨论 我让 AI 改一个 bug——它偷偷动了 5 个我没让它碰的地方

193577746(kyriewen) · July 22, 2026 · 8 hits

让 AI 改一行代码,diff 里却多了几处你没提的改动——用 AI 编程的人都遇到过。我整理了 5 种最常见的"暗改"模式,每种附真实代码和防范方法。

从一个简单的 bug 说起

最近改一个状态更新的 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 逻辑。

同类情况还有:

  • Vite 的import.meta.glob自动注册的路由/组件
  • Vue 的全局插件注册(app.use(plugin)所在文件被删)
  • require.context动态加载的模块(Webpack 项目常见)
  • Tailwind CSS 的content配置引用的工具 class 文件

防范: 看到 AI 删文件,先想一下这个文件是不是框架约定的。middlewareerrorlayoutloadingplugins/目录、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 调用
  • 构造函数里的同步初始化被改成 async(构造函数不支持 async)
  • 同步的校验逻辑被改成异步,导致校验还没完成就放行了

防范: AI 把同步改异步时,问自己一个问题:"这里用同步是不是有原因的?"启动流程、构造函数、校验逻辑——这三个地方的同步代码,通常都是故意的。


暗改三:重构时弄丢了一个 break

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 忽略,直接删掉 label
  • throw在 try-catch 重构后被 catch 吃掉,异常不再向上传播

防范: 重构后的第一件事——diff 里搜breakcontinuereturnthrow。这四个关键词的任何变动都要逐个核对原始逻辑。


暗改四:给你两行代码套了三层抽象

你让 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 写的专业架构",因为看起来太正式了,改了怕出问题。

同类情况:

  • 一个简单的 fetch 请求被包成了带重试、超时、拦截器的 HTTP Client 类
  • 一个环境变量读取被封装成了配置中心 + 热更新 + 类型校验
  • 一个数组过滤被改成了策略模式 + 责任链

防范: 如果你让 AI 写的功能用一句话就能描述清楚,结果代码超过 20 行——大概率过度设计了。删掉,让它重写,prompt 里加一句"用最少的代码实现"。


暗改五:把你手动改好的代码"优化"回去

这是最让人崩溃的一种。

你在 AI 生成的代码里发现了一个问题,手动改好了。过了一会儿让 AI 继续改另一个地方,它把你的修复给"优化"没了——因为它的上下文里记住了自己之前生成的版本,认为那才是"正确的"。

// 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 最离谱的"暗改"是什么?评论区聊聊。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.