問(wèn)答的工程實(shí)踐)
1. 從RAG到RIG為什么先生成再檢索救不了多跳問(wèn)題上個(gè)月我們?cè)趦?nèi)部知識(shí)庫(kù)上線的RAG問(wèn)答系統(tǒng)被業(yè)務(wù)方連續(xù)問(wèn)倒了三次。三次都栽在同一類問(wèn)題上A方案和B方案沖突時(shí)合同模板里哪一條優(yōu)先這個(gè)報(bào)錯(cuò)在舊版本文檔里有沒(méi)有對(duì)應(yīng)處理——單次檢索抓回來(lái)的文檔里根本沒(méi)有完整答案。我當(dāng)時(shí)的第一反應(yīng)是加大top_k、擴(kuò)窗口長(zhǎng)度結(jié)果只是把更多噪音塞進(jìn)了上下文答得更差。后來(lái)我換了個(gè)思路與其讓系統(tǒng)替模型把檢索提前做完不如讓模型自己在生成過(guò)程中隨時(shí)喊我需要查資料。這就是RIGRetrieval-Interleaved Generation檢索交織生成的核心想法而我用來(lái)落地這個(gè)想法的開源框架就是OpenRig。1.1 標(biāo)準(zhǔn)RAG的三個(gè)死穴標(biāo)準(zhǔn)的RAG pipeline大家都熟問(wèn)題進(jìn)來(lái)先向量檢索Top-K文檔塞進(jìn)上下文再讓模型生成。這個(gè)流程對(duì)單點(diǎn)事實(shí)查詢很管用比如公司的報(bào)銷上限是多少。但對(duì)稍微復(fù)雜的場(chǎng)景它有三個(gè)天生缺陷。第一個(gè)是檢索時(shí)機(jī)問(wèn)題。檢索發(fā)生在生成之前模型沒(méi)有后悔藥。問(wèn)題本身表述模糊的時(shí)候模型在生成中途需要補(bǔ)充的信息一次性檢索根本覆蓋不到。第二個(gè)是多跳問(wèn)題。很多真實(shí)業(yè)務(wù)問(wèn)題要先查事實(shí)A再根據(jù)A查事實(shí)B最后才能推出結(jié)論。單次檢索沒(méi)辦法做到查完A之后帶著A去查B。第三個(gè)是上下文污染。為了覆蓋可能性我們把top_k調(diào)大結(jié)果不相關(guān)文檔一起進(jìn)來(lái)模型的注意力被稀釋反而把正確信息淹沒(méi)掉。1.2 Agentic路線靈活但不可控既然單次檢索不夠很自然的想法就是上Agent讓模型自己決定調(diào)用檢索工具用ReAct式的循環(huán)拿到結(jié)果再繼續(xù)。這個(gè)方向的能力上限確實(shí)高多跳、總結(jié)、推理都能做。但落地之后你會(huì)發(fā)現(xiàn)另一個(gè)麻煩不可控。模型會(huì)為了一個(gè)只需要查一次的問(wèn)題連續(xù)調(diào)用三次工具會(huì)在兩個(gè)證據(jù)矛盾時(shí)反復(fù)橫跳不收斂會(huì)把上下文窗口塞滿歷史步驟導(dǎo)致token爆炸。每一步都要設(shè)計(jì)終止條件、防重入機(jī)制、時(shí)間限制調(diào)優(yōu)成本高得嚇人。我見(jiàn)過(guò)不少團(tuán)隊(duì)在這個(gè)路子上投入兩三個(gè)月最后產(chǎn)出的系統(tǒng)在評(píng)測(cè)集上還行一上真實(shí)流量就各種超時(shí)和循環(huán)。RIG走的是中間路線。它不像RAG那樣檢索一次就拉倒也不像Agent那樣把完整的工具調(diào)用權(quán)交給模型。它的設(shè)計(jì)原則很樸素模型在生成時(shí)如果發(fā)現(xiàn)自己缺信息就發(fā)出一個(gè)檢索信號(hào)框架收到信號(hào)后去檢索把結(jié)果追加回上下文模型繼續(xù)生成。檢索的決定權(quán)在模型手里但檢索的執(zhí)行是確定性的、單次的、可觀測(cè)的不會(huì)像Agent那樣產(chǎn)生自由發(fā)揮的工具調(diào)用序列。1.3 OpenRig到底解決什么問(wèn)題OpenRig這個(gè)名字Open是開源Rig在這里不是機(jī)器配置的意思而是RIG模式的動(dòng)詞化——它就是一個(gè)把檢索交織生成這套模式工程化的框架。它解決的是一類很具體的問(wèn)題你的業(yè)務(wù)需要多跳知識(shí)推理但又承受不了Agent系統(tǒng)的復(fù)雜度和不確定性。我把它用在合同條款問(wèn)答和故障診斷兩個(gè)場(chǎng)景上效果比預(yù)想好。后面我會(huì)從原理、部署、調(diào)優(yōu)、踩坑四個(gè)角度展開把我在生產(chǎn)環(huán)境里跑OpenRig的完整經(jīng)驗(yàn)整理出來(lái)。如果你正在做知識(shí)庫(kù)問(wèn)答、智能客服、文檔輔助決策這類系統(tǒng)這篇文章應(yīng)該能幫你少走不少?gòu)澛贰?. OpenRig的核心設(shè)計(jì)與工作原理OpenRig的整體架構(gòu)不復(fù)雜但設(shè)計(jì)上有個(gè)關(guān)鍵選擇它怎么知道模型什么時(shí)候需要檢索。這一節(jié)把它的執(zhí)行鏈路拆開講清楚。2.1 一次完整的RIG執(zhí)行鏈路一次OpenRig請(qǐng)求完整走下來(lái)是這樣的用戶query進(jìn)入會(huì)話管理器初始上下文 system prompt 歷史對(duì)話 query。模型開始流式生成逐token輸出。模型在某個(gè)位置輸出一個(gè)檢索標(biāo)記retrieval marker。這個(gè)標(biāo)記可以是特殊token也可以是約定的文本格式OpenRig默認(rèn)支持|retrieve|這種特殊token也支持函數(shù)調(diào)用風(fēng)格的JSON輸出。解析器捕獲標(biāo)記暫停生成提取標(biāo)記中攜帶的檢索query模型自己生成的查詢?cè)~。檢索器執(zhí)行檢索返回Top-K文檔。結(jié)果按固定模板格式化追加到上下文中模型繼續(xù)生成。重復(fù)步驟2-6直到模型輸出結(jié)束標(biāo)記或達(dá)到max_iterations上限。值得注意的是模型發(fā)出的檢索query不是用戶原話。模型在生成過(guò)程中已經(jīng)產(chǎn)生了對(duì)問(wèn)題的中間理解所以它生成的檢索詞往往比原始query更精準(zhǔn)。比如用戶問(wèn)這個(gè)錯(cuò)誤在哪個(gè)版本被修復(fù)模型生成到一半發(fā)現(xiàn)需要版本號(hào)它會(huì)自己生成v2.3.1 changelog bug fix這樣的檢索query。2.2 檢索標(biāo)記與生成循環(huán)的實(shí)現(xiàn)細(xì)節(jié)標(biāo)記機(jī)制是整個(gè)框架的靈魂。OpenRig采用了一個(gè)很務(wù)實(shí)的方案不用特殊token做訓(xùn)練而是用提示詞約束 生成時(shí)解析。也就是說(shuō)框架在system prompt里明確告訴模型如果你需要補(bǔ)充信息輸出|retrieve|需要查詢的內(nèi)容|endofretrieve|推理時(shí)解析器去匹配這個(gè)結(jié)構(gòu)。這樣做的好處是不需要對(duì)模型做任何微調(diào)任何支持復(fù)雜system prompt的模型都能跑。代價(jià)是模型偶爾不遵守格式或者在不該檢索的時(shí)候亂發(fā)信號(hào)。這個(gè)坑我后面專門講。底層循環(huán)的偽代碼邏輯大致是這樣def run(question, config): context build_initial_context(question) iterations 0 while iterations config.max_iterations: # 流式生成并監(jiān)聽檢索標(biāo)記 for chunk in llm.stream(context, stop_markersconfig.stop_markers): if parser.detect_retrieve(chunk.text): retrieve_query parser.extract_query(chunk.text) docs retriever.search(retrieve_query, top_kconfig.top_k) context append_docs(context, docs, chunk.prefix_text) break else: # 沒(méi)有檢索標(biāo)記說(shuō)明生成正常結(jié)束 return context iterations 1 return context # 達(dá)到迭代上限強(qiáng)制返回這個(gè)循環(huán)最關(guān)鍵的參數(shù)是max_iterations。設(shè)大了模型可以多跳幾次但延遲和token開銷跟著漲設(shè)小了多跳問(wèn)題又解決不了。我生產(chǎn)環(huán)境里一般設(shè)3極少有查詢需要超過(guò)3次檢索。2.3 與Agent框架相比OpenRig省掉了什么用過(guò)LangGraph或自研ReAct框架的人應(yīng)該有同感Agent方案的復(fù)雜度大頭在工具注冊(cè)、狀態(tài)管理、循環(huán)控制、終止條件。OpenRig把這些都砍掉了換來(lái)的是一個(gè)更窄的邊界。維度單次RAGOpenRig (RIG)Agent (ReAct)檢索觸發(fā)方式生成前一次性生成中按需多次工具調(diào)用循環(huán)模型自由度低中高是否有多跳能力弱強(qiáng)強(qiáng)上下文增長(zhǎng)固定每次檢索新增步驟日志累積調(diào)試難度低中高可預(yù)測(cè)性高中高低這個(gè)表格不是想說(shuō)Agent不好而是想說(shuō)明它們解決的問(wèn)題不一樣。如果你的需求是讓模型自主決定怎么完成任務(wù)Agent是對(duì)的如果你的需求是讓模型在回答知識(shí)類問(wèn)題時(shí)自己補(bǔ)檢索OpenRig這種RIG框架更劃算。我團(tuán)隊(duì)里現(xiàn)在有部分簡(jiǎn)單需求直接先用OpenRig跑跑不動(dòng)了再升級(jí)到Agent這個(gè)成本曲線平緩很多。3. 本地部署OpenRig從零跑通一個(gè)多跳問(wèn)答服務(wù)3.1 環(huán)境準(zhǔn)備與依賴選型OpenRig對(duì)運(yùn)行環(huán)境的要求不算苛刻。我的部署機(jī)器是一臺(tái)32核CPU、128GB內(nèi)存、單張RTX 4090的服務(wù)器LLM推理用vLLM起了一個(gè)Qwen2.5-32B-Instruct服務(wù)向量庫(kù)用Milvus檢索器混用了向量檢索和BM25。依賴清單大致如下Python 3.10一個(gè)OpenAI兼容的LLM推理服務(wù)vLLM、TGI、Ollama都行向量數(shù)據(jù)庫(kù)Milvus / Qdrant / Chroma均可小規(guī)模用Chroma最省事OpenRig本體pip install openrig安裝完成之后先確認(rèn)LLM服務(wù)能用OpenAI SDK調(diào)用curl http://localhost:8000/v1/models \ -H Authorization: Bearer EMPTY如果返回模型列表說(shuō)明推理服務(wù)就緒可以進(jìn)入下一步。3.2 配置向量庫(kù)和檢索器OpenRig的檢索接口設(shè)計(jì)成可插拔。你只需要實(shí)現(xiàn)一個(gè)search(query, top_k)返回文檔列表的函數(shù)或者直接用內(nèi)置的Milvus適配器。我習(xí)慣把配置寫在一個(gè)YAML里統(tǒng)一管理# openrig.yaml model: base_url: http://localhost:8000/v1 name: Qwen2.5-32B-Instruct max_tokens: 512 retriever: type: milvus host: localhost port: 19530 collection: contracts embedding_model: BAAI/bge-large-zh-v1.5 top_k: 5 score_threshold: 0.35 rig: max_iterations: 3 marker_style: special_token # 可選 special_token / json append_format: evidence這里有個(gè)我踩過(guò)坑的參數(shù)score_threshold。設(shè)置太低垃圾文檔大量進(jìn)入上下文設(shè)置太高正確的檢索被過(guò)濾掉。我建議先跑一批真實(shí)查詢把相似度分?jǐn)?shù)分布打出來(lái)再定閾值不要憑感覺(jué)拍腦袋。3.3 啟動(dòng)服務(wù)并驗(yàn)證檢索觸發(fā)配置寫好后啟動(dòng)服務(wù)只需要一個(gè)Python腳本。OpenRig對(duì)外暴露的核心對(duì)象是RigSessionfrom openrig import RigSession, RigConfig config RigConfig.from_yaml(openrig.yaml) session RigSession(config) question 供應(yīng)商延遲交付合同里有沒(méi)有自動(dòng)順延交貨期的條款順延期間的質(zhì)量責(zé)任怎么算 answer, trace session.ask(question) print(answer) print(檢索過(guò)程) for step in trace.retrieval_steps: print(f第{step.round}輪檢索: {step.query}) print(f命中文檔: {[d.title for d in step.docs]})跑完之后如果一切正常日志里能看到多次檢索事件每次檢索的query都是模型在生成過(guò)程中自動(dòng)產(chǎn)出的。第一次跑通的時(shí)候我還挺興奮因?yàn)樗_實(shí)做到了模型查到一半主動(dòng)說(shuō)我還需要看另一個(gè)條款。不過(guò)這里有個(gè)現(xiàn)象值得注意模型第一次生成的檢索query往往和用戶原話高度重合。真正顯示RIG價(jià)值的是第二輪、第三輪檢索那時(shí)候的query會(huì)帶著上一輪結(jié)果的上下文信息比如順延條款中沒(méi)有質(zhì)量責(zé)任說(shuō)明查找違約條款中關(guān)于交付延期責(zé)任的規(guī)定。這種帶有推理痕跡的檢索詞是普通RAG給不了的。4. 關(guān)鍵參數(shù)調(diào)優(yōu)與評(píng)測(cè)方法4.1 迭代次數(shù)和檢索閾值的權(quán)衡max_iterations是最需要認(rèn)真調(diào)的參數(shù)。從直覺(jué)上說(shuō)多跳問(wèn)題自然需要多次檢索但每一輪檢索都會(huì)帶來(lái)三個(gè)成本延遲、token、上下文污染風(fēng)險(xiǎn)。我做過(guò)一組實(shí)驗(yàn)用合同問(wèn)答的50條測(cè)試問(wèn)題在max_iterations分別為1、2、3、5時(shí)評(píng)測(cè)max_iterations正確率平均延遲平均token消耗148%2.1s870271%3.4s1350379%4.7s2050578%8.2s4100數(shù)據(jù)看得很清楚3輪之后正確率不再上升成本和延遲卻幾乎翻倍。這就是典型的收益遞減曲線。我現(xiàn)在的建議是新業(yè)務(wù)一律從3起跳用評(píng)測(cè)數(shù)據(jù)說(shuō)話不要為了省延遲砍到2除非你的業(yè)務(wù)確認(rèn)沒(méi)有多跳需求。score_threshold的調(diào)法前面提到過(guò)另一種更高效的方案是加一個(gè)reranker。OpenRig支持在檢索器后面掛rerank層用bge-reranker-v2-m3把Top-K初篩結(jié)果再排一遍只保留排序前2。加上reranker之后我的正確率從79%提到86%延遲增加不到0.5秒性價(jià)比很高。4.2 延遲預(yù)算一次多跳查詢到底花了多少時(shí)間很多團(tuán)隊(duì)最終不是被正確率勸退的是被延遲勸退的。這里給一個(gè)真實(shí)的耗時(shí)拆解讓你心里有底LLM首token生成1.5s32B模型40904-bit量化第一輪檢索和嵌入0.3s第二輪生成1.8s第二輪檢索0.3s最終生成1.2s總計(jì)約5.1s對(duì)一個(gè)內(nèi)部知識(shí)庫(kù)問(wèn)答來(lái)說(shuō)5秒是可接受的但如果你要做面向C端的實(shí)時(shí)客服就得想辦法壓。我試過(guò)幾條路徑用更小的模型14B做前三跳生成最后用32B匯總把embedding模型換成更快的小模型把Milvus索引換成內(nèi)存態(tài)。綜合下來(lái)延遲能壓到3秒左右正確率只降2%。4.3 用評(píng)測(cè)腳本量化收益OpenRig自稱是面向檢索增強(qiáng)生成的工程框架所以它很重視評(píng)測(cè)閉環(huán)。項(xiàng)目里帶了一個(gè)openrig eval命令可以用起來(lái)。它支持兩種模式一是跑公開多跳數(shù)據(jù)集比如HotpotQA的子集二是跑你自己的JSONL標(biāo)注集。我強(qiáng)烈建議用后者因?yàn)楣_數(shù)據(jù)集和業(yè)務(wù)分布差異太大。評(píng)估數(shù)據(jù)格式很簡(jiǎn)單JSONL里每行一個(gè)問(wèn)題、標(biāo)準(zhǔn)答案、以及可選的支持文檔列表openrig eval \ --config openrig.yaml \ --dataset ./data/contract_eval.jsonl \ --metric accuracy retrieval_precision token_overhead輸出是一張表格除了最終答案正確率還會(huì)報(bào)告每次檢索的有效率——也就是這次檢索到的文檔是否真的支持了最終答案。這個(gè)指標(biāo)很有用它能把模型亂檢索但碰巧答對(duì)的情況暴露出來(lái)。我在初版配置上跑出來(lái)retrieval_precision只有42%說(shuō)明模型有一半的檢索是沒(méi)必要的后面通過(guò)修改system prompt才把它拉到75%。5. 實(shí)測(cè)中踩過(guò)的坑與規(guī)避方案5.1 模型不配合檢索標(biāo)記時(shí)有時(shí)無(wú)用OpenRig第一個(gè)星期最讓我頭疼的問(wèn)題是模型經(jīng)常不輸出檢索標(biāo)記。明明上下文里沒(méi)有答案它還是硬編一個(gè)。排查之后發(fā)現(xiàn)原因在system prompt的寫法上。OpenRig默認(rèn)的prompt只寫了如果需要更多信息請(qǐng)使用檢索標(biāo)記。但指令微調(diào)模型對(duì)如果需要這種條件型指令理解得不夠堅(jiān)決。我把提示詞改成強(qiáng)約束版本效果立竿見(jiàn)影提示 檢索觸發(fā)條件要寫硬規(guī)則不要寫建議。例如你必須檢查當(dāng)前上下文是否包含足夠信息回答問(wèn)題。如果信息不充分你必須先輸出檢索標(biāo)記再生成最終回答。禁止在信息不充分時(shí)直接回答。另外不同模型的格式偏好差別很大。Qwen和Yi對(duì)特殊token風(fēng)格接受度高GPT系和Claude系更習(xí)慣JSON函數(shù)調(diào)用。如果發(fā)現(xiàn)模型經(jīng)常格式錯(cuò)誤把marker_style從special_token換成json解析穩(wěn)定性和生成遵守率都會(huì)好不少。5.2 上下文膨脹失控多輪檢索意味著多輪文檔追加上下文長(zhǎng)度是線性甚至超線性增長(zhǎng)的。第一輪檢索塞5個(gè)文檔第二輪又塞5個(gè)加上前一輪的文檔還在上下文很快就幾千token了。上下文一長(zhǎng)模型注意力開始渙散后面檢索的結(jié)果反而覆蓋了前面有用的證據(jù)。我的對(duì)策是控制每輪追加的文檔數(shù)量和對(duì)舊證據(jù)做壓縮。具體做法top_k設(shè)5但經(jīng)reranker后只保留2個(gè)結(jié)果第二輪開始把第一輪的原始文檔替換成模型生成的簡(jiǎn)短摘要。這樣既保留了證據(jù)鏈又不讓上下文無(wú)限膨脹。OpenRig提供了append_format參數(shù)可以選full或summarysummary模式下框架會(huì)自動(dòng)要求模型對(duì)上一輪證據(jù)做三句話以內(nèi)的總結(jié)。5.3 檢索到壞文檔導(dǎo)致越繞越遠(yuǎn)RIG一個(gè)隱蔽的失敗模式是模型在錯(cuò)誤證據(jù)的引導(dǎo)下生成一個(gè)同樣錯(cuò)誤的檢索query然后一路錯(cuò)下去。比如最初檢索到一份已廢止的合同條款模型基于它繼續(xù)追問(wèn)檢索出的更多文檔都是圍繞舊版的最終答案完全跑偏。這個(gè)問(wèn)題單靠OpenRig參數(shù)解決不了得從數(shù)據(jù)側(cè)動(dòng)手。我做了三件事第一在向量庫(kù)里給文檔加上生效/失效元數(shù)據(jù)檢索時(shí)過(guò)濾失效文檔第二對(duì)高沖突域比如合同版本單獨(dú)建索引避免新舊版本混在一起第三在提示詞里加一條如果發(fā)現(xiàn)檢索結(jié)果與當(dāng)前上下文證據(jù)沖突優(yōu)先信任最新的生效文檔并說(shuō)明沖突。第三點(diǎn)尤其重要——它讓模型對(duì)自己產(chǎn)生的證據(jù)鏈有批判意識(shí)而不是被上一輪結(jié)果牽著走。除了以上三個(gè)大坑還有個(gè)評(píng)測(cè)期容易踩的隱蔽問(wèn)題模型在缺少檢索標(biāo)記的情況下偶爾會(huì)假裝用了檢索。它生成的內(nèi)容里直接寫根據(jù)相關(guān)資料顯示但OpenRig的trace里根本沒(méi)有檢索事件。這本質(zhì)上是模型幻覺(jué)的變體。排查方法也很簡(jiǎn)單檢查trace里每一步的證據(jù)來(lái)源凡是最終答案引用了不存在于證據(jù)鏈的內(nèi)容都算失敗樣例。6. 什么樣的業(yè)務(wù)適合上OpenRig跑了大半年之后我對(duì)這個(gè)框架的邊界有了比較清晰的認(rèn)識(shí)。OpenRig不是萬(wàn)金油但也不是小眾玩具。最適合它的場(chǎng)景有幾類內(nèi)部知識(shí)庫(kù)問(wèn)答用戶問(wèn)題經(jīng)常需要連續(xù)查多個(gè)知識(shí)點(diǎn)才能回答比如合同、規(guī)章制度、技術(shù)文檔。診斷式客服用戶報(bào)一個(gè)現(xiàn)象系統(tǒng)需要先查現(xiàn)象對(duì)應(yīng)的模塊再查該模塊的常見(jiàn)故障再查修復(fù)步驟。文檔輔助決策比如某個(gè)操作是否合規(guī)需要對(duì)比多個(gè)制度條款后得出結(jié)論。這幾類業(yè)務(wù)的共同點(diǎn)是問(wèn)題本身有結(jié)構(gòu)答案需要跨文檔拼接同時(shí)用戶能接受3到5秒的延遲。反過(guò)來(lái)如果你的業(yè)務(wù)是高頻簡(jiǎn)單問(wèn)答、低延遲強(qiáng)需求或者你的語(yǔ)料非常單一、一次檢索就有九成把握那老老實(shí)實(shí)用經(jīng)典RAG反而更好。OpenRig的價(jià)值恰好就是讓夠用和過(guò)度聰明之間多了一個(gè)中間檔。最后分享一個(gè)我個(gè)人的運(yùn)維習(xí)慣OpenRig跑起來(lái)之后一定要把它的trace日志接進(jìn)監(jiān)控。每次檢索事件、每輪query、每一跳的延遲都記錄下來(lái)。我后來(lái)發(fā)現(xiàn)很多系統(tǒng)變蠢的case都不是框架壞了而是embedding模型那側(cè)的數(shù)據(jù)沒(méi)更新或者新文檔進(jìn)了向量庫(kù)但score_threshold把它們?nèi)^(guò)濾掉了。有了trace日志這些問(wèn)題十分鐘就能定位。工具再智能也不如你手里有一條完整的證據(jù)鏈可查。