ComfyUI依赖管理:自动生成requirements.txt,实现环境可复现与快速迁移
发布时间:2026/9/8 2:13:25来源:尧图网络
说实话给 ComfyUI 写一篇“自动生成 requirements.txt”的文章第一反应会有人觉得冷门。但真正常年折腾 ComfyUI 的人应该都被同一个问题折磨过换电脑、换整合包、重新部署云端的时候自定义节点一个接一个地红报错信息五花八门大多数都是ModuleNotFoundError。更气人的是某个节点明明在你本机跑得好好的搬到另一台机器上就是缺包。这背后的根源其实就是依赖没有梳理干净。这个主题真正解决的是 ComfyUI 环境可复现的问题。你不需要去理解每个节点的 Python 依赖到底藏在哪儿也不用在报错里一条条猜只需要跑一个脚本把整个 ComfyUI 环境里所有第三方 Python 包汇总成一个 requirements.txt之后在新机器上一条pip install -r requirements.txt就能把环境拉起来。这篇文章适合所有用 ComfyUI 的人不管你是用秋叶整合包还是原生部署也不管你是本地跑还是跑云端都能直接抄作业。1. 为什么要给 ComfyUI 自动生成 requirements.txt1.1 手动管理依赖的痛点拆解先聊聊为什么手动方案走不通。ComfyUI 的依赖管理和普通 Python 项目不一样普通项目可能就一个 requirements.txt但 ComfyUI 的依赖分散在全项目十几个甚至几十个目录里你手动根本管不过来。最直观的问题是“缺一个装一个”。工作流加载失败后报错日志会告诉你缺哪个包你装完再跑可能又冒出来第二个缺包周而复始。如果一个工作流涉及 8 个自定义节点其中四五个都依赖不同的第三方库你光是排队装依赖就要折腾半天。更隐蔽的是有些节点不会在加载时立刻报错而是在某个特定功能被触发的瞬间才报ModuleNotFoundError这简直让人抓狂。手动去读节点源码也不现实。每个自定义节点的依赖声明方式五花八门有的写了 requirements.txt有的只写在 README 里有的干脆在install.py里通过pip install装还有的用 pyproject.toml。靠人眼把所有节点扒一遍既慢又容易漏。那有人会说直接用pip freeze导出一个完整环境列表不就行了但这个方法在 ComfyUI 场景下问题很多。pip freeze会把环境里所有的包都导出来包括很多与 ComfyUI 无关的底层包。在你本机跑完全没问题可一旦换到另一台机器版本兼容性就会失控。比如某个包的版本在你机器上是2.0.1但另一个包的依赖要求它只能是1.9.x两边一冲突新环境直接崩。所以全量导出看着省事实际是给自己埋雷。1.2 自动生成的核心设计思路这里说的自动生成并不是简单地调用pip freeze而是按依赖的来源去收集。ComfyUI 的 Python 依赖其实可以分为两大类一类来自主项目自身也就是 ComfyUI 根目录下的 requirements.txt另一类来自 custom_nodes 里的每一个自定义节点每个节点可能有自己的依赖声明。所以脚本的设计思路应该是遍历主项目依赖 遍历所有自定义节点的依赖声明文件合并去重后再锁定当前环境里实际安装的版本最终输出成一个干净的 requirements.txt。这个方案比pip freeze强在两点。第一它区分了依赖来源不会把很多无关的环境包混进来第二它会锁定当前环境中实测可用的版本而不是只保留原作者声明里的版本区间。很多节点作者会在 requirements.txt 里写package1.2这种宽泛的约束你换一台机器装可能装到完全不兼容的新版本而锁定你本机验证过的版本迁移之后才能保证行为一致。2. ComfyUI 依赖体系拆解与脚本设计要点2.1 依赖都藏在哪些文件里要写自动收集脚本第一步就是搞清楚 ComfyUI 的依赖到底藏在哪里。我按优先级整理了一下来源位置内容特征典型问题主项目依赖ComfyUI/requirements.txt包含 torch、torchvision、einops、safetensors 等基础库很少会缺但有时版本过低节点依赖文件custom_nodes/*/requirements.txt每个节点自己声明的第三方依赖经常写了等于没写或者没有版本约束安装脚本custom_nodes/*/install.py运行时通过 pip 安装依赖依赖隐藏在代码里不运行不会暴露项目元数据custom_nodes/*/pyproject.toml部分新节点用[project] dependencies声明需要解析 TOML处理不当容易出错最常见的还是节点目录下的 requirements.txt大部分优质节点都会写。但坑也最多。有的节点会用-r other.txt引用其他文件有的会写package[extra]1.0这种带 extra 的写法还有的会把torch和torchvision这种大包也写进去。如果只是简单地把这些行拼接进最终文件可能什么都做不成必须做解析和清洗。install.py 是第二个重点关注对象。这个文件在 ComfyUI 节点里很常见节点第一次启动时会自动执行里面的 Python 代码来装依赖。问题是很多用户装完节点后根本不知道 install.py 在后台装了什么包只知道自己用的是整合包看起来一切正常。等到环境迁移这些隐藏依赖就全丢了。所以脚本里必须专门处理 install.py通过解析它包含的 pip 安装命令把包名提取出来。pyproject.toml 相对是少数派主要是一些喜欢用新式打包方式的开发者会写。但这几年占比明显上升解析成本其实不高用tomllib可以很方便地读出[project] dependencies字段。2.2 脚本设计的关键取舍写这个脚本之前有几个设计上的取舍必须想清楚不然就退化成一个笨拙的pip freeze。第一个取舍为什么按来源收集而不是全量快照。我已经说了全量快照会把很多与 ComfyUI 无关的包混进去。你也许觉得“反正一起装也没事”但事实是很多基础包之间存在隐式约束比如某个节点可能要求旧版 numpy而另一个要求新版最终能跑通的环境是动态平衡的结果。全量快照会把这个动态平衡里所有无关变量也一起锁死迁移后很难复现。第二个取舍版本锁定策略。收集到的依赖声明可能是无版本的也可能带或。我的建议是生成时统一转为当前环境的实际安装版本输出包名当前版本的形式。锁定版本虽然看起来不够“灵活”但换来的是确定性。对 ComfyUI 这种依赖非常多、冲突频繁的项目来说确定性比灵活性重要得多。第三个取舍排除大框架包。torch、torchvision、torchaudio 这类大框架包最好不要锁在 requirements.txt 里。因为在大整合包环境中torch 的安装方式很特殊往往带 CUDA 版本差异强行锁版本反而会带来各种问题。脚本默认会把这个几个包排除掉等换环境后再由主项目或者整合包自行处理。3. 完整实现自动生成 requirements.txt 的脚本3.1 脚本整体流程脚本的逻辑并不复杂核心步骤分五步读取 ComfyUI 主项目根目录下的 requirements.txt。遍历 custom_nodes 下每一个子目录分别读取 requirements.txt、install.py、pyproject.toml。把收集到的依赖包名统一规范化去掉版本号、extra 标记、环境标记。用当前 Python 环境里importlib.metadata.version()查询每个包的实际安装版本生成包名版本格式。合并去重、标记未能找到版本的缺失包输出最终的 requirements.txt。这段流程里最容易被忽略的是包名规范化。Python 包名里连字符和下划线可以混用大小写也不敏感比如opencv-python和OpenCV_Python是同一个包。如果不做归一化合并去重就形同虚设同一个依赖会出现好几遍版本还可能互相矛盾。3.2 核心代码与说明下面给出一个完整可用的脚本我建议你直接保存为generate_requirements.py放在 ComfyUI 根目录下运行。import os import re import sys import tomllib from pathlib import Path from importlib.metadata import version, PackageNotFoundError # 需要排除的大框架包默认不写入最终文件 DEFAULT_EXCLUDE { torch, torchvision, torchaudio, nvidia-cublas-cu12, nvidia-cuda-cupti-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-runtime-cu12, nvidia-cudnn-cu12, nvidia-cufft-cu12, nvidia-curand-cu12, nvidia-cusolver-cu12, nvidia-cusparse-cu12, nvidia-nccl-cu12, nvidia-nvjx, nvidia-nvjitlink-cu12, nvidia-nvtx-cu12, pytorch-triton, triton, } BASE_DIR Path(__file__).resolve().parent CUSTOM_NODES_DIR BASE_DIR / custom_nodes # 简单的包名规范化 def normalize_pkg(name: str) - str: return name.lower().replace(_, -).strip() # 从一行依赖声明中提取包名去掉版本、extra、环境标记 def parse_requirement_line(line: str): line line.strip() if not line or line.startswith(#): return None if line.startswith(-r) or line.startswith(--): return None # 去掉行尾注释 line re.sub(r\s#.*$, , line).strip() if not line: return None # 提取包名部分 # 处理 package[extra]1.0; python_version3.10 之类 match re.match(r^([A-Za-z0-9._-]), line) if not match: return None return normalize_pkg(match.group(1)) def parse_requirements_file(path: Path): deps set() try: lines path.read_text(encodingutf-8, errorsignore).splitlines() except Exception: return deps for line in lines: pkg parse_requirement_line(line) if pkg: deps.add(pkg) return deps # 从 install.py 里用正则提取 pip install 后面的包名 def parse_install_py(path: Path): deps set() try: content path.read_text(encodingutf-8, errorsignore) except Exception: return deps # 匹配 pip install 包名 或 pip install -r xxx.txt patterns [ rpip install\s([^\s;\\]), rpip\.main\(\[[\\](?:install)[\\]\s*,\s*[\\]([^\\])[\\], rsubprocess\.check_call\([^\]]*[\\]([^\\]\.tar\.gz)[\\], ] for pat in patterns: for m in re.findall(pat, content): pkg normalize_pkg(m.strip().strip(\[]).split([)[0]) if not pkg or pkg.endswith((.txt, .toml, .whl, .tar.gz)): continue deps.add(pkg) return deps # 解析 pyproject.toml 的 dependencies 字段 def parse_pyproject(path: Path): deps set() try: with path.open(rb) as f: data tomllib.load(f) project data.get(project, {}) for dep in project.get(dependencies, []): pkg parse_requirement_line(str(dep)) if pkg: deps.add(pkg) except Exception: pass return deps def get_installed_version(pkg: str): try: return version(pkg) except PackageNotFoundError: return None def collect_dependencies(): collected set() source_info [] # 1. 主项目 requirements.txt main_req BASE_DIR / requirements.txt if main_req.exists(): deps parse_requirements_file(main_req) collected.update(deps) source_info.append(f[main] {main_req} - {len(deps)} deps) # 2. 自定义节点目录 if CUSTOM_NODES_DIR.exists(): for node_dir in CUSTOM_NODES_DIR.iterdir(): if not node_dir.is_dir(): continue node_req node_dir / requirements.txt if node_req.exists(): deps parse_requirements_file(node_req) collected.update(deps) source_info.append(f[node] {node_req} - {len(deps)} deps) install_py node_dir / install.py if install_py.exists(): deps parse_install_py(install_py) collected.update(deps) source_info.append(f[node] {install_py} - {len(deps)} deps) pyproject node_dir / pyproject.toml if pyproject.exists(): deps parse_pyproject(pyproject) collected.update(deps) source_info.append(f[node] {pyproject} - {len(deps)} deps) return collected, source_info def main(): exclude set(DEFAULT_EXCLUDE) # 允许通过环境变量扩展排除项逗号分隔 extra os.environ.get(EXCLUDE_DEPS, ) for name in extra.split(,): name name.strip() if name: exclude.add(normalize_pkg(name)) deps, source_info collect_dependencies() # 记录最终写入的行 output_lines [# Generated by generate_requirements.py, # 仅包含 ComfyUI 相关 Python 依赖torch 等框架包默认排除] missing [] for pkg in sorted(deps): if pkg in exclude: continue installed get_installed_version(pkg) if installed: output_lines.append(f{pkg}{installed}) else: missing.append(pkg) out_file BASE_DIR / requirements.txt out_file.write_text(\n.join(output_lines) \n, encodingutf-8) print(依赖来源统计:) for line in source_info: print( line) print(f\n收集到依赖总数: {len(deps)}) print(f排除框架包数量: {len(exclude)}) print(f写入 requirements.txt 条目数: {len(output_lines) - 2}) if missing: print(\n警告: 以下包在当前环境中未找到已跳过:) for pkg in sorted(missing): print(f - {pkg}) print(f\n文件已输出到: {out_file}) if __name__ __main__: sys.exit(main())脚本本身的逻辑很直观。核心就是parse_requirements_file、parse_install_py、parse_pyproject这三个解析函数分别对应三种依赖声明形态。parse_requirements_file里我用正则^([A-Za-z0-9._-])提取每行的包名部分这样像opencv-python-headless4.8这种写法也能正确识别出opencv-python-headless这个名字。parse_install_py里我做了三种模式匹配最常见的是直接出现pip install package字符串其次是pip.main或subprocess.call里传参数列表的写法最后是安装本地 tar.gz 包的模式。这里必须注意的是很多节点的 install.py 实际上是在偷偷装 requirements.txt 里的内容写法是pip install -r requirements.txt这种字符串在正则里会把-r后的文件名当作包名所以我特意把以.txt、.toml结尾的名字过滤掉了。注意这种隐式安装的依赖因为我们已经在节点目录的 requirements.txt 处理逻辑里覆盖了所以不会漏。3.3 在秋叶整合包和原生环境里怎么跑脚本写完之后实际执行时最容易踩的坑是“跑错 Python 环境”。很多人习惯直接双击脚本或者用系统里的 Python 跑结果生成出来的 requirements.txt 里记录的版本根本不是 ComfyUI 实际使用的版本因为 ComfyUI 整合包使用的往往是嵌入式 Python 环境和系统 Python 是两个世界。秋叶整合包的环境结构大致是这样的整合包解压后ComfyUI 主目录下有一个python_embeded文件夹里面才是整合包自带的 Python 解释器。在 Windows 上打开一个 CMD 窗口然后执行cd /d C:\Users\你的用户名\Desktop\秋叶ComfyUI整合包\ComfyUI python_embeded\python.exe generate_requirements.py注意这里必须用python_embeded\python.exe来运行脚本而不是系统里的python。如果你用错了解释器importlib.metadata.version()查询到的将是系统 Python 环境里已安装的包版本很可能与 ComfyUI 实际运行环境完全不同。更严重的是有些包在系统 Python 里根本没装脚本会报一大堆“未找到”的警告生成的文件完全不能用。原生部署的场景反而简单一些。如果你是按官方流程用 conda 或者 venv 创建了独立的 Python 环境那么只要确保运行脚本时激活了对应环境即可。在 Linux 或 macOS 上命令大概是cd ~/ComfyUI python generate_requirements.py只要你能在终端里直接启动 ComfyUI那么用同一个终端跑脚本环境基本就不会错。如果你想把这个脚本变成“一键顺手执行”的工具可以在秋叶整合包里建一个自动生成依赖清单.bat文件里面写上echo off cd /d %~dp0 python_embeded\python.exe generate_requirements.py pause以后想导出依赖清单双击这个 bat 就行不用每次手动敲命令。如果你熟悉秋叶整合包的一键启动脚本逻辑应该能感受到这种方式其实和它的思路一脉相承。3.4 实际生成效果示例我在一个已经装满十几个自定义节点的环境里跑完脚本生成的 requirements.txt 大概长这样# Generated by generate_requirements.py # 仅包含 ComfyUI 相关 Python 依赖torch 等框架包默认排除 aiohttp3.9.5 aiosqlite0.20.0 albumentations1.4.5 altair5.3.0 av12.2.0 bitsandbytes0.43.3 certifi2024.7.4 cffi1.16.0 charset-normalizer3.3.2 colorama0.4.6 contourpy1.2.1 customtkinter5.2.2 diffusers0.30.0 einops0.8.0 ...省略... spandrel0.3.7 timm1.0.7 tokenizers0.19.1 transformers4.44.0 trimesh4.4.3 ultralytics8.2.78 websocket-client1.8.0 xformers0.0.27.post2 yapf0.43.0这个文件看起来平淡无奇但它是按“当前环境实测可用版本”锁定的。也就是说只要在新环境里执行python -m pip install -r requirements.txt理论上就能把 ComfyUI 所有直接相关的 Python 依赖装齐了。当然实际迁移时还是有一些注意事项这个等到第四部分再说。4. 常见问题与排查技巧实录4.1 生成后在新环境仍然报缺包这种情况我遇到得最多。三个字总结原因有遗漏。但不是脚本有 bug而是依赖的形态远不止 requirements.txt 这么简单。第一种遗漏是系统级依赖。有些节点除了 Python 包还依赖 FFmpeg、ImageMagick、Git 等外部程序。这类东西根本不在 pip 的管理范围内requirements.txt 管不到。比如视频相关节点需要 FFmpeg你光装 ffmpeg-python 这个包没用系统里还得有可执行的 ffmpeg 命令。第二种遗漏是插件级依赖。有些自定义节点依赖另一个自定义节点这种在 ComfyUI 生态里非常普遍。比如某些放大类节点会要求先安装 ControlNet 辅助节点如果你没有安装即使用 requirements.txt 把 Python 包装全了节点依然会加载失败。这种依赖脚本检测不出来只能靠节点文档或者报错信息来判断。第三种遗漏是安装时动态生成的依赖。部分节点作者习惯在 install.py 里用subprocess调起 shell 命令比如先执行git clone再去 clone 下来的目录里读 requirements.txt。这种动态逻辑靠静态正则很难完美还原。遇到这种情况我的建议是不要硬靠脚本解决而是把脚本当做一个起点。生成完 requirements.txt 后在新环境里跑一遍记录下报错再手动补齐那些脚本检测不到的部分。脚本已经帮你解决了 80% 的依赖问题剩下的 20% 靠人工判断是合理的。4.2 版本冲突为什么一个包出现多个版本约束你在收集依赖时可能会发现一个有趣的现象同一个包A 节点要求旧版B 节点要求新版。这种版本冲突在 ComfyUI 里特别常见典型代表是opencv-python、numpy、pillow这几个包。原因是 ComfyUI 生态迭代太快不同节点作者基于不同时期的依赖版本开发。比如某个老节点写死numpy1.24.4而另一个新节点要求numpy2.0。两个约束本身是互斥的。这时候如果你直接生成numpy1.24.4新节点可能跑不起来如果你生成numpy2.0.x老节点可能因为 API 变化而崩溃。我的建议是以当前环境实测可用的版本为准。也就是说你本机跑得好好的说明当前版本的 numpy 就是能让这些节点妥协的版本。脚本里用importlib.metadata.version()来锁定本机版本本质上是把你当前调试好的“平衡点”记录下来。所以不要在生成之前删掉那些看起来没用的包也不要为了追求“干净”随意卸载旧版本你当前的环境本身就是一个重要的参考基准。4.3 在 Linux 云端部署时为什么 Windows 生成的 requirements 有时装不上如果你是从 Windows 本地环境迁移到 Ubuntu 云服务器这个问题很常见。Windows 环境的依赖清单里可能包含pywin32、pywinpty这类 Windows 专属包在 Linux 上根本没有对应版本pip 会直接报错找不到。处理办法有两个。一个是迁移前先在目标平台上重新生成一次 requirements.txt确保包列表是针对目标系统的。另一个是在生成时设置EXCLUDE_DEPS环境变量把 Windows 专属包排除掉。比如EXCLUDE_DEPSpywin32,pywinpty,pywin32-ctypes,discord python generate_requirements.py另外Linux 上编译某些带 C 扩展的包会因为缺少系统开发库而失败。比如dlib、face_recognition这类安装前需要先安装build-essential、cmake等软件包。这个和 requirements.txt 无关是系统环境层面的准备部署前务必先检查。4.4 生成脚本本身常见的报错还有人会在运行脚本时直接报错。最常见的是 Python 版本太低导致tomllib导入失败。tomllib在 Python 3.11 以后才成为标准库如果你的 Python 是 3.10 或更低脚本会在导入阶段直接抛出ModuleNotFoundError。解决方法是要么升级到 Python 3.11要么把tomllib换成第三方库tomli。在脚本开头改成这样即可try: import tomllib except ModuleNotFoundError: import tomli as tomllib另一个运行时错误是没有任何输出。如果你在非 ComfyUI 根目录的位置运行脚本BASE_DIR会定位到错误的地方自然读不到任何依赖。解决方法是把脚本放在 ComfyUI 根目录或者在代码里把BASE_DIR改成通过参数传入。我提供的版本默认用脚本所在目录定位只要你把脚本放在 ComfyUI 根目录下不会出问题。5. 从“能跑”到“可分享”迁移与发布的最佳实践5.1 用 requirements.txt 做工作流迁移现在你手上已经有了一份干净的 requirements.txt接下来怎么用它来迁移环境就有章可循了。我的惯用流程是先把整个 ComfyUI 目录里的custom_nodes文件夹压缩打包再把工作流的 JSON 文件、模型清单、以及这份 requirements.txt 放在一起。到新机器上先安装 ComfyUI 主项目再解压 custom_nodes然后用pip install -r requirements.txt装依赖。启动时遇到个别缺包的错误单独补一下即可。需要注意requirements.txt 只管 Python 包模型权重文件、VAE、LoRA 这些大文件不在它的职责范围内。模型文件动辄几个 GB一般情况下你也不需要把它们放进 pip 依赖里。迁移时单独用网盘或者移动硬盘拷贝模型目录就行。5.2 用依赖清单辅助工作流分享如果你有分享工作流的习惯依赖清单就更重要了。把别人分享的工作流 JSON 下载下来经常遇到节点红色报错。这时候你可以先跑一遍自动生成脚本看看自己环境里已经装了哪些包再对照工作流报错的节点逐个补齐。反过来当你自己把工作流分享出去时最好也附上一份环境说明。不用一上来就甩出完整的环境快照而是列出“基础环境版本 自定义节点列表 依赖文件”这样对方比较容易对照排查。ComfyUI 生态的通用做法是提供一个“环境要求”段落类似这样Python: 3.10 / 3.11 / 3.12PyTorch: 2.x with CUDA 12.1必装自定义节点: 工作流涉及的节点列表依赖文件: requirements.txt这一套下来分享者和使用者的沟通成本会大幅降低。你自己从社区下载别人的工作流时也可以用同样方式快速判断环境差异。5.3 自动化的小技巧如果你经常处理复杂工作流还可以稍微扩展一下这个脚本的能力。比如把collect_dependencies()的结果输出成 JSON方便程序化地和其他工具对接。另一个很实用的小技巧是在跑完脚本后顺手生成一个“运行时环境摘要”把 Python 版本、操作系统、CPU/GPU 信息也写入一个文本文件。这个文件你可以叫environment_report.txt它虽然不能直接用来安装依赖但在排查问题时非常有用。调试过很多环境问题之后你会发现很多坑根本不是缺包而是 Python 版本不对、CUDA 版本对不上、或者用户在 Linux 上跑 Windows 专用的整合包。环境摘要能帮你在远程协助时第一时间排除这些底层因素。还有一个小技巧可以配合使用如果你经常需要对比两个环境之间的差异可以把两份pip list --formatfreeze输出互相 diff 一下。虽然我之前不建议全量 freeze 用于迁移但用于对比环境差异时全量列表反而更有优势。你可以在旧环境执行一次pip list --formatfreeze old_env.txt在新环境再执行一次然后手动对比差异快速定位缺包。这个操作比逐个看报错高效得多。5.4 使用脚本时的最终建议最后分享一点我自己的使用体会。折腾 ComfyUI 这么久我的固定流程已经变成了这样下载一个新工作流先在自己的环境里跑通确认无误后跑一遍自动生成脚本把依赖记录下来。这个习惯帮我避开了很多次“全部重装 solved 环境”的尴尬。这个脚本真正的价值不只是导出一个文件而是把“环境不可知”变成了“环境可复现”。尤其在整合包、云端部署、换电脑这几个场景下省下的时间不是一点半点。我见过太多人因为缺依赖问题重装整个整合包重装之后之前配好的工作流全要重新调一遍。有了一份可靠的 requirements.txt下次再遇到这类问题你只需要在新环境里把依赖拉起来然后继续干活。延伸阅读提示如果对 ComfyUI 环境管理有进一步兴趣可以关注自定义节点管理工具比如 ComfyUI Manager 的依赖管理能力它能帮你自动识别节点依赖并提示安装。不过它的问题在于依赖安装的过程往往是在线解析不一定能完整覆盖离线场景。结合本文的自动生成脚本你在离线和迁移场景下的体验会更稳。
网站建设高端定制企业官网