:彌合用戶Query與檢索器的表達鴻溝)
做RAG做到一定階段你遲早會遇到一個特別憋屈的問題檢索器配置沒毛病、知識庫質(zhì)量也說得過去可用戶問出來的話檢索結(jié)果就是南轅北轍。我第一次認真把查詢重寫Query Rewriting當成RAG管線里的獨立環(huán)節(jié)是在某知識庫問答項目的線上反饋里用戶問“那個權(quán)限申請的流程現(xiàn)在還能線上走嗎”召回的全是舊版線下流程文檔新版內(nèi)容一篇都沒進top 5。當時我第一反應是換embedding模型后來把query和文檔拉出來逐條對比才意識到問題根本不在模型而在用戶這句口語化、帶著指代和時態(tài)的話壓根不是檢索系統(tǒng)該直接吃的輸入。從那時起我開始系統(tǒng)性地研究查詢重寫把用戶query轉(zhuǎn)成更適合檢索的形式再進入向量檢索和重排環(huán)節(jié)。這一篇是Advanced RAG系列里關(guān)于查詢重寫的探索記錄覆蓋策略選型、提示詞設計、工程改造和上線評測適合已經(jīng)跑通RAG基礎鏈路、想繼續(xù)拉高檢索上限和答案質(zhì)量的開發(fā)者。下面的內(nèi)容都來自我自己的項目踩坑經(jīng)歷方案未必放之四海皆準但思路和排坑路徑是通用的。1. 先定位問題查詢重寫到底在彌合哪條“表達鴻溝”要理解查詢重寫首先要接受一件事檢索器不是理解器。很多人覺得“我已經(jīng)用了很強的embedding模型語義檢索應該很聰明”但實際跑起來就會發(fā)現(xiàn)embedding模型對“表達方式”的敏感程度遠超你的預期。1.1 檢索器不是理解器原始query直接進檢索的隱患embedding檢索的本質(zhì)是把query和文檔都映射進同一個向量空間然后用余弦相似度或內(nèi)積排序。這套機制對“表達方式”極其敏感。同一個意思換一種說法向量可能就偏到另一個區(qū)域。最典型的例子是電商場景里用戶說“不要白色的”文檔里寫的是“黑色/藍色”否定表達在embedding里幾乎不起作用。技術(shù)文檔問答里更常見用戶習慣說“這功能咋關(guān)閉”文檔正文寫的卻是“如何停用該模塊”兩者之間不是簡單的同義詞關(guān)系而是句式和語域的差異。短query是另一個麻煩。用戶只輸入四五個詞向量被壓到一片模糊區(qū)域top k里混進大量主題沾邊但不精確的內(nèi)容。再加上用戶的query多是聊天式口語而知識庫文檔是結(jié)構(gòu)化書面語兩者天然存在分布差異。這種鴻溝不是靠換一個更強的embedding模型就能完全解決的因為問題發(fā)生在輸入端——query這個“句子”本身信息密度不夠。所以查詢重寫的角色可以理解成在用戶query和檢索器之間加一個適配層把一句口語化、短小、帶指代的話翻譯成更接近文檔表達習慣、信息更完整的檢索式。它不是為了讓query變得更“像人話”而是為了讓query在向量空間里離目標文檔更近。這個視角一旦建立后面所有策略的選擇邏輯都會變得很清晰。1.2 三類最典型的“問非所想”場景我做過的項目里檢索效果崩掉的query基本能歸成三類。第一類是指代與口語化。比如用戶在先前的對話里問過“上傳發(fā)票的步驟”下一句緊跟“那上傳之后多久能審核”系統(tǒng)如果只看后一句根本不知道“上傳”指什么。這種query重寫必須結(jié)合對話歷史把指代還原成“上傳發(fā)票之后審核需要多長時間”。這類問題在客服機器人、企業(yè)內(nèi)部門戶里最普遍也是大家最容易忽略的一類——因為單看每一句query你總覺得“人話沒問題啊”但檢索器眼里全是漏洞。第二類是復合問題。一個提問里揉了兩三個意圖比如“怎么查日志以及怎么通過日志定位超時”前半句在問操作后半句在問診斷方法。直接檢索時兩個意圖互相干擾召回結(jié)果哪邊都不精準。這類query要做子問題分解拆開檢索再合流。比較麻煩的是有些復合問題拆開之后單獨看都成立但用戶真正想要的是兩件事之間的關(guān)聯(lián)后續(xù)生成環(huán)節(jié)需要額外處理。第三類是具體問題背后其實是抽象需求。用戶問“為什么我們召回率比之前低了3個點”直接拿“召回率 低 3個點”去檢索什么都查不到。他真正需要的是“RAG評測指標有哪些、召回率下降通常由哪些環(huán)節(jié)導致”的背景知識。這類query適合做step-back式抽象重寫。還有一類隱藏場景是query本身包含術(shù)語變體比如用戶說“卡頓”文檔里寫“延遲”和“性能劣化”這種同義改寫問題用多查詢擴展通常能覆蓋。1.3 重寫的前提約束別把原意也改沒了查詢重寫不是讓LLM自由發(fā)揮不加約束就上生產(chǎn)等于往系統(tǒng)里埋雷。我自己定了四條鐵律每一句都會寫進提示詞里。第一保留原始查詢中的專有名詞、數(shù)字、縮寫絕不允許替換?!癊S”不能改成“搜索引擎”“V2”不能改成“第二版”。第二不添加外部知識重寫只能基于query本身和對知識庫結(jié)構(gòu)的理解不能擅自塞入query里沒有的事實。第三保持問題類型疑問句還是疑問句陳述式需求還是陳述式。第四也是我后來加上的一條如果原始查詢已經(jīng)足夠清晰直接原樣輸出。這條“如果足夠清晰就原樣輸出”特別重要。不加這條約束LLM會把很多本來沒問題的query也重寫一遍改完反而引入噪聲。我在提示詞里加了這一句之后負面改寫率肉眼可見地降了下來。所謂負面改寫就是重寫結(jié)果比原文更差、檢索效果反而下降的情況。別小看這個問題它在真實線上環(huán)境里出現(xiàn)的頻率遠比你想象得高。2. 五種主流查詢重寫手段的適用邊界與提示詞設計重寫不是只有一種做法。我按自己的項目經(jīng)驗把實踐下來真正有效的手段分成五類Multi-Query擴展、子問題分解、Step-Back抽象、HyDE假設文檔以及不依賴LLM的輕量規(guī)則重寫。每一類都有明確的適用場景用錯了地方還不如不重寫。2.1 Multi-Query擴展一路變多路召回率最先見效Multi-Query是最好上手、收益最穩(wěn)的一種。思路很簡單讓LLM基于原始query生成多個不同角度的檢索query每路query跑一次召回最后合并。舉個例子。用戶問“平臺支持哪些登錄方式”原始query在向量空間里只對應一個點。擴展成“郵箱登錄的說明”“手機驗證碼登錄方式”“第三方統(tǒng)一認證登錄流程”之后三路query分別落在文檔庫不同的語義區(qū)域召回互補性很強。我自己的測試里很多原本只能命中一兩篇相關(guān)文檔的問題用3路Multi-Query之后可以穩(wěn)定覆蓋到三四篇。提示詞我通常這樣寫請基于用戶的原始查詢生成3-5個從不同角度表述的搜索查詢用于在知識庫中檢索相關(guān)資料。 要求 1. 保留原始查詢中的專有名詞和實體 2. 每個查詢是獨立、完整的疑問句 3. 覆蓋同義改寫、具體化、場景化等不同角度 4. 如果原始查詢已經(jīng)清晰可以只輸出原始查詢的一個加強版本 5. 直接輸出JSON數(shù)組不要解釋。 原始查詢{query}N取多少我的經(jīng)驗是3-5路比較合適。中小規(guī)模知識庫里5路以上收益明顯遞減而且多路召回合并時的去重成本會上升。Multi-Query最適合query短、實體抽象、同一個意思有多種說法的場景。如果你的query本身已經(jīng)是精確的術(shù)語或編號比如“KT-2024-0912”這種就別多加精確匹配反而更快更準。2.2 子問題分解多跳問題拆成單跳再合流子問題分解針對的是復合型、多跳型問題。典型例子是“A人員的申請和B人員的申請在同一個流程里為什么狀態(tài)不一致”直接檢索這句話向量檢索大概率會召回流程狀態(tài)流轉(zhuǎn)的通稿但回答不了“為什么不一致”。我的做法是先讓LLM判斷能否拆分、怎么拆再吐出一個結(jié)構(gòu)化的拆分結(jié)果。提示詞核心部分判斷下面的用戶問題是否可以拆分為多個可以獨立檢索的子問題。 如果可以輸出JSON數(shù)組每個元素包含 - sub_query獨立的檢索問句 - reason為什么需要這個信息 如果無法拆分直接輸出原始問題。 問題{query}上面那個例子拆出來就是三條A人員申請的流程狀態(tài)流轉(zhuǎn)規(guī)則是什么B人員申請的流程狀態(tài)流轉(zhuǎn)規(guī)則是什么同一流程下不同人員狀態(tài)不一致通常由哪些原因?qū)е?。三路檢索分別命中不同文檔生成時再按主問題匯總。這里有個工程細節(jié)值得注意子問題并行檢索通常更快但有些子問題之間存在依賴。比如先要知道“日志文件在哪個目錄”才能檢索“怎么分析這個目錄里的日志”。我的做法是給子問題拆解加一個最大深度限制默認兩層如果第一輪檢索到的新文檔里沒有足夠信息再基于已有信息補一次子問題檢索。拆解不是越細越好拆到單個子問題無法獨立檢索就該停。2.3 Step-Back抽象先抽象一層再回到細節(jié)Step-Back和Multi-Query的區(qū)別在于方向。Multi-Query是橫向撒網(wǎng)Step-Back是縱向抽象先從具體問題退到一個更一般化的問題檢索原理性、概念性文檔再回到原始細節(jié)問題補一輪精確召回。我遇到的一個真實場景是用戶問“為什么我們切換模型之后首字延遲變高了”。直接檢索“首字延遲 切換模型”效果很差因為文檔里根本沒有這么具體的組合描述。Step-Back之后變成“LLM服務延遲評估有哪些指標、模型切換對推理性能的影響因素有哪些”一下子就命中了方法論文檔。拿到這些背景之后再拿原始query做第二輪精確檢索兩輪結(jié)果合并給生成答案完整度明顯更高。Step-Back的prompt不用太復雜請把下面的具體問題改寫成一個更一般化、更抽象的搜索問題 以便在技術(shù)知識庫中找到相關(guān)背景知識。 保持問題的核心主題不變抽象到領(lǐng)域概念或方法論層面。 問題{query}這個策略最適合那種“問得太具體、背后依賴領(lǐng)域概念文檔”的場景。但如果你的知識庫里只有操作手冊、沒有原理文檔Step-Back收益會比較有限因為它檢索的是抽象概念層。另外要注意Step-Back通常不能單獨用它產(chǎn)出的抽象query結(jié)果更偏背景資料需要再配合原始query的精確檢索兩輪結(jié)果一起喂給生成器才完整。2.4 HyDE用“假設答案”去撞向量相似度HyDE的思路很有意思讓LLM不檢索就先生成一個假設性答案然后用這個假設答案的embedding去檢索相關(guān)文檔。它跟Multi-Query、Step-Back不一樣后兩者改的是“問法”HyDE改的是“和檢索目標之間的句子形態(tài)”。為什么有效用戶query往往很短短句的embedding和長文檔的embedding天然有形態(tài)差。但LLM生成的假設回答是一段完整的、類似文檔口吻的文字在向量空間里和真實文檔距離更近。等于先把用戶意圖“翻譯”成文檔的語言再去做相似度匹配。但必須清晰一點HyDE的假設答案是不可信的內(nèi)容它只用來做檢索的key絕對不能混進生成階段的上下文。我讓LLM按“如果我在文檔里看到這個問題的答案內(nèi)容可能會這樣寫”的口吻生成一段100-150字的段落然后取它的embedding走一遍召回。實測下來HyDE對概念性、綜述性問題效果明顯對強實體、強事實型查詢反而可能幫倒忙。比如用戶問“今年預算審批截止日期”假設答案可能寫出一個錯誤的日期把檢索結(jié)果帶偏。這類查詢用輕量關(guān)鍵詞擴展更穩(wěn)。所以在我的方案里HyDE不是默認開啟的而是只針對檢測到“綜述性、比較性、概念解釋”意圖時才啟用。2.5 輕量級重寫不調(diào)LLM也一樣能救召回不是所有場景都值得為查詢重寫多調(diào)一次LLM。數(shù)據(jù)量大、延遲敏感、離線批處理成本受限的時候規(guī)則與NER結(jié)合的輕量重寫方案同樣有效。我能落地的輕量重寫包括同義詞詞典替換比如“卡頓”替換為“延遲 性能問題”語氣詞清理“咋”“呢”“那個”直接刪除實體與縮寫標準化把“權(quán)限系統(tǒng)V2”這種說法歸一成標準寫法基于對話歷史的指代消解先用規(guī)則把“它”“這個功能”替換成前文實體。這套做法的好處是零幻覺、穩(wěn)定、可控、幾乎沒有額外延遲缺點是覆蓋面窄同義詞詞典需要長期維護。實際生產(chǎn)里我更推薦“兩級重寫”第一級用規(guī)則先清理和標準化第二級只對規(guī)則處理不了的復雜query調(diào)用LLM做擴展或分解。這樣既控制成本又保留了LLM重寫的彈性。很多人一上來就上LLM重寫忽略了規(guī)則層導致每一條query都多一次模型調(diào)用成本上去不說那些本來很清晰的query反而被無謂地改了一遍。3. 重寫鏈路嵌入RAG管線后的工程改造點策略選好后重寫不是一個獨立模塊它會牽動檢索、合并、生成、緩存一整條鏈路。這里說幾個我改管線時踩過的坎。3.1 多路召回合并RRF與去重要一起做多查詢和子問題分解都會產(chǎn)生多路召回于是合并策略就變成了一個不能繞開的問題。最簡單直覺是把各路分數(shù)相加排序?qū)崪y會發(fā)現(xiàn)行不通不同路線的分數(shù)分布差異巨大embedding的余弦相似度和稀疏檢索的score根本不可比直接相加等于默認它們權(quán)重相同。我用的是RRFReciprocal Rank Fusion加去重。RRF不看原始分數(shù)只看文檔在每路結(jié)果里的排名def rrf_fusion(rankings, k60): scores {} for ranking in rankings: for rank, doc_id in enumerate(ranking): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)k取60是常見默認值實驗效果也比較穩(wěn)。RRF天然對“多路里都排得靠前”的文檔友好這正是我們想要的結(jié)果一個文檔如果在三路query里都出現(xiàn)在前10它大概率真的相關(guān)。但RRF不去重。多路query本身語義接近召回的文檔大量重疊我會先做文本歸一化后的精確去重再用embedding相似度做一遍相似去重閾值大概在0.9左右。不然高頻重疊內(nèi)容會擠占上下文窗口生成時全在復讀同一段話。合并之后的排序結(jié)果還會再進一輪重排模型但那是另一個話題這里不多展開。3.2 重寫后的query拿去檢索還是也進生成這是我在項目里被反復問到的問題。我的實驗結(jié)論是重寫query應當參與檢索但生成階段要以原始query為主重寫query為輔。如果只用重寫query做生成輸入風險是LLM給出一個改得面目全非的query答案跟著跑偏。我有一次把“怎么申請服務器資源”重寫成了“服務器資源申請流程和審批細則”檢索沒問題但生成回答變成了審批細則科普偏離用戶真正想要的操作步驟。這種“檢索對了、回答偏了”的case端到端體驗非常奇怪。折衷方案是生成提示詞里同時放“原始query”和“重寫后的query”并明確告訴模型“優(yōu)先按原始問題回答重寫問題僅作為補充上下文”。這樣既享受了重寫帶來的更全召回又不會被重寫方向帶偏。這個方案在評測里忠實度分數(shù)比“只喂重寫query”高出一截。3.3 延遲、成本與兜底策略查詢重寫本質(zhì)上是多了一次LLM調(diào)用延遲和成本必須納入設計。延遲敏感的線上服務我的做法是“異步重寫加兜底”請求進來先用原始query馬上跑一版召回同時后臺把query送去重寫重寫結(jié)果回來后再做一輪補充召回兩輪結(jié)果合并。用戶首包延遲幾乎不增加只有最終答案會因為補召回稍微變好。如果重寫環(huán)節(jié)超時或者失敗直接放棄補充召回絕不阻塞主流程。這個兜底邏輯一定要寫死否則重寫一旦抖動整個RAG服務都會跟著雪崩。成本控制上重寫任務比生成簡單沒必要每次都上大模型。一個專門蒸餾的小模型完全夠用我自己用7B量級模型跑重寫質(zhì)量和100B以上模型在生產(chǎn)場景里差異很小主要差距在復雜子問題分解上。另外重寫結(jié)果可以做緩存相同query或者高相似度query直接命中緩存可以省掉大量重復調(diào)用。對于企業(yè)知識庫這類query分布相對穩(wěn)定的場景緩存命中率能到三成以上成本優(yōu)化效果很明顯。4. 從指標到上線查詢重寫的評測方法與排坑心得查詢重寫很容易陷入“體感有效但說不上來有效在哪”的狀態(tài)。我后來用了一套兩段式評測一段看檢索層一段看端到端答案質(zhì)量兩邊一起定策略。只做檢索層評測容易自嗨只做端到端評測又很難定位問題出在重寫還是生成。4.1 檢索層指標命中率、Recallk與MRR評測檢索層需要先準備一組query和對應的gold文檔。從線上日志抽幾百條真實query人工標注每條query應該在知識庫里命中哪些文檔然后分別跑三條鏈路原始query、重寫后query單路、重寫后多路召回合并。關(guān)注三個指標。命中率看gold文檔有沒有進top kRecallk看gold文檔在top k里的覆蓋率MRR看gold文檔首次出現(xiàn)的位置它比命中率更能反映排序質(zhì)量。為了說清楚趨勢我放一張自己項目里整理出來的示意數(shù)據(jù)數(shù)字不一定適用于你的場景但趨勢是典型的鏈路/策略命中率Recall3MRR適用query類型原始query直接檢索52%0.410.38全量Multi-Query三路擴展68%0.550.47短query、口語化子問題分解76%0.620.53復合/多跳問題Step-Back抽象61%0.480.44概念依賴型問題HyDE假設文檔58%0.460.42綜述/概念型問題關(guān)鍵是要分場景看而不是只看整體均值。整體均值會掩蓋“這個策略只對某類query有效”的事實。我建議按query類型分桶統(tǒng)計否則你會在一個不合適的桶里得出錯誤結(jié)論。比如子問題分解在全量query上平均提升有限但在復合問題子集上優(yōu)勢拉開得非常大。4.2 端到端評測答案質(zhì)量才是最終裁判檢索指標好不代表最終答案好。我見過命中率從50%拉到70%、但用戶主觀回答滿意度反而下降的情況原因是多路召回引入了大量相關(guān)但不關(guān)鍵的片段生成模型被碎片信息帶偏。所以端到端評測不可省。我從業(yè)務側(cè)收集了一條200條的評估集每條包含query、gold答案和“策略標簽”比如這個query適合Multi-Query還是適合Step-Back。評測時用LLM裁判按相關(guān)性、忠實度、完整性三項打分每批再抽20%做人工復核防止LLM裁判自帶偏好。LLM裁判的絕對分數(shù)參考價值有限但同一批數(shù)據(jù)跑不同鏈路之后的相對差異可信度還是夠的。這里必須提醒一點單獨看“用重寫平均分更高”不夠我會額外看“重寫后分數(shù)下降的query”有多少也就是負面率。正向率和負面率一起看才公平。如果一個策略把某些query的分從9拉到10同時把另外一些從7拉到4那負面樣本里的問題必須優(yōu)先修。與其追求平均分最高不如追求“沒有明顯變差的case”。4.3 上線后被現(xiàn)實教育出來的三個坑最后說三個我實際踩過、修復成本都不低的坑希望你能繞開。第一個坑是實體丟失。我在重寫prompt里沒寫“不得替換專有名詞和縮寫”結(jié)果LLM把“ES”改寫成“搜索引擎”把“Grafana”改寫成“監(jiān)控面板”檢索結(jié)果全部偏到通用文檔上。修復方案就是我在1.3節(jié)寫的四條鐵律把它寫死在prompt頂層。這個坑之所以隱蔽是因為單看重寫后的query每一句都通順合理只有對照檢索結(jié)果才發(fā)現(xiàn)路徑全歪了。第二個坑是重復片段擠占上下文。多路召回合并之后忘了去重top 5里出現(xiàn)三篇內(nèi)容幾乎一樣的文檔整個上下文窗口有一半在復讀。修復方案是RRF排序之前先做一輪精確去重和相似去重。這個坑在Multi-Query場景里特別容易出因為三路query本身講的是同一件事的不同說法。第三個坑是子問題分解完成后丟失問題間關(guān)聯(lián)。子問題各自檢索都很精準但生成階段模型只看到三堆子答案回答邏輯是斷的。修復方案是生成提示詞中同時重述完整主問題然后把子問題之間的邏輯關(guān)系按順序?qū)懬宄屔善髦雷约涸诨卮鹨粋€整體性問題。這個問題我一開始沒意識到直到有用戶反饋“你們回答里每個部分都對但連起來不像在回答我的問題”才回頭去看生成鏈路。另外一個小習慣上線后持續(xù)抽檢線上query的重寫結(jié)果看有沒有出現(xiàn)“把query改得更糟”的case。這類case攢下來反向補充到prompt約束或規(guī)則層里重寫效果會越滾越好。我每個月會導出一次重寫日志隨機抽200條人工過一遍成本不高但能攔住好多模型升級后突然冒出來的行為漂移。最后留一個我沿用了很久的小習慣重寫prompt里永遠放一句“如果原始查詢已經(jīng)足夠清晰請原樣輸出”。這一句讓負面改寫率掉了不少算是全篇里性價比最高的一行字。查詢重寫不是一次性做完就結(jié)束的組件它的策略組合會跟著知識庫內(nèi)容、用戶query分布的變化而漂移。我目前的存量配置是規(guī)則清理打底Multi-Query做通用擴展復雜query走子問題分解概念依賴型query補一次Step-BackHyDE只在綜述型場景下單獨開啟。如果你也想給RAG提上限我建議別一上來就鋪開所有策略先跑通Multi-Query加RRF加去重這一組最小閉環(huán)拿線上真實query對比命中率。確認有增益之后再逐步加入別的策略——每一步都能單獨回滾才不會改亂了沒法收場。