惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园 - 司徒正美
Security Archives - TechRepublic
Security Archives - TechRepublic
P
Palo Alto Networks Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyberwarzone
Cyberwarzone
小众软件
小众软件
T
Threatpost
Latest news
Latest news
J
Java Code Geeks
博客园 - Franky
博客园 - 三生石上(FineUI控件)
Project Zero
Project Zero
P
Privacy & Cybersecurity Law Blog
T
Tenable Blog
L
Lohrmann on Cybersecurity
大猫的无限游戏
大猫的无限游戏
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
Scott Helme
Scott Helme
Simon Willison's Weblog
Simon Willison's Weblog
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
人人都是产品经理
人人都是产品经理
S
Schneier on Security
T
The Blog of Author Tim Ferriss
V
V2EX
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
罗磊的独立博客
IT之家
IT之家
雷峰网
雷峰网
H
Help Net Security
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tor Project blog
C
Cybersecurity and Infrastructure Security Agency CISA
I
InfoQ
GbyAI
GbyAI
博客园 - 叶小钗
PCI Perspectives
PCI Perspectives
The GitHub Blog
The GitHub Blog
Martin Fowler
Martin Fowler
H
Heimdal Security Blog
Spread Privacy
Spread Privacy
博客园_首页
A
About on SuperTechFans
T
Tailwind CSS Blog
The Register - Security
The Register - Security

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
企业级嵌入模型微调实战,基于RTX 4000算力
Rest探路者 · 2026-05-13 · via 博客园_首页

Embedding 模型微调实战:从 22% 到 97.9% 的踩坑记录

本文记录了在真实 IT 支持工单(Mantis Bug Tracker)数据集上微调 Embedding 模型的完整过程,包括所有踩过的坑和最终有效的方案。


背景

我们有一个基于 RAG(检索增强生成)的 AI 客服系统,核心逻辑是:用户提交新工单时,先把它向量化,然后去历史工单库里找相似的,再把检索结果喂给 LLM 生成回复。

向量化用的是 Embedding 模型,部署在本地 Ollama 上。系统跑着还行,但总感觉检索质量一般,于是决定用自己的工单数据微调一下。

测试的三个模型:

模型 ChromaDB 集合 类型
mxbai-embed-large docs 基线
mxbai-mnrl-finetuned docs_mxbai_finetuned 微调版
bge-m3 docs_bge 基线

数据准备:构建 Triplet 训练集

微调 Embedding 模型用的是 Triplet 结构:每条训练样本由三部分组成 — Anchor(锚点)、Positive(正例)、Negative(负例)

  • Anchor:一个工单
  • Positive:与 Anchor 存在关联关系的工单(重复工单、相关工单等)
  • Negative:与 Anchor 无关的工单

关系数据来自 Mantis 的 mantis_bug_relationship_table,总共有 5715 个有关系的工单对。

# 只用重复工单关系(rel-types 0=related_to, 1=duplicate_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 显存。


损失函数

余弦相似度

\[\text{sim}(a, b) = \frac{a \cdot b}{\|a\|\,\|b\|} \]

符号 含义
a, b 两个文本向量(模型的 Embedding 输出)
a · b 点积:各分量乘积之和
‖a‖ 向量 a 的长度(范数)
sim(a, b) 取值范围 −1 到 1;1 = 完全相同,0 = 完全无关

TripletLoss

有显式负例时使用(--train-jsonl 数据带 negative 列):

\[\mathcal{L} = \max\!\left(0,\ d(a, p) - d(a, n) + m\right) \]

符号 含义
L 损失值;训练过程中模型会最小化它
a Anchor — 查询工单
p Positive — 语义相似的工单
n Negative — 语义无关的工单
d(a, p) Anchor 与 Positive 的距离(欧氏距离)— 越小越好
d(a, n) Anchor 与 Negative 的距离(欧氏距离)— 越大越好
m 最小间距(= 0.5):d(a, n) 必须比 d(a, p) 大出这个值,否则产生损失

MultipleNegativesRankingLoss(MNRL)

只需 Anchor + Positive,用 --use-mnrl 开启:

\[\mathcal{L} = -\frac{1}{N} \sum_{i=1}^{N} \log \frac{e^{\,\text{sim}(a_i,\, p_i) / \tau}}{\displaystyle\sum_{j=1}^{N} e^{\,\text{sim}(a_i,\, p_j) / \tau}} \]

符号 含义
N Batch 大小(每步的训练样本数)
i 当前 Anchor 的序号
j Batch 中其他所有样本的序号 — 它们自动充当负例
aᵢ, pᵢ 第 i 个 Anchor 和对应的 Positive
e^(·) 指数函数(让所有值为正)
τ(Tau) 温度系数;越小区分越清晰
log 自然对数;损失最小化意味着 sim(aᵢ, pᵢ) 在 Batch 中最大

关键: Batch 中其他所有 Positive 都作为负例(In-Batch Negatives)。N 越大,负例越多,训练信号越强。gradient_accumulation_steps 不会增大 N,只有 batch_size 才算。


训练命令

mxbai — 最佳方案(MNRL + 大 Batch)

source ~/venv/bin/activate
PYTORCH_CUDA_ALLOC_CONF=expandable_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_CONF=expandable_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%)


训练监控(TensorBoard)

tensorboard --logdir=finetune/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 → Ollama(GGUF 导出)

Ollama 只能运行模型,不能训练。HuggingFace 的 PyTorch 格式需要先转成 GGUF 才能用 Ollama 部署。

Step 1:转换为 GGUF(fp32 → 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 f16

Step 2:注册到 Ollama

echo 'FROM finetune/models/mxbai-mnrl-finetuned.gguf' > Modelfile
ollama create mxbai-mnrl-finetuned -f Modelfile

Step 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.990(GGUF f16 对方向的保真度很好)。

Step 4:写入 ChromaDB

python -m src.createemb \
    --embedding-model mxbai-mnrl-finetuned:latest \
    --collection docs_mxbai_finetuned

端到端评估(Recall@K / MRR)

Recall@K — 查询中,正确工单出现在前 K 个结果里的比例:

\[\text{Recall@K} = \frac{1}{|Q|} \sum_{q \in Q} \mathbf{1}\!\left[\text{相关工单在 Top-}K\text{ 中}\right] \]

符号 含义
Q 全部测试查询集合
|Q| 测试查询的数量
K 看前 K 个结果(例如 K=5:看前 5 名)
1[...] 正确工单在前 K 名则为 1,否则为 0
结果解读 Recall@5 = 0.5 表示:50% 的查询里正确工单在前 5 名内

MRR(Mean Reciprocal Rank) — 第一个正确结果排名的倒数均值:

\[\text{MRR} = \frac{1}{|Q|} \sum_{q \in Q} \frac{1}{\text{rank}_q} \]

符号 含义
rank_q 查询 q 的第一个正确结果排名(第 1 名最好)
1/rank_q 倒数:第 1 名 → 1.0;第 2 名 → 0.5;第 5 名 → 0.2;未找到 → 0
结果解读 MRR = 1.0:每次都排第 1;MRR = 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

手动搜索测试结果

查询 1:"Bestellung ist ohne Wareneingang abgeschlossen"

排名 mxbai-baseline (docs) mxbai-finetuned (docs_mxbai_finetuned) bge-m3 (docs_bge)
#1 18534 "GOODS_RECEIPT-Telegramme" 0.985 ❌ 43165 "Bestellung ist ohne Wareneingang abgeschlossen" 0.980 ✓ 43165 1.000 ✓
#2 31337 0.983 18534 0.951 35104 0.931

微调版 mxbai 和 bge-m3 都将 Issue 43165(与查询完全一致)排在第 1 位;mxbai 基线漏掉了它。


查询 2:"Login Fehler"

排名 mxbai-baseline (docs) mxbai-finetuned (docs_mxbai_finetuned) bge-m3 (docs_bge)
#1 41258 "Login fehlgeschlagen" 0.948 ✓ 33252 "Fehlermeldung beim Kommissionieren" 0.957 ❌ 41258 "Login fehlgeschlagen" 0.977 ✓
#2 33252 "Kommissionieren" 0.946 ❌ 19454 "Fehler" 0.952 ❌ 30743 "Fehler bei der Anmeldung" 0.947 ✓
#3–5 "Fehler" 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 Accuracy(TripletEvaluator 指标):Triplet 中 Anchor 与 Positive 的相似度高于 Negative 的比例:

\[\text{cosine_accuracy} = \frac{1}{N} \sum_{i=1}^{N} \mathbf{1}\!\left[\,\text{sim}(a_i,\, p_i) > \text{sim}(a_i,\, n_i)\,\right] \]

符号 含义
N 测试集中 Triplet 总数
1[...] 条件为真时为 1,否则为 0
aᵢ, pᵢ, nᵢ 第 i 个 Triplet 的 Anchor、Positive、Negative
结果 百分比,0%–100%;随机基线 = 50%

MXBAI

模型 full-type-random dup-random
mxbai-base 88.9% 89.8%
triplet_baseline 77.4% 80.0%
duplicate_triplet 95.7% 96.9%
hard_negative_triplet 88.8% 88.9%
mnrl_large_batch 97.9% 98.0%

BGE-M3

模型 full-type-random dup-random
bge-m3-base 94.9% 94.5%
duplicate_triplet 95.4% 96.4%
mnrl_batch16 95.4% 95.9%

注: 微调版 bge-m3 尚未转换为 GGUF 格式,也没有对应的 ChromaDB 集合,因此不在端到端评估中。

端到端 RAG 评估(200 个工单,seed=42,threshold=0.00)

仅对已部署为 Ollama GGUF 并写入 ChromaDB 的模型:

Recall@K(全部 K 值)

模型 K=1 K=3 K=5 K=10 MRR Embedding 失败数
mxbai-embed-large(基线) 0.210 0.280 0.315 0.390 0.261 74
mxbai-mnrl-finetuned 0.250 0.305 0.330 0.365 0.289 70
bge-m3(基线) 0.345 0.450 0.505 0.540 0.413 1

按关系类型的 Recall@10

关系类型 mxbai-基线 mxbai-finetuned bge-m3
related_to 0.480 0.483 0.880
duplicate_of 0.436 0.375 ↓ 0.537
parent_of 0.370 0.500 ↑ 0.600

关键发现: mxbai-finetuned 在 duplicate_of(重复工单)上的表现比基线下降了(0.375 vs 0.436),而这正是微调的目标场景。这是 Hub-Node 问题在核心场景上的直接危害。


踩坑记录:为什么准确率从 22% 涨到 97.9%

这是本文最有价值的部分。第一次跑训练的结果远低于随机基线(50%),说明模型不只是"没学好",而是主动学错了。

阶段一:22% —— 三个 Bug 同时存在(4 月 30 日)

Bug 1:TripletLoss 的 margin 默认值是 5.0,应该用 0.5

# 错误写法(sentence-transformers 默认 margin = 5.0)
loss = TripletLoss(model)

# 正确写法(commit 9fb67fd,5 月 5 日修复)
loss = TripletLoss(model, triplet_margin=0.5)

归一化向量的欧氏距离范围是 0 到 2。Loss 归零的条件是 d(a,n) − d(a,p) > m,而 margin=5.0 要求距离差大于 5.0——但最大差值只有 2.0,这个条件永远无法满足,Loss 始终为正值,梯度方向持续错误,模型越训越糟。

Bug 2:模型以 bf16 格式保存,加载后出现 NaN

# 错误写法:直接以训练精度保存
model.save_pretrained(config.output_dir)

# 正确写法(commit 085f4db,5 月 5 日修复)
model.float()  # bf16 → fp32
model.save_pretrained(config.output_dir)

bf16 只有 3 位尾数精度。序列化时的舍入误差,在反序列化后变成了 NaN 值。NaN 向量算出来的余弦相似度也是 NaN,NaN > NaN 永远是 False,TripletEvaluator 每条都判错 → 准确率趋近 0%。

Bug 3:没有冻结层,灾难性遗忘

不加 --freeze-layers 时,335M 个参数全部参与更新。结合 Bug 1 产生的错误梯度,底层通用语言知识被大幅覆盖。


阶段二:44% —— 修了 NaN 和 margin,其他问题还在(5 月 5 日后)

修复 Bug 1 和 Bug 2 后,模型终于输出了有效的 Embedding。44% 略低于随机基线(50%),说明模型没有完全崩溃,但由于全量参数训练和训练数据噪声,还是损失了不少通用知识。

此时还未解决:

  • 全部 335M 参数可训练(无层冻结)
  • 训练数据包含大量"无意义模板文本"(如"Auslieferung der aktuellen Serverklassen")
  • MNRL 模式下 TripletEvaluator 根本没有运行(_trainer.py 里有一个判断写错了)

阶段三:97.9% —— 三项改进后的质变(5 月 6 日起)

改进 1:冻结前 22 层(commit 9d15a94,5 月 5 日)

只训练最顶部的 2 层 + Pooling,底层 22 层的通用语言知识完整保留,灾难性遗忘消失。

改进 2:过滤训练数据中的模板文本(commit d02c6c0,5 月 6 日)

像"Auslieferung der aktuellen Serverklassen"这类工单的 Summary 对模型来说没有任何语义区分度,加进去只会污染训练。在 dataset.py 里加了 LOW_QUALITY_PREFIXES / LOW_QUALITY_PHRASES 黑名单过滤。

改进 3:切换到 MNRL + batch_size=32

TripletLoss 每个 Anchor 只有 1 个负例,而 MNRL 在 batch_size=32 时,每个 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 模式不运行 TripletEvaluator MNRL 结果无法测量 修正 _trainer.py 中的判断

最终结论

离线指标(TripletEvaluator)

mxbai-mnrl-large-batch 以 97.9%/98.0% 拿下最高分,明显高于 bge-m3 基线(94.9%)。mxbai 微调效果提升很大;bge-m3 本身基线已经很强,微调的增益有限。
在生产库上为mxbai-mnrl-finetuned嵌入了专用chroma collection, mxbai-finetuned实测结果不如bge-m3。所以嵌入模型更换成品模型比微调收益大且更稳定。但是我不认为折腾这么些是浪费时间,至少知道了嵌入模型微调具体是怎么操作的。

端到端 RAG

bge-m3(无微调)在真实检索场景中全面胜出

  • Recall@5:0.505 vs 0.315 / 0.330(绝对提升 +0.190 / +0.175)
  • Recall@10:0.540 vs 0.390 / 0.365
  • MRR:0.413 vs 0.261 / 0.289(绝对提升 +0.152 / +0.124)
  • Embedding 失败数:1 vs 74 / 70,因为 bge-m3 上下文窗口 8192 token,mxbai 只有 512 token,长工单直接截断

微调对 mxbai 的提升极为有限(Recall@5 仅 +0.015,MRR +0.028),同时让 duplicate_of 召回率从 0.436 下降到 0.375——这恰恰是微调专门针对的场景,说明 Hub-Node 问题已实质性地损害了模型的核心价值。

手动搜索测试(见上一节)显示 mxbai-mnrl-large-batch 的表现不稳定:一个查询排名正确,另一个查询的正确答案完全不在前 5 名,且 4 条结果相似度完全相同,确认存在 Hub-Node 问题。行为依赖具体查询,生产环境不够可靠。

当前生产建议

bge-m3(docs_bge)是当前最稳的选择,在定量和定性两个维度均优于其他模型。mxbai-mnrl-finetuned 因 Hub-Node 不稳定不建议上生产。


已知问题

  • docs_mxbai_finetuned 里的 HNSW Hub-Node 问题: Block 类型的 Embedding 在相同查询下可能返回不同结果。原因:微调压缩了 Embedding 分布,放大了 HNSW 的局部最优问题。docsdocs_bge 不受影响。
  • bf16 → fp32 必须在保存前执行: _trainer.py 第 207 行 model.float() 不能删。bf16 序列化会产生 NaN,加载后模型完全损坏。