AI 改代码后 build 挂了:恢复与诊断

AI 跑完一轮 npm run build 报错。用 diff 分级、tsc --noEmit 和定向 git restore,10 分钟内定位坏改动,不用全 reset。

你让 Claude Code 跑一个 refactor,半小时后回来发现 npm run build 红了一片:Cannot find name 'XYZ'Module has no exported member 'foo'vite.config.ts:12 Unexpected token。Agent 自信地说”已修复并通过测试”,但 build 不会撒谎。这种”AI 一通改完 build 烂掉”是当下最高频的事故之一,原因多数是它动了不该动的 config 文件、加了不存在的 import、或留了开发期的 mock。

最快修复(覆盖九成情况):先跑 npx tsc --noEmit 一次性看到所有错(而不是 npm run build 卡在第一个错就停),然后对风险最高的改动文件用 git restore --source=HEAD~1 -- <文件> 逐组回滚——先 config 文件,再 lib/utils 这类公共目录,每回滚一组就重新 build。这样二分定位,10 分钟左右就能找到坏改动,不用 git reset 全清。下面是完整流程,以及怎么让它别再犯。

常见原因

按命中率从高到低排序。

1. Agent 改了配置文件(tsconfig / vite.config / next.config)

最常见也最痛。Agent 看到一个类型错就去松 tsconfig.jsonstrict,或者把 vite.config.tsresolve.alias 重写一遍——结果一处类型错变成全局 build 失败。

error TS5023: Unknown compiler option 'allowImportingTsExtensions'.
error during build:
RollupError: "default" is not exported by "src/utils/helpers"

如何判断git diff main -- '*.config.*' tsconfig*.json 看所有 config 文件是否被动过。

2. 加了不存在的 import

Agent 自己虚构了一个 utility 路径(@/utils/superhelper./lib/notFound),TypeScript 编译过不去:

src/components/Form.tsx:5:18 - error TS2307: Cannot find module
  '@/utils/superhelper' or its corresponding type declarations.

如何判断tsc --noEmit 把所有 TS2307 / TS2305 列出来。

3. 残留了 agent 测试期间的 mock

Agent 在调试时塞了 vi.mock('next/router') 或在 next.config.js 里加了 experimental: { mock: true },提交时忘了移除。

Error: <Link> requires an href prop

如何判断:在 diff 里搜 mockstubfixtureTODOXXX 关键字。

4. 删了被其他文件用到的 export

Agent 觉得某个函数没用,删了它。结果其他 5 个文件还在 import:

src/pages/index.tsx:8:10 - error TS2305: Module './lib/auth'
  has no exported member 'verifyToken'.

如何判断:build 失败信息里有 has no exported membercannot find name

5. 包版本不兼容(API breaking)

Agent 升级了某个包的主版本(比如 React 18 → 19、Next.js 14 → 15),但代码里还在用旧 API:

TypeError: ReactDOM.render is not a function

如何判断git diff main -- package.json 看是否有主版本跳跃。

6. ESM / CJS 混用

Agent 在 ESM 项目里写了 require(),或在 CJS 项目里写了顶层 import

SyntaxError: Cannot use import statement outside a module

如何判断:看 package.json"type": "module" 是否设置,对照 import / require 语法。

最短修复路径

按收益排序。Step 1-3 通常能定位 90% 的问题。

Step 1:先 stash,留干净环境对照

不要直接动代码。先存档:

git stash push -m "ai-broken-build-snapshot"   # 存档当前状态
git stash apply                                # 重新应用,不删 stash

这样你随时可以 git stash pop 回到 AI 改完的样子。如果改动已经 commit 了:

git tag broken-build-snapshot                   # 给当前 commit 打个 tag

Step 2:用 git diff 列出所有改过的文件,按风险排序

git diff main --stat

按这个顺序排查(风险从高到低):

优先级文件类型风险
1tsconfig*.json *.config.{js,ts,mjs}一处坏全站
2package.json package-lock.json依赖不兼容
3src/lib/** src/utils/**被多处 import
4src/components/**影响范围有限
5src/pages/** src/app/**单页面失败

Step 3:跑 tsc --noEmit 暴露所有类型错

不要直接 npm run build,打包器通常在第一个错就停。tsc --noEmit 会对整个项目做类型检查但不产出 JS,一遍把所有错都报出来:

npx tsc --noEmit --pretty | head -50

--pretty 会带上出错的源码行并加上颜色,扫起来更快。这样能立刻看出是”一处改动引发的连锁错”还是”多处独立错”。如果所有错都集中在某一个文件相关的 import 上,culprit 就已经找到了。注意:tsc 是按整个项目跑的,没法只指定单个文件,但输出里的文件清单就是你的定位地图。

Step 4:定向 revert 高风险文件

回滚单个文件用 git restore。截至 2026 年,它是官方推荐的写法,优于老的 git checkout -- <文件>(Git 把两件事拆开了:git switch 管切分支、git restore 管恢复文件,意图清晰,也不会手滑切了分支)。--source 指定从哪里恢复——可以是 main、某个 commit SHA,或者 agent 的改动是最后一个 commit 时用 HEAD~1。先 revert 配置文件试试 build:

git restore --source=main -- tsconfig.json vite.config.ts next.config.js
npm run build

(老版本 Git 或习惯用法:git checkout main -- tsconfig.json vite.config.ts next.config.js 效果一样。)

如果 build 过了,问题在 config;如果还挂,把 config 改动恢复回来(git stash pop,或重新 apply agent 那版),再 revert 下一组:

git restore --source=main -- src/lib/ src/utils/
npm run build

这样二分定位比全 reset 强:agent 改对的地方保留,只回滚出问题的那一组。每 revert 一组就 build 一次,直到变绿。

Step 5:把根因写回 prompt 让 agent 重做

定位到具体哪几个文件 / 哪几行后,不要自己手改,而是回头给 agent 一个明确的 prompt:

你上次的改动让 build 挂了。具体错误:
[贴完整 tsc --noEmit 输出]

请只做这些事:
1. 不要碰 tsconfig.json / vite.config.ts
2. 修复以上所有类型错,每个错对应一个最小改动
3. 改完先跑 tsc --noEmit,全绿后再说"完成"

明确禁止它再动 config,这样它不会重蹈覆辙。

怎么确认真的修好了

定向 revert 之后,别只看到”build 过了”就收工。三条都跑一遍:

npx tsc --noEmit          # 0 个类型错
npm run build             # 退出码 0,dist/ 已生成
git diff --stat main      # 改动文件只剩你想保留的那些

第三条最关键:它能证明你回滚某一组时没有把 agent 改对的某个文件也一起弄丢。如果发现某个该留的文件没出现在 diff 里,单独把它 apply 回来(git restore --source=stash@{0} -- 路径/文件)再重新 build。

预防建议

  • 在 agent 规则文件里明确禁止改 config 文件,列出具体路径:tsconfig.jsonvite.config.*next.config.*astro.config.*package.json(除非走 npm install)。截至 2026 年,AGENTS.md 是跨工具通用标准,Claude Code、Cursor、Codex CLI 都原生读取;CLAUDE.md.cursorrules 仍可作为工具专属的覆盖文件
  • pre-commit hook(Husky + lint-staged)跑 tsc --noEmitnpm run build,退出码非 0 就拦下这次 commit
  • 让 agent 每次完工前必须自己跑 npm run build,而不仅仅是 npm test——测试常常过了,生产打包却挂
  • 长 refactor 拆成小 commit,每个 commit 都能独立 build,这样坏的那个一行 git restore 就回滚了
  • 配置文件改动单独 review,不和业务代码混在一个 PR
  • git worktree 让 agent 在隔离分支干活,main 始终保持可 build

常见问题

dev server 跑得好好的,为什么 npm run build 就挂?

vite dev / next dev很宽松:它们按需逐文件转译,跳过整项目的类型检查和 tree-shaking。npm run build 会用 Rollup/Turbopack 跑整张依赖图,外加一遍严格的 tsc,所以能抓到 dev server 永远看不到的死 import、类型错和 ESM/CJS 问题。一个典型症状就是只在 build 时报 RollupError: "X" is not exported by "Y"——通常是把一个类型当成值 re-export 了,或者某个包的 exports 字段在打包时解析方式不同。完工前永远要先跑一遍 build,别只信 agent。

干脆 git reset --hard 从头来一遍行不行?

只在万不得已时用。hard reset 会把所有改动都扔掉,包括 agent 改对的部分,而且你什么也没学到——不知道到底哪里坏了。定向 git restore 流程能保住好的成果,几分钟内锁定坏的那一组。只有当 diff 很小、或者你确实想整轮作废时才用 reset。

回滚单个文件用 git restore 还是 git checkout

都行。git restore --source=<ref> -- <文件> 是 Git 截至 2026 年推荐的写法——它只干一件事(恢复文件内容),不会像 git checkout 那样手滑切了分支。git checkout <ref> -- <文件> 是老的等价写法,习惯了用也没问题。

怎么让 agent 修好又不再把 config 改坏?

把完整的 tsc --noEmit 输出贴回去,并把任务约束死:按确切路径禁止它碰 config 文件、要求每个错对应一个最小改动、并且改完先自己跑 tsc --noEmit 全绿后才能说”完成”。具体 prompt 见 Step 5。更稳的做法是把”禁改 config”写进 AGENTS.md,让它对以后每一轮都生效。

tsc --noEmit 全绿了,但 npm run build 还挂,怎么办?

那说明问题在打包器层面,不是类型层面:可能是某个运行时 import 解析不到、缺资源文件、build 时读不到环境变量,或者 ESM/CJS 边界问题。直接读 build 报错(它会指出文件和 importer),并先查 git diff main -- package.json '*.config.*'——版本跳跃和 config 改动是”只在 build 挂”最常见的原因。

相关阅读

标签: #AI 编程 #排查 #排查