1. 这条报错是谁抛的:顺着 traceback 摸到抛错位置
raise Exception("IPAdapter model not found.")这条红字,几乎每个折腾过 IPAdapter 的人都在控制台里见过。它不像显存溢出那样有一堆数字,也不像维度不匹配那样给你一串 shape,它就是一句干脆的"模型没找到",然后就停在那儿了。新手第一反应通常是重装节点、重启软件、把工作流删掉重搭一遍,折腾两小时,问题还在。
先把结论放在前面:这条异常跟节点装没装好基本无关,跟 Python 环境、显卡驱动、依赖版本也基本无关。它只有一个含义——节点拿到了一个文件名,把这个文件名拼成了一个完整路径,然后去磁盘上验证这个路径是否存在,验证失败,于是抛异常。也就是说,问题 100% 出在"文件在哪、文件叫什么"这两件事上。你要做的不是重装,而是搞清楚它拼出来的那个路径到底长什么样。
这篇内容适合三类人看:第一次配 IPAdapter、导入别人的工作流直接报这条错的;换了机器、换了盘符、换了整合包之后突然不工作的;以及想把这类"模型找不到"问题一次性搞明白、以后不再被同类报错绊住的。文中涉及的目录结构、文件名对照和排查脚本,都可以直接抄去用。
1.1 别急着重装,先看 traceback 的最后三行
控制台里那一大坨红色信息,90% 的人只看了最后一句就关掉了。真正有价值的信息在倒数第三行到倒数第一行之间,尤其是带File "..."的那几行。它会告诉你三件事:抛错的文件是哪个、行号是多少、当时正在执行哪个函数。
典型的形态长这样:
File "ComfyUI/custom_nodes/ComfyUI_IPAdapter_plus/IPAdapterPlus.py", line 5xx, in load_ipadapter_model raise Exception("IPAdapter model not found.") Exception: IPAdapter model not found.看到这行,你就知道抛错点是 IPAdapter Plus 这个自定义节点内部的load_ipadapter_model函数。接下来要做的动作很明确:打开这个.py文件,跳到对应行号附近,往上翻二三十行,找到它是怎么把文件名拼成路径的。这一步是整个排查的锚点,跳过它,后面全是瞎猜。
顺便说一句,不同版本的节点措辞不完全一样。有的版本是raise Exception("IPAdapter model not found."),有的版本改成了走folder_paths.get_full_path_or_raise(),抛出来的是FileNotFoundError,还有的会在后面附一句"please download the model"。文本不同,但源头是同一处逻辑:文件名到真实路径的解析失败了。所以哪怕你的报错文案跟标题差几个字,排查思路完全通用。
还有一种情况值得单独拎出来:报错信息里带上了完整的绝对路径,比如.../models/ipadapter/ip-adapter_sd15.safetensors。这种反而最好办,因为它直接把答案告诉你了——你只需要去那个路径上看一眼,文件在不在、名字对不对。怕的是只给一句"not found",那就要靠后面的方法把路径反推出来。
1.2 IPAdapter Plus 的模型查找其实是两次校验
很多人以为节点是"扫一遍目录,有就用",实际不是。它做的事情分成两步,任何一步失败都会导致最终报错,但失败的形态不一样。
第一步是枚举。节点在构建界面的时候,会去调用类似folder_paths.get_filename_list("ipadapter")的接口,把模型目录下所有合法后缀的文件列出来,填进那个下拉框。这一步决定了你在节点上能看到哪些选项。如果你打开节点,下拉框是空的,或者里面只有一两个莫名其妙的名字,说明枚举环节就没拿到东西,问题在目录注册或目录内容上。
第二步是解析。你选了一个名字(或者工作流 JSON 里带着一个名字),节点把这个名字交给folder_paths.get_full_path("ipadapter", name)之类的接口,拿到绝对路径,然后用os.path.isfile()判断。这一步失败,就是你看到的那条报错。
这两步失败的原因完全不同。枚举失败通常是目录根本没被注册进搜索路径,或者目录层级多套了一层;解析失败通常是文件名对不上——注意,是逐字符对不上,不是"看起来差不多"。曾经有个案例,文件名里用的是全角连字符而不是半角减号,肉眼几乎分辨不出来,节点扫出来的列表里明明有这个文件,但工作流里存的是另一个名字,解析就失败。这种坑只有靠后面 4.1 节的脚本才能快速定位。
1.3 为什么它不做"找不到就用默认"的降级
有人会问,既然只是找不到文件,为什么不干脆跳过这个节点,让工作流跑完?这个设计其实是对的。IPAdapter 的作用是往注意力层里注入图像特征,如果权重缺失却继续往下跑,你会得到一个"看起来跑完了但图像完全没受影响"的结果,或者更糟——在某个中间层崩掉,报一个跟真实原因毫无关系的 shape 错误。相比之下,在加载阶段就明确抛异常,是成本最低、最不容易误导人的做法。
理解了这一点,你在排查时就应该建立一个心态:这条报错是好消息,它把问题锁定在了最外层。真正难查的是那些不报错但效果不对的情况,比如权重版本和底座模型不匹配,图能出,但风格迁移几乎无效。那种问题没有异常提示,只能靠自己比对文件清单。
2. IPAdapter 到底依赖几个文件:主权重、图像编码器与版本匹配
绝大多数人卡在这条报错上,根本原因是脑子里只有一个"IPAdapter 模型"的概念,实际上它至少涉及两类文件,而且在很多组合下是三类。搞不清这个结构,你把文件放进去也照样报错。
2.1 权重和 CLIP Vision 是两套东西,缺一不可
IPAdapter 的工作方式,说白了是拿一张参考图,先用一个图像编码器把它变成一组特征向量,再把这组向量通过一个小型的投影网络注入到主模型的注意力层里。这里有两个独立的东西:那个图像编码器,和那个投影网络。
图像编码器就是 CLIP Vision,它是一个独立的、几百兆到几个 G 的模型文件,放在models/clip_vision目录下。投影网络才是我们平时说的 "IPAdapter 模型",放在models/ipadapter目录下。两者是分开加载的,节点上也是两个不同的加载器。
所以你会遇到两种长得很像但含义不同的报错:
| 报错文本 | 缺的是什么 | 该放的位置 |
|---|---|---|
| IPAdapter model not found. | 投影网络权重 | ComfyUI/models/ipadapter/ |
| Clip vision model not found. | CLIP Vision 编码器 | ComfyUI/models/clip_vision/ |
标题里这条属于第一种。但实际排查时经常发现,用户是两样都没放,只是先撞上了第一个。所以看到这条错,别只盯着一个目录补,顺手把clip_vision目录也检查一遍,能省下第二轮折腾。
至于为什么要用两套文件而不是打包成一个,这是设计上的取舍:图像编码器在不同 IPAdapter 变体之间是共用的,单独放一份可以避免每个权重都重复塞几百兆的视觉塔。代价就是用户得多管一个目录。
2.2 SD1.5、SDXL、SD3 的 IPAdapter 权重不能互换
第二个高频误区是版本混用。IPAdapter 的权重是跟底座的注意力维度绑死的,SD1.5 的权重塞给 SDXL,不会报"not found",而是会在加载后崩在矩阵乘法上,报一串mat1 and mat2 shapes cannot be multiplied之类的东西。所以这里的关键不是"有没有文件",而是"有没有对的文件"。
下面这份对照表是我自己整理在笔记里的,按底座分类,文件名以发布页的原始命名为准(不同发布者可能带前缀,以实际下载到的为准):
| 权重文件名 | 适配底座 | 配套 CLIP Vision |
|---|---|---|
| ip-adapter_sd15.safetensors | SD1.5 | ViT-H |
| ip-adapter_sd15_light.safetensors | SD1.5 | ViT-H |
| ip-adapter-plus_sd15.safetensors | SD1.5 | ViT-H |
| ip-adapter-plus-face_sd15.safetensors | SD1.5(人脸) | ViT-H |
| ip-adapter-full-face_sd15.safetensors | SD1.5(人脸) | ViT-H |
| ip-adapter_sdxl.safetensors | SDXL | bigG |
| ip-adapter_sdxl_vit-h.safetensors | SDXL | ViT-H |
| ip-adapter-plus_sdxl_vit-h.safetensors | SDXL | ViT-H |
| ip-adapter-plus-face_sdxl_vit-h.safetensors | SDXL | ViT-H |
看这张表能发现一个规律:文件名里带vit-h的,配 ViT-H 编码器;不带的 SDXL 版本,配 bigG。SD1.5 系列则统一用 ViT-H。如果你的节点里同时有多个 CLIP Vision 可选,选错了不一定立刻报错,但输出会明显跑偏——这是比"not found"更隐蔽的坑。
至于 SD3 和更新的那些底座,对应的 IPAdapter 变体命名规则又不一样,通常会在文件名里带底座标识。这类权重目前生态还在变,唯一可靠的做法是下载时看清楚发布页的说明,别靠文件名猜。
2.3 文件名必须逐字符一致:大小写、空格与扩展名
这是最容易被忽略、也最让人抓狂的一类。节点解析文件名的时候,是拿工作流里存的字符串去和磁盘上的实际文件名做精确匹配(部分版本会做大小写不敏感的兜底,但不要指望它)。下面这些差异都会导致解析失败:
- 文件名里的
-被写成了_,或者反过来的 - 中间多了一个空格,或者两个空格
- 扩展名是
.bin而工作流里存的是.safetensors - 大小写不同,比如
IP-Adapter和ip-adapter
还有一种特别隐蔽的:扩展名重复。Windows 默认隐藏已知文件类型的扩展名,你从浏览器下载一个文件,系统可能自动补了一次后缀,实际文件名变成ip-adapter_sd15.safetensors.safetensors。在资源管理器里看,它显示为ip-adapter_sd15.safetensors,一切正常;但在目录列表接口里,它是一个后缀不合法的文件,直接不被收录。同理还有下载工具留下的.part、.crdownload、.downloading后缀,这些文件存在但等于不存在。
最后提一个跨平台的问题:从 macOS 拷过来的文件名,可能是 NFD 归一化形式(某些带音标的字符会被拆解),而 Windows 和 Linux 上是 NFC。肉眼看起来一模一样,字节层面完全不同。中文或特殊字符的文件名尤其容易踩这个坑。解决办法很简单——把文件名改成纯 ASCII 的小写字母、数字、下划线、连字符,别用中文、别用空格。这条规则我坚持了很多年,省下的时间远超改名的成本。
3. 文件明明放对了还报错:五类目录层面的错位
确认了文件已经下载、也改好了名,结果还是报同一条错,这时候问题基本可以锁定在目录结构上。下面这五种情况,按我遇到的频率从高到低排。
3.1 多套了一层文件夹,扫描器只看第一层
从压缩包解压出来的模型,经常自带一层目录,比如ip-adapter_sd15/ip-adapter_sd15.safetensors。你把整个文件夹拖进models/ipadapter,结果就变成了:
ComfyUI/models/ipadapter/ └── ip-adapter_sd15/ └── ip-adapter_sd15.safetensors而枚举接口默认只扫一层(有些版本支持递归,但依赖配置,不要赌)。于是列表里什么都没有,节点解析失败,报 not found。
正确的结构应该是一层平铺:
ComfyUI/models/ipadapter/ ├── ip-adapter_sd15.safetensors ├── ip-adapter-plus_sd15.safetensors └── ip-adapter_sdxl_vit-h.safetensors验证方法很简单,把目录完整路径粘到资源管理器地址栏回车,看到的应该直接是文件,而不是又一个文件夹。同理,models/clip_vision也要平铺,别把 CLIP Vision 的文件夹整个塞进去。
3.2 下载中断留下的残file与双扩展名
模型文件动辄几百兆到几个 G,网络抖动一下就可能中断。有些下载器会留下一个看起来完整、实际只有前几兆的文件。这种情况下文件名是对的、目录是对的,枚举也能列出它,但在加载阶段读取文件头时会失败。
判断文件是否完整,最直接的是看大小。同一类权重,正常体积在几十兆到几百兆之间,如果只有几百 KB,几乎肯定是残的。更严谨的做法是读一下 safetensors 的文件头:
import json import struct import os path = r"D:\ComfyUI\models\ipadapter\ip-adapter_sd15.safetensors" size = os.path.getsize(path) with open(path, "rb") as f: header_len = struct.unpack("<Q", f.read(8))[0] if header_len > size - 8: print("文件头长度异常,文件已损坏或不完整") else: header = json.loads(f.read(header_len)) print("张量数量:", len(header)) print("文件大小:", size)正常文件能打印出张量数量,损坏的文件要么struct.unpack抛异常,要么json.loads解不出来。这个脚本我放在桌面好几年了,每次下完大模型顺手跑一下,比等到工作流崩了再回头查省事得多。
顺带区分一下:文件损坏时,报错文本通常是SafetensorError: Error while deserializing header之类,不是not found。但很多人因为文件没加载成功,节点后续又走了一遍路径检查,最终看到的还是 not found,所以别只凭报错文本判断,实际去 list 一下目录更靠谱。
3.3 老版本节点与老工作流留下的路径记忆
工作流 JSON 里存的是文件名,不是绝对路径,这一点是好事。但如果你导入的工作流来自一个用了不同命名习惯的环境,里面存的可能是ip-adapter-plus_sd15.bin,而你本地只有.safetensors版本,名字对不上,解析就失败。
还有一种情况是节点版本差异。早期版本的 IPAdapter Plus 把模型目录放在custom_nodes/ComfyUI_IPAdapter_plus/models/下面,后来改成了ComfyUI/models/ipadapter/。如果你装的是新版节点,却按老教程把文件放进了自定义节点目录,新版扫描的路径里根本没有那个位置,自然找不到。反过来,老节点配新目录也一样。
遇到这种版本对不上的情况,最省事的做法不是猜,而是直接去节点的__init__.py或者主文件里搜add_model_folder_path,看看它到底往哪个目录注册了:
grep -rn "add_model_folder_path\|ipadapter" \ ComfyUI/custom_nodes/ComfyUI_IPAdapter_plus/__init__.pyLinux 和 macOS 用 grep,Windows 可以用 PowerShell 的Select-String。搜出来的那行代码里的路径,就是它的真实期望位置,以这个为准,别以教程为准。
3.4 便携版、多环境、extra_model_paths 的叠加
如果你同时装了多个环境(比如一个整合包、一个手动部署的),或者用了extra_model_paths.yaml做模型目录共享,问题会变得隐蔽。表现是:你明明在 A 目录放了文件,节点也确实能列出来,但运行时报 not found——原因可能是配置里挂了好几个路径,节点先命中了 B 目录的列表缓存,而 B 目录里没有那个文件。
排查这类问题的办法是让程序自己把配置打出来。在节点的加载函数里临时插一行打印,或者在 ComfyUI 启动之后用它的接口查一下对象信息,看ipadapter这个键下到底列出了哪些文件、来自哪个路径。如果列表里出现的文件名和你放的不一致,说明它读的是另一个目录。
配置文件里的路径建议一律写绝对路径,不要写相对路径。相对路径的基准目录在不同启动方式下可能不同,从桌面快捷方式启动和从命令行cd进去启动,解析出来的结果可能差一个层级。
3.5 列表缓存没刷新,节点下拉框还是旧的
这一条纯粹是操作层面的坑:你放了新文件进去,但 ComfyUI 还开着,页面上的下拉框还是旧内容。因为模型列表在启动时扫一次,之后走缓存。你右键刷新节点、或者单纯按 F5 重载页面,多数情况下能拿到新列表;如果还不行,就得完整重启一次进程。
判断是不是缓存问题有个很简单的办法:看下拉框里有没有你刚放进去的那个文件。有,说明缓存已更新,问题在解析;没有,说明缓存没更新或者文件没被收录,先去解决收录问题。这两种情况的修复动作完全不同,别混着来。
4. 一套可复现的排查动作:从绝对路径到最小工作流
前面讲的是"哪里可能错",这一节讲"按什么顺序查"。我自己的习惯是三道关,每道关都给出明确的是/否判断,避免边猜边改越改越乱。
4.1 用脚本列出目录真实文件名并做归一化比对
第一关:磁盘上到底有什么。不看资源管理器,直接用代码列,因为资源管理器会隐藏扩展名、会做符号显示,骗人的地方太多。
import os import unicodedata dirs = [ r"D:\ComfyUI\models\ipadapter", r"D:\ComfyUI\models\clip_vision", ] for d in dirs: print("=" * 60) print("目录:", d) if not os.path.isdir(d): print(" [目录不存在]") continue for name in sorted(os.listdir(d)): full = os.path.join(d, name) kind = "DIR " if os.path.isdir(full) else "FILE" size = 0 if os.path.isdir(full) else os.path.getsize(full) nfc = unicodedata.normalize("NFC", name) == name print(f" {kind} {name!r} {size/1024/1024:.1f}MB NFC={nfc}")这段代码有几个刻意的设计:{name!r}用 repr 输出,能把前后空格、不可见字符原形毕露;打印大小,能一眼看出几百 KB 的残文件;打印 NFC 标记,能发现跨平台文件名归一化问题;遇到子目录会标成DIR,正好用来发现 3.1 节的嵌套问题。
跑完这段,你应该能明确回答:目录存在吗?文件是第一层还是套了一层?大小正常吗?名字里有空格或奇怪字符吗?
4.2 回到源码确认它期望的完整路径
第二关:程序到底在找什么。回到 1.1 节定位到的那一行,往上读,找到拼接路径的代码。一般在同一个函数里会有类似这样的逻辑:
ipadapter_path = folder_paths.get_full_path("ipadapter", ipadapter_file) if not os.path.isfile(ipadapter_path): raise Exception("IPAdapter model not found.")注意get_full_path的行为:它会在所有注册过的ipadapter目录里依次查找这个文件名,找到第一个就返回。如果都没找到,返回None,然后os.path.isfile(None)直接抛 TypeError 而不是你看到的这条——所以如果你看到的确实是 not found,说明路径拿到了,但文件不存在,或者拿到的是一个拼接出来但磁盘上没有的路径。
想看得更清楚,可以临时在这行前面加一句打印,把变量打出来:
print("[DEBUG] looking for:", repr(ipadapter_file)) print("[DEBUG] resolved to:", repr(ipadapter_path)) print("[DEBUG] exists:", os.path.isfile(ipadapter_path or ""))改完记得重启进程。跑一次,控制台里那三行会直接告诉你:程序想要的名字是什么、拼出来的路径是什么、存不存在。这一步做完,问题基本就定性了。注意排查完把这几行删掉或者注释掉,别留在生产环境里。
4.3 搭一个只有三个节点的最小验证流
第三关:确认整条链路通了。很多人改完文件就直接去跑那个几百节点的复杂工作流,一报错又不知道是新问题还是老问题。正确做法是先搭一个最小流:
- 一个加载底座的节点(Checkpoint Loader),选一个你确定能用的 SD1.5 或 SDXL 模型
- 一个 IPAdapter 模型加载器,选你刚放进去的权重
- 一个 IPAdapter 应用节点,串在模型和采样器之间
- 后面接最普通的空 latent、采样器、VAE 解码、保存图像
这个流里不要放 LoRA、不要放 ControlNet、不要放任何其他自定义节点。跑通了,说明模型文件和目录结构没问题,可以回去接你的复杂工作流;跑不通,说明问题还在文件层面,别往别处找。
如果最小流能跑但复杂流报错,那问题就变成"复杂流里存的文件名和你本地的不一致",去检查那个工作流 JSON 里ipadapter_file字段的值,直接文本搜索就行。这个字段名叫什么可能因版本而异,搜ipadapter一般都能搜到。
5. 跑通之后:参数、显存与模型库的整理习惯
报错解决只是开始。IPAdapter 这个东西,能加载不等于能用好,下面几点是我在实际项目里反复验证过的经验。
5.1 权重强度与起止步数怎么配合
IPAdapter 应用节点上有两个关键参数:weight(权重强度)和 start_at / end_at(起止百分比)。默认 weight 是 1.0,全程生效,这个默认值在多数场景下偏强,容易把原图的构图和色彩完全压掉。
我的经验区间是这样的:纯风格参考(比如只想要画面的质感和色调),weight 放在 0.4 到 0.6,end_at 设到 0.6 到 0.8,让采样后半段回归底座模型自己的发挥;构图参考(要保留参考图的结构),weight 提到 0.7 到 0.9,全程生效;人脸一致性场景,weight 通常要 0.8 以上,而且要配合 FaceID 类的专用权重,普通权重做人脸一致性效果很勉强。
至于为什么 end_at 有用,可以这样理解:扩散采样早期决定构图和大色块,晚期决定细节和质感。让 IPAdapter 只影响早期,等于"用参考图定调子,剩下的交给模型自己画",出来的图既像参考图又不至于死板。这个技巧在出图发灰、细节糊成一团的时候特别管用。
5.2 显存吃紧时的取舍
IPAdapter 的显存开销主要来自那个图像编码器。ViT-H 大约 2.5G 参数量级,bigG 更大。在显存吃紧的机器上,同时挂着底座、VAE、CLIP、CLIP Vision,峰值很容易顶到上限。
几个可以立刻生效的调整:优先用 ViT-H 版本的权重而不是 bigG 版本;采样时用较低的分辨率出图,再用放大节点二次处理;如果节点支持,把 CLIP Vision 的加载放在采样之前、用完即释放,而不是常驻。另外注意别在同一张图里叠加多个 IPAdapter 应用节点,每多一个都要多跑一遍编码,显存和时间都是线性增长的。
还有一个不太直观的点:报 not found 的时候,有些人会误以为是显存不足导致加载失败。不会的。显存不足的报错文本里一定有out of memory或者CUDA字样,两者不会混淆。
5.3 给模型库建一份自己的清单
这个问题我踩过不止一次,后来养成了一个习惯:在模型目录旁边放一个manifest.txt,每次新增文件就追加一行,写清楚文件名、来源、适配底座、配套编码器、下载日期。听起来很土,但当你半年后回来翻,完全不记得那个ip-adapter-plus-face_sdxl_vit-h到底该配哪个编码器的时候,这份清单能救你。
更规范一点的做法是写成 JSON,配合前面 4.1 节的脚本做自动校验:
{ "ip-adapter-plus_sd15.safetensors": { "base": "sd15", "clip_vision": "CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors", "note": "通用风格参考,weight 0.5 起调" }, "ip-adapter_sdxl_vit-h.safetensors": { "base": "sdxl", "clip_vision": "CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors", "note": "SDXL 配 ViT-H 版本,显存友好" } }写个脚本扫目录、跟 JSON 比对,缺文件的、多出来的、大小异常的,一次性全列出来。这个脚本我每周跑一次,比等到报错再查主动得多。
最后分享一个我自己的教训。曾经因为一条 not found 折腾了大半天,最后发现是文件放在了另一块盘的同名目录里,而extra_model_paths.yaml里那条路径的盘符写错了。从那以后,我给自己定了个规矩:遇到"找不到"类报错,第一件事永远是让程序把它的实际路径打出来,而不是凭记忆去猜它应该在哪。程序不会撒谎,记忆会。