據(jù)進(jìn)RAG全攻略:從CSV到數(shù)據(jù)庫(kù)的導(dǎo)入與實(shí)戰(zhàn))
表格類數(shù)據(jù)進(jìn) RAG我一直覺得是被低估的硬骨頭。文本類文檔大家已經(jīng)玩得比較順了PDF、Word、Markdown 各有各的解析套路但一換成 CSV、Excel、數(shù)據(jù)庫(kù)表很多人就卡住了直接整表轉(zhuǎn)文本丟進(jìn)向量庫(kù)檢索效果稀碎每行單獨(dú)轉(zhuǎn)成 Document又不知道元數(shù)據(jù)怎么掛才合理真要連數(shù)據(jù)庫(kù)連接串、查詢語(yǔ)句、增量同步又是一堆新問題。這篇是 RAG 數(shù)據(jù)導(dǎo)入與解析系列的第三篇專門把表格和數(shù)據(jù)庫(kù)這條路走一遍從 CSV 到 Excel 再到 LlamaHub 連庫(kù)實(shí)戰(zhàn)每一步我都會(huì)給出可復(fù)現(xiàn)的代碼和實(shí)際踩坑記錄。先說清楚這篇適合誰(shuí)你已經(jīng)在用 LlamaIndex 或類似框架處理過文本數(shù)據(jù)想把手里的結(jié)構(gòu)化數(shù)據(jù)訂單表、配置表、產(chǎn)品清單真正接進(jìn) RAG或者你還沒動(dòng)手但對(duì)「表格數(shù)據(jù)到底該怎么進(jìn)知識(shí)庫(kù)」這件事沒想明白想先看一條完整的落地方案。這篇不堆概念全是實(shí)操。1. 表格數(shù)據(jù)在 RAG 里的特殊位置為什么不能照抄文檔解析的老路子很多人拿到表格數(shù)據(jù)后的第一反應(yīng)是跟 PDF 一樣讀進(jìn)來切塊丟進(jìn)索引完事。這個(gè)思路對(duì)文本沒問題但對(duì)表格幾乎一定會(huì)翻車。想明白為什么是這篇所有操作的基礎(chǔ)。1.1 二維數(shù)據(jù)與「一刀切」切塊的沖突文本是線性的段落和段落之間有天然的順序關(guān)系所以按固定窗口切塊、加一點(diǎn)重疊語(yǔ)義損失通??山邮堋5砀袷嵌S的橫向是字段列名縱向是記錄行每一格的語(yǔ)義價(jià)值高度依賴它的表頭。如果你把整張表轉(zhuǎn)成一段長(zhǎng)文本再按 token 切常見的結(jié)果有兩種。第一種情況一張 50 列、2000 行的表文本化之后可能上萬 token固定窗口切出來有的 chunk 會(huì)截?cái)嗲皫琢杏械?chunk 會(huì)把同一行數(shù)據(jù)拆到兩個(gè) chunk 里。檢索的時(shí)候你命中了一個(gè) chunk但關(guān)鍵的數(shù)值字段在另一個(gè) chunk 里模型根本拿不到完整記錄。第二種情況更隱蔽某些表的列名本身沒有語(yǔ)義比如col1、col2、item_code、region_id。切塊后這些 chunk 里全是孤立數(shù)值embedding 模型對(duì)這些短數(shù)字片段的編碼效果非常差檢索召回基本靠運(yùn)氣。所以處理表格數(shù)據(jù)的第一原則是不要默認(rèn)走文本切塊 pipeline先看表的形態(tài)再?zèng)Q定每一行、每一塊到底要以什么粒度進(jìn)入索引。1.2 記錄型與分析型先分清你手頭是什么表我習(xí)慣把表格先分成兩類處理邏輯完全不同。記錄型表格典型是業(yè)務(wù)流水、訂單明細(xì)、用戶列表每一行都是獨(dú)立的、完整的事實(shí)。這種表最適合的行粒度是「一行 一個(gè) Document」因?yàn)橐恍袃?nèi)的字段組合起來構(gòu)成完整語(yǔ)義行與行之間沒有強(qiáng)上下文依賴。檢索場(chǎng)景通常是「查某個(gè)特定條件下的記錄」比如某訂單金額、某客戶所在地區(qū)。分析型表格典型是統(tǒng)計(jì)報(bào)表、匯總表、指標(biāo)看板行與行、列與列之間存在維度關(guān)系某一格單獨(dú)拿出來沒意義必須依賴表格整體結(jié)構(gòu)。比如「華東區(qū)一季度銷售額」它的上下文是行表頭和列表頭的組合。這種表直接按行拆會(huì)丟失結(jié)構(gòu)比較合理的做法是先做「表格摘要 局部摘錄」或者把表按業(yè)務(wù)語(yǔ)義塊拆分再把每個(gè)塊轉(zhuǎn)成自然語(yǔ)言描述而不是單純貼數(shù)值。這兩種表如果混在一起統(tǒng)一處理后面召回測(cè)試會(huì)很難受因?yàn)槟銓?duì)「正確結(jié)果」的判斷標(biāo)準(zhǔn)都不一樣。1.3 從「最終要回答什么問題」倒推導(dǎo)入方案這是我想強(qiáng)調(diào)的習(xí)慣動(dòng)手導(dǎo)入之前先列出業(yè)務(wù)側(cè)真實(shí)會(huì)問的問題。我做某個(gè)項(xiàng)目時(shí)對(duì)方列了一堆表要導(dǎo)入我讓他們先給我十個(gè)問題。結(jié)果發(fā)現(xiàn)他們關(guān)心的幾乎都是「某個(gè)商品最近 30 天在華南區(qū)的銷量」「哪幾個(gè)訂單超過萬元并且還沒發(fā)貨」這類帶條件的查詢。這些問題直接決定了三件事一是必須保留哪些字段作為過濾條件二是哪些字段應(yīng)該寫進(jìn)正文文本三是每行文本應(yīng)該如何組織語(yǔ)言。反過來看如果一開始就把所有表不分青紅皂白轉(zhuǎn)文本入庫(kù)檢索會(huì)把大量無關(guān)行撈回來query engine 再聰明也被噪聲淹沒。這個(gè)環(huán)節(jié)不寫代碼但我覺得它比代碼更重要。表格數(shù)據(jù)進(jìn) RAG本質(zhì)上不是「把數(shù)據(jù)塞進(jìn)去」而是「為特定問題設(shè)計(jì)一種可檢索的文本視圖」。2. CSV 導(dǎo)入實(shí)戰(zhàn)不止 read_csv 一下就完事CSV 看起來最簡(jiǎn)單其實(shí)最容易出問題。我見過太多「CSV 文件插入向量庫(kù)之后一問三不知」的情況原因大多不在向量庫(kù)而在 CSV 導(dǎo)入那一步的數(shù)據(jù)質(zhì)量和行文本組織。2.1 數(shù)據(jù)體檢編碼、分隔符、空值的那些事CSV 作為純文本格式第一關(guān)就是編碼。國(guó)內(nèi)環(huán)境中 GBK 編碼的 CSV 仍然大量存在Excel 導(dǎo)出的老版本 CSV 甚至默認(rèn) ANSI。如果你用 pandas 直接讀遇到 GBK 文件會(huì)直接拋 UnicodeDecodeError。處理方式很簡(jiǎn)單import pandas as pd # 先探測(cè)編碼再讀取 def detect_encoding(file_path): import chardet with open(file_path, rb) as f: raw f.read(100000) return chardet.detect(raw).get(encoding, utf-8) enc detect_encoding(sales.csv) df pd.read_csv(sales.csv, encodingenc)分隔符也值得確認(rèn)一下。部分業(yè)務(wù)系統(tǒng)導(dǎo)出的 CSV 不是逗號(hào)分隔而是|或\t還有的企業(yè)數(shù)據(jù)里字段本身含逗號(hào)但沒有按 CSV 標(biāo)準(zhǔn)加引號(hào)。這個(gè)不提前檢查后面整行解析錯(cuò)位導(dǎo)入的文本全是亂的而且很難發(fā)現(xiàn)。建議讀取前先pd.read_csv一個(gè) 5 行的樣本打印出來看一眼。空值處理更是必做項(xiàng)。如果某一行關(guān)鍵字段是 NaN你直接str(row)會(huì)把nan寫進(jìn)文本檢索時(shí)如果用戶問「哪些訂單沒填寫客戶名」模型會(huì)被這些nan誤導(dǎo)。導(dǎo)入前必須明確空值策略要么丟棄該行要么用「未知」等有效文本占位。2.2 每行轉(zhuǎn) Document還是整表轉(zhuǎn)大文本我在 1.1 里說了記錄型表按行轉(zhuǎn) Document。但這里有個(gè)容易忽略的細(xì)節(jié)Document的 text 字段不能是「列名: 值」的機(jī)械拼接而是要盡量轉(zhuǎn)成一句完整自然語(yǔ)言。舉個(gè)例子下面這樣的原始行order_idproductregionamountstatusA1001無線耳機(jī)華東1280已發(fā)貨機(jī)械拼接的結(jié)果是order_id: A1001, product: 無線耳機(jī), region: 華東, amount: 1280, status: 已發(fā)貨。檢索「華東區(qū)發(fā)了多少錢的貨」時(shí)這句文本的語(yǔ)義密度很低因?yàn)?embedding 模型對(duì)這種「字段名短值」結(jié)構(gòu)的編碼并不好。我推薦做一次行級(jí)文本化def row_to_text(row, table_desc訂單記錄): parts [] if order_id in row and pd.notna(row[order_id]): parts.append(f訂單編號(hào)為{row[order_id]}) if product in row and pd.notna(row[product]): parts.append(f商品為{row[product]}) if region in row and pd.notna(row[region]): parts.append(f銷售區(qū)域?yàn)閧row[region]}) if amount in row and pd.notna(row[amount]): parts.append(f金額為{row[amount]}元) if status in row and pd.notna(row[status]): parts.append(f狀態(tài)為{row[status]}) return f這是一條{table_desc}{.join(parts)}。這樣轉(zhuǎn)出來的是自然語(yǔ)言短句embedding 檢索的命中率明顯提升。這個(gè)步驟多花不了多少時(shí)間但對(duì)結(jié)果是決定性的。我在多個(gè)項(xiàng)目里對(duì)比過機(jī)械拼接檢索 top-5 命中正確行概率大概 40%自然語(yǔ)言化之后能到 75% 以上。2.3 一套可直接用的 CSV 自定義導(dǎo)入模板LlamaHub 上其實(shí)有現(xiàn)成的 CSVReader但我很少直接用原因是它對(duì) CSV 的假設(shè)太理想了默認(rèn) UTF-8、默認(rèn)逗號(hào)分隔、沒有空值處理、按整個(gè)文件內(nèi)容作為一個(gè)大 Document 處理。對(duì)干凈的小文件它很方便但對(duì)真實(shí)業(yè)務(wù)數(shù)據(jù)不夠用。我更傾向于用 pandas 清洗 自定義 Document 構(gòu)建import pandas as pd from llama_index.core.schema import Document def load_csv_as_documents( file_path, table_desc數(shù)據(jù)表, encodingNone, sep,, drop_emptyTrue, filter_empty_cols[order_id, amount], ): if encoding is None: encoding detect_encoding(file_path) df pd.read_csv(file_path, encodingencoding, sepsep, dtypestr) # 空值處理 df df.dropna(howall) # 全空行直接丟 if drop_empty and filter_empty_cols: df df.dropna(subsetfilter_empty_cols) # 遍歷每一行轉(zhuǎn)自然語(yǔ)言 Document documents [] for idx, raw_row in df.iterrows(): row raw_row.dropna() # 去掉 NaN 字段避免文本里出現(xiàn) nan text row_to_text(row, table_desc) documents.append( Document( texttext, metadata{ source: file_path, table: table_desc, row_index: idx, }, ) ) return documents注意我把dtypestr強(qiáng)制全部轉(zhuǎn)字符串避免 ID 變成科學(xué)計(jì)數(shù)法或日期被 pandas 改格式。然后空值字段直接不寫進(jìn)文本只保留有效字段。這里有個(gè) metadata 設(shè)計(jì)細(xì)節(jié)想多說一句row_index一定要留。后面如果發(fā)現(xiàn)某條檢索結(jié)果內(nèi)容有問題你能直接通過 row_index 回原表定位定位速度比全文搜原文件快得多。數(shù)據(jù)量一大這個(gè)習(xí)慣能救你很多次。3. Excel 導(dǎo)入實(shí)戰(zhàn)多 Sheet、合并單元格與公式緩存的坑Excel 比 CSV 麻煩一個(gè)數(shù)量級(jí)。CSV 是純數(shù)據(jù)Excel 里埋著格式、公式、多個(gè) Sheet、合并單元格、日期類型等各種隱雷。我在項(xiàng)目里處理最多的問題有三個(gè)多 Sheet 映射、合并單元格導(dǎo)致的空值、公式值讀不到。3.1 選型PandasExcelReader 還是 openpyxl 自己來LlamaHub 里有兩個(gè)相關(guān)的 ReaderPandasExcelReader和PandasCSVReader之類。PandasExcelReader 的定位是「把每個(gè) Excel 文件轉(zhuǎn)成一個(gè)或多個(gè) Document」內(nèi)部用pd.read_excel對(duì)單 Sheet 的簡(jiǎn)單表夠用但對(duì)復(fù)雜 Excel 不夠靈活。復(fù)雜場(chǎng)景我基本直接用pd.read_excel(file, sheet_nameNone)把所有 Sheet 一次性讀進(jìn)來再逐個(gè)自定義處理。這樣每個(gè) Sheet 是獨(dú)立的 DataFrame你可以為不同 Sheet 配置不同的 table_desc、不同的空值策略、不同的行文本模板。另外一個(gè)值得記住的點(diǎn)pd.read_excel需要指定引擎。.xlsx默認(rèn) openpyxl.xls需要 xlrd。舊版.xls文件現(xiàn)在反而少見但遇到時(shí)引擎報(bào)錯(cuò)很容易讓人懵建議直接.xls文件先用 openpyxl 另存為.xlsx別跟它較勁。3.2 多 Sheet 怎么拆元數(shù)據(jù)怎么標(biāo)多 Sheet 的坑在于不同 Sheet 可能是完全不同的業(yè)務(wù)表。比如一個(gè)「月度銷售報(bào)表.xlsx」里Sheet1 是訂單明細(xì)Sheet2 是區(qū)域匯總Sheet3 是產(chǎn)品目錄。如果你把它們當(dāng)成同一個(gè)文件處理混在同一個(gè)索引里檢索時(shí) Sheet 之間的相互干擾非常嚴(yán)重。正確做法是每個(gè) Sheet 獨(dú)立構(gòu)建 Document并且把sheet_name放進(jìn) metadataimport pandas as pd from llama_index.core.schema import Document def load_excel_workbook(file_path, sheet_to_table_descNone): # sheet_to_table_desc 是 {訂單明細(xì): 訂單記錄, 區(qū)域匯總: 區(qū)域銷售統(tǒng)計(jì)} dfs pd.read_excel(file_path, sheet_nameNone, dtypestr) documents [] for sheet_name, df in dfs.items(): table_desc (sheet_to_table_desc or {}).get(sheet_name, sheet_name) # 清洗邏輯同 CSV略 for idx, raw_row in df.iterrows(): row raw_row.dropna() text row_to_text(row, table_desc) documents.append( Document( texttext, metadata{ source: file_path, sheet: sheet_name, table: table_desc, row_index: idx, }, ) ) return documentsmetadata 里加sheet字段后檢索時(shí)可以做到表級(jí)過濾——用戶問題里出現(xiàn)「區(qū)域匯總」相關(guān)描述時(shí)只在該 Sheet 的 Document 范圍內(nèi)檢索這比把所有 Sheet 混在一起喂給模型要準(zhǔn)得多。后面第 5 章我會(huì)細(xì)講過濾和路由的用法。3.3 合并單元格和公式緩存兩個(gè)低頻但致命的問題合并單元格是 Excel 導(dǎo)入里最陰間的坑。比如一個(gè)產(chǎn)品目錄表產(chǎn)品名稱列做了縱向合并合并后pd.read_excel讀進(jìn)來只有合并區(qū)域的第一行有值其余行全是 NaN。如果你不做處理導(dǎo)入后的文本里那些行就缺了產(chǎn)品名稱而數(shù)據(jù)本身是存在的你只是沒讀到。處理合并單元格的常見辦法是 forward fill# 對(duì)需要前向填充的列做合并單元格還原 df[產(chǎn)品名稱] df[產(chǎn)品名稱].ffill()注意ffill只能解決「上方合并」的情形如果有橫向合并單元格需要ffill(axis1)但橫向合并語(yǔ)義復(fù)雜建議先輸出到 Excel 里肉眼確認(rèn)一遍再動(dòng)。公式緩存又是另一個(gè)坑。某些 Excel 單元格是公式比如SUM(C2:C20)。openpyxl 讀單元格值時(shí)有data_onlyTrue參數(shù)能返回上次 Excel 打開文件時(shí)緩存的計(jì)算結(jié)果但如果文件是程序生成、從來沒被 Excel 打開過緩存就不存在讀出來是 None。pandas 的read_excel內(nèi)部按緩存值讀取一旦遇到無緩存公式你會(huì)得到一列空的數(shù)值而用戶看到原始 Excel 里明明是有的。我的建議導(dǎo)入 Excel 之前先用辦公軟件把文件打開并另存一遍這樣公式緩存基本都在如果文件是程序自動(dòng)生成的優(yōu)先去源頭導(dǎo)出「值」而不是「公式」。另外可以在導(dǎo)入后做個(gè)數(shù)量級(jí)校驗(yàn)統(tǒng)計(jì)每個(gè)數(shù)值列的 sum對(duì)比業(yè)務(wù)方給的手工數(shù)字差太多就說明公式或合并單元格出了問題。4. 數(shù)據(jù)庫(kù)連庫(kù)實(shí)戰(zhàn)LlamaHub DatabaseReader 用法拆解前兩章講的 CSV、Excel 本質(zhì)上還是「文件導(dǎo)入」會(huì)有更新不及時(shí)、數(shù)據(jù)漂移的問題。數(shù)據(jù)庫(kù)表的正確姿勢(shì)是連庫(kù)直接讀LlamaHub 里的 DatabaseReader 就是干這個(gè)的。這一章把配置、查詢參數(shù)、行文本化全部過一遍。4.1 為什么連庫(kù)而不是導(dǎo)出 CSV你可能會(huì)想「我從數(shù)據(jù)庫(kù)里 SELECT 出來導(dǎo)出個(gè) CSV再走第二章的方案不就行了嗎」對(duì)于一次性遷移可以但如果是持久化知識(shí)庫(kù)連庫(kù)有不可替代的優(yōu)勢(shì)。第一時(shí)效性。很多 RAG 項(xiàng)目要求數(shù)據(jù)按天更新你要是定期導(dǎo)出 CSV 再觸發(fā)導(dǎo)入鏈路長(zhǎng)、容易漏。連庫(kù)讀取可以做到每次構(gòu)建索引時(shí)直接查最新數(shù)據(jù)或者做增量查詢。第二數(shù)據(jù)一致性。手工導(dǎo)出 CSV 容易丟類型、丟精度尤其大數(shù)值和帶時(shí)區(qū)的時(shí)間戳。直接從數(shù)據(jù)庫(kù)查詢拿到的值更準(zhǔn)也能保留主鍵和索引字段方便后面增量去重。第三可溯源。連庫(kù)導(dǎo)入時(shí)可以直接把主鍵或業(yè)務(wù) ID 放進(jìn) metadata出問題查原始記錄比靠 row_index 猜快得多。4.2 DatabaseReader 的配置、查詢參數(shù)與行文本化LlamaHub 的 DatabaseReader 核心依賴 SQLAlchemy所以只要你安裝了對(duì)應(yīng)對(duì)應(yīng)庫(kù)的方言驅(qū)動(dòng)比如 psycopg2、pymysql就能連 PostgreSQL、MySQL、SQLite、SQL Server 等主流數(shù)據(jù)庫(kù)?;A(chǔ)用法如下from sqlalchemy import create_engine from llama_index.readers.database import DatabaseReader engine create_engine(postgresql://user:passwordlocalhost:5432/mydb) reader DatabaseReader(sql_databaseengine) documents reader.load_data( querySELECT * FROM orders WHERE created_at 2024-01-01 LIMIT 10000 )load_data的不同之處在于它需要你傳入一個(gè)query參數(shù)而不是像文件 Reader 那樣傳路徑。每個(gè)返回行會(huì)被轉(zhuǎn)成 Document列名會(huì)變成 metadata 的一部分。這個(gè)設(shè)計(jì)的好處是你可以用 SQL 完成大部分清洗工作WHERE過濾、條件聚合、甚至 JOIN 后直接 SELECT 出你想要的上下游上下文。但我實(shí)際操作中的體會(huì)是DatabaseReader 直接返回的 Document text 仍然偏機(jī)械基本是 key: value 形式。所以我對(duì)它的用法是先用它拿到 DataFrame 或 List[Dict]再走一遍自定義行文本化和 metadata 構(gòu)建。如果你不想改源碼可以不用 load_data而是自己用 SQLAlchemy 查詢import pandas as pd from sqlalchemy import create_engine engine create_engine(postgresql://user:passwordlocalhost:5432/mydb) sql SELECT order_id, product, region, amount FROM orders WHERE created_at 2024-01-01 df pd.read_sql(sql, engine) # 復(fù)用第二章的 row_to_text 和 Document 構(gòu)建邏輯 documents [] for idx, row in df.iterrows(): text row_to_text(row, 訂單記錄) documents.append(Document(texttext, metadata{ source: postgresql://mydb/orders, row_id: row.get(order_id), table: orders, }))這個(gè)方案的可控性更高適合不同庫(kù)之間字段差異大、需要逐個(gè)定制的情況。DatabaseReader 的價(jià)值在你圖省事、表結(jié)構(gòu)干凈時(shí)最能體現(xiàn)表結(jié)構(gòu)復(fù)雜時(shí)我建議自己寫查詢。4.3 行級(jí)自然語(yǔ)言轉(zhuǎn)換讓向量檢索真正「看懂」數(shù)值連庫(kù)讀數(shù)據(jù)有個(gè)額外挑戰(zhàn)數(shù)據(jù)庫(kù)表往往字段名是英文或縮寫比如pc_name、qty、amt_usd。如果你直接把這些英文列名帶進(jìn)文本embedding 的效果會(huì)打折扣——中文用戶問「哪個(gè)產(chǎn)品代碼賣得最多」和你索引里的pc_name完全對(duì)不上。我目前最常用的方案是在 SQL 查詢層做好「列名自然語(yǔ)言映射」直接 SELECT 出自帶語(yǔ)義的文本字段SELECT order_id AS 訂單編號(hào), product AS 商品名稱, region AS 銷售區(qū)域, amount AS 訂單金額元, status AS 發(fā)貨狀態(tài) FROM orders WHERE created_at 2024-01-01;然后把 pandas 讀出來的 DataFrame 逐行轉(zhuǎn)自然語(yǔ)言效果比直接讓 reader 轉(zhuǎn)好很多。此外某些庫(kù)里有枚舉型、字典型字段比如status 1表示已發(fā)貨。嚴(yán)格來說這屬于數(shù)據(jù)清洗但放在導(dǎo)入鏈路里做最高效SQL 層面 JOIN 字典表把1翻譯成「已發(fā)貨」再進(jìn) RAG。否則你問「已發(fā)貨的訂單」永遠(yuǎn)匹配不到status1。5. 導(dǎo)入只是開始召回驗(yàn)證與數(shù)據(jù)同步表格數(shù)據(jù)導(dǎo)入完成不等于 RAG 就能回答業(yè)務(wù)問題。入庫(kù)之后你要回答一個(gè)更實(shí)際的問題「它到底能不能把正確的那幾行撈出來?!惯@一章是全篇的臨門一腳也是最容易被跳過的一步。5.1 召回測(cè)試問一遍真實(shí)業(yè)務(wù)問題索引構(gòu)建完不要著急接 query engine先做一次最小的召回測(cè)試。用index.as_retriever(similarity_top_k5)把業(yè)務(wù)方那十幾個(gè)真實(shí)問題逐一輸入直接看召回出來的 top-5 Document 文本。這一步不用讓 LLM 生成回答只看檢索質(zhì)量。舉例來說索引里是訂單表你問「華北區(qū)最近有哪些大額訂單」如果 top-5 里混著華南區(qū)的訂單說明 region 字段沒有在文本和 metadata 里被充分表達(dá)或者行文本生成得不夠清晰。這時(shí)候調(diào)行文本模板比調(diào) embedding 模型參數(shù)有效得多。如果發(fā)現(xiàn)某類問題總是召不回正確行大概率是行文本里缺少用戶會(huì)用的同義詞。比如用戶習(xí)慣說「大客」你的表里是「重點(diǎn)客戶」那就在行文本生成時(shí)把別名也拼進(jìn)去。這類詞表工作很笨拙但效果好。5.2 用 metadata 做表級(jí)路由與過濾表格數(shù)據(jù)進(jìn) RAG 后多張表共存時(shí)最大的噪聲源是「問 A 表問題召回了 B 表數(shù)據(jù)」。解決的關(guān)鍵是 metadata filter。你可以在構(gòu)建 retriever 時(shí)按表過濾retriever index.as_retriever( similarity_top_k5, filtersMetadataFilters( filters[MetadataFilter(keytable, value訂單記錄)] ) )更進(jìn)一步可以用函數(shù)判斷問題的關(guān)鍵詞來決定使用哪個(gè) filter。比如問題里出現(xiàn)「區(qū)域匯總」就過濾table區(qū)域銷售統(tǒng)計(jì)出現(xiàn)「訂單」就過濾table訂單記錄。這就是表級(jí)路由比讓 LLM 自己猜要穩(wěn)定得多。我建議每張表導(dǎo)入時(shí)都把一個(gè)標(biāo)準(zhǔn)化的table字段寫進(jìn) metadata并保證全庫(kù)統(tǒng)一命名。命名不規(guī)范比如一張叫「訂單記錄」、另一張叫「orders」路由就得寫兩套邏輯。5.3 增量更新的兩種策略與 doc_id 去重對(duì)于數(shù)據(jù)庫(kù)表這種變更頻繁的數(shù)據(jù)源全量重建只適合初期或小數(shù)據(jù)量場(chǎng)景。數(shù)據(jù)量大以后必須做增量。我實(shí)踐下來有兩條可行的路。第一條是時(shí)間戳增量。在表里加一個(gè)updated_at導(dǎo)入時(shí)記錄上次同步時(shí)間last_sync每次只查WHERE updated_at last_sync。這個(gè)方案防漏更新但對(duì)刪除操作不敏感某行被刪了索引里還留著。第二條是 doc_id 去重重建。給每個(gè) Document 設(shè)置doc_id為業(yè)務(wù)主鍵比如order_A1001。同步時(shí)不管新增還是更新全部插入插入前先把已存在的同 doc_id 文檔刪掉或標(biāo)記覆蓋。LlamaIndex 的index.delete(doc_id)和index.insert(document)配合就能做到。這個(gè)方案能處理更新和刪除但每輪要全量拉取需要同步的數(shù)據(jù)適合表數(shù)據(jù)量可控的場(chǎng)景。兩種方案也可以組合時(shí)間戳圈定范圍doc_id 去重保證更新。對(duì)大多數(shù)中小規(guī)模業(yè)務(wù)表這個(gè)組合已經(jīng)非常穩(wěn)了。關(guān)于同步頻率我的經(jīng)驗(yàn)是不要每次都重建整個(gè)索引。如果只有幾百行增量直接 insert 新 Document 到已有索引里快很多。如果增量超過總量 30%重建可能更劃算因?yàn)橄蛄克饕膭h除累積會(huì)留下碎片檢索性能會(huì)慢慢下降。表格數(shù)據(jù)進(jìn) RAG我現(xiàn)在越來越覺得做得好的關(guān)鍵不在于用了多高級(jí)的 embedding 或者框架而在于導(dǎo)入前的數(shù)據(jù)建模和導(dǎo)入后的驗(yàn)證閉環(huán)。CSV 要處理編碼和空值Excel 要解決 Sheet 和公式問題數(shù)據(jù)庫(kù)連庫(kù)要設(shè)計(jì)好查詢和行文本。每一步都有固定的坑踩過一遍之后后面基本就是復(fù)制模板的問題。按照上面這套流程走一遍你的表格知識(shí)庫(kù)應(yīng)該能覆蓋絕大多數(shù)業(yè)務(wù)問答場(chǎng)景。