庫(kù)效果瓶頸)
簡(jiǎn)介這份源碼資源面向正在搭建RAG應(yīng)用、需要優(yōu)化長(zhǎng)文檔切分環(huán)節(jié)的開(kāi)發(fā)者與AI學(xué)習(xí)者圍繞檢索增強(qiáng)生成中的切片策略展開(kāi)幫助解決文檔切分粒度不當(dāng)導(dǎo)致檢索召回差、生成答案質(zhì)量低的問(wèn)題。資源包共5個(gè)文件以Python腳本為主另含requirements.txt依賴清單、.inscode與.gitignore等工程配置文件整體約13KB輕量易讀可直接運(yùn)行驗(yàn)證。內(nèi)容覆蓋改進(jìn)的固定長(zhǎng)度切片、語(yǔ)義切片、LLM語(yǔ)義切片、層次切片與滑動(dòng)窗口切片五種方法并配有結(jié)合FAISS搭建本地知識(shí)庫(kù)的流程圖與選型指南便于對(duì)照不同場(chǎng)景選擇合適策略。目前已有201人學(xué)習(xí)下載適合希望快速理解切片原理、動(dòng)手調(diào)試參數(shù)并落地到本地知識(shí)庫(kù)項(xiàng)目的中級(jí)開(kāi)發(fā)者參考。1. RAG 切片策略為什么成了知識(shí)庫(kù)效果的黑匣子做過(guò) RAG 知識(shí)庫(kù)的人大多有過(guò)這種體驗(yàn)向量庫(kù)建好了檢索鏈路也通了模型回答卻總是差一口氣——要么答非所問(wèn)要么把兩段不相干的內(nèi)容縫在一起。排查到最后問(wèn)題往往不在 embedding 模型也不在 rerank而是最上游那個(gè)最容易被忽略的環(huán)節(jié)切片策略。切片決定了知識(shí)以什么粒度進(jìn)入向量空間粒度錯(cuò)了后面所有環(huán)節(jié)都在為它擦屁股。這份 RAG 切片策略指南要解決的就是把「憑感覺(jué)切」變成「按文檔結(jié)構(gòu)切、按檢索目標(biāo)切」。它適合兩類人一類是剛跑通 RAG demo、準(zhǔn)備上真實(shí)知識(shí)庫(kù)的工程師另一類是知識(shí)庫(kù)已經(jīng)上線、但召回率和答案準(zhǔn)確率卡在瓶頸上不去的從業(yè)者。切片不是玄學(xué)它有一套可復(fù)現(xiàn)的參數(shù)體系和判斷標(biāo)準(zhǔn)源碼層面也就那么幾個(gè)關(guān)鍵函數(shù)。下面從原理到代碼把這條鏈路拆開(kāi)講。2. 切片策略的四種主流方案與選型依據(jù)2.1 固定長(zhǎng)度切片最簡(jiǎn)單也最容易翻車固定長(zhǎng)度切片是最常見(jiàn)的入門做法按字符數(shù)或 token 數(shù)硬切配一個(gè)重疊窗口。它的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單、切片數(shù)量可控缺點(diǎn)是幾乎不考慮語(yǔ)義邊界一句話被攔腰截?cái)嗍浅B(tài)。def fixed_chunk(text, chunk_size500, overlap50): # chunk_size 按字符計(jì)中文場(chǎng)景下 500 字約等于 350-400 token chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) # overlap 防止邊界信息丟失但會(huì)帶來(lái)冗余 start end - overlap return chunks這段代碼里chunk_size和overlap是兩個(gè)必須調(diào)的參數(shù)。中文技術(shù)文檔我一般用 400 到 600 字overlap 取 chunk_size 的 10% 到 15%。overlap 太小跨邊界的問(wèn)題答不出來(lái)太大向量庫(kù)里全是重復(fù)內(nèi)容檢索時(shí)擠占 top-k 名額。固定切片的適用場(chǎng)景很窄純敘述性、沒(méi)有明顯層級(jí)的文本比如訪談?dòng)涗洝?huì)議紀(jì)要。一旦文檔有標(biāo)題、表格、代碼塊固定切片就會(huì)把結(jié)構(gòu)信息切碎。2.2 遞歸字符切片按分隔符優(yōu)先級(jí)逐層降級(jí)遞歸切片是目前工程上最穩(wěn)的默認(rèn)方案LangChain 的RecursiveCharacterTextSplitter就是典型代表。它的思路是給一組分隔符按優(yōu)先級(jí)從高到低嘗試先按段落切段落還太大就按句子切句子還大就按逗號(hào)切最后才硬切。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, # 分隔符順序決定降級(jí)策略中文場(chǎng)景要把中文標(biāo)點(diǎn)加進(jìn)去 separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) chunks splitter.split_text(document)separators的順序是關(guān)鍵它決定了切片的語(yǔ)義完整度。中文文檔一定要把全角標(biāo)點(diǎn)放進(jìn)去否則默認(rèn)的英文分隔符在中文里幾乎不起作用退化成固定切片。chunk_overlap建議設(shè)為 chunk_size 的 15% 左右。遞歸切片的局限在于它只認(rèn)字符不認(rèn)文檔的真實(shí)結(jié)構(gòu)——一個(gè) Markdown 表格里的行會(huì)被當(dāng)成普通文本切表頭和數(shù)據(jù)行可能分到不同 chunk。2.3 結(jié)構(gòu)化切片按標(biāo)題層級(jí)和語(yǔ)義單元切當(dāng)知識(shí)庫(kù)以 Markdown、HTML、Word 這類帶結(jié)構(gòu)的文檔為主時(shí)結(jié)構(gòu)化切片的效果明顯優(yōu)于前兩種。核心思路是先解析出文檔的標(biāo)題樹(shù)再以最小語(yǔ)義單元一個(gè)三級(jí)標(biāo)題下的完整內(nèi)容為切片邊界。import re def structure_chunk(markdown_text, max_size800): # 按標(biāo)題層級(jí)切分保留標(biāo)題作為上下文前綴 sections re.split(r(?^#{1,3}\s), markdown_text, flagsre.MULTILINE) chunks [] for sec in sections: if not sec.strip(): continue # 超長(zhǎng)章節(jié)再按段落二次切分 if len(sec) max_size: paras sec.split(\n\n) buf for p in paras: if len(buf) len(p) max_size: chunks.append(buf) buf p else: buf \n\n p if buf: chunks.append(buf) else: chunks.append(sec) return chunks這里max_size是二次切分的兜底閾值防止某個(gè)章節(jié)過(guò)長(zhǎng)導(dǎo)致單條向量語(yǔ)義過(guò)載。結(jié)構(gòu)化切片最大的價(jià)值是保留了標(biāo)題路徑檢索命中時(shí)能直接知道這段內(nèi)容屬于哪個(gè)章節(jié)對(duì)生成答案的引用溯源特別有用。代價(jià)是需要針對(duì)不同文檔格式寫解析邏輯Markdown 好辦PDF 和掃描件就得先過(guò) OCR 和版面分析。2.4 語(yǔ)義切片用 embedding 相似度找斷點(diǎn)語(yǔ)義切片是效果上限最高、成本也最高的方案。它先把文本按句子拆開(kāi)計(jì)算相鄰句子的 embedding 相似度在相似度驟降的地方斷開(kāi)。import numpy as np from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) def semantic_chunk(sentences, threshold0.75): embs model.encode(sentences, normalize_embeddingsTrue) chunks, cur [], [sentences[0]] for i in range(1, len(sentences)): # 余弦相似度歸一化后直接點(diǎn)積 sim float(np.dot(embs[i-1], embs[i])) if sim threshold: chunks.append(.join(cur)) cur [sentences[i]] else: cur.append(sentences[i]) chunks.append(.join(cur)) return chunksthreshold是最難調(diào)的參數(shù)中文技術(shù)文檔我一般從 0.7 到 0.8 之間試。閾值太高切片過(guò)碎太低等于沒(méi)切。語(yǔ)義切片的成本在于每個(gè)句子都要過(guò)一次 embedding長(zhǎng)文檔處理慢而且斷點(diǎn)位置不穩(wěn)定同一份文檔兩次跑可能結(jié)果略有差異。它適合對(duì)召回精度要求極高、文檔量不大的場(chǎng)景比如法律條款、醫(yī)療指南。四種方案的選型可以對(duì)照下表方案實(shí)現(xiàn)成本語(yǔ)義完整度適用文檔主要風(fēng)險(xiǎn)固定長(zhǎng)度低低純敘述文本語(yǔ)義截?cái)噙f歸字符低中通用文本結(jié)構(gòu)丟失結(jié)構(gòu)化中高M(jìn)arkdown/HTML解析依賴語(yǔ)義切片高高高精度小庫(kù)參數(shù)敏感3. 從源碼角度拆解切片函數(shù)的參數(shù)與邊界3.1 chunk_size 到底該按字符還是按 token 算這是被問(wèn)得最多的問(wèn)題。結(jié)論是切片階段按字符算入庫(kù)前按 token 校驗(yàn)。原因是 embedding 模型有最大輸入長(zhǎng)度限制比如 bge 系列通常是 512 token如果切片按字符算出來(lái) 600 字中文里可能超過(guò) 512 token超長(zhǎng)部分會(huì)被靜默截?cái)嗄愀静恢纴G了什么。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(BAAI/bge-small-zh-v1.5) def validate_chunk(chunks, max_tokens500): valid [] for c in chunks: ids tokenizer.encode(c, add_special_tokensFalse) if len(ids) max_tokens: # 超長(zhǎng)切片按 token 邊界二次切 for i in range(0, len(ids), max_tokens): valid.append(tokenizer.decode(ids[i:imax_tokens])) else: valid.append(c) return validmax_tokens要留出余量模型上限 512 的話切片控制在 480 到 500 之間給特殊 token 留位置。這一步是很多知識(shí)庫(kù)效果差的隱藏原因切片看著沒(méi)問(wèn)題實(shí)際入庫(kù)時(shí)被截?cái)鄼z索到的內(nèi)容缺頭少尾。3.2 overlap 設(shè)置多少才不浪費(fèi)向量空間overlap 的本質(zhì)是用存儲(chǔ)冗余換邊界信息。經(jīng)驗(yàn)值是 chunk_size 的 10% 到 20%。低于 10%跨 chunk 的問(wèn)題容易丟上下文高于 20%向量庫(kù)里重復(fù)內(nèi)容占比明顯上升檢索時(shí) top-5 里可能三條都是同一段的重疊部分。判斷 overlap 是否合理有個(gè)簡(jiǎn)單方法隨機(jī)抽 20 個(gè)跨邊界的問(wèn)題看檢索結(jié)果里是否至少有一條能覆蓋完整答案。如果經(jīng)常需要拼兩條 chunk 才能答全說(shuō)明 overlap 偏小如果 top-k 里大量重復(fù)說(shuō)明偏大。這個(gè)驗(yàn)證比拍腦袋定參數(shù)靠譜得多。3.3 元數(shù)據(jù)怎么隨切片一起入庫(kù)切片不只是切文本元數(shù)據(jù)決定了檢索時(shí)能不能過(guò)濾、生成時(shí)能不能溯源。我一般給每個(gè) chunk 掛這幾項(xiàng)來(lái)源文件名、標(biāo)題路徑、chunk 序號(hào)、字符數(shù)。def build_chunk_records(chunks, source, title_path): records [] for idx, c in enumerate(chunks): records.append({ text: c, metadata: { source: source, title_path: title_path, chunk_index: idx, char_len: len(c), } }) return recordstitle_path在結(jié)構(gòu)化切片里尤其重要它讓檢索結(jié)果自帶層級(jí)上下文。生成答案時(shí)把 title_path 拼在 chunk 前面模型對(duì)內(nèi)容歸屬的判斷會(huì)準(zhǔn)很多。chunk_index用于相鄰 chunk 擴(kuò)展檢索命中某條后可以把前后各一條一起喂給模型彌補(bǔ)切片邊界的信息損失。4. 切片效果排查五類高頻翻車現(xiàn)場(chǎng)4.1 檢索命中但答案答非所問(wèn)現(xiàn)象檢索返回的 chunk 看起來(lái)相關(guān)但模型生成的答案跑偏。原因通常是 chunk 里混入了多個(gè)主題向量被平均化相似度虛高。解決檢查該 chunk 是否跨越了標(biāo)題邊界把結(jié)構(gòu)化切片的最小單元收緊到三級(jí)標(biāo)題超長(zhǎng)章節(jié)強(qiáng)制二次切分。4.2 表格和代碼塊被切碎現(xiàn)象問(wèn)表格里的某個(gè)數(shù)值檢索到的 chunk 只有半張表表頭在另一條里。原因遞歸切片把表格行當(dāng)普通文本處理。解決解析階段識(shí)別表格和代碼塊把它們作為不可分割單元整塊作為一個(gè) chunk超長(zhǎng)時(shí)按行切但每片都帶上表頭。4.3 短切片召回噪聲大現(xiàn)象top-5 里全是幾十字的短句信息量不足。原因語(yǔ)義切片閾值設(shè)太高或者文檔里本身短句多。解決設(shè)一個(gè)最小 chunk 長(zhǎng)度比如 100 字低于這個(gè)長(zhǎng)度的切片合并到相鄰切片同時(shí)檢查閾值是否過(guò)嚴(yán)。4.4 同一文檔切片結(jié)果不穩(wěn)定現(xiàn)象重新跑一次切片chunk 數(shù)量和邊界都變了。原因語(yǔ)義切片依賴 embedding模型版本或歸一化方式變動(dòng)都會(huì)影響斷點(diǎn)。解決切片結(jié)果落盤緩存入庫(kù)和檢索用同一份切片產(chǎn)物不要每次動(dòng)態(tài)切。4.5 多文檔標(biāo)題層級(jí)不一致導(dǎo)致切片粒度混亂現(xiàn)象有的文檔切出來(lái) 200 字一條有的 2000 字一條。原因不同來(lái)源文檔的標(biāo)題層級(jí)深度不同結(jié)構(gòu)化切片按標(biāo)題切時(shí)粒度失控。解決統(tǒng)一預(yù)處理把各級(jí)標(biāo)題歸一化到固定層級(jí)再按 max_size 兜底二次切分保證切片長(zhǎng)度分布集中。5. 把切片質(zhì)量量化一套可復(fù)用的驗(yàn)證腳本切片調(diào)完之后怎么知道它到底好不好靠肉眼看幾條是不夠的得有一套量化指標(biāo)。我常用的做法是構(gòu)造一組「問(wèn)題-答案-來(lái)源片段」的測(cè)試集跑檢索看命中率。def eval_retrieval(questions, retriever, top_k5): hit, total 0, 0 for q in questions: results retriever.search(q[query], top_ktop_k) total 1 # 命中判定檢索結(jié)果里是否包含答案所在片段的關(guān)鍵句 for r in results: if q[answer_key] in r[text]: hit 1 break return hit / total if total else 0.0answer_key是答案片段里的一個(gè)特征字符串用它判斷檢索是否命中。這個(gè)腳本跑下來(lái)命中率低于 80% 就說(shuō)明切片或檢索參數(shù)有問(wèn)題。測(cè)試集不用多30 到 50 條覆蓋各類問(wèn)題就夠關(guān)鍵是要包含跨 chunk 的問(wèn)題專門驗(yàn)證 overlap 和相鄰擴(kuò)展是否生效。除了命中率我還會(huì)看兩個(gè)分布指標(biāo)切片長(zhǎng)度分布和切片數(shù)量。長(zhǎng)度分布應(yīng)該集中在目標(biāo)區(qū)間出現(xiàn)大量超短或超長(zhǎng)切片就是參數(shù)沒(méi)調(diào)好切片數(shù)量突然比預(yù)期多很多往往是分隔符配置有問(wèn)題把不該切的地方切了。調(diào)參的順序建議是先定切片方案再調(diào) chunk_size 和 overlap最后用測(cè)試集驗(yàn)證。不要一上來(lái)就同時(shí)改多個(gè)參數(shù)那樣出了問(wèn)題根本不知道是哪個(gè)引起的。我自己的習(xí)慣是每次只動(dòng)一個(gè)參數(shù)跑一遍測(cè)試集記錄命中率找到拐點(diǎn)再動(dòng)下一個(gè)。切片這件事沒(méi)有一勞永逸的最優(yōu)解文檔變了、問(wèn)題類型變了參數(shù)就得重新看。把這套驗(yàn)證腳本留著每次知識(shí)庫(kù)更新后跑一遍比事后拍腦袋強(qiáng)得多。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取