幽灵依赖:本地跑得好好的,线上部署却炸了

11 阅读6分钟

这是我在开发 My-Notion 项目时踩的一个真实坑——本地开发一切正常,推到 GitHub 后 CI/CD 构建直接失败。排查后发现是幽灵依赖(Phantom Dependencies)问题,而罪魁祸首竟然是 AI Agent 用错了包管理器。

问题复现

某天我 push 代码后,GitHub Actions 的 Build 流水线报红了:

Error: Cannot find module '@qdrant/js-client-rest'

奇怪,我本地跑得好好的啊。

看了一下代码,packages/ai/rag/qdrantVectorStore.ts 里确实用了 @qdrant/js-client-rest

import { QdrantClient } from "@qdrant/js-client-rest";

再看 packages/ai/package.json,依赖声明是这样的:

{
  "dependencies": {
    "@langchain/qdrant": "^1.0.1",
    // ... 其他依赖
  }
}

注意——@qdrant/js-client-rest 并没有在 package.json 中声明,但代码里直接 import 了它。

本地能跑是因为 @langchain/qdrant 依赖了 @qdrant/js-client-rest,而 npm 在安装时会把它提升(hoist)到 node_modules 根目录,所以代码能找到这个包。但线上用 pnpm 构建时,pnpm 严格的依赖结构不允许访问未声明的依赖,直接报错。

什么是幽灵依赖

幽灵依赖(Phantom Dependencies)是指代码中实际使用了某个包,但该包没有在 package.json 中显式声明,而是通过其他包的依赖间接引入的

用一张图来解释:

你的代码
  └─ import { QdrantClient } from "@qdrant/js-client-rest"  ← 直接使用
       ↑
       │  (没有在 package.json 中声明)
       │
@langchain/qdrant (package.json 中声明了)
  └─ @qdrant/js-client-rest  ← 间接依赖

你的代码能访问 @qdrant/js-client-rest,完全是因为 @langchain/qdrant 装了它。但这个关系是隐式的、脆弱的——一旦 @langchain/qdrant 升级版本不再依赖 @qdrant/js-client-rest,或者换了一个替代包,你的代码就会莫名其妙地挂掉。

为什么 npm 会有幽灵依赖,pnpm 不会

核心区别在于 node_modules 的目录结构。

npm 的扁平结构(Flat)

npm v3+ 采用扁平化安装,所有依赖(包括间接依赖)都会被提升到 node_modules 根目录:

node_modules/
├── @qdrant/js-client-rest/     ← 被提升上来了,你的代码能直接访问
├── @langchain/qdrant/
│   └── node_modules/
│       └── (空的,因为被提升了)
├── langchain/
├── openai/
└── ...

这种设计的好处是安装快、兼容性好,但代价就是幽灵依赖——你可以 import 任何被提升到根目录的包,不管你有没有声明它。

pnpm 的严格结构(Strict)

pnpm 采用软链接 + 硬链接的方式,每个包只能访问自己声明的依赖:

node_modules/
├── .pnpm/                           ← 真实存储位置
│   ├── @qdrant+js-client-rest@1.17.0/
│   │   └── node_modules/
│   │       └── @qdrant/js-client-rest/
│   └── @langchain+qdrant@1.0.1/
│       └── node_modules/
│           ├── @langchain/qdrant/
│           └── @qdrant/js-client-rest/  ← 软链接,只有 @langchain/qdrant 能访问
├── @langchain/qdrant/               ← 软链接到 .pnpm
├── langchain/                        ← 软链接到 .pnpm
└── (没有 @qdrant/js-client-rest!)   ← 你的代码找不到它

在 pnpm 的结构下,@qdrant/js-client-rest 只存在于 @langchain/qdrant 的依赖树中,你的代码如果不显式声明,根本访问不到。

这正是 pnpm 的设计初衷——通过严格的依赖隔离,在开发阶段就暴露幽灵依赖问题,而不是等到线上部署才炸。

这个坑是怎么产生的

在我的场景中,问题出在 AI Agent 用了 npm install 而不是 pnpm add 来安装包。

我:帮我安装 @langchain/qdrant
Agent:npm install @langchain/qdrant   ← 用了 npm!

npm 安装后,@qdrant/js-client-rest 被提升到了 node_modules 根目录。Agent 在写代码时,直接 import 了 @qdrant/js-client-rest,本地运行完全没问题——因为 npm 的扁平结构让它"看得见"这个包。

但 CI/CD 环境用的是 pnpm,严格的依赖结构直接暴露了这个幽灵依赖。

怎么解决

1. 显式声明依赖

把代码中实际使用的间接依赖,显式添加到 package.json 中:

pnpm add @qdrant/js-client-rest
  {
    "dependencies": {
      "@langchain/qdrant": "^1.0.1",
+     "@qdrant/js-client-rest": "^1.17.0"
    }
  }

这是最根本的解决方案——你用了什么就声明什么,不依赖其他包的间接引入。

2. 清理并重装依赖

如果项目之前用 npm 装过包,node_modules 里可能残留着扁平结构下的幽灵依赖。需要彻底清理后用 pnpm 重装:

# 删除所有 node_modules
find . -name "node_modules" -type d -prune -exec rm -rf {} +

# 删除 lock 文件(如果有 package-lock.json)
find . -name "package-lock.json" -delete

# 用 pnpm 重新安装
pnpm install

重装后,pnpm 的严格结构会立刻暴露所有幽灵依赖——import 不到的包就是没声明的,一个个补上就行。

3. 让 AI Agent 统一使用 pnpm

问题的根源是 Agent 用了 npm。为了防止再犯,我让 Agent 写了一个全局 Skill,后续所有安装包的操作都强制使用 pnpm:

## 包管理器规则
- 本项目使用 pnpm 作为包管理器
- 安装依赖:pnpm add <package>
- 安装开发依赖:pnpm add -D <package>
- 全局禁止使用 npm install / yarn add
- Monorepo 中安装到指定包:pnpm --filter <package-name> add <dep>

这样 Agent 每次对话都会读取这条规则,不会再出现用错包管理器的问题。

如何检测幽灵依赖

除了等 pnpm 报错,还有更主动的检测方式:

pnpm 的 --strict-peer-dependencies

pnpm install --strict-peer-dependencies

安装时严格检查 peer dependencies,有冲突直接报错而不是静默跳过。

dpdm 工具

dpdm 可以扫描代码中的依赖引用,找出未声明的依赖:

npx dpdm src/index.ts

knip 工具

knip 可以检测未使用的依赖、未声明的依赖、以及各种死代码:

npx knip

总结

npmpnpm
依赖结构扁平化,间接依赖提升到根目录严格隔离,只能访问声明的依赖
幽灵依赖本地不会报错,线上可能炸开发阶段直接暴露
安装速度较慢快(硬链接 + 内容寻址)
磁盘占用每个项目独立存储全局存储,多项目共享

幽灵依赖的本质是依赖声明和实际使用不一致。npm 的扁平结构掩盖了这个问题,让它在本地"看起来没问题",但线上部署时就会暴露。pnpm 的严格结构在开发阶段就强制你声明所有使用的依赖,虽然前期多写几行 package.json,但换来的是部署时的安心。

如果你也在用 pnpm + Monorepo,建议:

  1. 永远不要混用 npm 和 pnpm——一旦用 npm 装过包,node_modules 结构就被污染了
  2. 代码中 import 了什么,package.json 就声明什么——不要依赖间接依赖
  3. 让 AI Agent 也遵守包管理器规则——写好项目规则文件,防止 Agent 用错工具
  4. CI/CD 用 pnpm 构建——线上构建和本地开发保持一致,问题在本地就能发现

本文基于 My-Notion 项目的真实踩坑经历撰写,项目是一个 AI 原生的个人版 Notion,欢迎 Star ⭐