尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

用Qwen3微调Embedding模型,解决RAG检索不准问题

发布时间:2026/9/4 14:18:46

资讯中心
01
ARTICLE

用Qwen3微调Embedding模型,解决RAG检索不准问题

用Qwen3微调Embedding模型,解决RAG检索不准问题
你迟早会遇到这种情况知识库里内容已经传进去了用户提的问题却还是得不到想要的答案。你反复修改 Prompt甚至加上“请严格根据上下文回答”这种强调句回答依然不稳定。一部分原因是生成端的问题但在大量 RAG 项目里真正的瓶颈发生在检索阶段该被召回的内容压根没有出现在 TopK 里。如果继续深挖“没被召回”往往不是数据库大小问题而是 Embedding 向量模型和你的业务语言不匹配。通用 Embedding 模型不认识你们企业内部“项目代号”“产品术语”“缩写惯例”它只能靠字面相似度去猜。这时指望大模型在生成阶段“脑补”出答案只会带来幻觉。与其反复调 Prompt不如回头把 Embedding 这条链路做一次针对性优化用 Qwen3 生成高质量训练数据再对开源 Embedding 模型做领域微调。这篇文章会从 RAG 检索链路讲清楚 Embedding 模型为什么是瓶颈再给出一条可落地的实践路径用 Qwen3 构造训练数据、训练自己的领域 Embedding 模型、建立测评集验证提升效果。你会明白 Embedding 微调和 LLM SFT 的差异也会知道哪些项目适合微调、哪些项目先别急着微调。1. RAG效果差通常差在哪一环很多团队做 RAG 的时候把注意力放在两个地方一个是文档解析和分块另一个是给大模型的 Prompt。这两个环节当然重要但它们并不能弥补检索阶段的缺陷。一个典型的 RAG 链路是这样用户问题输入后先被向量化成 Query Embedding然后在向量数据库里检索相似度最高的文档片段再把 TopK 片段拼进 Prompt 给大模型生成答案。如果 Query Embedding 和文档 Embedding 在向量空间中的表达不对那么后续无论做 Rerank 还是 Prompt 调优都只是在“矮子里拔将军”。最常见的现象是用户问的是“我们上个季度客户投诉最多的产品线是什么”但知识库原文写的是“2025 年 Q3 客诉分析”通用 Embedding 可能因为“季度”和“Q3”的表达不一致导致相关片段排到第 8 位、第 9 位最终被 TopK 截断。代码、工单、合同这类文本里大量出现项目代号例如“OX-213”“CC-GUI”通用模型很难理解一个代号可能代表“某硬件模块”。描述同一业务事件的几篇文档用词不同通用 Embedding 把它们当作不同语义召回了错误的一篇。这些问题说明一个道理检索质量的上限由你的 Embedding 模型决定。如果向量表达无法把“语义相关”和“字面无关但业务相关”映射到相近位置RAG 应用从一开始就是“带病运行”。在这一环上Rerank 可以缓解部分问题但不能完全替代更好的召回。Rerank 是对候选集合做精排假如候选集合前 50 条里根本没有正确答案精排也无能为力。所以Embedding 微调的真正价值在于提升召回质量让正确内容更容易进入候选集合。2. 通用Embedding在垂直领域失灵的原因先说结论通用 Embedding 模型是在海量开源语料上训练的它擅长理解“通用语义”但对某个垂直领域的“私有语义”理解和能力有限。用通俗的话解释Embedding 模型要学的是“什么句子和什么句子应该离得近”。通用模型见过大量新闻、百科、论文和网页知道“苹果”和“水果”相关但未必知道“补丁版本”和“漏洞修复验收单”在你们业务里是同一个概念。垂直领域文本还有一个特点描述内容高度相似但答案可能完全不同。比如一份内部技术支持文档里可能有“网络连接失败排查”和“网络连接超时排查”两个章节。字面上看这两段几乎长得很像通用 Embedding 会把它们算成高相似度你问“连不上内网怎么办“的时候模型可能先把两个章节都召回来甚至排前面的还是错误章节。这种细粒度差异需要业务数据来引导模型重新学习。这时候有必要设计一种适合特定领域数据分布的训练方式让模型看到领域语料里的句子对关系。所谓 Embedding 微调本质不是让模型记住更多知识而是让它的向量空间被“掰”到更符合业务语义关系的位置。另外要注意Embedding 微调和文本分类、文本生成的标准训练套路并不相同。做文本分类时你往往只需要在模型后接一个分类头用交叉熵做有监督训练。而 Embedding 微调的目标是让相似样本在向量空间里更近、不相似样本更远因此常用的方法是对比学习。2.1 用什么指标衡量检索效果在动手前应该先建立一套判断标准否则很容易“调完发现回答变好了但说不清到底哪里变好”。检索阶段比较常用的指标有四个指标含义适用判断HitRateK正确答案是否出现在前 K 条结果中判断召回能力实际业务最常用RecallK覆盖了多少正确答案多条正确答案时更有意义MRR第一个正确答案的排名越靠前分数越高判断 Top1 质量NDCG考虑结果列表整体相关度排序适合需要多层相关判断的场景很多文章会把 HitRate 和 RecallK 混用。如果每条 Query 只对应一个正确答案两者含义基本一致如果一条 Query 对应多条正确文档RecallK 就必须统计所有正确答案的覆盖率。建议在最开始就定义清楚评估数据集和指标。3. 先分清Embedding微调不是LLM的SFT微调在搜索资料的时候你会看到“大模型微调”“全量微调、Freeze 微调、LoRA 微调”这些关键词且容易产生一个误解Embedding 微调是不是也要拿 Qwen3 这种大模型去做 SFT其实这是两类不同的微调。LLM 的 SFT 是条件语言建模训练目标是给定用户指令后最大概率生成符合期望的 Assistant 回复。它改变的是生成 token 的概率分布。Embedding 微调是表示学习训练目标是让相关文本对的向量相似度更高、不相关文本对的相似度更低。它改变的是语义表达空间。因此LLM 微调常用损失是交叉熵损失评估方式是生成结果是否准确。Embedding 微调常用损失是对比损失例如 MultipleNegativesRankingLoss评估方式是检索排序效果。如果用错套路最常见的失败做法是把问答对直接当成“问题加回答”的文本拿去训练模型生成回答。这样做出来的模型依然是生成模型不是检索模型。正确做法应该是让一个 Query 和一个正确文档组成正样本对让模型学习它们之间的匹配关系。这一点必须想通否则你后面读代码时会被各种 Loss 搞晕。4. Qwen3在Embedding微调中的角色这篇文章标题中的“通过 Qwen3 对 Embedding 进行训练微调”可能会让人以为是直接把 Qwen3 模型改造为向量模型。更稳妥的做法通常是把 Qwen3 当成“数据生产者和标注者”用来生成高质量训练数据再对开源 Embedding 模型做领域微调。为什么不是直接用 Qwen3 做 Embedding 底座因为 Qwen3 是 Decoder-only LLM要把它变成向量模型需要额外设计 pooling 层和训练目标对显存、数据量和调参能力要求都比较高。对大多数 RAG 项目来说合理路线是用几十亿参数甚至更小的开源 Embedding 模型作为底座用 Qwen3 生成业务问题来微调它。这样训练成本更低部署更简单向量推理延迟也更低。Qwen3 在整条链路里可以承担三类工作生成问题样本给定一段知识库文本Qwen3 可以生成多种问法用它模拟真实用户提问。生成难负例Qwen3 可以判断哪些片段看起来相关、但实际上不包含答案这些片段作为难负例能有效逼着 Embedding 模型学到更细的差异。辅助构造评测集人工写评测集成本很高Qwen3 可以先生成候选 Query再由业务人员审核确认。这种“大模型产数据 小模型学表示”的模式是当前 RAG 工程里性价比很高的一条路。4.1 为什么需要一个开源底座既然 Qwen3 都能生成问题为什么不能让 Qwen3 自己完成检索大模型确实可以做“重排”也就是把召回结果逐条判断是否相关。但如果每一条 Query 都调用几十亿参数模型去重排成本和延迟会非常高。检索系统通常需要在上百万条文档里快速筛出 TopK这就适合由轻量 Embedding 模型完成。微调一个小的 Embedding 模型比如 BGE 系列中的 small 模型既可以在单卡训练又能在普通 CPU 环境下完成向量推理适合大多数中小规模 RAG 应用。5. 训练数据准备80%的工作量在这下面进入实操环节。假设你的知识库里已经有一部分精品文档来源可能是内部 Wiki、产品说明书、工单记录、代码注释或项目复盘。你需要做的第一件事不是写训练脚本而是把文档切分成“适合作为独立回答单位”的片段并准备好一批文档片段。5.1 环境准备建议使用 Linux 或 macOS 环境Python 版本建议使用 3.10 或更高。需要安装以下工具# 推荐使用 conda 创建独立环境 conda create -n rag-embedding python3.10 -y conda activate rag-embedding # 安装核心依赖 pip install sentence-transformers datasets pip install openai版本号请以当前最新稳定版本为准。本文重点演示通用思路版本不同不影响整体操作。如果你的 Qwen3 通过 vLLM、Ollama 或兼容 OpenAI 协议的服务提供需要准备好服务地址和模型名。以下示例基于 OpenAI SDK 调用 Qwen3 的服务接口。5.2 用Qwen3为文档片段生成问题你可以准备一个docs.jsonl文件每行存一个知识片段字段包括doc_id和content。# 文件路径scripts/generate_questions.py import json import os from openai import OpenAI # 若本地部署 Qwen3可配置 base_url 指向本地服务 client OpenAI( api_keyos.getenv(QWEN_API_KEY, EMPTY), base_urlos.getenv(QWEN_BASE_URL, http://127.0.0.1:8000/v1), ) MODEL os.getenv(QWEN_MODEL, Qwen3-8B) def call_qwen(messages, temperature0.7): resp client.chat.completions.create( modelMODEL, messagesmessages, temperaturetemperature, ) return resp.choices[0].message.content def generate_questions(doc_content: str): prompt f你是一名企业内部知识库运营专家。 请根据下面的知识片段生成 8 个用户可能会向知识库提问的问题。 要求 1. 所有问题都必须能从该片段中找到答案 2. 禁止直接复制片段原句 3. 至少包含 3 个口语化问题、2 个带业务术语的问题、2 个含隐含条件的问题 4. 只输出 JSON 数组不要输出其他解释。 知识片段 {doc_content} result call_qwen([ {role: system, content: 你只输出 JSON 数组。}, {role: user, content: prompt}, ]) # 防止模型输出多余文字 start result.find([) end result.rfind(]) 1 if start -1 or end 0: return [] return json.loads(result[start:end]) if __name__ __main__: input_path data/docs.jsonl output_path data/synthetic_questions.jsonl rows [] with open(input_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue doc json.loads(line) questions generate_questions(doc[content]) for q in questions: rows.append({ query: q, positive: doc[content], doc_id: doc[doc_id], }) with open(output_path, w, encodingutf-8) as f: for row in rows: f.write(json.dumps(row, ensure_asciiFalse) \n) print(f生成完成共 {len(rows)} 条训练样本)这个脚本把原始文档和 Qwen3 生成的问题包装成“Query 到 Positive 文档”的配对格式。后续训练时模型会学习把这些问题和对应文档片段映射到相近的向量位置。有一点需要提醒Qwen3 生成的 Question 只能作为训练数据的起点不能替代真实用户问题。如果项目已经积累了用户日志一定要把真实提问混入数据集比例可以按 1:1 或 2:3 来控制这样模型才不会只学会“AI 风格提问”。5.3 难负例从哪里来在对比学习中负例不能随便选。假设模型只需要区分“苹果”和“电脑”任务太简单很快就学会了但如果要区分“网络连接失败”和“网络连接超时”模型才会被迫注意细节差异。因此负例要足够“难”最好是看起来很像、实际不是答案的文本。有三种获取难负例的方式用当前 Embedding 模型检索取排名第 5 到第 50 的片段再人工或让 Qwen3 判断它们是否真的不包含答案。不包含答案且语义接近的片段就是难负例。利用同一份文档中的不同片段比如两个章节都是“故障排查”但一个讲网络、一个讲电源。让 Qwen3 或人工筛选出“语义主题相似但答案不对”的邻近片段。从相似标题或相似关键词的文档中抽取负例片段。示例数据格式如下{query: 系统提示连接超时应该从哪里开始排查, positive: 连接超时排查第一步是检查目标地址连通性……, negative: 连接失败排查第一步是检查防火墙规则……}不是所有训练框架都需要负例字段。使用 Sentence Transformer 的 MultipleNegativesRankingLoss 时同一个 Batch 里的其他 Positive 会自动充当负样本这在很多场景下已经足够。难负例的价值在于主动制造更难的 Batch 结构所以在构建数据时我建议优先把同一业务主题的很多片段打散并塞进同一个训练批。6. Embedding模型微调实操数据准备好之后下一步就是训练脚本。我们这里选择BAAI/bge-small-zh-v1.5作为底座。它体积小、社区资料多、中文效果稳定适合作为入门 Embedding 微调的第一站。6.1 加载数据集并训练把第 5 步生成的data/synthetic_questions.jsonl作为训练数据。为了便于演示下面统一用query、positive两个字段。# 文件路径scripts/train_embedding.py import json from torch.utils.data import DataLoader from sentence_transformers import ( InputExample, SentenceTransformer, losses, evaluation, ) # 加载训练数据 def load_pairs(path: str): pairs [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue row json.loads(line) query row[query] positive row[positive] positive positive.strip() if not query or not positive: continue # BGE 推荐在 Query 侧加指令帮助模型区分检索任务 pairs.append(InputExample(texts[query, positive])) return pairs def main(): train_pairs load_pairs(data/synthetic_questions.jsonl) print(f训练样本数量{len(train_pairs)}) # 可选从评测文件加载少量数据用于观察模型提升 eval_queries {} eval_corpus {} eval_relevant {} try: with open(data/eval_data.json, r, encodingutf-8) as f: eval_data json.load(f) for idx, item in enumerate(eval_data[queries]): qid idx eval_queries[str(qid)] item[query] eval_relevant[str(qid)] {str(item[gold_doc_id])} for doc in eval_data[corpus]: eval_corpus[str(doc[doc_id])] doc[content] except FileNotFoundError: print(未找到评价集本次训练将只输出训练 Loss。) model SentenceTransformer(BAAI/bge-small-zh-v1.5) dataloader DataLoader(train_pairs, batch_size32, shuffleTrue) train_loss losses.MultipleNegativesRankingLoss(model) if eval_queries: ir_evaluator evaluation.InformationRetrievalEvaluator( querieseval_queries, corpuseval_corpus, relevant_docseval_relevant, namerag_eval, ) evaluator ir_evaluator else: evaluator None model.fit( train_objectives[(dataloader, train_loss)], epochs5, warmup_steps100, evaluatorevaluator, evaluation_steps200, output_pathmodels/bge-small-zh-rag, save_best_modelTrue, ) if __name__ __main__: main()关于 Query 前是否要加指令前缀这里值得多解释一句。BGE 系列在表示“用户查询”的时候一般推荐加上“为这个句子生成表示以用于检索相关文章”前缀而在表示“文档”时不加。上面的示例为了让代码结构清晰没有在训练数据里拼前缀。如果你的训练效果不稳定可以尝试把 Query 加工为QUERY_PREFIX 为这个句子生成表示以用于检索相关文章 query QUERY_PREFIX row[query]训练结束后模型会保存在models/bge-small-zh-rag。使用这个目录做检索推理时同样要遵循“Query 加指令、文档不加指令”的规则。6.2 Freeze、LoRA与全量微调怎么选在 RAG 资料里经常看到“全量微调、Freeze 微调、LoRA 微调”等概念。严格来说它们最初主要指向大语言模型的参数更新策略用在 Embedding 模型上时需要考虑底座结构。全量微调更新整个 Embedding 模型所有参数适合数据量充足、领域特殊性强的场景。你选 BGE small 这类模型时全量微调成本不高效果通常也更好。Freeze 微调冻结底层编码层只训练顶层或部分层适合训练数据较少、担心模型忘记通用语义的场景。但完全冻结所有 Transformer 层、只训练 Pooling 层通常效果有限因为表征能力没有变化。LoRA 微调适合大语言模型底座的轻量微调。如果将来真的想尝试把 Qwen 等 LLM 改为向量底座LoRA 可以在少量显存下训练适配层。但 LLM 做 Embedding 的工程改造更复杂需要自己设计 Pooling、归一化、评估方法普通 RAG 项目不建议一上来就走这条路。对大多数团队用 BGE 或同类 Embedding 小模型做全量微调是“投入产出比”最稳妥的选择。7. 用评测脚本验证效果模型训练完后最怕的是自我感觉良好。你不能只看两三个问答效果就下结论应该做一次相对规范的离线评测。建议构建一个eval_data.json包含三块内容用户问题集合、文档片段集合、每个问题对应的正确答案文档 ID。这个评测集数量不需要非常大但要让业务同事评审过保证 Query 是真实场景。# 目录结构建议 data/ ├── docs.jsonl # 原始知识片段 ├── synthetic_questions.jsonl # 训练用问题-文档对 └── eval_data.json # 离线评测集评估脚本可以这样写# 文件路径scripts/evaluate_retrieval.py import json import sys from sentence_transformers import SentenceTransformer, util QUERY_PREFIX 为这个句子生成表示以用于检索相关文章 def load_eval_data(path: str): with open(path, r, encodingutf-8) as f: data json.load(f) queries [] query_ids [] corpus_ids [] corpus_docs [] relevant_map {} for item in data[queries]: queries.append(item[query]) query_ids.append(item[query_id]) relevant_map[item[query_id]] set(item[gold_doc_ids]) for doc in data[corpus]: corpus_ids.append(doc[doc_id]) corpus_docs.append(doc[content]) return queries, query_ids, corpus_ids, corpus_docs, relevant_map def hit_at_k(predicted_ids, gold_ids, k): return len(set(predicted_ids[:k]) gold_ids) 0 def main(): model_path sys.argv[1] if len(sys.argv) 1 else models/bge-small-zh-rag eval_path sys.argv[2] if len(sys.argv) 2 else data/eval_data.json model SentenceTransformer(model_path) queries, query_ids, corpus_ids, corpus_docs, relevant_map load_eval_data(eval_path) query_embs model.encode( [QUERY_PREFIX q for q in queries], normalize_embeddingsTrue, batch_size64, ) corpus_embs model.encode( corpus_docs, normalize_embeddingsTrue, batch_size128, ) total len(queries) hit1 0 hit5 0 mrr 0 for i, q_emb in enumerate(query_embs): scores util.cos_sim(q_emb, corpus_embs)[0] ranks scores.argsort(descendingTrue) predicted_ids [corpus_ids[idx] for idx in ranks.tolist()] gold_ids relevant_map[query_ids[i]] hit1 hit_at_k(predicted_ids, gold_ids, 1) hit5 hit_at_k(predicted_ids, gold_ids, 5) for rank, pid in enumerate(predicted_ids): if pid in gold_ids: mrr 1.0 / (rank 1) break print(f模型路径: {model_path}) print(f评估 Query 数: {total}) print(fHit1: {hit1 / total:.4f}) print(fHit5: {hit5 / total:.4f}) print(fMRR10: {mrr / total:.4f}) if __name__ __main__: main()运行方式python scripts/evaluate_retrieval.py models/bge-small-zh-rag data/eval_data.json如果路径无误终端会输出类似下面的结构具体数值一定以你自己的评测集为准模型路径: models/bge-small-zh-rag 评估 Query 数: 100 Hit1: 0.6xxx Hit5: 0.8xxx MRR10: 0.7xxx此时要做的是把微调前的原始模型也跑一遍同一份评测集得到两个模型在同一标准下的分数。不要只看绝对分数一定要看相对提升。如果你发现 Hit1 没有提升但 Hit5 明显提升说明 Embedding 模型已经能把正确答案送进候选集合接下来可以考虑搭配 Rerank 让正确答案排到更前。8. 常见问题与排查思路实践过程中容易遇到以下几类问题。把它们整理成一个表格方便遇到情况时快速对照。问题现象可能原因排查方式解决方案Loss 几乎不下降训练数据量太少或正样本对太简单检查训练样本数量和样本难度增加同主题难负例增加不同措辞的同义 Query检索效果反而比原始模型差训练集覆盖太窄造成灾难性遗忘在通用语料或评测集上做回归测试混合一部分通用检索样本或减少训练轮数多次跑结果不稳定随机种子不固定评测集太小固定 seed并增大评测集训练与评估时固定随机种子评测 Query 至少 100 条每个 Query 都召回同样几篇文档Query 前缀不一致或者模型表征退化检查推理时是否对 Query 加了指令前缀确保训练和推理使用同一套前缀规则生成的训练问题大量重复Qwen3 生成温度太低调整生成参数 temperature 到 0.7~1.0增加生成轮次并做去重处理模型训练时报显存不足batch_size 或模型规模超出显存观察显存占用情况减小 batch_size使用更小底座模型或开启梯度累积加了负例字段后训练时报错训练脚本没有消费 negative 字段查看完整堆栈信息将负例统一转换为 Batch 中的其他样本或改用专门支持三元组输入的 Loss这里特别提一下 Query 前缀不一致的问题。BGE 模型在训练阶段是否给 Query 加专用指令会影响实际向量空间。如果训练时加了前缀、部署时忘记加检索效果下降是很常见的。建议把前缀规则封装成同一个函数训练、评估、部署都调用它。9. 最佳实践与工程建议如果现在准备在真实项目里落地这套方案下面这些建议值得参考。第一不要一上来就微调。先把分块策略、元数据过滤、TopK 数量和 Rerank 模型梳理清楚。如果问题是文档分块太碎导致上下文缺失微调 Embedding 模型并不能完全解决。建议先做一个最小评测集跑通原始链路再决定是否进行 Embedding 微调。第二数据质量比数据数量重要。500 条经过业务确认的高质量“问题-文档”对可能比 5000 条自动生成的低质量样本更有价值。Qwen3 生成的数据要有人工抽检不能让存在事实偏差的样本直接进训练。第三训练数据要和真实用户问题尽量同分布。理想状态是把线上 RAG 的用户提问捞下来配上答案文档再由人工确认。这个闭环如果能跑起来Embedding 模型的迭代就有了持续性而不是做一个版本就停住。第四微调后要做双模型回归对比。把原始通用 Embedding 模型保存在一个独立目录不要直接覆盖。后续无论换底座、调数据、改训练轮数都用固定评测集对比新旧模型避免“越调越丢通用能力”。第五如果公司业务涉及私密知识库对外调用大模型服务生成训练数据前要注意数据合规和授权边界。能私有化部署 Qwen3 的情况下优先在本地完成数据生产线避免敏感内容进入第三方接口。第六Embedding 微调不等于 RAG 优化终点。如果微调完成后 Hit5 很高但最终回答仍然不好下一步可以引入 Rerank 模型、优化指令模板、合并多路召回结果甚至尝试 Agentic RAG 来分解复杂问题。但只有先把 Embedding 这条召回底座打牢后面这些优化才有意义。10. 总结与后续学习方向这篇内容重点解决了一个问题很多 RAG 项目效果不好不是大模型不会生成而是 Embedding 无法把业务相关的文档准确召回。通过 Qwen3 生成高质量的问题样本再用对比学习微调一个开源 Embedding 模型是一种工程上可落地、成本可控的优化路径。你需要掌握的关键点包括理解 Embedding 微调与 LLM SFT 的区别理解 HitRate、Recall、MRR 这些检索评价口径学会用 Qwen3 构造训练数据并建立一套“微调前后同评测集对比”的验证方法。下一步可以继续深入的方向分块策略如何与 Embedding 微调配合、Rerank 与双阶段检索的工程实现、领域模型上线后的增量更新策略。对于大多数团队先把这套数据闭环跑通比追逐更大的模型更有价值。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。