最近在做一个企业内部知识库的RAG系统,用的是开源embedding模型+LLM(ChatGLM3)。检索回来的文档经常混着不相关的内容,比如用户问“报销流程”,结果把“出差政策”也捞上来了。我尝试把LLM微调一下,让它能更好地过滤噪声,但发现微调后模型有时反而忽略了正确文档,输出变得很奇怪。
想请教下大家:
1. 微调时,训练数据里的“正确文档”和“噪声文档”应该怎么构造?要不要加一些负样本?
2. 有没有人试过在微调时让LLM学会“拒答”或者“指出文档不相关”?效果如何?
3. 还是说应该优先优化检索质量,微调只是辅助?
目前卡在这块了,求指点……
RAG场景下微调LLM时,怎么处理检索出来的噪声文档?
全部回复
共 7 条这问题我太熟了,之前也踩过一样的坑。我的经验是检索质量还是得优先优化,embedding模型换bge或者gte试过没?召回top_k别设太大,先保准再求全。微调的话,可以在训练数据里加一些“文档不相关”的负样本,让模型输出“无法基于文档回答”之类的固定句式,但比例别超过20%,否则模型容易摆烂。拒答能力确实能练出来,不过样本构造挺费工夫的,建议先调检索,微调当辅助手段。
同感,这个问题我在做RAG的时候也踩过坑。检索召回的质量确实直接影响微调效果,特别是像你说的“报销流程”混进“出差政策”,这种语义重叠但实际不相关的噪声,模型很容易被带偏。
关于你的第一个问题,我自己试过构造负样本,但关键是负样本要怎么选。如果只是随便拿一些完全不相关的文档(比如“报销流程”配“食堂菜谱”),模型很快就学废了,因为它太容易区分了。真正有效的负样本应该是那种“高相似度但意图不同”的文档,比如用户问“报销流程”时,把“出差政策”里涉及报销的部分、或者“财务审批制度”这类容易混淆的文档作为负样本。我当初的做法是先跑一遍检索,把top10里人工标注出来哪些是真正相关的、哪些是干扰项,然后在这个基础上微调,效果比随机选负样本好不少。
第二个问题,让LLM学会拒答,这个思路我试过。不过有个坑:模型在微调时如果对拒答样本学得太死,它可能变得过于保守,连正确文档也怀疑。我当时的折中方案是给“拒答”场景加一个特别的输出格式,比如“无法确认文档相关性,建议优化检索条件”,而不是直接让它说“不相关”。这样模型有个缓冲,不会直接拒绝所有含噪场景。
最后,我个人感觉检索质量的优先级还是高于微调。微调更像是锦上添花,如果检索本身能优化,比如用重排序模型(Reranker)或者混合检索(稀疏+稠密),能把噪声压到10%以下,微调的压力会小很多。你现在的ChatGLM3微调时,有没有考虑过把检索到的文档先做一个简单的相关性打分过滤再喂给模型?这个流程上的改动有时候比直接改模型参数见效快。
微调时加负样本确实容易翻车,我试过类似的做法,结果模型开始对正确文档也犹豫不决。后来我换成在训练数据里明确标注“噪音文档”并让模型输出“无关”或“跳过”,效果会稳定一些,但数据量要够大才行。检索质量还是根基,微调只是兜底,建议先优化embedding和检索策略,比如加个重排序模型把噪音压下去,再考虑微调。
这个问题问得很实在,属于RAG落地过程中最让人头疼的“最后一公里”问题。我做了几年大模型应用,在RAG上踩过不少坑,尤其是ChatGLM系列(包括3、4、6B和32K版本)我都深度调过。针对你的三个问题,我尽量把实操中的经验和教训说清楚。
先给你一个核心判断:不要试图用微调去解决检索噪声。微调能做的,是让模型在拿到正确文档时更稳定、更听话,而不是让它学会从一堆垃圾里挑金子。你遇到的“微调后忽略了正确文档”现象,本质是因为训练数据构造出了问题,模型学到了错误的相关性——它把“某些噪声特征”当成了“正确信号”,反而对真正的正确文档产生了抑制。
下面逐一展开。
第一个问题,训练数据怎么构造。这是你当前最核心的痛点。很多人做微调时,会直接把“检索到的Top-K文档”拼接起来作为输入,然后要求LLM基于这些文档生成答案。这种做法在数据干净时勉强可行,一旦检索结果有噪声,模型就会被带偏。正确的做法是:在训练数据中,显式地让模型区分“相关”和“不相关”的文档,而不是让它把所有文档都当作事实来源。
具体来说,我建议你把输入结构设计成“文档列表+分隔符+用户问题”的形式。例如:
文档1: [内容] 文档2: [内容] 文档3: [内容] 用户问题: 报销流程是什么?
然后,训练数据要包含三种类型: 第一类是“所有文档都相关”的正样本。这类数据让模型学会正常回答。 第二类是“全部文档都不相关”的负样本。这类数据让模型学会拒答或说“无法从提供文档中找到答案”。 第三类是“部分相关、部分不相关”的混合样本。这类数据最关键,它让模型学会忽略噪声,只关注相关文档。
在构造混合样本时,我踩过一个坑:我一开始简单地把“不相关文档”随机替换成其他领域的文档,结果模型学到了一个错误模式——它发现只要文档里出现某些高频词(比如“报销”),就认为是相关的;如果文档里没有这些词,就认为是噪声。这导致模型在真实场景中过度依赖关键词匹配,忽略了语义相关性。正确做法是:噪声文档应该来自同一个领域但语义不相关的内容。比如用户问“报销流程”,噪声文档可以是“公司差旅政策”中关于“住宿标准”的部分(虽然同属差旅领域,但和报销无关),而不是随机插入一段“机器学习原理”。这样模型才能学会基于语义层级做判断,而不是粗暴的词频匹配。
至于负样本的构造比例,我试过从1:1到1:5(正:负),最终觉得1:2左右比较合适。负样本太多,模型会变得过于保守,动不动就拒答;负样本太少,模型对噪声的容忍度太高。
第二个问题,关于“拒答”或“指出文档不相关”。这个思路完全正确,但在实现上有很多细节。我建议你采用“阶段式输出”策略,而不是让模型直接生成一个答案。具体做法是:在输出前,先让模型对每个文档做相关性判断。你可以把输出改成结构化形式:
{ “文档相关性”: [“相关”, “不相关”, “相关”], “答案”: “基于相关文档,报销流程是……” }
或者更自然一点,让模型先输出一句话“根据文档1和文档3,……”,然后接着回答。这样模型在训练时,必须显式地学习文档级别的判断。
我在实际项目中用的是另一种方式:在训练数据中,要求模型在回答前先复述“相关文档的摘要”。比如用户问“报销流程”,模型需要先输出“从文档1中可以看出,报销需要填写表格并提交审批;从文档3中可以看到,审批周期为3个工作日”,然后再给出完整答案。这种方法的好处是:如果模型复述的内容和用户问题无关,说明它可能忽略了你期望的文档,你可以通过这个中间步骤来监控和调试。
关于“拒答”的微调,我建议你给模型一个固定的拒绝模板,而不是让它自由发挥。比如“抱歉,您提供的文档中未包含与报销流程直接相关的信息,请尝试提供更准确的文档。” 这个模板要出现在训练数据中,并且只出现在“全部文档都不相关”的样本里。注意,不要出现在“部分相关”的样本中,否则模型会过度拒答。
第三个问题,检索质量和微调的关系。我的观点非常明确:优先优化检索,微调只做辅助。理由很简单:检索是一个确定性问题,微调是一个概率性问题。你可以在检索侧用非常明确的手段(比如BM25+向量检索的混合召回、reranker模型、查询改写等)来降低噪声,这些手段的效果是稳定的、可预期的。而微调是一个黑盒,你很难保证模型在训练集以外的噪声分布上还能表现良好。
我遇到过最典型的案例:某次微调后,模型在公司内部测试集上表现极好,能准确过滤掉所有人工构造的噪声。但上线后,生产环境的噪声分布和测试集完全不同(比如测试集噪声是“差旅政策”,生产环境噪声变成了“考勤规则”),模型瞬间失效。这就是过拟合噪声分布的问题。所以,微调只能作为兜底方案,用来处理那些检索系统无法完全消除的“软噪声”(比如语义相似但实际无关的文档),而不是用来对抗检索系统的质量缺陷。
具体到你的ChatGLM3,我建议你先做几件事: 第一,检查你的embedding模型。开源embedding模型在垂直领域的表现往往不理想,尤其是企业内部知识库这种术语密集的场景。你可以试着用bge-m3或者stella-base-zh-v3-5-1e,效果比早期的text2vec系列好很多。 第二,加一个reranker。这是性价比最高的优化手段。用bge-reranker或者cohere的rerank模型,对召回的前20个文档重新排序,只保留Top-3或Top-5给LLM。成本很低,但效果提升非常明显。我在一个知识库项目中,加了reranker后,有效回答率从65%直接跳到89%。 第三,做查询改写。用户输入的“报销流程”可能过于简略,你可以用LLM或规则将其扩展为“请查找关于费用报销的申请步骤、审批流程和所需材料”。这能显著减少检索结果的偏差。 第四,调整你的chunk策略。如果文档切得太碎,容易把不相关的片段拼在一起。我建议用“滑动窗口+语义边界”的方式,确保每个chunk内部主题一致。比如按markdown标题切分,或者用LLM做段落级分类。
如果你一定要走微调这条路,我建议你采用prompt-level微调,而不是全参数微调。具体来说,在训练数据中,把“文档”和“问题”作为输入,把“答案”作为输出,但不要改变模型原有的基础能力——只微调它如何“利用文档”这个行为。这类似于指令微调中的“上下文学习”增强。我在实践中发现,LoRA微调(rank=16, alpha=32)的效果比全参数微调好很多,因为全参数微调容易让模型“学死”,而LoRA保留了模型原始的语义理解能力,只是微调了它在特定输入格式下的输出倾向。
最后,给你一个避坑指南:不要在微调数据中加入“让模型学会判断文档是否相关”的通用逻辑,比如“如果文档不相关,就输出X”。这种逻辑在实际场景中很容易被打破,因为噪声的形态千变万化。更好的做法是,让模型学会“基于文档的语义一致性”来做判断,而不是基于固定的规则。例如,在训练数据中,让模型看到:当文档内容与问题高度重叠但逻辑矛盾时(比如文档说“报销需要审批”,但问题问的是“报销流程”),模型应该选择忽略该文档,而不是强行解释。
简单总结一下:先做好检索侧优化(reranker、查询改写、chunk策略),再考虑用LoRA微调做辅助。微调时构造混合样本(部分相关+部分不相关),并用结构化输出让模型显式判断文档相关性。不要期望微调能解决所有噪声问题,它只是一个锦上添花的工具。
如果你需要具体的代码实现思路,我可以提供一套在ChatGLM3上做LoRA微调的pipeline,包括训练数据的构造脚本和推理时的后处理逻辑。可以先从你的检索日志里随机采样100条真实用户query,人工标注一遍相关文档和噪声文档,然后用这些数据做微调——效果通常立竿见影。
这个坑我太熟了,之前做类似项目也被检索噪声搞到头大。说几个实操中的体会吧:
-
训练数据构造这块,我的经验是负样本一定要加,但比例要控制好。我试过1:1的正负样本比,结果模型直接变得特别保守,稍微有点模糊就拒答。后来调整到3:1左右,效果会好一些。负样本可以从检索返回的低排名文档里采样,或者干脆从其他完全不相关的问题里拿文档过来配。另外有个小技巧:把“正确文档”里混入少量噪声段落,模拟真实场景,比纯整篇文档的噪声更有效。
-
拒答能力这块,我们试过在微调时加一个特殊的
标记,让模型在判断文档不相关时输出类似“当前文档未包含相关信息,请尝试更换提问方式”的回复。效果有,但问题是模型有时候会过度触发拒答,连正确文档也拒。后来改成让模型先输出一个相关性评分再生成回答,虽然多了个步骤,但可控性好很多。 -
说句实话,微调能解决的噪声问题真的很有限。我现在的做法是两条腿走路:检索侧做rerank,用一个小的cross-encoder对top-k文档重新打分,把噪声压下去;微调侧主要让LLM学会在多个相关文档里做信息融合,而不是指望它去过滤噪声。毕竟LLM的幻觉问题已经很头疼了,再让它承担检索的活,容易两头不讨好。
对了,ChatGLM3的指令跟随能力其实还行,你可以试试在prompt里明确告诉它“只基于提供的文档回答,如果文档不相关就说明原因”,有时候不用微调也能改善不少。先把这个调透了再考虑微调,免得走弯路。
这个问题我最近也踩过类似的坑,说下我的实操经验。微调时构造负样本确实关键,我试过把检索回来的top-10文档里,人工标出完全不相关的作为负例,然后训练时让模型对正负样本的attention权重做差异化处理,效果比单纯给正确答案要好一些。不过你提到的拒答机制我还没试过,但直觉上这可能是个双刃剑,因为模型一旦学会“拒答”,可能会对部分相关但表述模糊的文档也产生误判,尤其在领域术语不够规范的企业场景里。我个人觉得,微调和检索优化其实得并行,检索质量是天花板,微调只能在这个天花板下优化。可以试试在微调前先用规则或小模型(比如基于关键词匹配的过滤)把检索结果粗筛一遍,把明显不相关的先去掉,再喂给LLM,这样微调的压力会小很多。另外,你用的ChatGLM3本身有prompt模板,可以试试在训练数据里加入类似“如果文档与问题无关,请回答‘未找到相关信息’”这样的指令,让模型学会输出拒答格式,比直接让它沉默或乱答要可控。
这个问题我也踩过类似的坑,个人感觉你提到的第3点优先级其实最高——检索质量如果本身波动大,微调很难从根本上兜住噪声。我之前试过在微调数据里构造负样本,就是把明显不相关的文档和正确文档混在一起,让模型输出“该文档与问题无关”的标记,但踩了个雷:负样本比例太高的话,模型会变得过于保守,连正确文档都开始怀疑,回答质量反而下降。
关于第2点,我试过在指令里加一个“拒答”的显式逻辑,比如让模型在判断文档无关时输出“当前检索内容未覆盖该问题”,效果还行,但前提是你要在训练数据里标注清楚哪些是强相关、哪些是弱相关,不能只分“正/负”两类。我觉得你可以试试用规则先给检索结果打一个相关性分数(比如用embedding相似度),然后只拿分数在某个阈值以上的文档去微调,这样模型学到的边界会更清晰。
另外,你提到微调后模型忽略正确文档,这可能是训练数据里的噪声文档本身信息量太大,模型学到了一种“偷懒”策略——直接忽略文档靠自身记忆回答。建议你检查下微调时有没有刻意控制上下文长度,或者把噪声文档放在末尾位置,让模型先看到正确文档再做判断。说到底,检索和微调是双线作战,检索端至少得保证召回率在80%以上,微调才敢放手去搞过滤,不然会陷入无穷无尽的补丁循环。