在我的一次部署排查中,应用首次使用时才加载 Embedding,并设置了 local_files_only=True。检查缓存后发现:主模型有 refs/main,却没有 snapshot;自定义实现的 snapshot 只剩 __pycache__/*.pyc。目录名看起来都在,实际文件却不足以离线加载。
这类问题要分开检查三个东西:模型文件是否完整,运行时能否加载并生成向量,以及应用能否继续完成检索和聊天。
为什么容器启动和首页检查发现不了
应用采用懒加载时,启动过程可能只创建一个模型服务对象,真正读取配置、权重和分词器发生在第一次 encode() 或 embed_query() 时。
容器启动 → API 可接收请求 → 首页正常
↓
第一次聊天
↓
加载 Embedding 模型失败
↓
还没有进入生成回答因此,首页 200 不是模型就绪检查。接口返回通用错误时,先按请求时间查应用日志,找到具体模型加载异常,不为了看堆栈直接把公网服务改成 debug。
缓存目录里有什么
Hugging Face Hub 的常见文件缓存结构如下:
model-cache/
└── models--组织名--模型名/
├── refs/
│ └── main
├── blobs/
│ └── 实际下载的文件内容
└── snapshots/
└── 某个提交哈希/
├── config.json
└── 指向 blobs 的文件链接refs/main 记录分支对应的提交,snapshots/<提交哈希> 表示某一版本的文件视图,文件通常通过相对符号链接指向 blobs 中的实际内容。不同平台和缓存设置下也可能使用实际文件,不能只靠是否有符号链接判断完整性。Hub 缓存结构
这解释了几种常见误判:
| 看到的内容 | 仍然缺少什么证据 |
|---|---|
| 模型目录存在 | 是否有配置、分词器、权重和必要实现 |
refs/main 存在 | 它指向的 snapshot 是否存在、是否完整 |
| snapshot 存在 | 链接目标是否存在,文件是否下载完成 |
只有 .pyc | 对应版本所需的 Python 源文件和依赖是否齐全 |
index.faiss、index.pkl 存在 | 查询模型是否能加载,两者是不同资产 |
不要只复制某个 snapshot 而遗漏它链接到的 blobs。也不要遍历 snapshots 后随手取第一个目录:文件系统枚举顺序不代表最新版本,更不代表建库时使用的版本。
自定义模型可能需要第二个仓库
以 Alibaba-NLP/gte-multilingual-base 为例,模型配置中的 auto_map 把实现指向 Alibaba-NLP/new-impl:
{
"auto_map": {
"AutoConfig": "Alibaba-NLP/new-impl--configuration.NewConfig",
"AutoModel": "Alibaba-NLP/new-impl--modeling.NewModel"
}
}这表示只准备主模型权重还不够,加载器还要取得对应实现代码。
trust_remote_code=True 是允许执行模型提供的自定义代码,不是允许联网的开关,也不表示代码自动变得可信。只对确认过来源的实现启用,并记录模型与代码各自的版本。Transformers 自定义模型加载
它还可能依赖额外的 Python 包。模型缓存不会替代 Python 依赖环境,不能把服务器缺少模块的错误继续当作权重下载失败。
用现有下载接口准备完整缓存
Hub 已提供 hf download 和 snapshot_download(),无需自己拼下载 URL,或手工创建 snapshot 目录。本例使用后者下载仓库,并保存本次实际解析到的两个提交版本。下载接口与 revision 参数
下面三份脚本放在同一工作目录。使用已经为应用验证过的 Python 依赖环境,包含 huggingface_hub、sentence_transformers、transformers 和 PyTorch。加载示例采用 Sentence Transformers 5.1 系列及 Transformers 4.x 的接口,与 用 LangChain 和 FAISS 为 Markdown 建立语义检索 中的模型用法衔接;不在修复运行容器时临时升级整套依赖。
先创建 prepare_models.py:
import json
import os
from pathlib import Path
from huggingface_hub import snapshot_download
root = Path(os.environ.get("MODEL_CACHE_DIR", "./model-cache")).resolve()
root.mkdir(parents=True, exist_ok=True)
repositories = {
"model": ("Alibaba-NLP/gte-multilingual-base", "MODEL_REVISION"),
"code": ("Alibaba-NLP/new-impl", "CODE_REVISION"),
}
manifest = {}
for key, (repo_id, variable) in repositories.items():
snapshot = Path(snapshot_download(
repo_id=repo_id,
revision=os.environ.get(variable, "main"),
cache_dir=str(root),
))
manifest[key] = {"repo_id": repo_id, "revision": snapshot.name}
print(key, snapshot)
(root / "prepared.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)在可以访问官方站的环境中执行:
HF_ENDPOINT=https://huggingface.co \
HF_HUB_OFFLINE=0 TRANSFORMERS_OFFLINE=0 \
python prepare_models.py两次下载全部成功后,才会生成 model-cache/prepared.json,记录本次的完整提交哈希。如果已有项目验证过的模型与实现版本,先设置 MODEL_REVISION 和 CODE_REVISION 为那两个完整哈希,避免更新 main 后与旧索引或依赖不兼容。
默认下载 main 是首次准备的一种方式,不等于两个仓库的任意最新提交都自动兼容;必须通过下面的实际加载检查。下载中断时命令会报错,不能把开始显示进度当成完成。重复运行可复用已有缓存,不必先清空目录。
检查实际文件和失效链接
创建 check_cache.py,按记录的版本检查,而不是猜测应选哪个目录:
import json
import os
from pathlib import Path
root = Path(os.environ.get("MODEL_CACHE_DIR", "./model-cache")).resolve()
manifest = json.loads((root / "prepared.json").read_text(encoding="utf-8"))
for item in manifest.values():
repo_dir = "models--" + item["repo_id"].replace("/", "--")
snapshot = root / repo_dir / "snapshots" / item["revision"]
if not snapshot.is_dir():
raise RuntimeError(f"snapshot 不存在:{snapshot}")
broken = [p for p in snapshot.rglob("*") if p.is_symlink() and not p.exists()]
files = [p for p in snapshot.rglob("*") if p.is_file()]
print(item["repo_id"], "文件数:", len(files))
if not files or broken:
raise RuntimeError(f"空 snapshot 或失效链接:{broken}")执行 python check_cache.py。它能发现目录为空和失效链接,却不能证明所有模型必需文件都齐全,也不校验每个文件的内容。仍要让真正的模型加载器运行一次。
在离线模式下生成一次向量
创建 verify_model.py:
import json
import os
from pathlib import Path
import numpy as np
from sentence_transformers import SentenceTransformer
root = Path(os.environ.get("MODEL_CACHE_DIR", "./model-cache")).resolve()
manifest = json.loads((root / "prepared.json").read_text(encoding="utf-8"))
model_info = manifest["model"]
code_revision = manifest["code"]["revision"]
model = SentenceTransformer(
model_info["repo_id"],
revision=model_info["revision"],
cache_folder=str(root),
local_files_only=True,
trust_remote_code=True,
config_kwargs={"code_revision": code_revision},
model_kwargs={"code_revision": code_revision},
device="cpu",
)
vector = model.encode("部署验收")
if vector.ndim != 1 or vector.size == 0 or not np.isfinite(vector).all():
raise RuntimeError("没有生成有效的一维向量")
print("离线向量维度:", vector.size)在一个新进程中执行:
HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 python verify_model.py环境变量在 Python 导入库之前生效;local_files_only=True 要求从已有缓存加载。模型的 revision 和自定义实现的 code_revision 分别固定,避免准备的是一版,检查时却又选了另一版。config_kwargs 和 model_kwargs 分别传给配置与模型加载接口。SentenceTransformer 参数说明
预期加载完成并输出非空向量维度。这只证明所选模型版本在这个依赖环境里能生成向量,不代表检索相关性或整段聊天已经通过。两个离线开关约束相关库的下载行为;若要严格验证所有自定义代码都没有网络依赖,还应在禁止出站访问的环境中运行。
在 Docker 中核对挂载和下载网络
假设 Compose 中把模型目录挂载为:
services:
api:
volumes:
- ./model-cache:/models容器里执行脚本时就设置 MODEL_CACHE_DIR=/models。检查必须在实际运行模型的容器环境中再做一次:宿主机可读,不代表容器挂载位置正确、运行用户有权限或 Python 依赖相同。还要让应用加载器使用同样的仓库、版本和缓存路径;独立脚本不会自动修改应用配置。
模型下载使用的容器网络,与 docker pull 的镜像仓库访问也不是同一层配置。增大超时只增加等待,不能解决链路本来不可达的问题。
另一台电脑下载后怎样搬过去
也可以在联网电脑运行准备脚本,把完整专用缓存打包。以下命令在脚本所在目录执行,打包过程中不要继续写入同一缓存:
tar -czf model-cache.tar.gz -C model-cache .
scp model-cache.tar.gz <服务器登录账户>@<服务器地址>:/tmp/model-cache.tar.gztar 默认保留符号链接,不加跟随链接的参数。这里只传模型缓存,不把登录 Token 或整个用户的 Hugging Face 配置一起打包。
在服务器先停止会加载模型的服务,备份旧缓存,把压缩包解到一个新的空目录。核对包内是预期模型内容,再在实际容器环境执行文件检查和离线向量检查。验证通过后切换挂载,保留旧目录用于回退,不直接清空旧缓存来赌下载是否完整。
如果改变了挂载或模型版本环境变量,要重新创建应用容器;进程已缓存旧模型对象时,也需要重新启动模型进程,才能验证新配置。
最后回到真实请求
离线模型检查通过后,继续验证查询向量与已有索引是否配套,再发起真实检索和聊天,观察回答、流式结束及同一时间的应用日志。
| 失败位置 | 继续检查 |
|---|---|
| snapshot 不存在或链接失效 | 下载结果、传输内容、挂载路径 |
| 缺少 Python 实现或模块 | 自定义实现仓库、版本、Python 依赖 |
| 进程退出 137 | 内存限制和系统日志,不能仅凭退出码认定原因 |
| 向量可生成但检索报错 | 索引文件、向量维度及建库模型配置 |
| 检索可用但聊天失败 | 数据库、缓存、工具或模型供应商调用 |
换 Embedding 模型可能需要重建索引,但补齐同一版本的缺失文件不等于换模型。不要用随意更换模型绕过下载问题,最后留下维度相同、向量空间却不兼容的索引。
原排查已经确认缓存不完整,但没有模型补齐后聊天恢复的记录。这里保留故障证据与可执行的检查方法,不把尚未验收的修复写成成功结果。