你让 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.json 的 strict,或者把 vite.config.ts 的 resolve.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 里搜 mock、stub、fixture、TODO、XXX 关键字。
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 member 或 cannot 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
按这个顺序排查(风险从高到低):
| 优先级 | 文件类型 | 风险 |
|---|---|---|
| 1 | tsconfig*.json *.config.{js,ts,mjs} | 一处坏全站 |
| 2 | package.json package-lock.json | 依赖不兼容 |
| 3 | src/lib/** src/utils/** | 被多处 import |
| 4 | src/components/** | 影响范围有限 |
| 5 | src/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.json、vite.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 --noEmit和npm 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 挂”最常见的原因。