做语义检索和 RAG 问答项目时,很多人第一步都是直接加载一个预训练嵌入模型,把 query 和文档分别编码成向量,再用余弦相似度做召回。这套流程在通用语料上效果还不错,但一遇到医疗问答、法律条款、电商客服这类垂直领域数据,排名效果就会明显下降。要解决这个问题,通常不是换一个更大的模型,而是用你自己的业务数据去训练或微调嵌入模型。本文围绕 Sentence Transformers 展开,先讲清楚单向量与多向量嵌入模型的区别,再分别给出数据准备、训练、评估的完整代码和参数说明,最后整理高频踩坑点与工程建议。适合正在做语义检索、RAG 问答、FAQ 匹配、向量召回的同学收藏。
1. 背景与核心概念
1.1 单向量嵌入模型的局限
所谓单向量嵌入模型,指的是输入一段文本后,模型把整句话“压缩”成一个固定维度的向量,比如 768 维或 1024 维。Sentence Transformers 里的SentenceTransformer类干的就是这件事:底层是一个 BERT 类的预训练语言模型,经过池化层后输出一个句子向量。检索阶段,我们把所有文档离线编码成向量,构造向量索引,线上把 query 编码成向量后,用 Faiss、Milvus 这类向量检索工具做 ANN 召回,最后计算余弦相似度排序。
单向量模型最大的优点是快,一个句子一次前向计算就得到结果,向量化之后还能利用成熟的 ANN 索引支撑百万千万级别的召回。但它的短板也很明显:句子被压成一个点,大量词级、短语级的细粒度信息都会丢失。比如“苹果公司发布了新款手机”和“苹果是一种很常见的水果”,这两句话主题完全不同,但在单向量空间里可能距离很近,因为“苹果”这个词在语义表征中占了很大权重。再比如“孩子咳嗽可以吃川贝吗”和“孩子咳嗽不能吃川贝”,只差一个字,单向量模型很容易把它们的向量拉得很近,导致召回结果出错。
这类问题在垂直领域尤其突出。通用预训练模型是在互联网语料上学习的,业务场景里大量专有名词、特定表达、文档格式它都没见过,直接拿来用,召回率和准确率都会打折扣。因此,掌握嵌入模型的训练与微调能力,是做好检索系统的一项基本功。
1.2 多向量嵌入模型是什么
多向量嵌入模型与单向量模型最大的区别在于:输入不再被压缩成一个向量,而是保留为一组向量。以最具代表性的 ColBERT 为例,query 里的每个 token 都会得到一个对应的 token 级向量,文档侧同样保留 token 级向量。计算相似度时,不使用余弦相似度,而是使用“晚期交互”(late interaction)机制,也就是 MaxSim 算法:
- 对 query 中的每一个 token 向量,分别与文档中所有 token 向量计算相似度;
- 取最大值,作为这个 query token 对该文档的匹配得分;
- 把所有 query token 的最大得分累加,得到最终相关度分数。
这种设计的优势是保留了词级匹配能力。query 中的“苹果”会重点与文档中的“苹果”进行精确匹配,而不是在全局向量中平均掉。“手机屏幕碎了怎么修复”和“屏幕碎了自己换还是找维修店”这种表达层面不完全一致、但关键词高度相关的句子,在多向量模型下往往能获得比单向量模型更高的匹配分数。
当然,多向量模型的代价也很直观:文档需要存储的不再是一个向量,而是一串 token 向量,存储成本显著上升;线上打分时也不能直接走 cosine 索引,需要专门的检索策略和打分逻辑,计算量更大。可以把它理解为:用存储和计算换精度。
1.3 为什么要自己训练或微调
很多同学以为嵌入模型是“开箱即用”的,实际上通用模型在垂直领域的效果往往只能算“能用”,离“好用”还有距离。最典型的改进方式就是微调:用自己的业务数据,对预训练模型做少量迭代训练,让模型记住领域内的专有词汇和语义模式。
多向量模型也是一样。早期很多团队直接拿现成的 ColBERT checkpoint 用,但遇到中文领域数据时,效果不一定理想。一方面,ColBERT 原始模型主要以英文为主;另一方面,领域数据里的 query 习惯、文档结构、口语化表达都不同,必须经过微调才能贴合实际业务。
换句话说,训练和微调是让嵌入模型从“通用”走向“专用”的关键一环。即使你不想从零训练一个大模型,掌握基于 Sentence Transformers 的微调流程,也能用少量标注数据快速提升检索质量。这就是本文想帮你解决的问题。
2. 环境准备与版本说明
2.1 安装依赖
建议使用独立的 Python 虚拟环境,避免与其他项目依赖冲突。Python 版本推荐 3.9 到 3.11,PyTorch 使用 2.x 版本。下面是完整的安装命令:
conda create -n st python=3.10 -y conda activate st # 按你的 CUDA 版本选择 pytorch 安装方式,这里以 cu121 为例 pip install torch --index-url https://download.pytorch.org/whl/cu121 pip install -U sentence-transformers pip install datasets安装完成后,可以运行下面的命令确认版本:
python -c "import sentence_transformers, torch; print('ST:', sentence_transformers.__version__); print('PyTorch:', torch.__version__)"2.2 版本与兼容性说明
Sentence Transformers 目前有两个常用的 API 版本。2.x 时代主要使用model.fit()方法,代码示例在网上非常常见;3.x 版本开始引入了SentenceTransformerTrainer,更接近 Hugging Face Transformers 的 Trainer 风格,训练流程更规范,支持日志、checkpoint、恢复训练等能力。
本文示例以 3.x 版本为主,同时保留一段 2.x 的fit()写法。如果你的环境是 2.x,建议直接使用旧版 API;如果你刚接触这个库,建议直接安装 3.x 并使用新版 Trainer。不同版本的参数名有差异,跑代码前先确认自己的版本,避免照抄报错。
另外,模型默认从 Hugging Face Hub 下载,国内网络不稳定时可能会失败,可以提前配置镜像源,例如设置HF_ENDPOINT环境变量为可用的镜像地址,或者从 ModelScope 下载模型后切换到本地路径加载。
2.3 硬件要求与显存预估
微调一个 BERT 规模的嵌入模型,显存需求并不算特别夸张。以BAAI/bge-small-zh-v1.5(约 118M 参数)为例,在 batch size 为 16、最大序列长度 128 的情况下,8G 显存的 GPU 基本可以跑起来;如果使用更大的模型,比如BAAI/bge-large-zh-v1.5,建议至少 16G 显存。
如果你的机器没有 GPU,先用很小的数据集体验流程也是可以的,但训练时间会非常慢。显存不够时,优先考虑四件事:调小 batch size、开启梯度累积、开启 fp16 混合精度、使用 LoRA 这类参数高效微调方法。这些内容会在后文展开。
3. 核心原理:单向量与多向量的区别
3.1 从 token 到句子向量:池化
BERT 类预训练模型接收一段文本后,会为每个 token 输出一个高维向量。这些向量包含上下文语义信息,但直接拿来当句子表示并不合适,因为不同句子的 token 数量不同,无法统一比较。因此,单向量嵌入模型会再接一个池化层,把 token 向量聚合成一个固定长度的句子向量。常用的池化方式有两种:
mean_pooling:对 token 向量求平均,过滤掉 padding 位置;cls_pooling:直接取[CLS]位置的向量作为句子表示。
Sentence Transformers 里的池化逻辑封装在Pooling模块中,正常情况下你不需要自己实现。理解这段原理是有意义的,因为多向量模型的核心,恰恰就是“跳过池化、保留 token 向量”。
3.2 late interaction 与 MaxSim 打分
多向量模型的代表性打分方式是 MaxSim。下面用一个简化例子来说明:
query: 怎么 修复 手机 屏幕 doc: 屏幕 碎了 怎么 办 query[0]="怎么" → 与 doc 中所有 token 算相似度,取最大值 query[1]="修复" → 与 doc 中所有 token 算相似度,取最大值 query[2]="手机" → 与 doc 中所有 token 算相似度,取最大值 query[3]="屏幕" → 与 doc 中所有 token 算相似度,取最大值 最终分数 = 4 个最大值的总和可以看到,late interaction 没有把整句话压成一个向量,而是让每个 query token 单独去文档里找最相关的 token。这种机制对短语匹配、同义词替换、词