
我们有一个基于 RAG检索增强生成的 AI 客服系统核心逻辑是用户提交新工单时先把它向量化然后去历史工单库里找相似的再把检索结果喂给 LLM 生成回复。向量化用的是 Embedding 模型部署在本地 Ollama 上。系统跑着还行但总感觉检索质量一般于是决定用自己的工单数据微调一下。测试的三个模型模型ChromaDB 集合类型mxbai-embed-largedocs基线mxbai-mnrl-finetuneddocs_mxbai_finetuned微调版bge-m3docs_bge基线数据准备构建 Triplet 训练集微调 Embedding 模型用的是 Triplet 结构每条训练样本由三部分组成 —Anchor锚点、Positive正例、Negative负例。Anchor一个工单Positive与 Anchor 存在关联关系的工单重复工单、相关工单等Negative与 Anchor 无关的工单关系数据来自 Mantis 的mantis_bug_relationship_table总共有 5715 个有关系的工单对。# 只用重复工单关系rel-types 0related_to, 1duplicate_of python finetune/train_mxbai.py --generate-data --rel-types 0 1 --negatives-per-positive 1 # 全部关系类型5715 个工单对 python finetune/train_mxbai.py --generate-data --rel-types 0 1 2 # GPU 显存不够时 python finetune/train_mxbai.py \ --generate-data \ --rel-types 0 1 2 \ --batch-size 8 \ --grad-accum 4 # 查看数据量 wc -l data/finetune/triplets_train.jsonl data/finetune/triplets_eval.jsonl注意评估集data/finetune/triplets_eval.jsonl是固定的不能被覆盖。跑不同实验时要用--data-base data/finetune/triplets_dup指定不同路径。模型结构与微调策略为什么冻结前 22 层mxbai-embed-large 基于 BERT-large共 24 层 Transformer。越底层学到的知识越通用层数学到的内容1–6基础语法、词形、位置信息跨语言通用7–16语义组合、句子结构17–23任务相关的高层语义 ←微调的目标24最终 Embedding 表示Pooling聚合层用--freeze-layers 22只训练最顶部的 2 层 Pooling大约只更新 3.5% 的参数~12M / 335M。这样既避免了灾难性遗忘Catastrophic Forgetting又节省了 GPU 显存。损失函数余弦相似度sim(a,b)a⋅b∥a∥∥b∥符号含义a, b两个文本向量模型的 Embedding 输出a · b点积各分量乘积之和‖a‖向量 a 的长度范数sim(a, b)取值范围 −1 到 11 完全相同0 完全无关TripletLoss有显式负例时使用--train-jsonl数据带negative列Lmax(0, d(a,p)−d(a,n)m)符号含义L损失值训练过程中模型会最小化它aAnchor — 查询工单pPositive — 语义相似的工单nNegative — 语义无关的工单d(a, p)Anchor 与 Positive 的距离欧氏距离— 越小越好d(a, n)Anchor 与 Negative 的距离欧氏距离— 越大越好m最小间距 0.5d(a, n) 必须比 d(a, p) 大出这个值否则产生损失MultipleNegativesRankingLossMNRL只需 Anchor Positive用--use-mnrl开启L−1NN∑i1logesim(ai,pi)/τN∑j1esim(ai,pj)/τ符号含义NBatch 大小每步的训练样本数i当前 Anchor 的序号jBatch 中其他所有样本的序号 — 它们自动充当负例aᵢ, pᵢ第 i 个 Anchor 和对应的 Positivee^(·)指数函数让所有值为正τTau温度系数越小区分越清晰log自然对数损失最小化意味着 sim(aᵢ, pᵢ) 在 Batch 中最大关键Batch 中其他所有 Positive 都作为负例In-Batch Negatives。N 越大负例越多训练信号越强。gradient_accumulation_steps不会增大 N只有batch_size才算。训练命令mxbai — 最佳方案MNRL 大 Batchsource ~/venv/bin/activate PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True \ python finetune/run_mxbai_experiments.py --run --preset mnrl_large_batch等价于python finetune/train_mxbai.py \ --output-dir finetune/models/mxbai-mnrl-large-batch \ --use-mnrl \ --freeze-layers 22 \ --batch-size 32 \ --grad-accum 1 \ --lr 2e-6 \ --epochs 1结果TripletEvaluator cosine_accuracy 97.9%基线88.9%mxbai — 备选方案TripletLoss聚焦重复工单python finetune/train_mxbai.py \ --output-dir finetune/models/mxbai-duplicate-triplet \ --generate-data \ --rel-types 1 4 \ --neg-strategy random \ --negatives-per-positive 1 \ --freeze-layers 22 \ --batch-size 8 \ --grad-accum 4 \ --lr 5e-6 \ --epochs 1结果96.0%bge-m3 — 最佳方案TripletLoss聚焦重复工单source ~/venv/bin/activate PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True \ python finetune/run_bge_m3_experiments.py --run --preset duplicate_triplet等价于python finetune/train_bge_m3.py \ --output-dir finetune/models/bge-m3-duplicate-triplet \ --generate-data \ --rel-types 1 4 \ --neg-strategy random \ --negatives-per-positive 1 \ --freeze-layers 22 \ --batch-size 4 \ --grad-accum 8 \ --lr 3e-6 \ --epochs 1结果96.4%基线94.5%训练监控TensorBoardtensorboard --logdirfinetune/models/ --host 0.0.0.0 --port 6006 # 浏览器访问http://gpu-server-ip:6006模型调试与对比# 检查架构、NaN、Embedding 坍缩 python finetune/debug_model.py --model-a finetune/models/mxbai-mnrl-large-batch # 基线 vs 微调版对比 运行 TripletEvaluator python finetune/debug_model.py \ --compare --eval \ --model-a mixedbread-ai/mxbai-embed-large-v1 \ --model-b finetune/models/mxbai-mnrl-large-batch \ --eval-jsonl data/finetune/triplets_eval.jsonl # 清理旧 checkpoint rm -rf finetune/models/mxbai-finetuned/checkpoint-*HuggingFace → OllamaGGUF 导出Ollama 只能运行模型不能训练。HuggingFace 的 PyTorch 格式需要先转成 GGUF 才能用 Ollama 部署。Step 1转换为 GGUFfp32 → f16# 需要安装 llama.cpp python llama.cpp/convert_hf_to_gguf.py finetune/models/mxbai-mnrl-large-batch \ --outfile finetune/models/mxbai-mnrl-finetuned.gguf \ --outtype f16Step 2注册到 Ollamaecho FROM finetune/models/mxbai-mnrl-finetuned.gguf Modelfile ollama create mxbai-mnrl-finetuned -f ModelfileStep 3对比 HuggingFace 与 Ollama 的输出精度验证python finetune/compare_hf_vs_ollama.py \ --hf-model finetune/models/mxbai-mnrl-large-batch \ --ollama-model mxbai-mnrl-finetuned:latest \ --ollama-host http://localhost:11434预期结果平均向量对齐度 0.990GGUF f16 对方向的保真度很好。Step 4写入 ChromaDBpython -m src.createemb \ --embedding-model mxbai-mnrl-finetuned:latest \ --collection docs_mxbai_finetuned端到端评估RecallK / MRRRecallK— 查询中正确工单出现在前 K 个结果里的比例RecallK1|Q|∑q∈Q1[相关工单在 Top-K 中]符号含义Q全部测试查询集合|Q|测试查询的数量K看前 K 个结果例如 K5看前 5 名1[...]正确工单在前 K 名则为 1否则为 0结果解读Recall5 0.5 表示50% 的查询里正确工单在前 5 名内MRRMean Reciprocal Rank— 第一个正确结果排名的倒数均值MRR1|Q|∑q∈Q1rankq符号含义rank_q查询 q 的第一个正确结果排名第 1 名最好1/rank_q倒数第 1 名 → 1.0第 2 名 → 0.5第 5 名 → 0.2未找到 → 0结果解读MRR 1.0每次都排第 1MRR 0.5平均排第 2两个指标均基于mantis_bug_relationship_table中的真实关系对。# 训练前跑基线 python tools/eval_recall.py --issues 200 --output-json results/recall_baseline.json # 三个模型对比相同 seed 相同查询工单 python tools/eval_recall.py --model bge-m3 --collection docs_bge --issues 200 --seed 42 --output-json results/eval_bge.json python tools/eval_recall.py --model mxbai-embed-large --collection docs --issues 200 --seed 42 --output-json results/eval_mxbai.json python tools/eval_recall.py --model mxbai-mnrl-finetuned --collection docs_mxbai_finetuned --issues 200 --seed 42 --output-json results/eval_mxbai_ft.json手动搜索测试结果查询 1Bestellung ist ohne Wareneingang abgeschlossen排名mxbai-baseline (docs)mxbai-finetuned (docs_mxbai_finetuned)bge-m3 (docs_bge)#118534 GOODS_RECEIPT-Telegramme 0.985 ❌43165 Bestellung ist ohne Wareneingang abgeschlossen0.980 ✓431651.000 ✓#231337 0.98318534 0.95135104 0.931微调版 mxbai 和 bge-m3 都将 Issue 43165与查询完全一致排在第 1 位mxbai 基线漏掉了它。查询 2Login Fehler排名mxbai-baseline (docs)mxbai-finetuned (docs_mxbai_finetuned)bge-m3 (docs_bge)#141258 Login fehlgeschlagen0.948 ✓33252 Fehlermeldung beim Kommissionieren 0.957 ❌41258 Login fehlgeschlagen0.977 ✓#233252 Kommissionieren 0.946 ❌19454 Fehler 0.952 ❌30743 Fehler bei der Anmeldung 0.947 ✓#3–5Fehler 0.933 ❌Fehler 0.952×4❌456 Fehlermeldung beim Anmelden 0.944 ✓mxbai-finetuned 的前 5 条里完全没有 Issue 41258。4 条结果相似度完全相同0.952这是 Hub-Node 问题的典型症状Issue 33252 的向量成了磁铁把大量无关查询都吸过去了。bge-m3 在两次查询中都稳定返回语义相关结果。评估结果TripletEvaluator离线cosine_accuracy两个测试集full-type-random全部关系类型712 个 Triplet和dup-random只有重复工单关系586 个 Triplet。随机基线50%。Cosine AccuracyTripletEvaluator 指标Triplet 中 Anchor 与 Positive 的相似度高于 Negative 的比例cosine_accuracy1NN∑i11[sim(ai,pi)sim(ai,ni)]符号含义N测试集中 Triplet 总数1[...]条件为真时为 1否则为 0aᵢ, pᵢ, nᵢ第 i 个 Triplet 的 Anchor、Positive、Negative结果百分比0%–100%随机基线 50%MXBAI模型full-type-randomdup-randommxbai-base88.9%89.8%triplet_baseline77.4%80.0%duplicate_triplet95.7%96.9%hard_negative_triplet88.8%88.9%mnrl_large_batch97.9%98.0%BGE-M3模型full-type-randomdup-randombge-m3-base94.9%94.5%duplicate_triplet95.4%96.4%mnrl_batch1695.4%95.9%注微调版 bge-m3 尚未转换为 GGUF 格式也没有对应的 ChromaDB 集合因此不在端到端评估中。端到端 RAG 评估200 个工单seed42threshold0.00仅对已部署为 Ollama GGUF 并写入 ChromaDB 的模型RecallK全部 K 值模型K1K3K5K10MRREmbedding 失败数mxbai-embed-large基线0.2100.2800.3150.3900.26174mxbai-mnrl-finetuned0.2500.3050.3300.3650.28970bge-m3基线0.3450.4500.5050.5400.4131按关系类型的 Recall10关系类型mxbai-基线mxbai-finetunedbge-m3related_to0.4800.4830.880duplicate_of0.4360.375 ↓0.537parent_of0.3700.500 ↑0.600关键发现mxbai-finetuned 在duplicate_of重复工单上的表现比基线下降了0.375 vs 0.436而这正是微调的目标场景。这是 Hub-Node 问题在核心场景上的直接危害。踩坑记录为什么准确率从 22% 涨到 97.9%这是本文最有价值的部分。第一次跑训练的结果远低于随机基线50%说明模型不只是没学好而是主动学错了。阶段一22% —— 三个 Bug 同时存在4 月 30 日Bug 1TripletLoss 的 margin 默认值是 5.0应该用 0.5# 错误写法sentence-transformers 默认 margin 5.0 loss TripletLoss(model) # 正确写法commit 9fb67fd5 月 5 日修复 loss TripletLoss(model, triplet_margin0.5)归一化向量的欧氏距离范围是 0 到 2。Loss 归零的条件是 d(a,n) − d(a,p) m而 margin5.0 要求距离差大于 5.0——但最大差值只有 2.0这个条件永远无法满足Loss 始终为正值梯度方向持续错误模型越训越糟。Bug 2模型以 bf16 格式保存加载后出现 NaN# 错误写法直接以训练精度保存 model.save_pretrained(config.output_dir) # 正确写法commit 085f4db5 月 5 日修复 model.float() # bf16 → fp32 model.save_pretrained(config.output_dir)bf16 只有 3 位尾数精度。序列化时的舍入误差在反序列化后变成了 NaN 值。NaN 向量算出来的余弦相似度也是 NaNNaN NaN永远是FalseTripletEvaluator 每条都判错 → 准确率趋近 0%。Bug 3没有冻结层灾难性遗忘不加--freeze-layers时335M 个参数全部参与更新。结合 Bug 1 产生的错误梯度底层通用语言知识被大幅覆盖。阶段二44% —— 修了 NaN 和 margin其他问题还在5 月 5 日后修复 Bug 1 和 Bug 2 后模型终于输出了有效的 Embedding。44% 略低于随机基线50%说明模型没有完全崩溃但由于全量参数训练和训练数据噪声还是损失了不少通用知识。此时还未解决全部 335M 参数可训练无层冻结训练数据包含大量无意义模板文本如Auslieferung der aktuellen ServerklassenMNRL 模式下 TripletEvaluator 根本没有运行_trainer.py里有一个判断写错了阶段三97.9% —— 三项改进后的质变5 月 6 日起改进 1冻结前 22 层commit 9d15a945 月 5 日只训练最顶部的 2 层 Pooling底层 22 层的通用语言知识完整保留灾难性遗忘消失。改进 2过滤训练数据中的模板文本commit d02c6c05 月 6 日像Auslieferung der aktuellen Serverklassen这类工单的 Summary 对模型来说没有任何语义区分度加进去只会污染训练。在dataset.py里加了LOW_QUALITY_PREFIXES/LOW_QUALITY_PHRASES黑名单过滤。改进 3切换到 MNRL batch_size32TripletLoss 每个 Anchor 只有 1 个负例而 MNRL 在 batch_size32 时每个 Anchor 有 31 个负例训练信号强得多。需要注意gradient_accumulation_steps不增加 MNRL 的负例数量只有batch_size才算。Bug 汇总表时间问题影响修复方法初始TripletLoss margin 5.0默认值梯度方向持续错误改为 margin 0.5初始模型以 bf16 保存NaN 值 → 准确率 ≈ 0%model.float()后再保存初始无层冻结灾难性遗忘--freeze-layers 22初始训练集含模板文本训练数据噪声高dataset.py加质量过滤初始MNRL 模式不运行 TripletEvaluatorMNRL 结果无法测量修正_trainer.py中的判断最终结论离线指标TripletEvaluatormxbai-mnrl-large-batch 以 97.9%/98.0% 拿下最高分明显高于 bge-m3 基线94.9%。mxbai 微调效果提升很大bge-m3 本身基线已经很强微调的增益有限。在生产库上为mxbai-mnrl-finetuned嵌入了专用chroma collection, mxbai-finetuned实测结果不如bge-m3。所以嵌入模型更换成品模型比微调收益大且更稳定。但是我不认为折腾这么些是浪费时间至少知道了嵌入模型微调具体是怎么操作的。端到端 RAGbge-m3无微调在真实检索场景中全面胜出Recall50.505 vs 0.315 / 0.330绝对提升 0.190 / 0.175Recall100.540 vs 0.390 / 0.365MRR0.413 vs 0.261 / 0.289绝对提升 0.152 / 0.124Embedding 失败数1 vs 74 / 70因为 bge-m3 上下文窗口 8192 tokenmxbai 只有 512 token长工单直接截断微调对 mxbai 的提升极为有限Recall5 仅 0.015MRR 0.028同时让 duplicate_of 召回率从 0.436 下降到 0.375——这恰恰是微调专门针对的场景说明 Hub-Node 问题已实质性地损害了模型的核心价值。手动搜索测试见上一节显示mxbai-mnrl-large-batch的表现不稳定一个查询排名正确另一个查询的正确答案完全不在前 5 名且 4 条结果相似度完全相同确认存在 Hub-Node 问题。行为依赖具体查询生产环境不够可靠。当前生产建议bge-m3docs_bge是当前最稳的选择在定量和定性两个维度均优于其他模型。mxbai-mnrl-finetuned因 Hub-Node 不稳定不建议上生产。已知问题docs_mxbai_finetuned里的 HNSW Hub-Node 问题Block 类型的 Embedding 在相同查询下可能返回不同结果。原因微调压缩了 Embedding 分布放大了 HNSW 的局部最优问题。docs和docs_bge不受影响。bf16 → fp32 必须在保存前执行_trainer.py第 207 行model.float()不能删。bf16 序列化会产生 NaN加载后模型完全损坏。参考文献[1] Schroff, F., Kalenichenko, D., Philbin, J. (2015).FaceNet: A Unified Embedding for Face Recognition and Clustering.CVPR 2015. arXiv:1503.03832— TripletLoss 的原始论文Lmax(0, d(a,p)−d(a,n)m)margin 参数的来源。[2] Karpukhin, V., Oğuz, B., Min, S., Lewis, P., Wu, L., Edunov, S., Yih, W. (2020).Dense Passage Retrieval for Open-Domain Question Answering.EMNLP 2020. arXiv:2004.04906— 将 In-Batch Negatives 确立为检索训练标准解释了为什么 batch_size 而非 gradient_accumulation 决定 MNRL 的训练信号强度。[3] Voorhees, E. M. (1999).The TREC-8 Question Answering Track Report.TREC 1999.— MRRMean Reciprocal Rank指标的来源论文。[4] Malkov, Y. A., Yashunin, D. A. (2018).Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs.IEEE TPAMI. arXiv:1603.09320— ChromaDB 默认索引算法 HNSW 的原始论文Hub-Node 问题的理论根源。[5] Kirkpatrick, J., Pascanu, R., Rabinowitz, N., et al. (2017).Overcoming Catastrophic Forgetting in Neural Networks.PNAS 114(13). arXiv:1612.00796— 灾难性遗忘的理论分析--freeze-layers 22策略的学术依据。作者Rest探路者出处Rest探路者 - 博客园本文版权归作者和博客园共有欢迎转载但未经作者同意请保留此段声明请在文章页面明显位置给出原文连接Githubcjy513203427 (Chen, Jinyao) · GitHub免责声明本内容来自平台创作者博客园系信息发布平台仅提供信息存储空间服务。好文要顶 关注我 收藏该文 微信分享Rest探路者粉丝 - 126 关注 - 9加关注10« 上一篇 重温星际2强化学习之QLearning(一)posted 2026-05-13 20:36 Rest探路者 阅读(244) 评论(2) 收藏 举报