據(jù)導(dǎo)入與解析:從txt到Markdown的結(jié)構(gòu)化處理全指南)
做 RAG 的人應(yīng)該都有一個(gè)共同的感受頂層設(shè)計(jì)再花哨模型選得再大最后跑起來效果不好十有八九是卡在了數(shù)據(jù)導(dǎo)入這一步。我之前接過好幾個(gè)所謂的“知識(shí)庫問答”需求對方上來就問用哪個(gè)向量庫、哪個(gè) embedding 模型等我把他們發(fā)來的原始資料打開一看——有的是從網(wǎng)上爬下來帶一堆標(biāo)簽的 HTML有的是掃描版 PDF 轉(zhuǎn)出來的純文本還有的是毫無規(guī)律的聊天記錄 txt。這種數(shù)據(jù)直接進(jìn) RAG 管線別說檢索效果了切塊那一步就亂成一鍋粥。這篇系列文章的第一篇我打算把最基礎(chǔ)也最容易被糊弄過去的環(huán)節(jié)徹底講透通用文本txt 類和結(jié)構(gòu)化文本Markdown 類的導(dǎo)入與解析。核心關(guān)鍵詞就三個(gè)RAG、數(shù)據(jù)導(dǎo)入、解析。我會(huì)按實(shí)際項(xiàng)目里的處理順序來拆解從拿到原始文件的第一個(gè)動(dòng)作到產(chǎn)出可喂給向量化模塊的標(biāo)準(zhǔn)片段每一步都講清楚“為什么這么做”以及“踩過的坑是什么”。1. 為什么數(shù)據(jù)解析決定了 RAG 的成敗先明確一個(gè)觀點(diǎn)RAG 不是檢索模型不行而是喂進(jìn)去的數(shù)據(jù)根本沒法檢索。你可以把 RAG 應(yīng)用想象成一家餐廳模型是廚師向量庫是冰箱而數(shù)據(jù)解析就是后廚的擇菜、洗菜、切菜環(huán)節(jié)。菜不洗、不切廚師技術(shù)再好也做不出一盤像樣的菜。1.1 原文檔與檢索單元之間的鴻溝絕大多數(shù)企業(yè)內(nèi)部的存量文檔長什么樣txt 里的回車換行大量缺失段落和段落之間用全角空格代替Markdown 文件看起來規(guī)整實(shí)際上標(biāo)題層級混亂、代碼塊誤用、表格里塞了圖片。這些原始形態(tài)和檢索單元之間存在一條巨大的鴻溝。檢索單元是什么向量庫里存的是有一定語義邊界的文本塊通常是幾百 token 一段。原始文檔是什么是一堆連續(xù)字符流或者只有少量格式標(biāo)記的文本。解析環(huán)節(jié)的價(jià)值就是從連續(xù)字符流中切出有語義邊界的 chunk同時(shí)保留必要的標(biāo)題層級信息讓每個(gè) chunk 自帶“出身背景”。這一步不做后面用再強(qiáng)的 Rerank 模型也救不回來。1.2 我見過的典型失敗案例很多人直接拿 LangChain 的TextLoader和RecursiveCharacterTextSplitter來處理全部文檔本地測試感覺還行一上生產(chǎn)就露餡。一個(gè)典型的失敗案例有人把一份 500 頁的運(yùn)維操作手冊導(dǎo)出的 HTML 轉(zhuǎn) txt 格式直接按 1000 字符切塊。結(jié)果是什么第 47 塊的標(biāo)題明明寫著“如何重啟數(shù)據(jù)庫”內(nèi)容里卻混著上一節(jié)“備份策略”的尾巴。用戶的提問是“數(shù)據(jù)庫重啟時(shí)需要注意什么”系統(tǒng)把這堆臟塊全部召回Rerank 之后找出來的內(nèi)容左右矛盾回答質(zhì)量慘不忍睹。問題出在哪出在整個(gè)管線沒有一個(gè)環(huán)節(jié)“理解”文檔的結(jié)構(gòu)。解析器只是做了字符層面的切分完全沒有感知到標(biāo)題、段落、列表這些結(jié)構(gòu)邊界。所以我始終堅(jiān)持一個(gè)原則先做結(jié)構(gòu)化解再做切塊結(jié)構(gòu)化解的程度直接決定切塊的上限。2. 通用文本解析先把 txt 變成“干凈的長文”txt 類文件是 RAG 導(dǎo)入中最常見也最容易被輕視的輸入。很多人認(rèn)為 txt 無非是open()讀進(jìn)來、按字符切一切就完事真實(shí)處理起來遠(yuǎn)沒有這么簡單。編碼混亂、字符污染、無效換行、段落粘連每一項(xiàng)都足以把后續(xù)的解析鏈路帶偏。2.1 編碼識(shí)別與統(tǒng)一第一步就翻車是家常便飯我接手過一個(gè)詞典類 txt打開一看全是類似鍥句功鍩虹 鏂囦歡的亂碼。原因很簡單——文件是 GBK 編碼但讀取時(shí)用了 UTF-8。這類問題在生產(chǎn)環(huán)境里發(fā)生率極高尤其是從舊系統(tǒng)導(dǎo)出的文檔。我的建議是不要用open(path, encodingutf-8)一把梭。穩(wěn)妥的做法是用charset-normalizer或者cchardet先做編碼探測把輸入統(tǒng)一轉(zhuǎn)成 UTF-8。# 編碼識(shí)別與統(tǒng)一讀取 from charset_normalizer import from_path def load_text_auto(path: str) - str: result from_path(path).best() if result is None: raise ValueError(f無法識(shí)別文件編碼: {path}) # 統(tǒng)一轉(zhuǎn)為 UTF-8 字符串 return str(result)實(shí)測下來charset-normalizer對 GBK、BIG5、Latin-1 的識(shí)別準(zhǔn)確率比老的 chardet 高不少尤其是在短文本場景下。轉(zhuǎn)換之后建議再對內(nèi)容做一次強(qiáng)制的 UTF-8 校驗(yàn)避免中英文混排時(shí)出現(xiàn)非法碼點(diǎn)。2.2 清洗規(guī)則不可見字符與異常換行一起處理編碼搞定之后另一個(gè)高頻問題是文件里混了大量肉眼看不見的臟數(shù)據(jù)。比如從 PDF 轉(zhuǎn)出來的 txt 會(huì)自動(dòng)插入一些制表位字符、零寬空格U200B、不換行空格U00A0還有 Windows 和 Unix 混用的換行符號。這些字符不會(huì)讓程序直接報(bào)錯(cuò)但到了切塊和向量化階段容易殘留成孤立 token檢索時(shí)反而制造噪聲。我習(xí)慣用一套正則預(yù)處理分三步走把\r\n、\r全部統(tǒng)一成\n。剔除所有控制字符和零寬字符但保留\n和\t。把全角空格統(tǒng)一轉(zhuǎn)半角多行空行壓縮成單行空行。import re def normalize_text(raw: str) - str: # 統(tǒng)一換行符 text raw.replace(\r\n, \n).replace(\r, \n) # 去掉控制字符與零寬字符保留 \n \t text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\u200b\u00a0], , text) # 全角空格轉(zhuǎn)半角并合并連續(xù)的空白行 text text.replace(\u3000, ) text re.sub(r[ \t]\n, \n, text) text re.sub(r\n{3,}, \n\n, text) return text.strip()這一步看著基礎(chǔ)但真的能解決很多下游玄學(xué)問題。之前有次做知識(shí)庫問答用戶問“如何申請退款”系統(tǒng)老是召回一些奇怪的片段兜兜轉(zhuǎn)轉(zhuǎn)查到最后發(fā)現(xiàn)是因?yàn)樵谋纠飱A雜了全角空格和零寬字符導(dǎo)致語義被硬生生切斷。清洗完之后召回質(zhì)量立刻上了一個(gè)臺(tái)階。2.3 語義段落聚合不要急著切塊清洗干凈的 txt還得經(jīng)過一道“段落聚合”。純文本不比 Markdown它沒有標(biāo)題結(jié)構(gòu)只有模糊的段落感。文檔里的“章節(jié)”往往是通過空行、縮進(jìn)、連續(xù)大寫標(biāo)題來暗示的。這時(shí)候如果直接按固定字符數(shù)切塊就會(huì)無視這些語義邊界。我通常是先按空行把文本切成段落列表再把過短的段落與相鄰段落做聚合最后輸出一版“語義化長文”。聚合規(guī)則不玄乎核心就兩條如果某段字?jǐn)?shù)小于 30 字且不是列表項(xiàng)特征則合并到前一段。如果某段以數(shù)字編號或“第x章”“前言”“概述”開頭單獨(dú)標(biāo)記為標(biāo)題段不要合并。def paragraphs_to_doc(paragraphs: list[str], min_chars: int 30): merged [] for para in paragraphs: para para.strip() if not para: continue if len(para) min_chars and merged and not _looks_like_title(para): merged[-1] para else: merged.append(para) return merged def _looks_like_title(text: str) - bool: return bool(re.match(r^(第[一二三四五六七八九十百千0-9][章節(jié)篇]|前言|概述|附錄|結(jié)語|references), text))這一步的意義在于給后續(xù)的切分器提供更“完整”的語義塊。短段落單獨(dú)成塊很容易變成無頭無尾的碎片合并之后才能保證一個(gè) chunk 內(nèi)部至少有一個(gè)完整論點(diǎn)。3. Markdown 結(jié)構(gòu)化解析把標(biāo)題變成檢索的骨架如果說 txt 處理是“洗干凈”那 Markdown 處理就是“搭骨架”。我對 Markdown 情有獨(dú)鐘因?yàn)樗悄壳吧儆械?、人類可讀且機(jī)器可解析的輕量結(jié)構(gòu)化格式。做 RAG 解析時(shí)從 Markdown 里提取標(biāo)題層級、代碼塊、表格、列表比從 PDF 或 HTML 里抽結(jié)構(gòu)要省太多力氣。3.1 為什么選 Markdown 作為中轉(zhuǎn)格式很多項(xiàng)目的數(shù)據(jù)源是 HTML或者是從各種爬蟲工具導(dǎo)出的富文本。我的建議是統(tǒng)一轉(zhuǎn)成 Markdown 再做結(jié)構(gòu)化解析而不是直接在 HTML 上切塊。原因有三Markdown 把復(fù)雜的 DOM 樹壓縮成了線性文本配合markdown或markdown-it這類解析器可以無損還原標(biāo)題結(jié)構(gòu)。HTML 里大量無語義的div嵌套和 inline 樣式對切塊沒有任何幫助轉(zhuǎn)成 Markdown 后這些噪聲自動(dòng)消失。現(xiàn)代 LLM 對 Markdown 的理解能力很強(qiáng)后面做片段摘要、父子切塊時(shí)Markdown 片段直接可以當(dāng)作優(yōu)質(zhì)上下文喂給模型。我在項(xiàng)目中常寫一個(gè)html2md的預(yù)處理函數(shù)內(nèi)部用markdownify把 HTML 轉(zhuǎn)成 Markdown然后再走結(jié)構(gòu)化解析。這一步跑通后整個(gè)知識(shí)庫的文檔形態(tài)就統(tǒng)一了。3.2 構(gòu)建 Markdown AST從線性文本到嵌套樹解析 Markdown 不能靠正則逐行猜最穩(wěn)的方式是用語法樹AST。Python 生態(tài)里我推薦用markdown_it配合自定義 renderer或者直接用mistune。這兩個(gè)庫都能把 Markdown 解析成節(jié)點(diǎn)樹每個(gè)節(jié)點(diǎn)帶類型heading、paragraph、code、table、list和層級H1-H6。拿到 AST 之后我才真正開始做文章結(jié)構(gòu)理解。我的做法是遍歷 AST把 heading 節(jié)點(diǎn)作為分段的“錨點(diǎn)”。每個(gè) heading 及其后續(xù)兄弟節(jié)點(diǎn)歸為一個(gè)結(jié)構(gòu)塊。結(jié)構(gòu)塊內(nèi)部再細(xì)分段落節(jié)點(diǎn)、列表節(jié)點(diǎn)、代碼塊節(jié)點(diǎn)、表格節(jié)點(diǎn)。from mistune import create_markdown def md_to_struct_blocks(md_text: str) - list[dict]: md create_markdown(rendererast) nodes md(md_text) blocks [] current None for node in nodes: if node[type] heading: # 遇到新標(biāo)題開啟新的結(jié)構(gòu)塊 current { title: node[text], level: node[attrs][level], children: [], } blocks.append(current) else: if current is not None: current[children].append(node) else: # 無標(biāo)題直接開頭的段落放在“文檔首部”塊 blocks.insert(0, {title: 文檔首部, level: 0, children: [node]}) return blocks實(shí)際輸出示例簡化后 [ {title: 環(huán)境準(zhǔn)備, level: 2, children: [ {type: paragraph, text: 建議使用 Python 3.10}, {type: list, text: [pip install langchain, pip install chromadb]} ]}, {title: 數(shù)據(jù)導(dǎo)入, level: 2, children: [ {type: paragraph, text: 本節(jié)介紹數(shù)據(jù)導(dǎo)入流程} ]} ]這段邏輯是整個(gè) Markdown 解析的核心分水嶺——從此之后文本不再是字符串而是有層級、有歸屬的節(jié)點(diǎn)流水線。后續(xù)切塊時(shí)每個(gè)塊都能自豪地說“我是從‘環(huán)境準(zhǔn)備’這個(gè)標(biāo)題下面切出來的”。3.3 特殊節(jié)點(diǎn)處理代碼塊、表格、數(shù)學(xué)公式不能一刀切這里必須先提醒一句不要把所有節(jié)點(diǎn)都無縫拼成長文本后切塊。代碼塊是按行組織的連續(xù)性文本表格是按行和列組織的二維數(shù)據(jù)數(shù)學(xué)公式是有著嚴(yán)格語義的 LaTeX 字符串。這三類內(nèi)容一旦被中間橫插一刀語義完整性就徹底碎了。我的處理策略如下代碼塊保留整體不拆分。代碼塊本身的語義邊界的完整度高于字符數(shù)如果代碼太長優(yōu)先按換行處的函數(shù)或類邊界去切而不是按字符數(shù)硬切。表格轉(zhuǎn)成一種“自然語言化”的文本格式再把整表作為單獨(dú)塊。例如把表格轉(zhuǎn)成列名: 值的枚舉文本保留可讀性同時(shí)便于向量化。數(shù)學(xué)公式分兩派。如果無需精確計(jì)算直接保留 LaTeX 源文本塊不轉(zhuǎn)圖片如果下游展示層需要渲染則單獨(dú)抽出來走渲染服務(wù)但向量化時(shí)仍然用 LaTeX 源文本。def format_table_node(node: dict) - str: header node[attrs][header] rows node[attrs][rows] lines [] for row in rows: pairs [f{h}: {v} for h, v in zip(header, row)] lines.append( | .join(pairs)) return \n.join(lines)實(shí)測中我發(fā)現(xiàn)表格轉(zhuǎn)成自然語言化文本后檢索效果往往比保留原始 Markdown 管道符寫法好很多。因?yàn)楣艿婪投嘤嗟目崭裨谙蛄炕瘯r(shí)會(huì)帶來無意義的 token 噪聲而語義化的電壓: 220V | 頻率: 50Hz這種格式更像 LLM 能直接消化的知識(shí)表達(dá)。3.4 鏈接與圖片該丟就丟該留標(biāo)記留標(biāo)記Markdown 里的鏈接和圖片處理上經(jīng)常引發(fā)糾結(jié)。鏈接的標(biāo)題文本往往就是一句話的精華比如[環(huán)境搭建文檔](./docs/setup.md)保留環(huán)境搭建文檔作為正文是有價(jià)值的但保留完整 URL 對向量化通常是噪聲。我的原則是標(biāo)題文本轉(zhuǎn)成普通文本URL 剝掉圖片直接抽取路徑或 alt 文本不把圖片二進(jìn)制喂給文本解析器。如果你需要處理“rag知識(shí)庫能存儲(chǔ)圖片嘛”這類問題我的建議是文本鏈路里不放圖片但保留圖片引用路徑和 alt 描述后續(xù)做多模態(tài)檢索時(shí)圖片走獨(dú)立的向量化通道在結(jié)果融合階段再和文本片段關(guān)聯(lián)。這一步的解析目標(biāo)是為將來留好“鉤子”而不是現(xiàn)在就把圖片塞進(jìn)文本模型。4. 邊界場景與實(shí)測中容易翻車的細(xì)節(jié)結(jié)構(gòu)化解析框架搭起來之后真正的考驗(yàn)在于邊界場景。我把自己在這一系列項(xiàng)目中反復(fù)踩過、也最終解決的幾個(gè)問題集中寫出來給同行們做個(gè)參考。4.1 短標(biāo)題、流水號標(biāo)題的誤判Markdown 或 txt 轉(zhuǎn)出來的文檔里常見一種現(xiàn)象正文行首剛好碰上了井號或數(shù)字比如“#1 一次生產(chǎn)事故復(fù)盤”“第 1 條不要用 root 跑服務(wù)”。這些根本不是標(biāo)題但解析器很容易把它們當(dāng)成 H1/H2 錨點(diǎn)導(dǎo)致一個(gè)文檔被切出幾十個(gè)語義碎片。我的解法是在生成結(jié)構(gòu)塊之前先做一道“標(biāo)題可信度”過濾。標(biāo)題長度不能小于 4 個(gè)字符。標(biāo)題不能以純數(shù)字、時(shí)間戳、序號開頭除非后續(xù)跟著中文字詞。標(biāo)題不能以句號、逗號、分號結(jié)尾。4.2 嵌套列表壓扁成一行字Markdown 列表在視覺上很清晰但解析進(jìn) AST 之后嵌套列表的父子關(guān)系處理不好就會(huì)被粗暴地拼成一個(gè)長段落。比如第一章1.1 安裝依賴用 pip 安裝如果壓扁成“第一章 1.1 安裝依賴 用 pip 安裝”層次感就丟了。我的處理方式是把每級縮進(jìn)轉(zhuǎn)成固定前綴符號例如兩空格或 a b 這類路徑式前綴然后按列表項(xiàng)逐項(xiàng)切塊。這樣每個(gè)列表項(xiàng)都保留了“第一章 1.1 安裝依賴 用 pip 安裝”這樣的路徑上下文。4.3 CSV 被誤判為 Markdown 表格這是“數(shù)據(jù)導(dǎo)入”環(huán)節(jié)經(jīng)常遇到的邊界問題。很多業(yè)務(wù)系統(tǒng)導(dǎo)出的文件是 CSV擴(kuò)展名卻是 txt內(nèi)容看起來又特別像 Markdown 表格。解析器若按 Markdown 表格去解析通常能跑通但對字段內(nèi)的逗號、引號處理不當(dāng)就會(huì)把一行拆成多行。我建議在解析之初先做格式嗅探如果文件里前幾行出現(xiàn)明顯的逗號分隔且字段數(shù)量一致就按 CSV 解析器處理否則按 Markdown 或純文本處理。兩個(gè)解析器走同一套“語義化文本”出口后續(xù)鏈路無需關(guān)心來源差異。4.4 HTML 標(biāo)簽殘留導(dǎo)致的臟標(biāo)記從網(wǎng)頁保存的 Markdown即便經(jīng)過了 html2md 轉(zhuǎn)換仍可能殘留部分span、div或 style 屬性。這些內(nèi)容在向量化時(shí)會(huì)被當(dāng)成普通文本產(chǎn)生大量無效 token。我在解析流水線末端加了一個(gè)正則清掃掃描所有文本節(jié)點(diǎn)把[^]以及class...、style...這類屬性剝掉再做最終清洗。有一種比較隱蔽的情況是Markdown 代碼塊里的 HTML 標(biāo)簽是合法內(nèi)容比如一份技術(shù)文檔的代碼示例里就寫了div。所以清掃標(biāo)簽一定要在 AST 節(jié)點(diǎn)的“正文文本”層做而不是在原始 Markdown 全文做。層級一錯(cuò)連代碼示例也被污染了。4.5 從 Markdown 轉(zhuǎn)檔時(shí)丟失的文檔元信息還有一類問題容易被忽略原始文檔的創(chuàng)建時(shí)間、作者、版本號、文號等信息。這些信息在解析階段如果被丟棄到了檢索階段想按時(shí)間過濾或按部門過濾就無能為力了。我的習(xí)慣是解析階段維護(hù)一份“文檔元信息”字典把文件名、首段描述、最近修改時(shí)間、來源路徑一并帶上。元信息和文本 chunk 是分開存儲(chǔ)的但在切塊時(shí)允許把某些元信息如版本號拼到對應(yīng)標(biāo)題下形成檢索端可用的過濾字段。這一步不是必須但在企業(yè)級知識(shí)庫場景下能省下大量返工成本。5. 結(jié)構(gòu)化信息如何與切塊策略聯(lián)動(dòng)解析環(huán)節(jié)完成之后接下來就到了切塊。很多教程把切塊說成“按 token 數(shù)切就行”但真正決定切塊質(zhì)量的是你前面解析時(shí)保留了哪些結(jié)構(gòu)信息。5.1 不要按固定字符數(shù)硬切固定字符數(shù)切塊的問題在于它無視標(biāo)題邊界和段落邊界。即使你前面已經(jīng)把 Markdown 分成了結(jié)構(gòu)塊最后硬切一刀下去依然會(huì)把一個(gè)結(jié)構(gòu)塊從中間斬?cái)?。所以我?qiáng)烈建議在結(jié)構(gòu)塊的基礎(chǔ)上做“語義最小單元”切分而不是“字符固定長度”切分。對一個(gè)結(jié)構(gòu)塊我先看它內(nèi)部的段落、列表項(xiàng)、代碼塊的數(shù)量。如果數(shù)量只有一個(gè)且長度適中比如小于 800 token整個(gè)塊可以作為一個(gè) chunk如果塊內(nèi)內(nèi)容過長我再按二級標(biāo)題或段落進(jìn)一步遞歸細(xì)分。這其實(shí)就是用解析得到的層級做了一次“有感知的切塊”。5.2 把標(biāo)題層級寫進(jìn) chunk 元數(shù)據(jù)切塊之后每個(gè) chunk 必須帶上它從哪個(gè)標(biāo)題層級下切出來的。比如chunk: 系統(tǒng)要求建議使用 Python 3.10 及以上版本 metadata: { h1: 快速開始, h2: 環(huán)境準(zhǔn)備, h3: 系統(tǒng)要求, source_file: docs/quickstart.md }這樣一個(gè) chunk 在檢索時(shí)即使匹配到的只是片段也能通過元數(shù)據(jù)把完整的層級路徑呈現(xiàn)給最終用戶。很多生產(chǎn)級 RAG 項(xiàng)目里這一步直接決定了“回答的可追溯性”好不好。5.3 父子切塊與標(biāo)題前綴拼接更進(jìn)階一點(diǎn)的方案是做父子切塊父塊是某個(gè)二級標(biāo)題下的完整章節(jié)子塊是按段落細(xì)分的片段。檢索時(shí)先召回子塊再根據(jù)元數(shù)據(jù)往上掛載父塊兩者一起給 LLM 當(dāng)上下文。這種方式能顯著緩解“片段太碎、丟失大語境”的問題。但請注意父塊的長度不能失控。如果某個(gè)二級標(biāo)題下有 10 屏內(nèi)容父塊就可能超出模型上下文窗口。因此我會(huì)給父塊設(shè)置一個(gè)軟上限比如 3000 token超過就自動(dòng)提升一個(gè)標(biāo)題層級再分。標(biāo)題前綴拼接也是另一種解法在子塊文本前面加上從 H1 到當(dāng)前標(biāo)題的路徑字符串讓 chunk 自帶上下文效果也比較穩(wěn)。5.4 解析后的驗(yàn)證清單解析流程跑完后不要直接去調(diào) embedding。先做一輪質(zhì)量抽檢我自己的驗(yàn)證清單大致這樣文本中不應(yīng)該存在長段無換行的粘連內(nèi)容。標(biāo)題層級路徑應(yīng)該覆蓋絕大部分 chunk。表格和代碼塊應(yīng)保持完整沒有在中間被切開。元數(shù)據(jù)與原始文件能一一對上定位無歧義。這一輪抽檢通常能發(fā)現(xiàn)引入臟亂數(shù)據(jù)源的問題避免把問題帶進(jìn)向量庫。6. 通用解析模塊的工程化落地從腳本到服務(wù)到這里解析邏輯的原理和關(guān)鍵細(xì)節(jié)我們都過了一遍。最后聊聊工程化落地因?yàn)楹芏嗳藢懲昴_本就跑忽略了部署和維護(hù)周期里的幾個(gè)關(guān)鍵點(diǎn)。我見過太多“本地跑著沒問題一上線數(shù)據(jù)多了就卡死”的情況。6.1 解析模塊的可插拔設(shè)計(jì)我把解析模塊拆成三個(gè)獨(dú)立環(huán)節(jié)Loader負(fù)責(zé)讀取不同格式、Preprocessor負(fù)責(zé)清洗與格式嗅探、Structurer負(fù)責(zé)結(jié)構(gòu)化切塊與元數(shù)據(jù)生成。每一環(huán)都面向接口編程不互相耦合。這樣做的好處是后續(xù)如果新增一種格式比如 epub、docx我只需要新寫一個(gè) Loader復(fù)用后面的 Preprocessor 和 Structurer。如果某類文本有特殊清洗邏輯我也只需要新增一個(gè) Preprocessor 實(shí)現(xiàn)不用動(dòng)主干鏈路。6.2 性能與并發(fā)別在解析上出瓶頸解析邏輯以 IO 和正則為主性能瓶頸一般不在解析本身而在讀取大文件。幾百 MB 的 txt硬讀進(jìn)內(nèi)存再處理內(nèi)存占用會(huì)一下子飆高。我的做法是對大文件先做分塊讀取按文件大小動(dòng)態(tài)調(diào)整讀取塊大小解析后的中間結(jié)果寫臨時(shí)文件或?qū)ο蟠鎯?chǔ)避免全部堆積內(nèi)存。6.3 失敗重試與臟數(shù)據(jù)隔離數(shù)據(jù)解析屬于典型的“輸入不可控”場景。有的文件編碼詭異有的文件內(nèi)容損壞。因此在工程實(shí)現(xiàn)上一定要對每個(gè)文件的解析結(jié)果做“成功/失敗/部分成功”三類標(biāo)記。失敗的文件不是直接丟棄而是落入待人工復(fù)核隊(duì)列。部分成功的文件要把解析成功的 chunk 先入庫同時(shí)輸出一份“問題摘要”給運(yùn)維人員。這套機(jī)制在長期運(yùn)行的知識(shí)庫系統(tǒng)里極其重要。沒有它任何一個(gè)角落里的臟文件都會(huì)成為檢索回答出錯(cuò)時(shí)最難排查的隱藏故障源。至此從 txt 到 Markdown 的通用文本與結(jié)構(gòu)化解整條鏈路已經(jīng)完整呈現(xiàn)。我個(gè)人的體會(huì)是數(shù)據(jù)解析很難靠一次性到位它更像一個(gè)持續(xù)迭代的打磨過程——每一次新的數(shù)據(jù)來源都會(huì)帶來新的坑結(jié)構(gòu)化解析的價(jià)值就是把這些坑提前在清洗和分層階段排掉而不是留給檢索階段“隨機(jī)爆炸”。下一篇系列文章里我打算接著寫 PDF 和 Word 這類富格式文檔的解析方案比 txt 和 Markdown 的復(fù)雜程度又要高出一個(gè)檔次。