1. 问题现场还原与核心症结定位
1.1 这个报错到底在说什么
如果你在 VS Code 里跑 Jupyter Notebook,突然弹出一行红字Kernel died with exit code 1,然后内核重启、变量全丢、跑了一半的单元格全部白跑——恭喜你,踩进了 Python 环境管理最经典的坑之一。这个报错本身信息量极低,它只告诉你“内核进程非正常退出了,退出码是 1”,但完全不告诉你为什么退出。退出码 1 在 Unix 体系里是个通用错误码,几乎任何初始化阶段的异常都会以它收场,所以光看这一行字,等于什么都没说。
真正要命的是它的触发场景:你明明在终端里python -c "import torch"一切正常,jupyter kernelspec list也能看到内核,但一在 VS Code 里选这个内核就死。这种“终端能跑、Notebook 不能跑”的分裂现象,根源几乎都指向同一个东西——VS Code 选择的内核解释器,和你以为的那个解释器,根本不是同一个。
1.2 为什么“不同环境”是重灾区
我做过统计,这类问题里超过八成发生在同时装了 conda、系统 Python、pyenv 或者多个 venv 的机器上。原因很简单:VS Code 的 Jupyter 扩展在解析内核时,有一套自己的查找逻辑,它会去扫描kernelspec目录、读取kernel.json、再结合当前工作区的 Python 解释器设置。而 conda 环境、venv 环境、系统环境各自注册内核的方式不一样,注册的路径也不一样。当这些信息对不上时,VS Code 就会拿着 A 环境的启动命令,去加载 B 环境的包,或者干脆启动了一个缺少关键依赖的裸解释器,内核在 import 阶段就崩了,退出码自然是 1。
举个我亲历的例子:某台开发机上有一个 conda 环境叫ml-dev,里面装了特定版本的数值计算库。我在 VS Code 里通过命令面板选了ml-dev的 Python 解释器,看起来一切正常,但一启动 Notebook 就 kernel died。后来查日志才发现,VS Code 实际调用的是系统 Python 注册的那个python3kernelspec,而那个系统 Python 里根本没装对应的库,import 直接抛异常,内核进程当场退出。表面上我选了环境,实际上内核跑在另一个环境里——这就是“不同环境”四个字背后真正的杀机。
1.3 排查前必须建立的三个认知
在动手之前,先把这三个认知刻进脑子里,能帮你少走至少两小时弯路。
第一,VS Code 的“Python 解释器选择”和“Jupyter 内核选择”是两套独立机制。前者影响的是终端、调试、代码补全;后者影响的是 Notebook 内核。很多人以为选了 Python 解释器,Notebook 就自动跟着走,这是错的。你必须在 Notebook 右上角单独确认内核。
第二,kernel.json是唯一真相来源。VS Code 启动内核时,读的就是这个文件里的argv字段。这个字段写的是哪个 python 路径,内核就跑在哪个环境。所有排查最终都要落到这个文件上。
第三,退出码 1 的日志一定藏在某个地方。VS Code 默认不把内核的 stderr 完整显示出来,但你可以通过输出面板或者手动复现启动命令,把真正的报错逼出来。看不到真实报错就瞎猜,是最低效的做法。
2. 内核启动链路深度拆解
2.1 从点击运行到内核进程诞生
要治这个病,得先知道这个“病人”的解剖结构。当你在 VS Code 里点下运行按钮,背后发生了一连串动作,我把它拆成五步。
第一步,Jupyter 扩展读取当前 Notebook 绑定的 kernelspec 名称。这个名称可能是python3、ml-dev、myenv之类,存在 Notebook 的元数据里。
第二步,扩展根据名称去若干候选目录里找对应的kernel.json。候选目录包括用户级目录(Linux/macOS 下通常是~/.local/share/jupyter/kernels,Windows 下在%APPDATA%\jupyter\kernels)、环境级目录(conda 环境自己的share/jupyter/kernels)、以及系统级目录。
第三步,解析kernel.json,取出argv数组。这个数组的第一项通常是一个 python 可执行文件的绝对路径,后面跟着-m ipykernel_launcher -f {connection_file}之类的参数。
第四步,扩展用这个argv启动一个子进程,同时生成一个连接文件(connection file),里面包含端口、密钥等信息,供前端和后端通信。
第五步,内核进程启动后,会尝试 importipykernel并初始化。如果这一步任何环节抛异常,进程就会以非零码退出,前端收到Kernel died with exit code 1。
关键点在于第三步和第四步之间:如果argv里的 python 路径指向的环境缺少 ipykernel,或者缺少你代码里 import 的包,内核必死。而 VS Code 在第二步找kernel.json时,可能找到了一个“陈旧”的注册项,指向一个早已被删除或改名的环境。
2.2 kernel.json 里到底写了什么
一个典型的kernel.json长这样:
{ "argv": [ "/home/user/miniconda3/envs/ml-dev/bin/python", "-m", "ipykernel_launcher", "-f", "{connection_file}" ], "display_name": "Python (ml-dev)", "language": "python" }这里每一个字段都有讲究。argv[0]是解释器绝对路径,这是排查的第一现场。-m ipykernel_launcher表示用该解释器去运行 ipykernel 的启动模块,所以这个解释器所在环境必须装了 ipykernel。{connection_file}是占位符,运行时会被替换成实际连接文件路径。display_name是你在 VS Code 内核列表里看到的名字,它和实际环境可以完全对不上——这就是为什么“名字看着对,跑起来却错”的根源。
我见过最坑的一种情况:有人手动复制过一个 kernelspec 目录,改了display_name但忘了改argv里的路径,结果内核列表里显示的是新环境名字,实际跑的却是旧环境。这种“挂羊头卖狗肉”的注册项,不打开kernel.json根本发现不了。
2.3 多环境并存时的路径冲突模型
当机器上同时存在 conda、venv、系统 Python 时,内核注册会出现“多份注册、互相覆盖”的局面。我画不出图,但可以用文字描述这个冲突模型。
假设你有三个环境:系统 Python(/usr/bin/python3)、conda base(~/miniconda3/bin/python)、conda 环境ml-dev(~/miniconda3/envs/ml-dev/bin/python)。每个环境如果都执行过python -m ipykernel install,就会在各自的 kernels 目录下生成注册项。但 VS Code 扫描时,会把所有能找到的 kernelspec 汇总成一个列表。如果两个注册项的display_name相同(比如都叫 “Python 3”),你在列表里就会看到两个一模一样的名字,选错一个就完蛋。
更隐蔽的是,conda 环境注册内核时,默认可能写到用户级目录,而不是环境级目录。这意味着你删除了ml-dev环境,但用户级目录里那个指向ml-dev的kernel.json还在,VS Code 依然会把它列出来。你选中它,它去启动一个已经不存在的 python 路径,内核秒死,退出码 1。
3. 分场景排查与实操修复
3.1 第一步永远是拿到真实报错
别急着改配置,先把真实报错逼出来。VS Code 里有两个地方能看内核日志。
第一个地方是输出面板。打开命令面板,运行Jupyter: Show Output或者直接看底部面板切到 “Jupyter” 频道。内核启动时的 stderr 会打在这里,但有时候被截断。
第二个地方更可靠:手动复现启动命令。从kernel.json里把argv抄出来,把{connection_file}换成一个临时文件路径,然后在终端里直接执行。比如:
/home/user/miniconda3/envs/ml-dev/bin/python -m ipykernel_launcher -f /tmp/test_conn.json如果这个命令在终端里也报错,那报错信息就完整暴露了。常见的真实报错有这几类:ModuleNotFoundError: No module named 'ipykernel'(环境没装 ipykernel)、ImportError: libxxx.so cannot open shared object file(动态库缺失,常见于 conda 环境被破坏)、ModuleNotFoundError: No module named 'numpy'(环境里缺你代码依赖的包)。拿到真实报错,问题就解决了一半。
提示:手动复现时如果命令挂住不退出,说明内核其实启动成功了,问题可能出在 VS Code 与内核的通信环节,而不是内核本身。这时候要往连接文件、端口占用方向查。
3.2 场景一:内核指向了错误的环境
这是最高频的场景。修复方法分三步。
第一步,列出所有已注册内核,看清楚每个的路径:
jupyter kernelspec list这个命令会输出每个 kernelspec 的名称和所在目录。逐个打开目录里的kernel.json,检查argv[0]指向的 python 是否存在、是否是你想要的环境。
第二步,删掉陈旧或错误的注册项:
jupyter kernelspec remove <kernel-name>第三步,在正确的环境里重新注册。先激活目标环境,再执行:
conda activate ml-dev python -m ipykernel install --user --name ml-dev --display-name "Python (ml-dev)"这里--user表示注册到用户级目录,--name是内核标识名,--display-name是显示名。我建议--name和--display-name都带上环境名,避免多个环境显示名撞车。
3.3 场景二:环境里根本没装 ipykernel
有些精简环境或者手动创建的 venv,创建时没装 ipykernel。这时候内核启动命令里的-m ipykernel_launcher直接失败。修复很简单,激活环境后装上:
conda activate ml-dev pip install ipykernel装完重新注册内核即可。这里有个细节:如果你用的是 venv,注册内核时不需要--user,直接python -m ipykernel install --name xxx会写到 venv 自己的目录,更干净。
3.4 场景三:conda 环境被破坏导致动态库缺失
这个场景最隐蔽,报错通常是ImportError加一堆.so文件找不到。根源往往是 conda 环境在安装某个包时,把底层的动态库版本搞冲突了。我遇到过装完某个科学计算包后,libstdc++.so.6版本被降级,导致 ipykernel 启动时加载失败。
排查方法是手动复现启动命令,看完整报错里缺哪个库。修复思路有两个:一是用conda install而不是pip install重装冲突的包,让 conda 自己解决依赖;二是重建环境,把依赖清单导出后在新环境里重装。重建虽然费时,但最彻底。
conda env export > env_backup.yml conda env remove -n ml-dev conda env create -f env_backup.yml3.5 场景四:VS Code 缓存了旧的内核信息
有时候你明明修好了kernel.json,VS Code 里还是报同样的错。这是因为 VS Code 缓存了内核列表。解决办法是重载窗口:命令面板运行Developer: Reload Window。如果还不行,删掉工作区下的.vscode目录里的相关缓存,或者干脆重启 VS Code。
我个人的习惯是,每次改完 kernelspec,先jupyter kernelspec list确认终端里看到的是新的,再重载 VS Code 窗口,最后在 Notebook 右上角重新选一次内核。三步走完,基本不会残留旧状态。
4. 高频问题速查与避坑心得
4.1 问题速查表
| 现象 | 最可能原因 | 快速验证方法 | 修复动作 |
|---|---|---|---|
| 终端能 import,Notebook 不能 | 内核指向错误环境 | 手动执行 kernel.json 的 argv | 重新注册正确内核 |
| 报错含 ModuleNotFoundError: ipykernel | 环境缺 ipykernel | 激活环境后 pip show ipykernel | pip install ipykernel |
| 报错含 .so 文件找不到 | 动态库冲突 | 手动复现看完整报错 | conda 重装冲突包或重建环境 |
| 改了配置仍报旧错 | VS Code 缓存 | 重载窗口后是否变化 | Reload Window |
| 内核列表出现重复名字 | 多份注册项冲突 | jupyter kernelspec list | 删除多余注册项 |
| 启动后立即退出无报错 | 连接文件或端口问题 | 手动复现是否挂住 | 检查端口占用与临时目录权限 |
4.2 三条血泪避坑心得
第一条,永远用绝对路径注册内核,永远检查 argv[0]。相对路径或者依赖 PATH 的写法,在不同工作区、不同终端下行为不一致,是定时炸弹。我现在的习惯是,注册完内核第一件事就是打开kernel.json确认argv[0]是绝对路径且文件存在。
第二条,display_name 必须带环境标识。别偷懒用默认的 “Python 3”,否则多个环境在列表里长得一模一样,选错是迟早的事。我统一用Python (环境名)的格式,一眼就能分辨。
第三条,环境变更后同步清理内核注册。删除 conda 环境、重命名 venv、升级 Python 版本之后,一定要回头jupyter kernelspec list检查一遍,把指向已失效路径的注册项删掉。这些“僵尸内核”平时不显眼,一旦被选中就是 kernel died。
4.3 一个可复用的排查脚本
为了省事,我写了个小脚本,放在项目根目录,出问题时先跑它:
import json import os import subprocess def check_kernels(): result = subprocess.run( ["jupyter", "kernelspec", "list", "--json"], capture_output=True, text=True ) specs = json.loads(result.stdout)["kernelspecs"] for name, info in specs.items(): kernel_json = os.path.join(info["resource_dir"], "kernel.json") with open(kernel_json) as f: data = json.load(f) python_path = data["argv"][0] exists = os.path.exists(python_path) print(f"内核: {name}") print(f" 显示名: {data.get('display_name')}") print(f" Python: {python_path}") print(f" 路径存在: {exists}") if exists: check = subprocess.run( [python_path, "-c", "import ipykernel; print('ipykernel OK')"], capture_output=True, text=True ) print(f" ipykernel: {check.stdout.strip() or check.stderr.strip()}") print() if __name__ == "__main__": check_kernels()这个脚本会遍历所有注册内核,检查 python 路径是否存在、ipykernel 是否能正常 import。跑一遍,哪个内核有问题一目了然。我把它当成环境体检工具,每次配好新环境都跑一次。
4.4 关于“不同环境”的最终建议
如果你经常需要在多个环境间切换跑 Notebook,我的建议是:一个项目对应一个环境,一个环境对应一个明确命名的内核。不要试图让一个内核服务多个项目,也不要在同一个项目里混用多个环境的内核。环境隔离做得越干净,kernel died 出现的概率越低。
另外,VS Code 的工作区设置里可以固定 Python 解释器路径,配合.vscode/settings.json里的python.defaultInterpreterPath,能减少选错环境的概率。但记住,这只影响 Python 扩展,Notebook 内核还是要在右上角单独确认。两者对齐了,才算真正配好。
最后分享一个我踩了无数次坑才养成的习惯:新建 Notebook 后,第一个单元格永远先跑import sys; print(sys.executable)。这行代码会打印出当前内核实际使用的 python 路径。路径对了,后面基本不会出问题;路径不对,立刻停下来查内核,别等跑到一半才崩。这个习惯帮我省下的调试时间,比我写过的所有排查脚本加起来都多。