1. 项目概述与背景

最近在折腾本地化的大语言模型应用,一个绕不开的核心组件就是文本嵌入模型。简单来说,嵌入模型能把一段文本(比如一句话、一个段落)转换成一串高维度的数字向量。这串数字就像是这段文本的“数字指纹”,包含了它的语义信息。有了这个“指纹”,我们就能做很多有趣的事情,比如语义搜索(找意思相近的文档)、文本分类、聚类,或者作为RAG(检索增强生成)系统的基石,为本地大模型提供精准的外部知识检索。

在中文领域,智源研究院开源的BGE系列模型是当之无愧的标杆。特别是 bge-base-zh-v1.5 这个版本,它在效果和效率之间取得了很好的平衡,768维的向量大小对于大多数本地部署场景来说非常友好。然而,直接使用Hugging Face上的PyTorch模型,在资源受限的环境(比如没有高性能GPU的云服务器,或者想追求极致推理速度)下运行,还是会面临内存占用大、推理速度不够快的问题。

这就引出了我们今天要做的核心工作:将 bge-base-zh-v1.5 这个PyTorch模型,转换成 ggml 格式。 ggml 是一个为在CPU上高效运行大型模型而设计的张量库和二进制格式,它通过量化等技术,能大幅降低模型的内存占用和提升推理速度,尤其适合在 LocalAI 这类本地AI框架中部署。而 embeddings.cpp 项目,正是 llama.cpp 生态中专门用于编译和运行嵌入模型的一个分支。

我们的目标很明确:在一台 autodl 租用的云服务器上,从零开始编译 embeddings.cpp 项目,然后使用其提供的转换工具,把 bge-base-zh-v1.5 模型转换成 ggml 格式,最后成功运行 main 程序进行本地推理测试。整个过程会涉及到环境配置、源码编译、模型下载与转换、参数调试等多个环节,我会把每一步的操作细节、踩过的坑和解决方案都详细记录下来。

2. 环境准备与项目编译

2.1 Autodl实例选择与初始化

autodl 是一个提供GPU/CPU云计算资源的平台,非常适合做这种一次性的模型转换和测试工作。我们的任务主要是编译和转换,对GPU没有硬性要求,但编译过程需要一定的CPU算力。因此,选择一个性价比高的CPU实例即可。我选择的是“基础镜像”中的 Ubuntu 20.04 ,配置为4核CPU、16GB内存的实例。这个配置对于编译 embeddings.cpp 和转换 bge-base-zh-v1.5 模型来说绰绰有余。

实例创建后,第一件事是通过 autodl 提供的Web Terminal或者VSCode远程开发功能连接上去。我个人更喜欢用VSCode Remote SSH,因为文件管理和终端操作都更方便。连接成功后,我们先更新系统包并安装一些基础依赖:

sudo apt update
sudo apt upgrade -y
sudo apt install -y build-essential cmake git wget

这里 build-essential 包含了GCC/G++编译器等核心工具链, cmake 是项目构建工具, git 用于拉取代码, wget 用于下载文件。

2.2 获取embeddings.cpp源码

embeddings.cpp 项目是 llama.cpp 的一个分支,专门优化了对Sentence Transformers这类嵌入模型的支持。我们需要从GitHub上克隆它:

git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp

注意 :这里直接克隆了主仓库 llama.cpp 。因为 embeddings.cpp 的功能已经合并到了主分支。我们需要确认当前分支包含了所需的转换和推理功能。通常主分支的 master main 都是可用的。我们可以通过查看目录下是否有 convert.py 或类似的脚本,以及 examples/embedding 目录来判断。

2.3 编译项目

llama.cpp 项目使用 CMake 进行构建。为了支持所有可能的优化(如AVX2, AVX512指令集加速),我们采用从源码构建的方式:

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j4

这里有几个关键点:

  1. mkdir build && cd build :这是标准的“out-of-source”构建方式,所有编译产生的文件都会放在 build 目录下,保持源码目录的整洁。
  2. -DCMAKE_BUILD_TYPE=Release :指定构建类型为发布模式,编译器会进行最高级别的优化,去掉调试信息,生成性能最高的可执行文件。
  3. make -j4 :开始并行编译, -j4 表示使用4个并行任务,这个数字通常设置为你的CPU核心数,可以加快编译速度。

编译过程可能需要几分钟。如果一切顺利,在 build 目录下你会看到生成的可执行文件,其中最重要的就是 bin/main 。我们可以测试一下它是否生成成功:

ls -lh bin/main

如果看到 bin/main 文件,并且有几十MB大小,说明编译基本成功了。但先别急,我们还需要一个关键的Python环境来运行模型转换脚本。

2.4 准备Python转换环境

模型转换脚本 convert.py 通常是用Python写的,并且依赖于 torch , transformers , sentencepiece , protobuf 等库。 autodl 的基础镜像可能没有安装Python,或者版本不对。我们使用 conda 来创建一个独立、干净的Python环境。

首先安装Miniconda(一个轻量级的conda发行版):

wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh -b -p $HOME/miniconda3

安装完成后,初始化conda并创建一个新的Python 3.10环境(3.10版本在兼容性上比较平衡):

source ~/miniconda3/etc/profile.d/conda.sh
conda create -n embed python=3.10 -y
conda activate embed

激活 embed 环境后,安装必要的Python包。这里需要特别注意版本兼容性:

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install transformers sentencepiece protobuf

第一行命令安装了纯CPU版本的PyTorch,因为我们的转换工作主要在CPU上完成,这样安装最轻量。第二行安装了转换模型必需的 transformers 库(用于加载Hugging Face模型)、 sentencepiece (某些Tokenizer需要)和 protobuf (协议缓冲区支持)。

3. 模型下载与GGML格式转换

3.1 下载BGE模型

模型转换的第一步是获取原始的PyTorch模型文件。 bge-base-zh-v1.5 模型托管在Hugging Face Hub上。我们可以使用 git 命令来克隆整个模型仓库,这是最稳妥的方式,能确保获取到所有必要的文件(包括模型权重 pytorch_model.bin 、配置文件 config.json 和词汇表 tokenizer.json 等)。

llama.cpp 目录外,找一个合适的位置存放模型:

cd ~
mkdir models
cd models
git lfs install
git clone https://huggingface.co/BAAI/bge-base-zh-v1.5

这里用到了 git lfs (大文件存储),因为模型文件通常很大。如果系统没有安装 git-lfs ,需要先安装: sudo apt install git-lfs -y 。克隆过程会下载大约400MB的数据,需要一些时间。

实操心得 :直接使用 git clone 比用 transformers 库的 from_pretrained 在线加载再保存更可靠,尤其是在网络不稳定的环境下。它能一次性获取所有相关文件,避免转换过程中因缺少配置文件而报错。

3.2 理解GGML转换的核心参数

在运行转换脚本之前,我们必须理解几个关键参数,它们决定了最终 ggml 模型的性能和精度。

  1. 量化类型(--outtype) :这是最重要的参数。量化是将模型权重从高精度(如FP32)转换为低精度(如FP16, INT8, INT4)的过程,能显著减少模型大小和内存占用,但可能会带来轻微的精度损失。 llama.cpp 支持多种量化类型,常见的有:

    • f32 :32位浮点数,无损,模型最大,速度最慢。
    • f16 :16位浮点数,几乎无损,模型大小减半,速度较快,推荐大多数情况使用。
    • q8_0 :8位整数量化,高精度量化,大小约为FP32的1/4,精度损失极小。
    • q4_0 , q4_1 :4位整数量化,模型非常小(约为FP32的1/8),速度很快,但精度损失相对明显。

    对于 bge-base-zh-v1.5 这种基础模型,为了在效果和效率间取得平衡,我推荐首次尝试使用 q8_0 f16 q8_0 在几乎不损失精度的情况下提供了4倍的压缩,性价比极高。

  2. 上下文长度(--ctx) :嵌入模型通常有固定的最大序列长度(比如512个token)。 bge-base-zh-v1.5 max_position_embeddings 是512,所以这里我们应该设置为 512 。设置得更大并不会提升模型处理长文本的能力,反而可能浪费资源。

  3. 模型架构(--model) :转换脚本需要知道原始模型的类型。BGE模型基于BERT架构,所以这里应该指定为 bert

3.3 执行模型转换

现在,我们回到 llama.cpp 目录,运行转换脚本。假设我们的模型下载在 ~/models/bge-base-zh-v1.5 ,我们想在 build 目录下生成转换后的模型。

首先,确保你在 llama.cpp 的根目录,并且Python环境已激活( conda activate embed ):

cd ~/llama.cpp
conda activate embed

然后运行转换命令:

python convert.py ~/models/bge-base-zh-v1.5 --outtype q8_0 --ctx 512 --model bert

让我们拆解这个命令:

  • python convert.py : 运行转换脚本。
  • ~/models/bge-base-zh-v1.5 : 原始PyTorch模型的本地路径。
  • --outtype q8_0 : 指定输出为8位量化格式。
  • --ctx 512 : 设置上下文长度为512。
  • --model bert : 指定模型架构为BERT。

执行这个命令后,脚本会开始工作。你会看到它依次加载模型、解析各层结构、进行量化计算的输出。整个过程可能需要1-2分钟。如果成功,你会在 llama.cpp 根目录(或脚本指定的输出目录,默认是当前目录)下看到一个或多个 .gguf 文件。 .gguf ggml 格式的新一代文件后缀。

通常,生成的文件名会类似于 ggml-model-q8_0.gguf 。我们可以检查一下文件大小:

ls -lh *.gguf

一个 q8_0 量化的 bge-base-zh-v1.5 模型,大小应该在130MB左右,相比原始的400MB,压缩效果非常明显。

常见问题与排查

  • 错误: ModuleNotFoundError: No module named 'torch' :这说明你的Python环境没有激活,或者没有安装PyTorch。请确认已执行 conda activate embed ,并重新安装PyTorch。
  • 错误: KeyError: 'llama' 或关于模型类型的错误 :这通常意味着 --model 参数指定错误,或者转换脚本无法自动识别模型类型。对于BGE模型,明确指定 --model bert 是关键。
  • 转换后文件特别小(如只有几十MB) :可能量化类型设置得过于激进(如 q4_0 ),或者转换过程出错。建议先用 f16 q8_0 这种高精度格式测试,确保流程正确,再尝试更低精度的量化。
  • 警告信息 :转换过程中可能会出现一些关于“无法识别的配置项”的警告,只要不是错误(Error),通常可以忽略。 ggml 转换脚本可能不支持原始模型配置文件里的所有参数,它会使用默认值或进行合理推断。

4. 本地运行与推理测试

4.1 编译嵌入推理示例程序

默认编译出的 main 程序是一个多功能工具,它需要通过参数来指定运行模式。为了更清晰地测试嵌入功能, llama.cpp 通常提供了一个专门的示例程序。我们需要确认并编译它:

llama.cpp/build 目录下,查看是否有 embedding 相关的目标:

cd ~/llama.cpp/build
make embedding

如果 Makefile 里定义了 embedding 这个目标,这条命令就会编译出 bin/embedding 可执行文件。如果没有,别担心, main 程序本身就支持嵌入模式。我们可以直接用 main ,但需要知道正确的参数。

4.2 准备测试文本与运行推理

首先,我们创建一个简单的文本文件 test.txt ,里面包含几行中文句子,用于测试嵌入向量的生成:

cd ~/llama.cpp
cat > test.txt << EOF
今天天气真好,阳光明媚。
人工智能是未来的发展方向。
如何学习编程?从基础语法开始。
EOF

接下来,我们使用编译好的 main 程序来为这些句子生成嵌入向量。关键参数如下:

  • -m : 指定我们刚刚转换好的GGML模型文件路径。
  • --embedding : 这个标志告诉程序运行嵌入模式,输出文本的向量表示,而不是进行文本生成。
  • -f : 指定输入文本文件。
  • -ngl : 将模型层转移到GPU的层数。如果我们的 autodl 实例有GPU并且想加速,可以设置为大于0的值(如 -ngl 20 )。对于纯CPU运行,则省略此参数或设为0。

运行命令(假设模型文件在 llama.cpp 根目录,名为 ggml-model-q8_0.gguf ):

./build/bin/main -m ./ggml-model-q8_0.gguf --embedding -f test.txt

如果一切正常,你会在终端看到大量的数字输出——这就是每一行文本对应的768维(对于 bge-base-zh-v1.5 )嵌入向量。输出格式通常是每行文本后跟着一行用空格分隔的浮点数。

4.3 解析输出与验证结果

直接看终端输出不直观,我们可以将输出重定向到文件,并编写一个简单的Python脚本来验证嵌入向量的基本性质。

首先,保存输出:

./build/bin/main -m ./ggml-model-q8_0.gguf --embedding -f test.txt > embeddings_output.txt

然后,创建一个Python脚本 check_embeddings.py 来加载和检查这些向量:

import numpy as np

# 读取输出文件
with open('embeddings_output.txt', 'r', encoding='utf-8') as f:
    lines = f.readlines()

vectors = []
current_vector = []
for line in lines:
    line = line.strip()
    if line and not line.startswith('今天') and not line.startswith('人工') and not line.startswith('如何'): # 过滤掉原始文本行
        # 假设向量数据是以空格分隔的
        try:
            numbers = list(map(float, line.split()))
            if len(numbers) == 768: # bge-base-zh-v1.5的维度是768
                vectors.append(numbers)
        except ValueError:
            continue # 跳过非数字行

vectors = np.array(vectors)
print(f"成功读取了 {vectors.shape[0]} 个向量,每个维度为 {vectors.shape[1]}")

# 检查1:向量是否归一化?BGE模型输出通常是归一化的。
norms = np.linalg.norm(vectors, axis=1)
print(f"向量范数(模长): {norms}")
print(f"范数接近1吗? (应接近1.0): {np.allclose(norms, 1.0, atol=1e-5)}")

# 检查2:计算句子之间的余弦相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity_matrix = cosine_similarity(vectors)
print("\n余弦相似度矩阵:")
print(similarity_matrix)

# 第一句和第二句(天气和AI)理论上语义不相关,相似度应较低。
# 我们可以直观判断一下。
print(f"\n'今天天气真好' 与 '人工智能是未来' 的相似度: {similarity_matrix[0, 1]:.4f}")
print(f"'如何学习编程' 与 '人工智能是未来' 的相似度: {similarity_matrix[2, 1]:.4f} (可能稍高,因为都涉及技术)")

运行这个脚本:

conda activate embed
pip install numpy scikit-learn # 如果尚未安装
python check_embeddings.py

如果输出显示向量范数接近1,并且相似度矩阵的值在合理的范围内(比如不相关的句子相似度在0.1-0.3左右,相关句子可能更高),那么就说明我们的模型转换和推理流程基本成功了!

实操心得 main 程序的嵌入模式输出可能包含一些日志信息。一个更干净的方法是使用 --no-display-prompt 参数来抑制不必要的提示输出,或者使用 --verbose-prompt 来更清晰地分离文本和向量。有时需要多尝试几个参数组合来获得最干净的数据。另外, llama.cpp embedding 示例程序(如果有的话)输出格式可能更规整。

5. 集成到LocalAI与高级配置

5.1 理解LocalAI的模型配置

成功运行 main 只是第一步,我们的最终目标是将这个模型集成到 LocalAI 中,作为一个嵌入服务来调用。 LocalAI 是一个本地化的AI API服务器,它兼容OpenAI的API格式,可以让你像调用OpenAI的 text-embedding-ada-002 一样调用本地模型。

要让 LocalAI 识别和使用我们的GGML模型,需要准备两个东西:

  1. 模型文件 :就是我们转换好的 ggml-model-q8_0.gguf
  2. 模型配置文件(YAML) :告诉 LocalAI 这是什么模型、如何加载、使用什么参数。

5.2 创建LocalAI模型配置文件

LocalAI 的模型目录(通常是 /models )下,为我们转换好的模型创建一个YAML配置文件。假设我们把模型文件放在了 LocalAI models 文件夹下,路径为 /models/bge-base-zh-v1.5/ggml-model-q8_0.gguf

那么,我们创建配置文件 /models/bge-base-zh-v1.5.yaml ,内容如下:

name: bge-base-zh-v1.5
backend: llama
parameters:
  model: ggml-model-q8_0.gguf
  # 对于嵌入模型,context_size需要与转换时指定的ctx一致
  context_size: 512
  # 嵌入模型需要指定f16为true,除非你用的是非量化的f32格式
  f16: true
  # 指定嵌入模式
  embedding: true
# 模型能力定义,告诉LocalAI这个模型可以用于嵌入任务
capabilities:
  embedding: true

关键配置解析

  • backend: llama LocalAI 使用 llama.cpp 作为后端来运行GGML模型,所以这里指定为 llama
  • parameters.model :相对于这个YAML文件所在目录的模型文件名。
  • parameters.context_size :必须与转换模型时使用的 --ctx 参数一致,这里是 512
  • parameters.f16: true :非常重要!即使我们用的是 q8_0 量化,在 llama.cpp 内部计算时,很多操作仍然是在 f16 精度下进行的。这个标志确保模型以正确的精度加载。如果设为 false ,可能会报错或得到错误结果。
  • parameters.embedding: true capabilities.embedding: true :这两个是必须的,明确告知 LocalAI 这是一个嵌入模型,并启用嵌入能力。

5.3 启动LocalAI并测试API

将模型文件( .gguf )和配置文件( .yaml )放到 LocalAI 的模型目录后,启动 LocalAI 服务。具体启动方式取决于你的安装方式(Docker或二进制)。

假设使用Docker,命令可能类似:

docker run -p 8080:8080 -v /path/to/your/models:/models localai/localai:latest

服务启动后,你就可以通过HTTP API来调用嵌入服务了。最直接的测试方法是使用 curl 命令:

curl http://localhost:8080/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bge-base-zh-v1.5",
    "input": "今天天气真好"
  }'

如果配置正确,你将收到一个JSON响应,其中包含一个 embedding 字段,里面就是“今天天气真好”这句话的768维向量。这个格式和OpenAI的嵌入API是完全兼容的。

5.4 性能调优与参数探索

成功运行后,你可能还想进一步优化性能或尝试不同配置:

  1. 线程数调优 :在 LocalAI 的配置文件中,可以添加 threads 参数来指定推理使用的CPU线程数。通常设置为物理核心数可以获得最佳性能。例如,在YAML文件的 parameters 部分添加: threads: 4

  2. 批处理 LocalAI 的API支持一次请求输入多个字符串(数组)。模型内部可能会进行批处理以提升效率。在客户端调用时,可以将多个句子放在一个请求中。

  3. 尝试不同量化等级 :你可以用同样的流程,转换出 f16 q4_0 等不同量化等级的模型,然后在配置文件中指向不同的模型文件。通过对比生成向量的质量(例如,在同一个下游任务上的表现)和推理速度,来选择最适合你场景的版本。对于生产环境, q8_0 通常是精度和速度的最佳平衡点。

  4. GPU加速 :如果你的 autodl 实例有GPU,可以在 LocalAI 配置中通过 f16: true (已设置)和确保CUDA库可用来自动启用GPU加速。 llama.cpp 后端会自动利用GPU。你可以在启动 LocalAI 时查看日志,确认是否检测到CUDA。

在整个过程中,最关键的还是第一步:确保 embeddings.cpp (或 llama.cpp )的 main 程序能正确加载你转换的模型并输出合理的向量。只要这一步通了,后续集成到 LocalAI 就是水到渠成的事情。这个流程不仅适用于 bge-base-zh-v1.5 ,也基本适用于其他任何支持转换为GGML格式的文本嵌入模型,为你构建本地化的语义搜索和RAG应用打下了坚实的基础。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐