
1. 为什么要把查询改写单独拎出来微调前阵子帮团队优化RAG检索效果折腾了一圈embedding模型、重排模型之后发现一个经常被忽略的瓶颈用户query太“脏”。口语化、指代不清、缺少实体信息这些问题不解决后面的召回和重排再强也是白搭。后来我决定用LoRA微调一个专属的查询改写器基于开源基座模型做低成本适配。整个流程从数据准备到训练部署走下来大概两周时间。这篇文章就把完整过程复盘一遍包括可以直接抄的训练配置、部署方案以及几处让我印象深刻的坑。先说清楚这东西适合谁如果你的RAG系统已经把embedding、重排都优化得差不多了但线上用户问题五花八门检索结果还是不稳定或者你的搜索/问答产品里query质量严重依赖大模型的prompt发挥那这篇对你有用。我讲的内容基于我自己实际跑通的一版方案结论不一定适合所有业务但思路和参数选择逻辑是通用的。1.1 查询改写器解决的是RAG链路里的哪个痛点RAG的标准流程大家都熟悉用户query进来embedding成向量去向量库里做相似度召回再把召回结果交给大模型生成答案。这套链路里有个很隐蔽的假设——用户query是适合检索的。但真实用户的输入根本不是这么回事。举个例子。用户先问“杭州明天天气怎么样”系统回复了天气情况用户紧接着追问“那后天呢”。如果没有对话历史这条query丢给embedding模型“后天”和“杭州”“天气”没有任何语义相关性召回结果大概率是乱的。再比如“想买个拍照好点的手机预算四千左右”这种口语化长query直接做向量化往往会带着一堆对检索没帮助的冗余词。这个阶段你需要一个改写器把“那后天呢”改写成“杭州后天天气怎么样”把口语query整理成更规范、更利于召回的检索表达。查询改写器做的事就是在检索之前把原始query转换成一个“更适合做向量召回”的查询语句。它不回答用户问题只负责把问题改得让检索系统更懂。我自己在优化过程中测过一条数据原始query是“小米那个折叠屏现在多少钱”embedding召回top5里只有2条和折叠屏相关改写为“小米折叠屏手机当前价格”之后top5里4条都是有效结果。改写器对检索质量的影响往往比换一个更贵的embedding模型来得更直接。1.2 提示词方案和全参微调为什么都不够好用很多人第一反应是让大模型在prompt里顺手改写不就行了这也是我之前线上的做法确实能跑但用一段时间之后会发现三个明显问题。第一是输出不稳定。同一个query今天改写出来的结果和明天改写出来的结果可能是两种风格尤其换了模型版本之后改写逻辑可能悄悄变化。RAG链路对稳定性要求很高检索query风格漂移会直接导致召回结果波动。第二是成本和延迟。每来一条用户query都多一次大模型调用高并发场景下token成本和响应时间都翻倍而且很多改写结果其实是没必要的——本来就能检索的query白白多花一次推理。第三是意图保持不可控。prompt里的几句指令约束远不足以让模型理解“你必须保留实体、必须消解指代、不能擅自扩充意图”。模型自由发挥的空间一大改出来的就可能是另一个意思。那全参微调呢效果上限确实高但代价也实打实。一个7B模型全参微调光权重显存就接近14GB加上优化器状态和梯度单卡基本要60GB以上的显存空间这对大多数团队来说太奢侈了。而且每调整一次业务方向就要重新训练一份完整模型权重存储成本、版本管理成本都高。更关键的是全参微调会把基座模型的通用能力覆盖掉一部分要是某天想换个场景用同一个基座还得另训一份。LoRA正好卡在中间训练时只更新极少量参数能跑在消费级显卡上产出的adapter文件只有几十到一两百MB可以同时保留多个业务场景的版本想切哪个切哪个。这就是我选LoRA的核心原因。1.3 LoRA的整体思路和优势LoRA的原理不复杂它假设模型权重在微调过程中的变化量是低秩的所以不需要直接更新整个参数矩阵而是把权重更新量拆成两个小矩阵相乘。大白话讲全参微调是把整本教材重新写一遍LoRA是在原教材里贴便利贴只补充需要改动的重点内容。推理的时候可以把便利贴原样保留也可以把便利贴的内容“压”回教材里变成一本新书。这个方案对“查询改写器”这种任务特别合适。查询改写的本质是让模型学会一种新的输入输出映射关系而不是让它重新学习语言知识所以需要用到的“知识点增量”很小LoRA的低秩假设完全够用。而且adapter文件小我同时训练了通用改写和电商场景改写两个版本切换成本极低。另外说一句如果你在网上搜“LoRA”看到一堆讲通信模块的内容别搞混那是物联网领域的LoRa无线通信技术拼写略有差异和大模型微调里的Low-Rank Adaptation完全是两码事。我这次讲的是后者别踩错门。2. 数据准备查询改写器的“教材”要怎么写微调圈有句话叫“数据决定了效果上限模型只是逼近这个上限”。这句话放在查询改写器上尤其成立。我见过不少人上来就套现成训练脚本结果跑完推理效果稀碎十有八九是数据环节出了问题。2.1 输入输出格式设计查询改写器的输入输出和普通指令微调不太一样。普通SFT是“instruction input - output”改写器则是输入一段系统指令 可选的对话历史 当前这条用户query输出一条独立的、适合检索的查询语句不是回答用户问题这里最关键的是“对话历史”怎么组织。有对话历史才能解决指代消解。我给一个实际训练样本的格式说明对话历史 用户杭州明天天气怎么样 助手杭州明天多云转晴气温18到26摄氏度。 当前查询那后天呢 改写结果杭州后天天气怎么样注意“改写结果”只要一句不加解释、不加客套更不要输出对话回复。这个约束要在数据里反复体现模型才能学好“改写器不是聊天机器人”这件事。我在构造数据时把输出统一成两种形态一种是完全独立的新query比如上面的“杭州后天天气怎么样”另一种是对原query做精简和术语归一比如“小米那个折叠屏现在多少钱”改写为“小米折叠屏手机当前价格”。两条路线覆盖了大多数场景。2.2 怎么造高质量训练样本训练数据我建议以“真实线上日志为主模型生成辅助扩展”的方式来做起点大概是3000到5000条高质量样本。注意这里说的是高质量不是单纯的数量。我曾试过扩张到2万条但质量没控住效果反而不如精挑细选的4000条。数据来源有三个按优先级排第一个是线上真实query日志。把用户问题拉出来去掉重复和垃圾内容之后找业务同事帮忙标注改写结果。这是最贴近真实分布的数据约占我训练集的60%。第二个是模拟构造。基于业务文档、商品title、FAQ问题用大模型批量生成不同问法再人工抽检修正。比如针对“杭州天气”这个意图可以生成“杭州明天几度”“杭州明天下不下雨”等多种口语变体。第三个是历史badcase。把之前检索效果差的query收集起来人工写出理想改写结果这些是提升效果最直接的数据。多轮对话样本需要单独构造重点覆盖指代消解比如“那明天呢”“第二个呢”“有没有便宜点的”。这类样本最好占20%左右否则线上多轮场景会垮。我最初的版本只放了个位数比例的指代样本结果线上测试里凡是带“那”“它”“这个”的query大半失败后来补数据才救回来。2.3 数据清洗与去重的几个细节数据清洗这块坑不少说几个我踩过的。去重要按“语义去重”而不是字符串去重。线上日志里“杭州天气怎么样”“杭州天气如何”“杭州天气咋样”可能是三条不同的字符串但语义是同一个意图全量保留会导致训练集分布失衡。我用embedding把语义相似的样本归簇每簇抽样保留3到5条即可。输入里的对话历史要控制长度。查询改写器的输入不像通用对话那么长我把历史轮次限制在最近两轮用户问题加一轮助手回复总长度控制在512到1024个token。超出部分直接截断防止模型学到“从超长历史里找信息”这种无关能力。另外数据里不要混入和改写无关的对话。比如有些日志里用户直接问“你好”这种寒暄模型不需要改写它但如果你在训练集里放了太多这类样本模型会倾向于大量原样输出因为“不改写”成了一个高频答案。后来我在数据里只保留了少量“原样输出”样本并且明确标注输出为原query本身同时配套一条指令规则如果原query已经适合检索可以原样返回。这样模型才学会了“什么时候该改、什么时候不该改”。3. LoRA微调实操从选基座到出权重数据准备完之后进入正式训练环节。这一节我把选模型、定参数、跑命令、看日志这些步骤完整过一遍给出的是我自己验证过的配置你可以基于这个模板调整。3.1 基座模型和训练工具怎么选基座模型方面我用的是Qwen2.5-7B-Instruct这个级别的模型。7B参数量在“改写质量”和“部署成本”之间是个不错的平衡点。显存紧张的团队也可以尝试更小的模型但说实话查询改写虽然任务简单对指令跟随能力还是有要求的模型太小容易出现输出格式漂移的问题。1.5B级别的模型我也试过简单改写能跑但一旦涉及多轮指代消解就明显吃力。训练工具我推荐直接用LlamaFactory它对数据集格式、对话模板、LoRA参数都做了封装不太容易出错。当然也可以用HuggingFace的PEFT库自己写训练脚本好处是灵活坏处是踩坑概率高。做技术验证阶段我更倾向于用LlamaFactory快速跑通之后如果要深度定制再迁移到自写脚本。3.2 LoRA关键超参逐个说这里把LoRA训练里最重要的几个参数讲清楚每个参数我都标注了为什么这么设。lora_rank决定了低秩矩阵的宽度。rank越大可学习的参数量越多但并不是越大越好。查询改写这个任务的数据量有限rank设得过大反而容易过拟合。我常用的配置是rank32数据量少于3000条时用rank16更稳。lora_alpha是缩放系数它和rank配合控制最终对原权重的修改幅度。经验上alpha设成rank的1到2倍比较稳妥比如rank32时alpha32或64。alpha太大会让LoRA增量盖过原权重模型的通用语言能力会受损。lora_dropout我设0.05这个值足够防止一些过拟合但也不会太高挡住有效学习。target_modules建议设成all也就是把attention和FFN层全都挂上LoRA适配器。只改q_proj和v_proj的做法在早期很流行但实测全量挂载的改写效果更稳。学习率这块和全参微调不一样LoRA的学习率可以给大一些。7B模型我常用1e-4到2e-4配合cosine退火和10%的warmup比例。batch size方面单卡显存允许时per_device_train_batch_size设2到4再用gradient_accumulation把总batch size凑到32到64。查询任务序列短总batch大一点有利于收敛稳定。3.3 一份可直接参考的训练配置下面是我实际用过的训练配置基于LlamaFactory的命令行方式。数据集放在data/目录下训练集文件为query_rewrite_train.json并在dataset_info.json里注册数据集名称。训练样本格式用的Alpaca风格我在2.1里已经给了示例。llamafactory-cli train \ --stage sft \ --model_name_or_path Qwen/Qwen2.5-7B-Instruct \ --finetuning_type lora \ --dataset query_rewrite_train \ --dataset_dir ./data \ --template qwen \ --output_dir ./output/query_rewrite_lora \ --overwrite_cache \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 16 \ --learning_rate 1e-4 \ --num_train_epochs 3 \ --lr_scheduler_type cosine \ --warmup_ratio 0.1 \ --logging_steps 10 \ --save_steps 200 \ --max_length 1024 \ --lora_rank 32 \ --lora_alpha 32 \ --lora_dropout 0.05 \ --target_modules all \ --fp16 true一句提醒LlamaFactory不同版本的参数名可能略有调整跑之前先看一眼对应版本的官方文档。这套命令在7B模型上24GB显存的单卡可以跑起来显存紧张的话把--per_device_train_batch_size降到1或者加上--quantization_bit 4走QLoRA路线显存占用能压到12GB左右。我训练时的监控方式是同时打开训练集loss和验证集loss。查询改写任务收敛很快3个epoch通常就够。如果第2个epoch之后训练loss还在降但验证loss开始回升说明开始过拟合了这时候应该提前停而不是机械地跑完3个epoch。3.4 训练过程中的判断与调整训练日志不能只看loss数字还要结合评测集的推理结果一起判断。我每训练500步就会用当时的checkpoint对固定评测集跑一遍推理人工看一眼改写质量。这个习惯帮我在早期就发现了一个问题前500步的checkpoint经常把对话历史里的助手回复也抄进改写结果。原因是我一开始数据格式不统一有些样本的输出里混入了“好的根据查询……”这类前缀模型学到了这种坏习惯。发现问题后回到数据清洗环节把所有输出里的多余前缀全部清理干净重新训练这个问题就消失了。还有一次训练卡在loss不停震荡查了半天发现是数据加载时对话历史被分词器截断了导致很多样本的指代信息丢失模型没法正确学习。解决办法是把max_length从512调到1024同时把对话历史从“只保留最近一轮”改成“保留最近两轮用户问题加一轮助手回复”。训练日志里的loss曲线很多时候不是在告诉你模型有没有学会而是在提醒你数据管线的某个环节是不是有问题。4. 部署接入合并权重还是动态挂载Adapter模型训练完接下来要考虑怎么部署。这里有两个方向一是保留adapter文件在推理时动态挂载二是把LoRA权重合并进原模型部署成普通模型。两者各有适用场景。4.1 两种部署方式怎么选动态挂载的好处是灵活一个基座模型可以同时挂载多个adapter切换业务场景不用重复加载模型。现在vLLM也支持动态加载LoRA adapter但多adapter的内存管理相对复杂线上出问题排查成本高。如果只是单一改写场景我建议直接合并权重部署成一个标准的开源模型格式。这样做的好处是后续用vLLM、TensorRT-LLM这些推理框架部署时没有任何额外适配成本行为也更好预测。我自己的线上方案是先把LoRA合并进基座模型部署成一个单独的“改写服务”。另一个场景需要第二个adapter时再单独合并部署一份。因为查询改写器的输入输出都很短请求量又大拆分部署还能方便单独扩容。4.2 合并权重的具体操作合并权重用LlamaFactory的export命令就行。我用的命令是llamafactory-cli export \ --model_name_or_path Qwen/Qwen2.5-7B-Instruct \ --adapter_name_or_path ./output/query_rewrite_lora \ --template qwen \ --finetuning_type lora \ --export_dir ./merged/query_rewriter_7b \ --export_legacy_format false合并之后的模型目录里就是一个完整的模型加载方式和原版模型没有任何区别。合并操作本身不消耗GPU内存纯CPU就能完成速度也比较快。合并完务必做一次“冒烟测试”拿训练集里的新样本来推理确认输出风格和训练时一致。这里有个容易忽略的点如果训练时用的是某条system prompt或对话模板合并后的模型并不会自动带上那段模板。推理服务里仍然需要按照训练时的格式组装输入否则效果会打折扣。我一开始就是忽略了这步部署完线上实测改写质量明显不如训练时的效果差点怀疑是合并操作出了问题最后发现是输入侧少了对话模板。4.3 推理参数与RAG流程接入查询改写器的推理参数和通用对话不太一样。改写结果要求准确、稳定不需要创造性所以temperature调到0.1到0.3top_p保持0.9max_new_tokens设64就够。不要给模型太多自由发挥空间改写任务最怕模型自作主张扩写。接入RAG的完整流程长这样用户query到达后先送改写服务改写结果经过一道简单的合法性检查通过检查后再用改写结果去做embedding召回召回结果走重排最后交给生成模型。我在合法性检查里设了两个规则一是输出非空二是改写结果不能和原query八竿子打不着简单做法是计算改写前后文本的向量相似度相似度低于一个很低的阈值就说明改写异常直接fallback到原query。这个兜底逻辑虽然简单但能挡住不少推理服务异常时的连锁故障。另外线上我一直开着延迟监控。加了改写器之后如果发现RAG整体延迟变高优先考虑把改写服务的max_new_tokens再压低一点或者对明显已经规范化的query做“过路直通”不进改写服务而是直接在入口处判断是否值得改写。5. 效果评估用检索结果说话训练完模型只是第一步效果评估才是真正决定这个改写器能不能上线的关键。查询改写器不能只看“改得对不对”还要看“改了之后检索结果是不是真的变好了”。5.1 自动评估的三个维度我搭了一套自动评估流程评测集大约300条覆盖线上主要query类型和典型的badcase。评估分三个维度。第一个维度是改写成功率输出是否为合法文本、是否非空、是否超过合理长度限制。这个维度主要防模型输出乱码或空白。第二个维度是意图一致性改写后的query和原query表达的核心意图是否一致。这个可以用规则校验加模型打分结合来做规则方面重点检查实体有没有被丢比如原query里的“苹果手机”、商品型号、城市名等。第三个维度也是最关键的——下游检索增益把每条评测query分别用“原query”和“改写后query”去向量库检索比较top5命中率。如果改写后检索效果没有提升甚至下降那说明这个改写器是在帮倒忙。我给一个简化的评估脚本思路方便你自己搭建# 伪代码示意实际实现时按自己的检索接口替换 for item in eval_set: original_query item[query] rewritten_query rewrite_model(item[input]) original_hits search(original_query, top_k5) rewritten_hits search(rewritten_query, top_k5) original_score relevance_score(original_hits, item[golden_docs]) rewritten_score relevance_score(rewritten_hits, item[golden_docs]) if rewritten_score original_score: gain_count 1 elif rewritten_score original_score: regression_count 1我实测下来一个好的改写器在评测集上的“有效提升比例”通常能到60%以上回归样本控制在10%以内。如果回归比例偏高优先检查是不是改写时把关键实体改没了。5.2 人工评估和badcase复盘自动评估能筛出大方向但人工抽检依然是必要的。我会每周抽50条线上真实改写记录按“原query、改写结果、是否可用、问题类型”四列整理成一个表。问题类型分成几类实体丢失、指代未消解、意图改变、格式异常、过度改写。连续两周的badcase复盘能很清楚地暴露数据配比的问题。我这边第一周最大的badcase集中在“指代未消解”原因是训练集里带对话历史的样本把对话历史写在input里但推理时组装格式漏了分隔符导致模型分不清哪段是历史、哪段是当前query。这类问题靠自动评估很难发现人工抽检时一眼就能看见。6. 常见问题排查速查表训练和部署过程中我遇到过的典型问题整理成一张速查表方便你对照排查。阶段现象可能原因解决办法训练训练loss基本不降学习率太低或数据格式错误导致模型学到噪音把学习率调到1e-4以上检查训练样本的输入输出字段训练eval loss先降后升过拟合减少训练epoch或增大数据量/数据多样性训练输出总是原样照抄训练集里“不需改写”样本太多降低原样输出样本比例强化需要改写的样本训练输出里带着对话历史输出标签混入了冗余前缀或分隔符清理输出文本只保留改写query本身推理本地效果好但线上差线上输入组装缺少训练时使用的模板/分隔符按训练时的输入格式完整组装system、对话历史和当前query推理实体被改写没了训练数据里缺少实体保留约束增加实体保留样本或在系统指令里显式强调“必须保留专有名词”部署合并后模型尺寸异常合并命令参数或路径配置错误检查adapter路径确认合并后的模型能正常加载部署高并发时延迟飙升改写服务生成token数太多降低max_new_tokens加缓存对无需改写的query直接旁路最后再分享一个个人经验查询改写器这个任务看着简单但真正决定上限的不是基座模型选多大也不是LoRA参数调多精而是数据设计和评估口径。我训练完最大的收获是养成了一个习惯——任何优化改动都要用“下游检索增益”来验证而不是只看改写结果本身顺不顺眼。如果你也想做类似的事先把评测集建好再动手训练你会少走很多弯路。