?多輪指代消解+云端語(yǔ)義向量+TXT章節(jié)切分實(shí)戰(zhàn))
本地 RAG 問(wèn)答做久了你會(huì)發(fā)現(xiàn)一個(gè)很尷尬的現(xiàn)象明明知識(shí)庫(kù)里的文檔寫得清清楚楚用戶問(wèn)一句它的參數(shù)是多少系統(tǒng)就開(kāi)始胡言亂語(yǔ)。問(wèn)題往往不在大模型本身而在于檢索環(huán)節(jié)——用戶這句話里的它到底指什么檢索器根本不知道。我前后折騰過(guò)好幾套本地 RAG 方案從最樸素的向量檢索到帶重排序的流水線踩過(guò)的坑基本都集中在三個(gè)地方多輪對(duì)話里的指代消解、向量化模型的選擇、以及 TXT 文檔的切分策略。這篇就把這三塊拆開(kāi)講透尤其是多輪指代消解 云端語(yǔ)義向量 TXT 章節(jié)切分這套組合拳是我實(shí)測(cè)下來(lái)在本地環(huán)境里性價(jià)比最高、最容易復(fù)現(xiàn)的方案。1. 為什么本地 RAG 的準(zhǔn)字最難做1.1 檢索不準(zhǔn)的三個(gè)真實(shí)來(lái)源很多人第一次搭 RAG思路很直接文檔切塊、向量化、存庫(kù)、檢索、拼 prompt。跑通 Demo 沒(méi)問(wèn)題但一上真實(shí)場(chǎng)景就露餡。我把檢索不準(zhǔn)的原因歸成三類這三類在本地環(huán)境里尤其明顯。第一類是查詢本身有歧義。多輪對(duì)話中用戶大量使用代詞和省略比如它支持多少并發(fā)那這個(gè)怎么配置上面說(shuō)的那個(gè)參數(shù)默認(rèn)值是多少。這些查詢單獨(dú)拿出來(lái)看向量化之后和知識(shí)庫(kù)里的任何一段都不像檢索器只能瞎猜。這是多輪 RAG 最致命的短板也是本文要重點(diǎn)解決的第一塊。第二類是向量語(yǔ)義表達(dá)不足。本地跑嵌入模型受限于顯存和算力往往選的是參數(shù)量很小的模型比如各種 mini、small 版本。這類模型對(duì)短查詢、專業(yè)術(shù)語(yǔ)、中英混排的語(yǔ)義捕捉能力有限導(dǎo)致語(yǔ)義相近的文檔排不到前面。這是第二塊要解決的。第三類是切分粒度不對(duì)。TXT 文檔沒(méi)有結(jié)構(gòu)信息如果按固定字符數(shù)硬切一個(gè)完整的章節(jié)可能被切成兩半檢索命中前半段卻丟了后半段的關(guān)鍵結(jié)論。這是第三塊要解決的。1.2 本地環(huán)境的能力邊界在哪先把預(yù)期擺正。本地 RAG 的優(yōu)勢(shì)是數(shù)據(jù)不出內(nèi)網(wǎng)、成本可控、可離線劣勢(shì)是算力和顯存有限。這意味著你不能指望本地跑一個(gè)幾百億參數(shù)的嵌入模型也不能指望本地大模型有云端那種推理能力。所以正確的思路是分工把重的語(yǔ)義理解交給云端嵌入 API把輕的編排和生成留在本地。云端語(yǔ)義向量服務(wù)按調(diào)用量計(jì)費(fèi)一次嵌入成本極低但語(yǔ)義質(zhì)量比本地小模型高一個(gè)檔次。而多輪指代消解這種需要結(jié)合上下文的邏輯可以在本地用規(guī)則 輕量模型完成不必上大模型。TXT 章節(jié)切分則是純工程問(wèn)題本地處理零成本。提示如果你的場(chǎng)景對(duì)數(shù)據(jù)出境極度敏感云端嵌入這條路走不通那就得接受本地小模型的效果折損或者自建嵌入服務(wù)。本文方案默認(rèn)你接受文本片段送云端做向量化這一前提。1.3 這套方案的整體數(shù)據(jù)流在動(dòng)手之前先把整條鏈路理清楚后面每一節(jié)都是在這條鏈路上做文章。用戶提問(wèn)進(jìn)來(lái)先經(jīng)過(guò)多輪指代消解模塊結(jié)合最近幾輪對(duì)話歷史把它這個(gè)那個(gè)替換成明確的實(shí)體輸出一個(gè)自包含的查詢Self-contained Query。這個(gè)查詢?cè)偎腿ピ贫苏Z(yǔ)義向量服務(wù)做嵌入得到查詢向量。然后用這個(gè)向量去本地向量庫(kù)檢索拿到 Top-K 候選片段。候選片段和改寫后的查詢一起拼進(jìn) prompt交給本地大模型生成答案。整條鏈路里指代消解決定了問(wèn)得對(duì)不對(duì)云端向量決定了找得準(zhǔn)不準(zhǔn)章節(jié)切分決定了切得全不全。三者缺一不可。2. 多輪指代消解讓它變成明確的實(shí)體2.1 指代消解到底在解決什么問(wèn)題指代消解Coreference Resolution在 NLP 里是個(gè)老話題但在 RAG 場(chǎng)景下我們要的不是學(xué)術(shù)意義上的完整消解而是一個(gè)更務(wù)實(shí)的目標(biāo)把依賴上下文的查詢改寫成不依賴上下文的獨(dú)立查詢。業(yè)內(nèi)常把這個(gè)動(dòng)作叫 Query Rewriting。舉個(gè)具體例子。第一輪用戶問(wèn)LangChain4j 的 Easy RAG 怎么配置第二輪問(wèn)它的默認(rèn)分塊大小是多少。如果直接把第二輪送去檢索它會(huì)被向量化成無(wú)意義的噪聲檢索結(jié)果大概率跑偏。但如果改寫成LangChain4j Easy RAG 的默認(rèn)分塊大小是多少檢索命中率立刻上來(lái)了。關(guān)鍵點(diǎn)在于改寫不是簡(jiǎn)單地把上一輪的主語(yǔ)拼過(guò)來(lái)而是要判斷當(dāng)前查詢里哪些詞是指代性的哪些是實(shí)體性的。指代性的詞需要替換實(shí)體性的詞要保留。2.2 三種改寫策略的取舍我在實(shí)際項(xiàng)目里試過(guò)三種策略各有適用場(chǎng)景。策略一規(guī)則替換。維護(hù)一個(gè)指代詞表它、他、這個(gè)、那個(gè)、該、此、上述、前面提到的……檢測(cè)到查詢里出現(xiàn)這些詞就把最近一輪對(duì)話里的核心實(shí)體填進(jìn)去。優(yōu)點(diǎn)是零成本、零延遲、完全本地缺點(diǎn)是遇到復(fù)雜指代比如它的第二個(gè)參數(shù)就抓瞎而且容易誤替換。策略二小模型改寫。本地跑一個(gè) 1B 到 3B 的指令微調(diào)模型專門做 Query Rewriting。給它一個(gè) prompt根據(jù)對(duì)話歷史把用戶最后一句話改寫成不依賴上下文的獨(dú)立問(wèn)題。優(yōu)點(diǎn)是能處理復(fù)雜指代缺點(diǎn)是要占顯存而且小模型改寫質(zhì)量不穩(wěn)定偶爾會(huì)把原意改歪。策略三規(guī)則 小模型兜底。先用規(guī)則快速判斷如果查詢里沒(méi)有指代詞直接放行零開(kāi)銷如果有指代詞再調(diào)用小模型改寫。這是我最終采用的方案兼顧了成本和效果。策略延遲顯存占用復(fù)雜指代處理推薦場(chǎng)景純規(guī)則替換極低無(wú)差對(duì)話簡(jiǎn)單、指代詞固定小模型改寫中1-3GB好對(duì)話復(fù)雜、算力充足規(guī)則小模型兜底低1-3GB好大多數(shù)本地場(chǎng)景2.3 規(guī)則層的實(shí)現(xiàn)細(xì)節(jié)規(guī)則層看起來(lái)簡(jiǎn)單但有幾個(gè)細(xì)節(jié)不注意就會(huì)翻車。第一指代詞檢測(cè)要區(qū)分詞性。它作為代詞要替換但其他里的他不能動(dòng)。中文沒(méi)有空格簡(jiǎn)單的字符串匹配會(huì)誤傷。我的做法是用分詞工具先切詞再在詞級(jí)別匹配指代詞表避免子串誤匹配。第二實(shí)體來(lái)源要選對(duì)。改寫時(shí)填入的實(shí)體應(yīng)該來(lái)自最近一輪的用戶查詢而不是上一輪的模型回答。因?yàn)槟P突卮鹄飳?shí)體太多容易填錯(cuò)。如果最近一輪用戶查詢里也沒(méi)有明確實(shí)體就往前再找一輪最多回溯三輪。第三改寫后要做校驗(yàn)。改寫完的查詢?nèi)绻L(zhǎng)度異常比如比原文短很多或者長(zhǎng)很多說(shuō)明改寫可能出錯(cuò)了這時(shí)候?qū)幙煞艞壐膶懹迷樵儥z索也不要送一個(gè)錯(cuò)誤的查詢進(jìn)去。# 規(guī)則層指代消解的核心邏輯示意 PRONOUNS {它, 他, 她, 這個(gè), 那個(gè), 該, 此, 上述, 其} def rewrite_by_rule(query, history, max_back3): tokens jieba.lcut(query) if not any(t in PRONOUNS for t in tokens): return query, False # 無(wú)指代直接放行 # 回溯最近幾輪用戶查詢找核心實(shí)體 entity None for turn in reversed(history[-max_back:]): if turn[role] user: entity extract_main_entity(turn[content]) if entity: break if not entity: return query, False # 找不到實(shí)體放棄改寫 rewritten query for p in PRONOUNS: rewritten rewritten.replace(p, entity) # 校驗(yàn)長(zhǎng)度變化過(guò)大則放棄 if len(rewritten) len(query) * 3 or len(rewritten) len(query) * 0.5: return query, False return rewritten, True2.4 小模型改寫的 prompt 設(shè)計(jì)規(guī)則層搞不定的復(fù)雜指代交給小模型。prompt 設(shè)計(jì)是成敗關(guān)鍵我調(diào)了好幾版才穩(wěn)定下來(lái)。核心原則有三條。一是給足上下文但別給太多最近三輪對(duì)話足夠給太多反而干擾。二是明確輸出格式要求模型只輸出改寫后的查詢不要解釋、不要加引號(hào)。三是給示例few-shot 能顯著提升小模型的穩(wěn)定性。你是一個(gè)查詢改寫助手。根據(jù)對(duì)話歷史把用戶最后一句話改寫成 不依賴上下文、可以獨(dú)立檢索的完整問(wèn)題。 規(guī)則 1. 只輸出改寫后的問(wèn)題不要任何解釋 2. 保留原句中的專業(yè)術(shù)語(yǔ)和實(shí)體 3. 如果原句已經(jīng)完整原樣輸出 對(duì)話歷史 用戶LangChain4j 的 Easy RAG 怎么配置 助手Easy RAG 通過(guò) EasyRagConfig 配置主要設(shè)置 embedding 和 store 用戶它的默認(rèn)分塊大小是多少 改寫結(jié)果LangChain4j Easy RAG 的默認(rèn)分塊大小是多少實(shí)測(cè)下來(lái)1.5B 到 3B 的指令模型在這個(gè)任務(wù)上表現(xiàn)已經(jīng)夠用再大就是浪費(fèi)。注意 prompt 里的示例要和你的實(shí)際領(lǐng)域貼近通用示例的效果會(huì)打折扣。2.5 改寫失敗的兜底與降級(jí)再好的改寫也有失敗的時(shí)候。我的兜底策略是雙路檢索改寫后的查詢和原始查詢各檢索一次把兩路結(jié)果合并去重再統(tǒng)一重排序。這樣即使改寫錯(cuò)了原始查詢還能兜住一部分正確結(jié)果。代價(jià)是檢索次數(shù)翻倍延遲增加。所以只在改寫置信度低的時(shí)候啟用雙路。置信度怎么判斷我的經(jīng)驗(yàn)是看改寫前后的向量相似度——如果改寫后查詢和原查詢的向量余弦相似度低于某個(gè)閾值比如 0.6說(shuō)明改寫改動(dòng)很大風(fēng)險(xiǎn)高就啟用雙路。3. 云端語(yǔ)義向量本地小模型的語(yǔ)義補(bǔ)強(qiáng)3.1 本地嵌入模型的真實(shí)短板先說(shuō)清楚本地嵌入模型差在哪才知道云端補(bǔ)的是什么。本地能跑的嵌入模型參數(shù)量普遍在 100M 到 500M 之間。這類模型在通用語(yǔ)義相似度任務(wù)上還行但遇到三種情況就拉胯專業(yè)術(shù)語(yǔ)密集的查詢比如ontology rag 和 kg 知識(shí)庫(kù)的區(qū)別、中英混排比如langchain4j easy rag 怎么用、短查詢比如分塊大小。短查詢尤其致命因?yàn)樾畔⒘刻傩∧P秃茈y把它映射到正確的語(yǔ)義空間。我做過(guò)一個(gè)對(duì)比測(cè)試同一批 50 個(gè)查詢本地小模型和云端嵌入服務(wù)的 Top-5 命中率差了將近 20 個(gè)百分點(diǎn)。這個(gè)差距在 Demo 里看不出來(lái)但在真實(shí)問(wèn)答里就是答對(duì)和答錯(cuò)的區(qū)別。3.2 云端嵌入的調(diào)用與成本控制云端語(yǔ)義向量服務(wù)的調(diào)用很簡(jiǎn)單基本就是發(fā) HTTP 請(qǐng)求傳文本數(shù)組拿回向量數(shù)組。但有幾個(gè)工程細(xì)節(jié)要注意。批量調(diào)用。不要一條一條調(diào)把多個(gè)文本片段打包成一批一次請(qǐng)求搞定。大多數(shù)服務(wù)單次支持幾十到上百條批量調(diào)用能把網(wǎng)絡(luò)往返開(kāi)銷攤薄。緩存。知識(shí)庫(kù)文檔的向量是固定的算一次存起來(lái)就行不要每次檢索都重算。只有查詢向量需要實(shí)時(shí)算。所以實(shí)際調(diào)用量 查詢次數(shù)而不是查詢次數(shù) × 文檔數(shù)。失敗重試。網(wǎng)絡(luò)請(qǐng)求會(huì)失敗要有重試機(jī)制但別無(wú)限重試。我的做法是重試兩次還失敗就降級(jí)到本地小模型保證服務(wù)不中斷。import hashlib import json class EmbeddingClient: def __init__(self, api_endpoint, api_key, cache_pathemb_cache.json): self.endpoint api_endpoint self.key api_key self.cache self._load_cache(cache_path) def embed(self, texts, batch_size64): results [] for i in range(0, len(texts), batch_size): batch texts[i:ibatch_size] # 先查緩存 miss [t for t in batch if self._hash(t) not in self.cache] if miss: vecs self._call_api(miss) # 帶重試 for t, v in zip(miss, vecs): self.cache[self._hash(t)] v results.extend(self.cache[self._hash(t)] for t in batch) return results def _hash(self, text): return hashlib.md5(text.encode()).hexdigest()注意緩存 key 用文本的哈希不要用文本本身否則緩存文件會(huì)巨大。另外緩存要定期落盤別只放內(nèi)存里進(jìn)程重啟就沒(méi)了。3.3 查詢向量和文檔向量必須同源這是最容易踩的坑也是很多人忽略的。查詢向量和文檔向量必須用同一個(gè)嵌入模型生成否則兩個(gè)向量根本不在同一個(gè)語(yǔ)義空間里余弦相似度算出來(lái)毫無(wú)意義。我見(jiàn)過(guò)有人文檔用云端模型嵌入查詢用本地模型嵌入然后納悶為什么檢索結(jié)果全是亂的。原因就在這里。向量空間不對(duì)齊相似度計(jì)算就是隨機(jī)數(shù)。所以方案里要明確既然文檔向量用了云端服務(wù)查詢向量也必須用同一個(gè)云端服務(wù)。本地小模型只在云端不可用時(shí)作為降級(jí)方案而且降級(jí)時(shí)要用本地模型重新嵌入整個(gè)知識(shí)庫(kù)保證同源。3.4 向量維度與存儲(chǔ)的權(quán)衡云端嵌入服務(wù)通常提供多種維度的模型比如 768、1024、1536、3072。維度越高語(yǔ)義表達(dá)越豐富但存儲(chǔ)和檢索成本也越高。本地向量庫(kù)比如 FAISS、Chroma存的是浮點(diǎn)數(shù)組維度翻倍存儲(chǔ)就翻倍。假設(shè)你有 10 萬(wàn)個(gè)文檔片段1536 維的 float32 向量大約占 600MB3072 維就是 1.2GB。本地環(huán)境內(nèi)存有限這個(gè)賬要算清楚。我的建議是優(yōu)先選 1024 或 1536 維這是效果和成本的平衡點(diǎn)。除非你的場(chǎng)景對(duì)語(yǔ)義精度要求極高否則沒(méi)必要上 3072 維。另外可以考慮量化存儲(chǔ)把 float32 壓成 int8存儲(chǔ)直接砍到四分之一精度損失在可接受范圍內(nèi)。維度10萬(wàn)片段存儲(chǔ)(float32)語(yǔ)義精度適用場(chǎng)景768約 300MB中資源緊張、場(chǎng)景簡(jiǎn)單1024約 400MB中高通用推薦1536約 600MB高專業(yè)領(lǐng)域、精度優(yōu)先3072約 1.2GB極高特殊需求、資源充足4. TXT 章節(jié)切分別讓固定長(zhǎng)度毀了檢索4.1 固定長(zhǎng)度切分的致命傷TXT 文檔沒(méi)有 HTML 標(biāo)簽、沒(méi)有 Markdown 標(biāo)題很多人圖省事就按固定字符數(shù)切比如每 500 字一塊重疊 50 字。這個(gè)做法在結(jié)構(gòu)松散的文檔上勉強(qiáng)能用但遇到有明確章節(jié)的文檔就是災(zāi)難。問(wèn)題出在語(yǔ)義完整性被破壞。假設(shè)一個(gè)章節(jié)講配置參數(shù)前半段列參數(shù)名后半段講每個(gè)參數(shù)的默認(rèn)值和取值范圍。固定切分正好切在中間前半塊只有參數(shù)名沒(méi)有默認(rèn)值后半塊只有默認(rèn)值不知道對(duì)應(yīng)哪個(gè)參數(shù)。用戶問(wèn)某參數(shù)默認(rèn)值是多少檢索命中后半塊但后半塊里沒(méi)有參數(shù)名模型根本不知道在說(shuō)哪個(gè)參數(shù)。4.2 基于章節(jié)特征的切分策略TXT 雖然沒(méi)有顯式結(jié)構(gòu)但章節(jié)特征往往藏在文本里。我總結(jié)了幾個(gè)可用的信號(hào)。編號(hào)信號(hào)。像第一章1.1一、一這類編號(hào)是天然的章節(jié)邊界。用正則匹配這些模式在匹配位置切分??招行盘?hào)。章節(jié)之間通常有空行分隔連續(xù)兩個(gè)以上換行往往意味著一個(gè)新章節(jié)開(kāi)始。標(biāo)題特征信號(hào)。章節(jié)標(biāo)題通常比較短一行以內(nèi)且后面緊跟正文??梢越Y(jié)合行長(zhǎng)度和上下文判斷。關(guān)鍵詞信號(hào)。像概述配置參數(shù)說(shuō)明注意事項(xiàng)示例這類詞經(jīng)常出現(xiàn)在章節(jié)標(biāo)題里。實(shí)際實(shí)現(xiàn)時(shí)我會(huì)把這些信號(hào)組合起來(lái)打分超過(guò)閾值就判定為章節(jié)邊界。純規(guī)則實(shí)現(xiàn)不依賴模型本地跑零成本。import re # 章節(jié)邊界檢測(cè)的正則模式 SECTION_PATTERNS [ r^第[一二三四五六七八九十百][章節(jié)部分], # 第一章、第二節(jié) r^\d\.\d\s, # 1.1 開(kāi)頭 r^\d\.\s, # 1. 開(kāi)頭 r^[一二三四五六七八九十]、, # 一、開(kāi)頭 r^[一二三四五六七八九十], # 一開(kāi)頭 r^(概述|簡(jiǎn)介|配置|參數(shù)|示例|注意|說(shuō)明|總結(jié)), # 關(guān)鍵詞開(kāi)頭 ] def is_section_boundary(line): line line.strip() if not line or len(line) 40: # 標(biāo)題一般不超過(guò)40字 return False for pat in SECTION_PATTERNS: if re.match(pat, line): return True return False def split_by_section(text): lines text.split(\n) sections [] current [] for i, line in enumerate(lines): # 空行 下一行是標(biāo)題特征 章節(jié)邊界 if is_section_boundary(line) and current: sections.append(\n.join(current).strip()) current [line] else: current.append(line) if current: sections.append(\n.join(current).strip()) return [s for s in sections if s]4.3 章節(jié)內(nèi)的二次切分章節(jié)切分完還要處理一個(gè)問(wèn)題有的章節(jié)太長(zhǎng)。一個(gè)章節(jié)幾千字直接嵌入會(huì)超出模型的最大輸入長(zhǎng)度而且語(yǔ)義太雜檢索精度反而下降。所以章節(jié)內(nèi)還要做二次切分。二次切分的策略和固定切分不同要優(yōu)先在段落邊界切。段落是天然的語(yǔ)義單元在段落之間切比在句子中間切好得多。我的做法是按段落累加長(zhǎng)度超過(guò)閾值比如 400 字就切一刀同時(shí)保留上一段的最后一句作為重疊保證跨塊的語(yǔ)義連貫。def split_section(section, max_len400, overlap_sentences1): paragraphs [p for p in section.split(\n) if p.strip()] chunks [] current for para in paragraphs: if len(current) len(para) max_len and current: chunks.append(current.strip()) # 保留上一塊最后一句作為重疊 last_sent re.split(r[。], current)[-2] if 。 in current else current last_sent para else: current para \n if current.strip(): chunks.append(current.strip()) return chunks4.4 切分參數(shù)的調(diào)優(yōu)經(jīng)驗(yàn)切分參數(shù)沒(méi)有萬(wàn)能值要根據(jù)文檔特點(diǎn)調(diào)。我總結(jié)了幾條經(jīng)驗(yàn)。塊大小。中文文檔單塊 300 到 500 字比較合適。太小語(yǔ)義不完整太大檢索精度下降。英文文檔可以適當(dāng)放大到 500 到 800 詞。重疊長(zhǎng)度。重疊是為了防止關(guān)鍵信息正好落在切分點(diǎn)上被切斷。重疊 10% 到 15% 比較合適也就是 400 字的塊重疊 40 到 60 字。重疊太多會(huì)導(dǎo)致檢索結(jié)果重復(fù)浪費(fèi)上下文窗口。章節(jié)優(yōu)先于長(zhǎng)度。如果章節(jié)邊界和長(zhǎng)度閾值沖突優(yōu)先按章節(jié)切。寧可某一塊短一點(diǎn)也不要跨章節(jié)切??缯鹿?jié)的塊語(yǔ)義混亂檢索出來(lái)會(huì)誤導(dǎo)模型。提示切分完一定要人工抽查幾塊看看有沒(méi)有把完整語(yǔ)義切碎。這一步花十分鐘能省掉后面幾小時(shí)的調(diào)參。5. 三塊拼起來(lái)完整鏈路的聯(lián)調(diào)與驗(yàn)證5.1 聯(lián)調(diào)時(shí)最容易出問(wèn)題的環(huán)節(jié)三塊單獨(dú)跑通不難拼起來(lái)才是考驗(yàn)。我踩過(guò)的聯(lián)調(diào)坑主要有三個(gè)??右桓膶懞蟮牟樵儧](méi)同步給檢索和生成。改寫發(fā)生在檢索前但生成階段拼 prompt 時(shí)如果用的是原始查詢而不是改寫后的查詢模型看到的上下文和檢索結(jié)果對(duì)不上答案會(huì)跑偏。記住改寫后的查詢要貫穿整條鏈路??佣蛄烤彺婧臀臋n更新不同步。文檔更新了但緩存里還是舊向量檢索結(jié)果就是舊的。解決辦法是給文檔加版本號(hào)或更新時(shí)間戳文檔一變就清掉對(duì)應(yīng)緩存??尤鹿?jié)切分和向量化的順序。正確順序是先切分再向量化每個(gè)塊單獨(dú)嵌入。如果先整篇嵌入再切分切出來(lái)的塊沒(méi)有對(duì)應(yīng)向量檢索無(wú)從談起。5.2 一套可復(fù)現(xiàn)的驗(yàn)證方法怎么判斷這套方案到底有沒(méi)有效果不能靠感覺(jué)要有量化指標(biāo)。我搭了一個(gè)小評(píng)測(cè)集準(zhǔn)備 50 到 100 個(gè)真實(shí)問(wèn)題每個(gè)問(wèn)題標(biāo)注好它應(yīng)該命中的文檔片段ground truth。然后跑檢索看 Top-1、Top-3、Top-5 的命中率。對(duì)比開(kāi)啟和關(guān)閉指代消解、本地和云端嵌入、固定切分和章節(jié)切分這幾組變量就能清楚看到每塊的價(jià)值。配置Top-1 命中率Top-3 命中率說(shuō)明基線固定切分本地嵌入無(wú)改寫52%68%對(duì)照組章節(jié)切分61%76%切分貢獻(xiàn)明顯云端嵌入74%87%嵌入貢獻(xiàn)最大指代消解82%92%多輪場(chǎng)景提升顯著這組數(shù)據(jù)是我自己項(xiàng)目里的實(shí)測(cè)值具體數(shù)字會(huì)因文檔和查詢而異但趨勢(shì)是穩(wěn)定的云端嵌入帶來(lái)的提升最大指代消解在多輪場(chǎng)景下提升明顯章節(jié)切分是基礎(chǔ)但不可省。5.3 延遲與成本的實(shí)測(cè)賬效果之外還要算延遲和成本的賬否則方案再準(zhǔn)也落不了地。延遲方面指代消解規(guī)則層幾乎零延遲小模型改寫增加 200 到 500 毫秒云端嵌入一次請(qǐng)求 100 到 300 毫秒本地向量檢索幾十毫秒本地大模型生成是主要耗時(shí)幾秒到十幾秒。整體看檢索鏈路增加的開(kāi)銷在可接受范圍內(nèi)瓶頸還是生成。成本方面云端嵌入按 token 計(jì)費(fèi)一次查詢通常幾十個(gè) token成本可以忽略。真正要控制的是別把文檔向量反復(fù)重算靠緩存解決。只要緩存做得好日常運(yùn)行的成本基本就是查詢嵌入的費(fèi)用非常低。5.4 幾個(gè)提升魯棒性的小技巧最后分享幾個(gè)讓方案更穩(wěn)的小技巧。查詢改寫加超時(shí)。小模型改寫偶爾會(huì)卡住加個(gè)超時(shí)比如 1 秒超時(shí)就退回規(guī)則層結(jié)果別讓整個(gè)請(qǐng)求掛死。檢索結(jié)果去重。雙路檢索或者重疊切分會(huì)導(dǎo)致重復(fù)片段檢索后按內(nèi)容哈希去重避免同樣的內(nèi)容占滿上下文窗口。保留原始查詢做日志。改寫后的查詢用于檢索但日志里要同時(shí)記錄原始查詢和改寫結(jié)果方便排查為什么這次答錯(cuò)了。很多時(shí)候問(wèn)題就出在改寫把原意改歪了。定期重建索引。文檔更新、嵌入模型升級(jí)、切分策略調(diào)整都會(huì)讓舊索引失效。定個(gè)周期比如每周重建一次索引保證檢索質(zhì)量。這套方案我在幾個(gè)本地知識(shí)庫(kù)項(xiàng)目里都跑過(guò)從技術(shù)文檔問(wèn)答到內(nèi)部資料檢索效果比裸奔的 RAG 穩(wěn)定太多。核心就一句話把查詢改對(duì)、把向量算準(zhǔn)、把文檔切好剩下的交給大模型。