做 RAG 或者语义检索的朋友,应该都遇到过模型下载这关。Hugging Face 上的模型虽然可以直接在网页里看,但真要用于工程化,最稳的方式还是用 huggingface-cli 这类命令行工具拉下来,再放到你真正运行的环境里。这次我记录的是把 BAAI/bge-small-zh-v1.5 下载到本地、再打包进 Docker 的一次完整操作,整个过程不快但很稳,适合正在做中文 Embedding 服务、想离线部署或者容器化交付的开发者。BGE 这个系列在中文语义向量上口碑不错,small 版本参数少、效果好,非常适合 CPU 环境或轻量服务。你跟着这套流程走,会发现难点不在下载本身,而在理解 Hugging Face 的缓存机制、CLI 新旧命令差异,以及 Docker 里如何把模型文件干净地“放进去”。
1. 项目背景与需求拆解
1.1 为什么偏偏是 BAAI/bge-small-zh-v1.5
做中文自然语言处理的人对 BGE 应该不陌生,BAAI/bge-small-zh-v1.5 是北京智源研究院开源的中文 Embedding 模型。Embedding 模型做的事情很简单:把一段文本变成一串固定维度的数值向量,让语义相近的句子在向量空间里也靠得近。
这个模型有几点非常适合实际工程:
- 参数量小,只有 24M 左右,加载后的显存或内存占用非常可控。
- 输出向量维度 512,不像一些大模型动辄 1024、1536,存储和检索成本更低。
- 中文表现扎实,在同类 small 级别模型里属于第一梯队。
- 单机 CPU 也能跑,不需要 GPU,部署条件极其宽松。
所以它的典型应用场景包括:文档问答的召回阶段、文本去重、相似问题聚类、知识库预检索、商品标题匹配等。只要你的业务需要“比较两句话像不像”,它就能派上用场。
很多项目一开始会直接用sentence-transformers在线加载这个模型,也就是传模型名给SentenceTransformer("BAAI/bge-small-zh-v1.5")。但线上环境不一定能访问 Hugging Face,生产容器也不应该每次启动都去下载权重。这就引出项目真正的目标:先把模型完整下载到本地,再把模型文件送进 Docker,让容器完全离线运行。
1.2 为什么用 huggingface-cli 而不是网页手动下载
网页下载听起来很简单:打开模型页面,点一下下载按钮,解压放到项目里就完事。实际做一次你就知道有多痛:
- Hugging Face 模型仓库不是单个文件,而是由
config.json、model.safetensors、tokenizer.json、tokenizer_config.json、vocab.txt等一组文件组成。手动下载容易漏。 - 手工下载没有版本哈希校验。模型仓库更新后,你本地可能混着新旧文件,加载时只报一个莫名其妙的错误。
- 命令行下载天然支持断点续传和缓存复用。中途网络断了,重新执行命令会继续下载,不是从头开始。
huggingface-cli是 Hugging Face Hub 官方提供的命令行工具,本质上是把snapshot_download的能力封装成终端命令。它能保证下载产物和远端仓库一致,也方便写进脚本或 CI 流程。对于“下载模型再打进 Docker”这种任务,命令行是唯一值得推荐的方式。
1.3 从下载到 Docker 的完整链路
整个流程可以拆成四步:
- 在本地准备好 Python 环境和
huggingface_hub。 - 使用 CLI 把 BAAI/bge-small-zh-v1.5 下载到项目里的独立目录。
- 检查下载产物,确认权重、分词器、配置文件齐全。
- 通过 Dockerfile 的
COPY或 Docker 的-v挂载,把模型文件放给容器使用。
核心坑主要集中在第 2 步和第 4 步。只要理解了 Hugging Face 的缓存机制和 Docker 上下文目录,基本不会出大问题。
2. 环境准备与工具选型解析
2.1 安装 Python 与 huggingface_hub CLI
首先需要一个 Python 3.9 以上的环境。如果你只在 Windows 上做这一步,建议用虚拟环境,避免把包装进系统 Python:
python -m venv .venv # Windows .\.venv\Scripts\activate # Linux / macOS source .venv/bin/activate然后安装huggingface_hub的 CLI 扩展:
pip install -U "huggingface_hub[cli]"装完后检查命令是否存在:
huggingface-cli version如果显示版本号,说明环境已经通了。需要留意的是,huggingface-cli本身也在迭代。新版本里,官方主推的命令变成了hf,旧命令虽然还能用,但会打印一行很显眼的提示:
warning: 'huggingface-cli download' is deprecated. use 'hf download' instead这不是错误,不影响下载结果。但既然官方已经给出迁移方向,新项目建议直接按新命令写。
2.2 新旧命令差异:huggingface-cli 与 hf
老命令的完整形式是:
huggingface-cli download 模型仓库名 --local-dir 目标目录新命令则是:
hf download 模型仓库名 --local-dir 目标目录两者参数高度相似。实际操作中,你可能会看到两种写法并存。这很正常,很多旧教程还在用huggingface-cli。我的建议是:遇到旧命令时照常能用,但自己在文档或脚本里优先用hf download,早切早省心。
命令的核心参数有这几个:
--local-dir:把文件下载到指定目录,而不是默认缓存目录。--include/--exclude:按文件名模式筛选要下载的文件。--revision:指定模型仓库的 commit 版本,生产环境强烈建议锁定。
例如只下载必要文件:
hf download BAAI/bge-small-zh-v1.5 \ --include "*.json" "*.txt" "*.safetensors" \ --local-dir ./models/bge-small-zh-v1.5这个筛选特别适合大模型。虽然 BGE small 不大,但养成按需下载的习惯,以后下几十 GB 模型时会感谢自己。
2.3 先理解缓存与符号链接,再决定参数
很多人第一次用 CLI 下载时会困惑:明明指定了--local-dir,目录里看起来有文件,但一检查发现是快捷方式或者符号链接。
Hugging Face Hub 默认会把所有模型放到系统用户目录下的缓存,比如~/.cache/huggingface/hub。早期版本在用--local-dir时,为了节省磁盘空间,会在本地目录里生成指向缓存文件的符号链接,而不是真实拷贝。这个设计对直接跑 Python 程序没有影响,但你要打包进 Docker 就成了麻烦:
- Docker build 的上下文里如果出现符号链接,
COPY可能把链接本身拷进去,也可能因为链接指向了上下文之外的目录而失败。 - 即使构建成功,容器里也可能出现路径断裂,模型加载时直接报找不到文件。
所以在下载后,务必确认--local-dir目录里都是真实文件。老版本 CLI 可以通过参数关闭符号链接:
huggingface-cli download BAAI/bge-small-zh-v1.5 \ --local-dir ./models/bge-small-zh-v1.5 \ --local-dir-use-symlinks False新版本里--local-dir的行为已经更接近直接下载,但保险起见,下载完用ls -l看一遍,文件列表里不能有指向别处的->标记。Windows 下则可以看文件夹里是不是出现了“快捷方式”。
3. 实操:用命令行下载模型到本地
3.1 下载前的目录规划
我习惯把模型放在项目根目录下的models/里,而不是放到系统下载目录。原因很简单:后面要执行docker build,Docker 默认把当前目录作为构建上下文。如果模型文件放在奇怪的位置,COPY指令要么够不到,要么会把无关的大文件也带进 Docker 上下文,拖慢构建速度。
我的项目结构大致是这样:
my-embedding-service/ ├── .venv/ ├── models/ │ └── bge-small-zh-v1.5/ ├── Dockerfile └── app.py创建目录:
mkdir -p models3.2 执行下载:老命令与新命令都可以
进入项目根目录后,执行下载。推荐新命令:
hf download BAAI/bge-small-zh-v1.5 --local-dir ./models/bge-small-zh-v1.5如果你习惯旧命令也没问题:
huggingface-cli download BAAI/bge-small-zh-v1.5 \ --local-dir ./models/bge-small-zh-v1.5 \ --local-dir-use-symlinks False下载过程中能看到进度条,因为模型不大,通常几十秒到几分钟就完成了。如果中途断了,直接重新执行同一命令,它会从断点继续。
下载完成后检查目录:
ls -l ./models/bge-small-zh-v1.5预期能看到这些文件:
config.json model.safetensors tokenizer.json tokenizer_config.json vocab.txt看到这个列表就说明权重、分词器、配置都齐了。缺少任何一个,容器里加载模型都会报错。
3.3 下载产物与本机验证
不要急着写 Dockerfile,先在本地验证模型能不能加载。这样能把“模型文件损坏”和“Docker 配置错误”两类问题隔离开。
写一个极简脚本验证:
python -c " from sentence_transformers import SentenceTransformer model = SentenceTransformer('./models/bge-small-zh-v1.5') vec = model.encode(['Docker 部署模型', '中文语义检索'], normalize_embeddings=True) print(vec.shape) "如果本地没装sentence-transformers,先安装:
pip install sentence-transformers输出里能看到矩阵维度,大概率是(2, 512)。这说明模型加载成功,并且能正常编码。
这里有个容易被忽略的点:本地验证时用的是本地路径,而不是模型名。如果写成SentenceTransformer("BAAI/bge-small-zh-v1.5"),它会尝试联网下载,那这个验证步骤就失去了“离线检查”的意义。
4. 把模型交付给 Docker 的两种主流方式
4.1 方式 A:COPY 进镜像,保证可复现
第一种方式是把模型文件直接写进镜像。好处是镜像拿出去就能跑,不需要额外挂载,适合交付和部署。
Dockerfile 可以这样写:
FROM python:3.10-slim WORKDIR /app RUN pip install --no-cache-dir sentence-transformers COPY ./models/bge-small-zh-v1.5 /app/models/bge-small-zh-v1.5 CMD ["python", "-c", "from sentence_transformers import SentenceTransformer; m = SentenceTransformer('/app/models/bge-small-zh-v1.5'); print(m.encode(['docker test'], normalize_embeddings=True).shape)"]注意我把COPY放在RUN pip install后面。因为 Docker 构建有层缓存,模型文件变动时不需要重新安装 Python 依赖,构建速度会快很多。
构建并运行:
docker build -t bge-test . docker run --rm bge-test容器启动后如果能打印出向量维度,说明模型已经成功进入 Docker。bge-small-zh-v1.5模型权重文件大概 90M 左右,加上 Python 依赖后镜像体积约 500M 上下,还算可控。
4.2 方式 B:Volume 挂载,适合模型迭代
第二种方式是模型不进镜像,而是启动容器时把宿主机的模型目录挂载进去。这样更换模型不用重新构建镜像,只需更新宿主机目录里的文件。
示例:
docker run --rm \ -v "$PWD/models/bge-small-zh-v1.5:/app/models/bge-small-zh-v1.5" \ bge-test这里假设前面 Dockerfile 已经把bge-test镜像构建好,只是启动时用挂载覆盖镜像里的模型目录。
两种方式怎么选?我的判断标准很简单:
- 模型很稳定、要交付给外部团队,选 COPY。
- 模型还在调优、需要频繁替换版本,选 Volume。
- 容器要跨机器迁移,选 COPY。
- 宿主机磁盘资源紧张,想省一份模型拷贝,选 Volume。
BGE small 只有 90M 左右,两份文件也不占多少空间,但方式背后的思路比命令本身重要。
4.3 方式 C:docker cp 到运行中容器,应急可以用
如果你不想重新构建镜像,容器已经运行中,想临时把模型塞进去,可以用docker cp:
docker cp ./models/bge-small-zh-v1.5 容器名:/app/models/bge-small-zh-v1.5但这个方式我不推荐作为常规方案。容器一旦重建,文件就丢了。它只适合临时调试,不属于可复现的交付方式。正经部署还是应该用 COPY 或 Volume。
5. 常见问题与排查技巧实录
5.1 下载报错与网络问题速查
| 问题 | 现象 | 处理建议 |
|---|---|---|
| CLI 提示 deprecated | 出现huggingface-cli download is deprecated | 不影响使用,改用hf download |
| 下载卡在进度条 | 网络不稳定或连接超时 | 重新执行命令,CLI 支持断点续传 |
--local-dir目录出现符号链接 | ls -l能看到->指向缓存 | 使用--local-dir-use-symlinks False重新下载 |
| 下载文件不完整 | 缺少model.safetensors或tokenizer.json | 删除目录后重新下载,不要手动拼凑 |
| 模型加载报 OSError | 本地文件损坏或版本不匹配 | 清空--local-dir后重新拉取完整仓库 |
下载阶段最重要的一课就是:不要手动从网页下载单个文件再改名字。Hugging Face 模型仓库是一个整体,除了权重文件,tokenizer_config.json和config.json之间还有版本配套关系。手动混搭很容易让加载阶段出现“键值对不上”“词表大小不一致”这类诡异错误。
5.2 Docker 构建与启动问题速查
| 问题 | 现象 | 处理建议 |
|---|---|---|
COPY找不到模型文件 | 构建时提示failed to compute cache key | 检查模型目录是否在 Docker 上下文内 |
| Docker Desktop 无法启动 | 提示virtualization support not detected | 检查 Windows 的 CPU 虚拟化是否开启,并确认 WSL2 已启用 |
| 容器连接失败 | 提示failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen | 确认 Docker Desktop 后端启动完成,或重启 Docker |
| 构建上下文过大 | 终端提示Sending build context to Docker daemon很久 | 创建.dockerignore,把.venv、models排除(如果你用 COPY 可以例外) |
| 挂载目录为空 | 容器里模型路径存在但没文件 | 确认宿主机的绝对路径正确,Windows 下用${PWD}或绝对路径 |
如果你在 Windows 上用 Docker Desktop,最容易踩的坑不是命令写错,而是 Docker 后端没起来。尤其刚装完 Docker Desktop,第一次启动时 WSL2 内核还没初始化,经常报虚拟化相关错误。这时候不要在容器层面纠缠,先解决 Docker 本身。
5.3 模型在容器内加载异常
容器里加载模型报错,优先检查三件事:
- 模型路径是否正确。容器内路径不同于宿主机,千万不要写死成
C:/...或/home/xxx。 - 文件是否真实存在。进入容器检查:
docker run --rm -it bge-test /bin/bash ls -l /app/models/bge-small-zh-v1.5- 是否还在尝试联网。如果代码里传的是模型名而不是本地路径,
SentenceTransformer会尝试访问 Hugging Face,离线环境下就会超时。
排查思路其实和“本机能不能跑”对比。本机能跑、容器不能跑,多半是路径或文件拷贝问题;本机就不能跑,那就先回头修模型文件,别折腾 Docker。
6. 实操心得与后续扩展
最后聊几点我自己的经验。
BGE small 这种体积的模型,其实没必要一开始就上 GPU。CPU 容器里跑 512 维度的 Embedding,吞吐量完全够用。打包进镜像后,算法团队更新模型时,再由运维重建一次镜像就行,整个流程很线性。
另外建议在代码里显式设置normalize_embeddings=True。BGE 系列的模型在计算向量相似度时,后续经常要用余弦相似度,归一化之后可以省掉再算一遍模长的步骤。别小看这个小参数,离线部署时少一个依赖,排查问题就少一层。
如果你后续要扩展,还可以在这个镜像基础上做三件事:
- 模型文件加载后放进内存缓存,避免每个请求重复初始化模型。
- 用
sentence-transformers的批处理能力,把多条文本一次性编码,提高吞吐。 - 将容器接入现有的 Flask 或 FastAPI 服务,对外暴露一个
/encode接口,让知识库检索、相似度匹配这些业务直接通过 HTTP 调用。
这次操作的核心就一句话:用 CLI 把模型当成代码一样管理,先验证本地,再决定用 COPY 还是 Volume 交给 Docker。只要把模型文件路径和缓存机制搞清楚,整套流程会变得很顺。我自己试下来,最省时间的就是下载完立刻做一次本地加载验证,这个习惯能帮你避开后续百分之八九十的坑。