做检索系统这些年我越来越觉得嵌入模型才是决定整体效果上限的那一环。模型结构再花哨、向量库再快如果 embedding 给不出语义准确的向量后面召回、排序、重排全都白搭。这章正好讲数据工程与定制化我就把“怎么创建更优的嵌入模型”这件事完整拆开聊一遍从判断要不要定制、训练数据怎么构造到对比学习怎么调参、评估指标怎么选全部基于实际跑过的项目经验来写。适合正在做 RAG、语义检索、向量召回、商品匹配这类场景的开发者也适合刚入手嵌入模型但总感觉通用模型效果“差点意思”的同学。如果你只是接一个 API 或者直接 pip 拉个现成 embedding 模型就上线大概率会遇到这类问题搜索词和文档里明明说的是同一件事但向量偏了专业术语被拆得支离破碎或者同义词根本对齐不了。这些不是模型下载错了而是通用嵌入模型压根没见过你的业务数据。所以“创建更优的嵌入模型”不是炫技是检索系统做到一定阶段绕不开的必经之路。1. 嵌入模型为什么需要“更优”先看清它在检索链路里的位置1.1 从语义检索说起嵌入模型干的事本质上就是把一段文本变成一个稠密向量。这个向量不是随便压缩出来的而是要尽可能把“语义相似”的内容映射到相近的位置上。传统关键词检索靠字面匹配用户搜“怎么退货”你文档里写的是“七天无理由退换货政策”字面重合度很低召回不到。有了 embedding 之后两句话虽然用词完全不一样只要它们在语义空间里离得近就能被召回。这玩意儿在 RAG 链路里是召回环节的核心组件。用户 query 先被编码成一个向量再拿这个向量去向量数据库里“摸最近的邻居”把 topK 个最相关的文档片段捞出来喂给大模型做生成。如果向量本身分得不准后面大模型再聪明也看不到该看的内容生成的回答就会一本正经地胡说八道。那为什么通用的嵌入模型在某些场景下会失灵因为大多数开源嵌入模型是在网页文本、百科语料、通用问答对这类数据上训练的。它们学的语义关系是“宽而广”的比如知道“电脑”和“笔记本”在大多数语境下意思相近但它不一定知道在你们某个垂直系统里“电脑”指的是设备、而“电脑端”是另一个业务入口。领域里常见的黑话、缩写、内部叫法通用模型基本是无感知的。我自己踩过一个很典型的坑。某团队做一个医疗器械问答系统里面大量出现“AMI”这个缩写。通用嵌入模型把它理解成“人名”的概率很大因为它的训练语料里 AMI 最常见的语境是 Ami 这个人名。但实际上在这个系统里 AMI 指的是急性心肌梗死。检索时用户搜“心梗的早期症状”候选文档里写“AMI 患者需要注意……”但两个向量距离很远根本召不回来。这种领域术语错位就是通用模型的死角。1.2 通用嵌入模型什么时候该被替换不是所有场景都需要定制嵌入模型。我一般先做一个诊断拿你业务里真实 query 到真实文档库里去测试用现成的通用模型跑一遍召回看 top10 里到底有没有正确结果。如果准确率达 80% 以上说明业务文本风格和通用语料差异不大直接省事用通用模型就行。但如果出现下面几类特征就得认真考虑定制第一领域术语占比高。医疗、法律、金融、工业制造、半导体这种专业领域术语密度极高而且很多术语在通用语料里低频。第二文本风格和日常语言差异大。比如代码检索、工单记录、客服对话、审批意见这种文本充满简写、口语、错别字、甚至只有公司内部才懂的缩略说法。第三相似性定义不是单纯语义相似。你需要的是“用户问法和标准问题匹配”“需求描述和功能模块匹配”“缺陷描述和修复方案匹配”本质上是一种业务映射关系。第四跨语言或中英混写场景。大量技术文档是中文夹杂英文术语比如“通过 REST API 调用数据接口进行数据增强”通用模型对混写文本的编码能力经常不够稳。我见过很多团队一上来就埋头训练自己的嵌入模型但完全没说清楚新模型到底要比旧模型好在哪。训练之前先把问题定义清楚你是要解决术语召回问题还是要解决同义词对齐还是要解决长尾 query 的相关性排序目标不同训练数据构造方式完全不同。这个思考过程不应该被跳过否则你很可能花了大量成本训出一个“指标涨了但业务没感知”的模型。2. 数据工程比模型本身更重要的一步2.1 训练数据长什么样正面样本对与负面样本对嵌入模型的训练本质上是学习“什么东西该靠近什么东西该远离”。所以训练数据的核心就是构造正样本对和负样本对。正样本对指的是语义上一回事的两段文本比如用户搜索词“手机进水屏幕不亮”和文档标题“手机进水紧急处理指南”这就是一个正样本对。负样本对则是语义不相关或者表面上很像但实际不是同一回事的文本。构造正样本对有几个可靠来源。第一是用户行为日志搜索词和用户最终点击查看的文档天然就是一个隐式正样本对。第二是 FAQ 数据问题本身和对应答案的第一句话或者把问题拆成 query、把答案标题当 doc。第三是表格与文本混合数据比如把每个商品详情页的标题和说明当成正样本。第四是已有系统里人工整理的映射关系表。我特别建议优先利用点击日志。某做电商场景的模拟项目里我们收集了数月搜索日志把搜索词和被点击的商品详情页文本组成正样本对。注意不是只看点了什么还要看停留时长点进去两秒就关掉的要去掉。这样筛出来的正样本对质量明显更高因为用户停留时间长说明这个结果大概率是他想要的内容。关键点在于正样本对数量不需要多到夸张。一个垂直场景里一两万条高质量 query-doc 对已经能做出非常明显的效果提升。真正决定上限的不是正样本而是负样本的质量。很多人训练嵌入模型效果不佳问题基本都出在负样本上。2.2 难负样本挖掘数据质量的分水岭训练嵌入模型时最常见的操作是“batch 内随机负样本”——也就是一个 batch 里其他样本的文档部分都当成当前 query 的负样本。这种方式实现简单跑起来效果也不差但它有一个明显的盲区随机负样本太“容易”了模型很容易就学会区分完全不相关的内容但对那些表面相似、实际语义不同的样本仍然毫无分辨能力。这就是难负样本hard negative存在的意义。所谓难负样本是指和 query 有很强的表面相关性、但不是正确答案的文档。比如用户搜“手机进水怎么办”一个候选文档叫“手机进水是不是还在保修范围内”这个文档里也有“手机进水”这个词但它讲的不是处理指南而是保修政策。如果模型只靠词面重合去匹配就会把它误判为相关。难负样本就是专门用来逼模型学会理解语义差异的。挖掘难负样本的通用流程是先把所有候选文档库用基础嵌入模型比如当前的通用模型编码成向量拿训练集里每个 query 去检索 top50检索出来的结果里去掉已经被标注为正样本的文档剩下的那些就是候选难负样本。然后人工抽样检查把那些看起来很像但语义不匹配的样本标注出来加入训练集。我习惯的做法是用规则再粗筛一轮字符级重合度高、但语义类别标签不同的自动进入难负样本池。难负样本的比例我建议控制在整个负样本的 20%~40%。比例太低模型学不会区分细粒度差异比例太高模型会变得过度保守连合理的相似都被推开。有个团队做过一组对比实验随机负样本占满全部负样本时评测集 Recall10 只能做到 68%加入约 30% 难负样本后直接跳到 83%。代价是数据准备阶段的哪些额外功夫但投资回报极高。2.3 数据清洗与数量门槛训练嵌入模型前一定要做数据清洗最容易被忽略的是“伪相关”问题。用户行为的隐式正样本并不都是靠谱的用户点了这个商品可能是因为它排在第一位也可能是因为主图吸引人并不代表文本描述真的对齐了用户需求。我通常会把点击率很低的热门 query、停留时间过短的点击全部过滤掉。此外同一个 query 对应多个互斥文档的情况也要检查如果两个文档讲的内容冲突而你同时把它们当正样本模型会非常迷惑。另一个需要处理的是文本长度。训练嵌入模型时输入长度和线上推理时的长度最好保持一致。BERT 类模型最长输入普遍是 512 token如果你的线上文档被切成 256 token 的段落训练时却喂整篇 500 token 的文档推理时效果会大打折扣。我的经验是训练和推理保持一样的切分策略否则离线指标会虚高上线就拉胯。还有大量重复或变体重复的文本要去掉。同一个商品来自不同供应商、同一个政策在不同页面复制了三遍这类重复数据会严重扭曲模型学到的语义分布。用 MinHash 或者简单的归一化后进行相似度去重都可以这一步能省下很多无效计算量。那么训练数据量到底需要多少这是新人问得最多的问题。我的答案是没有绝对门槛但低于 5000 个正样本对时效果提升非常有限1 万到 5 万之间是性价比最高的区间超过 10 万后边际收益递减这时更值得做的是提升难负样本质量而不是堆数量。如果只有两三千条手工标注数据也不用慌可以先跑一版再拿这版模型去做难负样本挖掘数据会在迭代过程中持续增长。3. 微调与训练从对比学习到参数细节3.1 选底座模型和训练方式训练嵌入模型没必要从零预训练继承一个通用底座再做对比学习微调是目前最稳妥的方案。选择底座时主要看三点支持的最大长度够不够、语言覆盖是否匹配、底座本身在中英文语义任务上的表现是否合格。中文场景我会优先选在中文语料上做过预训练的底座不是说你不能选英文底座而是中文原生的底座对中文词汇边界的理解通常更好。微调方式上我建议视数据量而定。如果你的训练数据只有几千到两三万条优先尝试只微调顶层的那几个 Transformer 层或者干脆只训练一个额外的池化层和映射层这样能显著降低过拟合风险。数据量超过五万条之后再做全量微调或者 LoRA 微调。LoRA 是现在非常主流的做法。它冻结原始权重只训练低秩矩阵参数量只有全量微调的很小一部分在单块消费级显卡上就能跑起来。嵌入模型微调用 LoRA 有个额外好处因为底座权重没变不同任务训练出来的 adapter 可以按需切换不需要为每个业务场景部署单独模型。当然代价是 LoRA 的容量有限如果领域差异太大效果天花板可能不如全量微调。3.2 损失函数、温度系数与 batch size嵌入模型训练最常用的损失是对比学习损失核心思想非常直白给定一个 query要让它的正样本文档在向量空间中尽量靠近同时让所有负样本尽量远离。公式里最关键的符号是一个温度系数 τ它控制着相似度分布被“拉伸”或“压缩”的程度。温度系数这个参数非常值得花时间调。τ 太大时所有样本的相似度会被压向一个均匀区间模型区分正负样本的压力变小收敛会很慢学出来的向量往往区分度不足。τ 太小时模型会把注意力过度集中在那些最难区分的负样本上训练过程容易震荡甚至把一些合理的相似语义也推开。我常用的取值范围是 0.03 到 0.1在大部分场景下0.05 是一个好用的起点。如果训练过程中 loss 不稳定先检查是不是 τ 设得太低了。batch size 也是一个关键参数。对比学习里一个 batch 内除了当前样本自己的正样本外其余全部算作负样本。所以 batch 越大每个 query 拥有的负样本就越多训练效果通常越好。但显存是有限的我常用的做法是先用 batch size 256 在当前显存上限内跑一版如果跑不动再配合梯度累积把有效 batch size 撑到 256 以上。至少在四百到一千这个范围内的有效负样本数量是比较稳的低于 64 就需要非常小心你可能根本学不出东西。3.3 一个可以直接参考的训练配置我用 sentence-transformers 做嵌入模型微调已经很久了这里给一个通用的最小可跑配置你可以替换成自己的数据格式和底座路径。from sentence_transformers import SentenceTransformer, InputExample from sentence_transformers.losses import MultipleNegativesRankingLoss from torch.utils.data import DataLoader model SentenceTransformer(your_base_model_path) train_examples [ InputExample(texts[手机进水屏幕不亮怎么处理, 手机进水紧急处理指南]), InputExample(texts[订单超过七天还能退吗, 七天无理由退换货政策适用条件]), # 加入更多正样本对每对是一组(query, doc) ] train_dataloader DataLoader(train_examples, batch_size128, shuffleTrue) train_loss MultipleNegativesRankingLoss(modelmodel) model.fit( train_objectives[(train_dataloader, train_loss)], epochs2, warmup_steps200, use_ampTrue, show_progress_barTrue, output_path./finetuned_embedding )这里用的 MultipleNegativesRankingLoss 在内部实现上就是 batch 内负样本的对比学习负样本数量等于 batch size 减一。所以 batch size 设为 128每个 query 就对应 127 个负样本数量上已经处于合理区间。如果你想要加入显式标注的难负样本可以换成 ContrastiveLoss 或者 CombinedDataset把 (query, positive, negative) 三元组喂进去效果会更可控。关于学习率对比学习微调一般比普通分类任务温和一点。我用过最稳的区间是 2e-5 到 5e-5。如果你用的是 LoRA学习率可以适当放到 1e-4 级别。训练轮次则控制在 2 到 3 轮多了几乎必然过拟合。每一轮结束都跑一次评测集看着指标趋势做判断并不是轮次越多越好。3.4 池化策略与文本截断细节训练嵌入模型时常常会忽略池化方式实际上它对向量质量影响也很大。大多数句子模型默认会用 CLS 向量或者均值池化但如果你的场景是短文本匹配 query 与段落均值池化通常更稳定如果分类词的语义信息集中在特定位置CLS 可能更好。这个不能拍脑袋最好在评测集上各跑一遍对比。另一个细节是文本截断。训练时如果直接把超长文本截断到固定长度会丢失文档后半部分的关键信息。我现在的做法是给文档做分段处理每段控制在 200 到 400 token单独过嵌入模型。检索时先召回段落再做段落级别的重排再拼上下文给大模型。这样既绕开了嵌入模型长度限制又保留完整语义是 RAG 场景下的标准做法。4. 评估怎么证明新嵌入真的更优4.1 离线和在线指标怎么选评估嵌入模型的指标最常用的是 RecallK、MRR、NDCG。RecallK 最容易理解前 K 个结果里有没有包含正确答案。MRR 更关心正确答案排得多靠前如果正确答案排在第一位得分就是 1排在第二位得 0.5。NDCG 则考虑了多级相关性的排序质量适合评测集里有“强相关、弱相关、不相关”多档标记的场景。我个人的经验是早期先用 Recall10 做粗筛模型之间差别大不大一眼就能看出来。等候选模型少到两三个的时候再看 MRR 和 NDCG 做细选因为线上用户体验非常在意正确答案排在第几位而不是只管有没有在 Top10 里。如果训练只是为了让“答案在 top10 里出现”用户要翻很久才能找到还是会觉得系统蠢。注意离线指标涨了不等于线上效果一定好原因通常是离线评测集和线上数据分布不一致。比如评测集里的 query 大多是标准化的而线上真实用户 query 往往带口语、错别字、长尾表达。所以我会刻意在评测集里加入 30% 的“脏 query”比如“手机进睡怎么办”这种带错别字和口语化的表达用于模拟真实输入。能扛住脏输入的模型上线才稳。4.2 评测集构建与对比基线评测集至少要准备一百条标注数据太少的话指标方差过大两三个点的提升根本看不出是真实效果还是噪音。标注时每一条 query 都要标记出真正相关的文档 ID、可能相关的文档 ID、完全不相关的文档 ID。相关程度能分档就分档至少这样能支持 NDCG 指标。我建议构建评测集时让业务方深度参与至少 30% 的样例来自他们的真实长尾 query。技术团队很容易把评测集构造成“模型擅长回答的问题”最终自欺欺人。某跨平台系统项目里我们把评测集分成标准 query 和长尾 query 两批标准 query 上模型提高了两个点但长尾 query 上提升了接近十个点。如果不拆开看根本发现不了这个差距也就不会意识到定制嵌入模型真正的价值在长尾。对比基线方面不要只对着通用嵌入模型比。如果你之前线上用的是 BM25 关键词召回、或者某开箱即用的 embedding API都要放进对比列表。至少同时跑三个版本旧方案、通用嵌入模型、微调后模型这样你才能说清楚提升到底从哪里来。实际对比时我还会统计每个 query 在微调前后排名变化挑出“排名倒退”的样本去看原因。如果某个类别大量倒退说明训练数据里缺那个类别的样本补数据比瞎调参高效得多。5. 常见问题与排查技巧实录5.1 数据量不足怎么办很多人卡在“我没那么多标注数据”这一步。我的第一个建议是别急着人工标注先把你现有的用户行为日志里能挖的隐式正样本全挖出来。用户搜索日志、点击日志、收藏记录、工单流转记录这些都是现成的训练信号。再不行可以用大模型生成 query 变体比如把标准问题改写成口语化问法、换个说法、加语气词和错别字模拟然后用这些变体作为正样本。这种合成数据质量虽然不如人工标注但用来做预训练、或者作为难负样本挖掘的起点完全够用。另外一种很有效的路径是小样本训练 强难负样本。即使只有三五千条正样本只要构造出高质量的难负样本模型也能学到很强的区分能力。因为对比学习本质上是在学习一种“排序偏好”并不需要见过所有文本形式它只需要知道哪些东西应该排到前面。5.2 训练不收敛、效果不升反降训练嵌入模型跑着跑着 loss 不降或者震荡严重最常见的原因是学习率太高或者温度系数太小。先按我前面说的区间对应检查学习率降到 3e-5 以下温度调到 0.05 以上大多数不收敛问题都能缓解。如果还是震荡看看是不是数据里有标签冲突——同一个 query 对应两个互相矛盾的文档。这种冲突会让模型无所适从最简单的处理方式是直接把这类样本清掉。效果不升反降的另一个常见原因是评测集和训练集的分布不一致。比如训练数据里大量是“用户问题”到“标准答案文档”的映射但评测集里却混着“用户问题”到“商品描述”的映射模型能力根本没有在评测方向上被训练过分数自然上不去。这种情况得回头检查数据构造逻辑不是调参能解决的。过拟合也值得警惕。训练轮次太多、或正样本太少的时候模型会把训练集里的表面特征背下来比如记住某个商品标题里包含特定型号数字而不是学会语义匹配。表现为训练集指标很高评测集指标纹丝不动甚至下降。遇到这种情况减少轮次、增加难负样本、引入 dropout 都是有效的方法。5.3 推理性能与工程落地定制嵌入模型上线前需要考虑推理性能。嵌入模型和生成模型不一样它的计算量远小于大模型但仍然要考虑 QPS 和显存成本。如果线上流量高可以把模型量化到 FP16 或者 INT8召回效果通常只有很小的损失。另一个常用方案是知识蒸馏用大模型蒸馏一个小模型比如把 350M 的模型蒸馏到 30M 级别推理速度翻很多倍但会牺牲少量召回精度。实际部署时我还会做一个缓存层。同一个 query 在短时间内的向量是高度重复的特别是搜索场景里热词命中率很高直接在缓存里查向量可以减少大量的重复计算。向量检索库方面如果数据量在百万级以内用普通支持余弦距离的向量索引就行到了千万级再考虑分区索引、PQ 压缩这些更重的方案。另外别忘了给嵌入模型单独做版本管理。向量空间是跟模型强绑定的换了模型参数之后旧的向量库里所有向量都需要做一次全量刷新否则新旧向量不处于同一语义空间检索结果会完全错乱。我在多个项目里都吃过这个亏上午换了新模型下午发现召回结果变差排查了半天才发现是旧索引没重建。5.4 一个值得尝试的迭代循环最后分享一个在实践中很有效的迭代循环先用通用模型挖出难负样本训练一个 v1 模型再用 v1 模型去重新挖掘难负样本、重新扩展训练集然后训练 v2。每一轮迭代数据质量和模型能力都会同步提升效果通常在前两轮迭代中增长最明显。这个循环听起来简单但我见过很多团队没有坚持跑下去因为每一轮都要人工审核一部分难负样本。这一步如果省掉模型很容易被噪音带偏。嵌入模型优化的核心从来不只是“跑一个训练脚本”而是数据、模型、评估三者之间的闭环。你只要能把这个闭环跑顺从通用模型到一个领域适配的更优嵌入模型只是时间问题。
阅读完成 · 觉得有帮助?