在我的一次部署排查中,应用首次使用时才加载 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.gz

tar 默认保留符号链接,不加跟随链接的参数。这里只传模型缓存,不把登录 Token 或整个用户的 Hugging Face 配置一起打包。

在服务器先停止会加载模型的服务,备份旧缓存,把压缩包解到一个新的空目录。核对包内是预期模型内容,再在实际容器环境执行文件检查和离线向量检查。验证通过后切换挂载,保留旧目录用于回退,不直接清空旧缓存来赌下载是否完整。

如果改变了挂载或模型版本环境变量,要重新创建应用容器;进程已缓存旧模型对象时,也需要重新启动模型进程,才能验证新配置。

最后回到真实请求

离线模型检查通过后,继续验证查询向量与已有索引是否配套,再发起真实检索和聊天,观察回答、流式结束及同一时间的应用日志。

失败位置继续检查
snapshot 不存在或链接失效下载结果、传输内容、挂载路径
缺少 Python 实现或模块自定义实现仓库、版本、Python 依赖
进程退出 137内存限制和系统日志,不能仅凭退出码认定原因
向量可生成但检索报错索引文件、向量维度及建库模型配置
检索可用但聊天失败数据库、缓存、工具或模型供应商调用

换 Embedding 模型可能需要重建索引,但补齐同一版本的缺失文件不等于换模型。不要用随意更换模型绕过下载问题,最后留下维度相同、向量空间却不兼容的索引。

原排查已经确认缓存不完整,但没有模型补齐后聊天恢复的记录。这里保留故障证据与可执行的检查方法,不把尚未验收的修复写成成功结果。