Build 报 Cannot find module './utils/superhelper',你在项目里 grep 一圈完全没这个文件——Claude Code、Cursor 或 Codex 凭空写了一条 import,引用了它”觉得应该存在”的 utility。它根据训练数据 + 类似项目的常见命名,“补全”了一个看起来很合理的路径,但实际并没创建对应文件。
最快修法:跑 npx tsc --noEmit(或 tsc --build)拿到每一条坏 import 的精确行号,然后逐条处理——要么指向项目里已有的等价实现,要么让 agent 真的把文件建出来。每条 import 只有两种正确结局:实现它,或换掉它。
一个重要例外:如果缺的是 npm 包(不是本地文件),不要盲目 npm install AI 写的那个名字。幻觉出来的包名如今是一种正在被利用的供应链攻击手段(“slopsquatting”,投毒抢注)。先确认这个包真实存在且有人维护。下文细说。
常见原因
按命中率从高到低排序。
1. Agent 自己混淆了 plan 和 code
它在思考阶段说”我会用一个 formatDate 工具”,结果在 code 阶段直接 import { formatDate } from './utils/formatDate'——它把自己计划里的名字当成了已存在的代码。
// agent 写的
import { formatDate } from '@/utils/formatDate';
// 但项目里实际是
import dayjs from 'dayjs';
如何判断:看 agent 的 reasoning trace,它有没有提过 “create” / “implement”。如果只有 import、没有”创建”动作,就是混淆了。
2. 它默认这种 utility 在项目里”应该”存在
LLM 见过太多项目里有 src/utils/cn.ts、src/lib/utils.ts、src/helpers/format.ts,于是默认你的项目也有。typescript 脚手架、Next.js / shadcn/ui 默认会带 lib/utils.ts(里面有 cn() 这个 helper),所以哪怕你从没跑过 npx shadcn init,模型也会默认它存在。
如何判断:搜常见模板路径——src/lib/utils、src/utils/cn、@/lib/db——看是不是你压根没建过的”标配”路径。
3. 训练数据里见过的包名(以及 “slopsquatting”)
它写 import { useDebounce } from 'react-use',但你装的是 usehooks-ts,没装 react-use。错误是 Cannot find module 'react-use'。这其实是包幻觉,不是文件幻觉,但症状相同。
这一类有真实风险。USENIX Security 2025 一项研究分析了 16 个模型、57.6 万份代码样本,发现 生成的包依赖里有 19.7% 是幻觉出来的(商业模型约 5.2%,开源模型约 21.7%),而且 58% 的假包名会在多次运行里重复出现。攻击者就专门去 npm / PyPI 抢注这些可预测的假名字,于是下一个照搬 AI 建议去 install 的人就会装到恶意包。已有记录的真实案例包括 react-codeshift(把真实的 jscodeshift 和 react-codemod 拼成的假名)和 unused-imports(抢注真实包 eslint-plugin-unused-imports 的名)。所以遇到陌生包名要当成”可疑”,而不是当成一个随手放行的拼写错误。
如何判断:检查 package.json;npm ls <package>(或 npm list)没结果就说明从没装过这个包。装之前先看下面的”安装前先验证这个包是不是真的”。
4. Agent 在 monorepo 里走错 workspace
你的 monorepo 里 packages/ui/src/Button.tsx 存在,但 agent 在 packages/app 里写 import { Button } from '../ui/Button',相对路径算错了。文件存在,但路径解析不到。
如何判断:错误信息里的路径是相对路径而且层级看着可疑(../../../ui/),就检查 tsconfig 的 paths / references 设置和实际目录层级。
5. 大小写错误(macOS 本地 OK,CI 挂)
import './Button' 但文件叫 button.tsx。macOS 和 Windows(大小写不敏感的文件系统)能解析,Linux 上的 CI build 直接挂。严格来说不是幻觉,但症状一样,而且是”本地能跑、CI 挂”的头号原因之一。
如何判断:git ls-files | grep -i button 看磁盘上的实际大小写。在 tsconfig 里设 "forceConsistentCasingInFileNames": true(TS 5.x 默认开)能让这种问题在本地就暴露,而不是只在 CI 才炸。
6. Agent 引用了它”准备创建”但忘了创建的文件
它在 plan 里说”我会创建 src/api/posts.ts”,但实际只 import 没创建——可能是上下文窗口在中途截断,或者它跳过了那一步。
如何判断:grep agent 这轮所有的 Write / Edit 工具调用,看 path 列表里有没有缺失的那个文件。
最短修复路径
按收益排序。绝大多数能在 5 分钟内修完。
Step 1:用 tsc --noEmit 一次列出所有幻觉 import
npx tsc --noEmit 2>&1 | grep -E "TS2307|TS2305|Cannot find"
你会得到类似:
src/components/Form.tsx:5:23 - error TS2307: Cannot find module '@/utils/superhelper'
src/pages/posts.tsx:8:30 - error TS2307: Cannot find module '@/lib/db'
src/hooks/useAuth.ts:3:15 - error TS2305: Module '@/utils' has no exported member 'verifyToken'
TS2307 表示模块 / 文件根本找不到。TS2305 表示文件找到了,但它并没导出那个名字(agent 编造了一个导出,或名字写错了)。每一条对应一个待修。如果你不用 TypeScript,ESLint 的 import/no-unresolved 规则,或者直接跑构建(vite build、next build)也能给你同样的清单。
Step 2:判断这个东西该不该存在
打开报错的那个文件,看它怎么用这个 import:
import { superhelper } from '@/utils/superhelper';
// ...
const result = superhelper(data, { format: 'json' });
问自己两个问题:
- 这个功能项目里已经有等价物吗?(如
lodash、dayjs、你自己写过的 helper) - 如果没有,值得为它建一个新文件吗?
| 情况 | 修法 |
|---|---|
| 项目里已有等价物 | 换 import,不建新文件 |
是个小功能(< 20 行) | 直接 inline 写在调用处 |
| 是个能被多处复用的工具 | 真的把文件建出来 |
| 本地路径拼错 / monorepo 路径算错 | 修相对路径,或改用 tsconfig 别名 |
| 大小写不一致(只在 CI 报 TS2307) | 把 import 改成跟磁盘上完全一致的大小写 |
| 缺 npm 包 | 先验证它是真包(见下节),再装真包或换成已装的等价包 |
Step 3:用 grep 找替代
确认是不是已有等价物:
# 按功能名搜
grep -rn "formatDate\|format_date\|dateFormat" src/
# 按导出签名搜
grep -rn "export function.*Date.*string" src/
# 找已装的相关包
grep -E "(date|format|debounce|throttle)" package.json
如果有现成的,把幻觉 import 替换掉即可。
Step 4:安装前先验证这个包是不是真的
如果缺的 import 是个包,在 npm install 之前先做这一步。幻觉出来的名字可能根本没注册(顶多是个无害的构建报错),但更糟的情况是它已经被攻击者抢注了。
# 它在 registry 上存在吗?有多少人用?
npm view react-use # 404 = 不存在;否则会显示版本 / 维护者
npm view react-use time # 看首次发布时间——刚发布不久 + 跟 AI 写的名字一模一样,是危险信号
加进项目前先做几个 sanity check:
- 周下载量 / 仓库:在 npmjs.com 或 pypi.org 上打开这个包。真实工具会有 GitHub 仓库、有历史、下载量不会太低。一个”听起来很主流”的名字却只有几十的周下载量,很可疑。
- 名字拼接:AI 给的名字是不是两个真实包的混搭(比如
react-codeshift=jscodeshift+react-codemod)?把两半分别搜一下——真包通常是其中之一。 - 发布者:信任发布者和历史记录,而不是名字本身。slopsquatting 靠的就是你看到一个像模像样的名字就放行。
拿不准就直接搜 <生态> 里做 X 用什么包,自己装那个公认的包,而不是照搬 AI 吐出来的字符串。
Step 5:让 agent 自己修,给精确 prompt
不要手动修每一个——把列表喂回 agent:
build 失败。以下 import 引用了不存在的文件或包:
[贴 tsc --noEmit 的输出]
修复规则(严格遵守):
1. 不要创建新文件,除非项目里真的没有等价实现
2. 修每个 import 前先 grep 项目,确认是否有现成实现
3. 任何缺失的 npm 包,停下来,把包名 + npm 周下载量告诉我。
由我决定是装还是换。不要自己跑 npm install。
4. 修完跑 `tsc --noEmit` 必须全绿
5. 不要改 tsconfig 的 paths
Step 6:对于真的需要创建的文件,让 agent 单独提议
如果有 utility 确实需要新建:
你要创建 src/utils/X.ts,先输出:
1. 完整文件内容(含类型)
2. 使用它的所有调用点
3. 为什么不用现成的 lodash / dayjs / 内置 API
等我批准再创建。
强制人工 review,避免它再创造一堆只用一次的”辅助文件”。
如何确认真的修好了
按顺序跑:
npx tsc --noEmit # 必须 exit 0,没有 TS2307 / TS2305
npm ls 2>&1 | grep -i "missing\|invalid" # 没有缺失 / 多余的依赖
npm run build # 真正的构建必须通过
本地 tsc 全绿 + 构建干净,才算真的”完成”。如果你只是把编辑器里的红波浪线修没了但 build 还挂,说明你漏了一个 TypeScript 检查不到的路径(比如只在运行时用的 require、资源文件 import,或者只在 Linux CI 上才发作的大小写问题)。在大小写敏感的环境或 CI 上重跑一次构建,把原因 5 兜住。
常见问题
为什么 AI 老是编造 import,而不是承认自己不知道?
LLM 是在生成”下一个最可能的 token”,而在 import { formatDate } from 之后,最可能的续写就是一个路径。模型本身没有”这个文件存在吗”的内置检查——除非它是带 file-search 工具的 agent,而且真的去用了。所以你的修复循环应该强制 grep / tsc 校验,而不是靠信任。
直接 npm install 它建议的包名危险吗?
有可能危险。大约每 5 个 AI 建议的包名就有 1 个不存在(USENIX Security 2025),而攻击者会预先抢注那些常见的假名字——这就是 “slopsquatting”。装之前一定先确认这个包真实存在、有人维护、而且确实是你要的那个。先跑 npm view <name>,看它的仓库和下载量。
tsc --noEmit 全绿了但 build 还是挂,为什么?
TypeScript 只检查它会类型检查的东西。动态 require()、图片 / CSS / 资源文件的 import、以及 Linux 上只差大小写的路径,都会绕过它。跑真正的构建(next build、vite build 等),最好在大小写敏感的文件系统或 CI 上跑。
怎么区分是文件幻觉,还是大小写 / 路径 bug?
如果 git ls-files | grep -i <名字> 能找到文件,那就是大小写或相对路径 bug(原因 4 和 5),不是幻觉——文件在,只是 import 字符串写错了。如果哪儿都搜不到,那就是从没创建过。
怎么从根上避免每次会话都出这事?
在 CLAUDE.md / AGENTS.md / .cursorrules 里写一条硬规则:“导入前必须 grep / find 确认目标存在;不能编造路径;任何新包都要先停下来问。“把 tsc --noEmit 写进 agent 的完成标准,再加一个 pre-commit hook,让 TS2307 / TS2305 根本提交不上去。
预防建议
- 在
CLAUDE.md/AGENTS.md/.cursorrules写:“导入前必须先grep或find确认目标存在;不能编造路径;不确认是真包就不许安装。” - 让 agent 完工前自动跑
tsc --noEmit——把它写进”完成标准”。 - pre-commit hook 跑
tsc --noEmit,TS2307/TS2305直接拦。 - 项目根放一个
docs/utils.md列出常用 helper 和它们的实际路径,喂给 agent 当上下文。 - 给 agent 装 file-search 工具时确保它会用——很多 agent 默认不会主动 search 文件结构。
- 强制小 commit:每次
npm install单独提一个 commit,方便回看”这一周到底装了什么、名字是不是真的”。 - 对 monorepo,在 root tsconfig 用
paths别名而不是一长串相对路径,幻觉 / 路径算错的概率明显下降。 - 在 tsconfig 设
"forceConsistentCasingInFileNames": true,让大小写问题在本地就挂掉,而不是只在 CI 挂。