:讓大模型寫Pandas代碼,而不是心算Excel數(shù)據(jù))
Excel表格扔給ChatGPT直接問“算一下每個季度的總銷售額”大概率的結(jié)局就是兩個它一本正經(jīng)地給你一個錯誤的數(shù)字或者干脆說自己處理不了。我這兩年跟表格數(shù)據(jù)打交道的時間不少試過各種姿勢之后得出了一個比較扎心的結(jié)論——在“算數(shù)”這件事上你不能把大模型當成計算器用而是要把RAG系統(tǒng)的架構(gòu)徹底改造成適合表格數(shù)據(jù)的形態(tài)讓模型去調(diào)度計算而不是去心算。這篇東西的定位不是科普是一份可以直接照做的實戰(zhàn)記錄。適合的人群很明確手里捏著大量Excel報表、每天做重復統(tǒng)計的數(shù)據(jù)分析、運營和項目經(jīng)理以及想在企業(yè)內(nèi)部搭一個“能對話的表格助手”的開發(fā)者。我會把整個系統(tǒng)的架構(gòu)、為什么這么設計、關(guān)鍵代碼邏輯、以及實際測試中的翻車記錄都寫清楚建議先收藏再慢慢看。1. 直接用ChatGPT處理Excel到底難在哪先潑一盆冷水。如果你只是偶爾拿一個幾十行的Excel讓ChatGPT看一眼它確實能應付因為那本質(zhì)上不是“計算”是“復述”。一旦數(shù)據(jù)量上去、問題帶條件、列名不統(tǒng)一問題就全都暴露了。拆開來看難點集中在三塊。1.1 大模型的數(shù)字處理邏輯和你想的不一樣大模型本質(zhì)上是個基于概率的文本生成器它看到“12345 67890”的時候并不是在腦內(nèi)運行加法電路而是在預測“這段文本后面最可能接什么樣的token序列”。數(shù)字被分詞器切碎之后它們之間的運算關(guān)系對模型來說只是一種語料中的統(tǒng)計規(guī)律。所以你會看到一種很詭異的現(xiàn)象問ChatGPT“250000乘以4”它可能算得對因為它見過太多類似算式但如果你的Excel里有幾十個互相關(guān)聯(lián)的字段要算“2023年華東區(qū)A類客戶的平均賬期天數(shù)”它就開始自由發(fā)揮了。這種“自由發(fā)揮”在報表場景里就是災難因為財務數(shù)據(jù)不允許模糊。1.2 常規(guī)RAG在表格數(shù)據(jù)上會結(jié)構(gòu)性地失效RAG的標準流程是把文檔切塊、向量化、存庫、檢索、拼上下文、讓模型作答。這套流程處理PDF、Word、網(wǎng)頁文本時表現(xiàn)不錯但套到Excel上就會出大事。問題出在“切塊”這一步。一個Excel表格切開之后切成的是各行碎片表頭和列名跟數(shù)據(jù)行被拆散了。檢索系統(tǒng)可能命中了第157行的數(shù)據(jù)但完全不知道這一行對應的是“哪個地區(qū)、哪個月的、什么指標”。換句話說常規(guī)RAG切碎的是一張有結(jié)構(gòu)關(guān)系的表而不是一段連續(xù)的散文。你當然可以增加塊的大小讓每個塊包含表頭但這又帶來第二個問題——Excel的寬度往往遠大于長度。一個寬表有40列只保留表頭加幾行數(shù)據(jù)一個塊就爆了向量化之后語義信息互相干擾檢索出來的結(jié)果可能牛頭不對馬嘴。1.3 表格解析本身就是一道坎Excel的結(jié)構(gòu)遠比看起來復雜。一個sheet可能有合并單元格、多層表頭、行列轉(zhuǎn)置、公式單元格、宏表格甚至一個sheet里存了多張相互獨立的報表。我見過最離譜的一個業(yè)務表同一個sheet里上面是“月度銷售概覽”下面隔兩行又貼了一個“庫存周轉(zhuǎn)明細”共享同一列但語義完全無關(guān)。這種表解析出來以后要是不做區(qū)域識別后面的檢索和計算全都會錯。這也是為什么有些人試了市面上的通用RAG工具來處理Excel效果奇差無比——管線底層根本不理解表格。2. 給表格單獨設計RAG架構(gòu)四層分工別有玄機把問題拆清楚之后我開始動手改造系統(tǒng)。核心思路只有一句話讓大模型干它擅長的事把計算任務甩給確定性的代碼去執(zhí)行。傳統(tǒng)RAG把“理解-檢索-計算-回答”全壓在一個模型身上。表格場景不行。我最后落地的架構(gòu)是四層各司其職結(jié)構(gòu)解析層負責把Excel讀成結(jié)構(gòu)化對象識別sheet、列名、合并單元格、數(shù)據(jù)類型生成每個sheet的schema模式摘要?;旌纤饕龑訉Ρ砀駜?nèi)容做“自包含切片”后同時建立向量索引和關(guān)鍵詞索引。向量負責語義召回關(guān)鍵詞負責精確命中列名和專有名詞。計算引擎層內(nèi)嵌一個Python代碼解釋器環(huán)境當用戶問題涉及聚合、計算、篩選時由模型生成Pandas代碼在受控環(huán)境里執(zhí)行拿到精確結(jié)果。調(diào)度與生成層最后的Orchestrator編排器負責判斷當前問題應該走檢索還是走計算還是兩者結(jié)合并組織最終的用戶可讀答案。這么設計的原因是吸取了我之前一個失敗項目的教訓。一開始我試圖讓大模型直接從檢索出來的表格片段里“提煉”答案結(jié)果它把求和都能算錯。后來我調(diào)整策略把它當成一個“會寫代碼的分析師”而不是“會算數(shù)的計算器”正確率一下就上去了。打個比方吧這就跟你讓一個實習生去處理報表一樣。你不該指望他心算能力過人而應該讓他用Excel的SUM函數(shù)做匯總把公式和截圖交給你核驗。代碼執(zhí)行器就是這個“實習生手里的SUM函數(shù)”。3. “算數(shù)”的關(guān)鍵讓模型寫代碼而不是給你答案很多教程講表格RAG講完檢索就結(jié)束了對“算數(shù)”這個核心需求基本是一筆帶過。但恰恰這才是實戰(zhàn)里最花時間的部分。3.1 為什么代碼執(zhí)行方案比模型直接回答可靠確定性是第一位的。Pandas算出來的數(shù)字只要代碼邏輯正確那結(jié)果就是數(shù)學上的精確值不存在“可能錯了但表面看起來像是對的”。這一點在日常報表場景里是生死線。其次是可追溯性。代碼是可以回看的每一步對數(shù)據(jù)做了什么處理有跡可循。前面提到的系統(tǒng)里每次調(diào)用計算引擎都會記錄三樣東西用戶原始問題、模型生成的代碼、執(zhí)行后的返回結(jié)果。出現(xiàn)爭議的時候直接把代碼拿出來看是哪一步的問題而不是對著一個幻覺數(shù)字干瞪眼。第三它能處理超大表。Excel動輒幾十萬行你不可能把全表塞進大模型上下文。但代碼執(zhí)行器不一樣它是直接加載原始文件做處理表多大都不影響。3.2 設計一個大模型能“讀懂”的計算調(diào)度Prompt模型要正確生成代碼前提是它得先知道表格里有什么。所以我會在調(diào)用計算引擎之前把結(jié)構(gòu)化schema和檢索到的數(shù)據(jù)樣例喂給模型。Prompt的大致結(jié)構(gòu)是任務說明你是數(shù)據(jù)分析師根據(jù)用戶的自然語言問題生成Python代碼。數(shù)據(jù)上下文列名、數(shù)據(jù)類型、每列的含義說明、分隔符。受限條件只能使用給定的數(shù)據(jù)文件路徑和Pandas不能訪問網(wǎng)絡路徑。輸出格式只輸出可執(zhí)行的Python代碼塊。這里有個容易忽略的細節(jié)如果你把幾十列全塞進Prompt模型生成代碼時反而容易用錯列名。所以我只注入經(jīng)過檢索篩選后相關(guān)的列信息無關(guān)列直接扔掉既省token又提高準確率。3.3 執(zhí)行安全別在你的真實環(huán)境里跑模型生成的代碼這一步必須慎重講。模型生成的代碼本質(zhì)上是動態(tài)執(zhí)行的安全性不可控。你總不能讓業(yè)務人員拿著這個工具結(jié)果模型生成了刪除文件的代碼。我的做法是把它關(guān)進一個沙箱環(huán)境。常見實現(xiàn)方案有好幾種輕重程度不同。個人開發(fā)者階段可以用Docker容器限制文件系統(tǒng)和CPU內(nèi)存把數(shù)據(jù)文件以只讀方式掛載進去代碼執(zhí)行完只允許輸出結(jié)果和日志文件不允許寫回原始數(shù)據(jù)。也可以在進程級別用權(quán)限隔離。企業(yè)場景下可以上一個跨平臺的函數(shù)計算平臺跑隔離任務。環(huán)境準備階段給你的代碼執(zhí)行器單獨建一個基礎鏡像預裝Pandas、NumPy等庫然后用定時任務自動拉取需要分析的業(yè)務表跑完就丟這樣最省心。4. 完整搭建路徑從表格解析到檢索再到計算驗證理論講完下面是可以直接照著搭的實操鏈路。我這里用Python生態(tài)做示范核心庫是pandas、openpyxl、langchain、以及一個兼容OpenAI格式的embedding接口。4.1 第一步解析Excel并生成結(jié)構(gòu)化schema這一步的目標不是把Excel全文切碎而是先把“骨架”提出來。包括文件里有哪些sheet每個sheet的列名、行數(shù)、列類型以及識別出合并單元格和多級表頭。from openpyxl import load_workbook def parse_workbook_schema(file_path): wb load_workbook(file_path, data_onlyTrue, read_onlyTrue) result [] for sheet_name in wb.sheetnames: ws wb[sheet_name] headers [] for row in ws.iter_rows(min_row1, max_row2, values_onlyTrue): headers.append([str(c) if c is not None else for c in row]) merged_regions list(ws.merged_cells.ranges) result.append({ sheet_name: sheet_name, headers: headers, merged_cells: [str(r) for r in merged_regions], max_row: ws.max_row, max_column: ws.max_column }) return result注意data_onlyTrue這個參數(shù)很重要它拿的是公式計算后的緩存值否則你讀出來的可能是公式字符串而不是數(shù)值。多級表頭這里先保留前兩行后面生成自包含切片時會用到。解析出來后的每個sheet我會額外用Pandas讀一次全量數(shù)據(jù)生成每列的統(tǒng)計摘要非空值數(shù)量、唯一值前20個、數(shù)值列的最小最大值。這個摘要不是給用戶看的是給后續(xù)檢索和計算調(diào)度用的上下文。4.2 第二步設計“自包含切片”而不是“文本切片”表格切片的單位不能是一個500字的文本塊而應該是一個“帶上下文的記錄單元”。我的做法是一個切片 該行的數(shù)據(jù) 該行的表頭關(guān)聯(lián)信息。最樸素的方式是把每行數(shù)據(jù)格式化成一段自然語言描述把表頭讀出來拼接進去。比如一行數(shù)據(jù)有兩個字段“銷量”和“月份”切片文本就是“銷量12345月份2024年3月所屬sheet月度銷售表”。def make_row_chunk(df, sheet_name, headers, row): pair_parts [] for col, value in zip(headers, row): if value is not None: pair_parts.append(f{col}: {value}) text f[{sheet_name}] | .join(pair_parts) return {text: text, sheet: sheet_name, row_index: row_index}這么切的好處是檢索系統(tǒng)命中任何一個切片都能拿到完整的“這一行是什么”的信息。缺點是每個切片的embedding會稍微大一些但數(shù)據(jù)量在幾萬行級別時完全可接受。4.3 第三步混合索引向量和關(guān)鍵詞一個都不能少表格場景里用戶的提問方式分成兩類一類是語義模糊的比如“哪個區(qū)域賣得最差”一類指代精確的比如“華北區(qū)2024年1月”。前者要靠向量檢索因為“賣得最差”和“銷量、負增長、虧損”這些詞在語義上是近鄰。后者則靠關(guān)鍵詞檢索效果更好因為“華北區(qū)”和“2024年1月”這些字段值是精確字面匹配向量化反而會模糊掉邊界。所以我在索引層同時掛了Embedding向量數(shù)據(jù)庫和BM25關(guān)鍵詞索引。檢索時分兩路進行再做一個簡單的分數(shù)歸一化融合def hybrid_search(query, top_k8): token_res bm25_index.search(query, ktop_k) vec_res vector_index.search(query, ktop_k) merged {} for item in token_res: merged[item[id]] merged.get(item[id], 0) item[score] * 0.6 for item in vec_res: merged[item[id]] merged.get(item[id], 0) item[score] * 0.4 ranked sorted(merged.items(), keylambda x: x[1], reverseTrue)[:top_k] return ranked權(quán)重的選擇我這里給的是0.6/0.4的初始值。經(jīng)過多輪實測關(guān)鍵詞權(quán)重要稍微高一點因為表格里的列名和唯一標識符基本都是精確詞匯向量召回容易跑偏。不過這個比例不是固定的如果你的數(shù)據(jù)表里語義模糊的描述字段占多數(shù)可以把它調(diào)成0.5/0.5再測。4.4 第四步計算執(zhí)行器與結(jié)果回傳當用戶的問題包含“求和、平均、對比、占比、增長率”這類計算意圖時檢索本身解決不了問題必須走代碼執(zhí)行路徑。流程是這樣的用戶的自然語言問題 前面生成的schema摘要 檢索到的相關(guān)樣例行一起發(fā)給大模型讓它生成一段Pandas代碼。代碼放進沙箱執(zhí)行返回結(jié)果再把結(jié)果連同計算過程短代碼一起丟回給模型讓它組織成一段自然的文字回答。一開始我把這一步簡化成“模型直接返回代碼執(zhí)行結(jié)果”就是單純把數(shù)字暴露給用戶結(jié)果反饋很不好。后來改成“結(jié)論關(guān)鍵計算邏輯原始數(shù)據(jù)抽樣”三段式輸出用戶才真正覺得工具可信。這個改法源于一個實際場景測算月度環(huán)比增長率時模型直接給了個百分比數(shù)值用戶拿不準這是怎么算出來的審計的時候又過來問口徑相當折騰。后來每輸出一個問題都在后面加一行“計算邏輯說明”矛盾就消失了。4.5 第五步采樣核對別讓模型蒙混過關(guān)這是很多人忽略的一環(huán)。檢索可能召回干擾數(shù)據(jù)模型也可能寫錯代碼生成假結(jié)果。我的做法是在計算引擎執(zhí)行完代碼之后加一個自動核對步驟從計算涉及的數(shù)據(jù)源隨機抽取5-10條原始記錄人工或半自動地驗證關(guān)鍵數(shù)值是否與原始表一致。你可能會覺得這一步多余但我負責任地說沒有這個驗證機制的表格RAG系統(tǒng)上線之后一定會被業(yè)務人員挑出毛病。他們隨便翻開Excel看一眼就能發(fā)現(xiàn)你系統(tǒng)算的和表里的不一樣這會極大消耗信任。抽樣核對的具體實現(xiàn)可以很簡單def verify_sample(df_sample_path, result_values, problem_columns): raw_df pd.read_excel(df_sample_path) mismatches {} for col, expected_value in problem_columns.items(): actual_value raw_df.loc[0, col] if abs(actual_value - expected_value) 1e-6: mismatches[col] {expected: expected_value, actual: actual_value} return mismatches當然全自動核對很難覆蓋所有場景所以這個機制更多是兜底讓你在早期調(diào)參和驗證時能快速發(fā)現(xiàn)系統(tǒng)性問題而不是盲目相信端到端效果。5. 實測效果、翻車記錄跟幾個必須聊的取舍理論歸理論這系統(tǒng)能不能用還得看實測數(shù)據(jù)。我用一份模擬某零售公司銷售數(shù)據(jù)的Excel做了對比測試。表結(jié)構(gòu)大概有近10萬行、30列包含區(qū)域、城市、門店、商品類目、銷售額、成本、日期等信息。5.1 不同方案的準確率對比我在同樣一組問題上分別測了三種方案直接問大模型原始表格截斷版常規(guī)文檔型RAG管道本文的四層表格RAG系統(tǒng)方案數(shù)值精確率條件篩選正確率召回相關(guān)度可追溯性直接把數(shù)據(jù)塞給模型約2成很低中等無常規(guī)RAG約3成低低無四層表格RAG接近9成高高完整數(shù)值精確率這塊主要體現(xiàn)在求和、均值、環(huán)比增長這類問題上。常規(guī)RAG的問題在于檢索出來的片段經(jīng)常帶著表頭噪聲模型要么讀了重復行要么漏讀關(guān)鍵行而四層架構(gòu)用代碼執(zhí)行器從根源上避免了這種“數(shù)錯行”的問題。5.2 踩坑記錄切片缺上下文和列名歧義第一坑切片缺少關(guān)聯(lián)表頭。早期我在做自包含切片時只保留了當前行數(shù)據(jù)拼的文本沒帶上層表頭信息。結(jié)果檢索“哪個門店毛利率最低”時按向量召回命中的幾行里有的行沒有毛利率字段模型只能瞎猜。后來在切片文本里強制加上了sheet名、一級表頭、二級表頭拼接出的“列全名”問題才改善。如果你也遇到檢索到的片段看起來“還行”但模型就是答不準的情況優(yōu)先檢查是不是切片上下文不夠完整。特別是多層表頭場景光有列名還不夠要有“列路徑”。比如“銷量”在表里其實是“2024年上半年銷量”如果切片里只保留“銷量”檢索時信息損耗會很嚴重。第二坑列名歧義。同一列在原始Excel里叫“銷售額”但在另一份關(guān)聯(lián)表里叫“收入”。如果兩章表被一起塞進索引模型生成的代碼可能會搞混。我的解決方案是在schema摘要里設置同義詞組讓調(diào)度層在做計算前把用戶問題里的詞匯映射到正確的列名上。SYNONYM_MAP { 銷售額: [收入, 銷售額, total_sales], 成本: [成本金額, 成本價, cost], }這個映射表需要跟業(yè)務方一起維護成本不小但確實有效。5.3 關(guān)于本地小模型能不能干這活的思考有些團隊因為數(shù)據(jù)敏感不能用云API一定要本地部署大模型。我也試過。結(jié)論是效果取決于模型寫Pandas代碼的能力而不是模型“聰明不聰明”。一個在代碼推理測試上表現(xiàn)不錯的本地模型配合結(jié)構(gòu)清晰的schema和嚴格的代碼沙箱在“生成計算代碼”這個環(huán)節(jié)基本夠用。但在“組織自然語言回答”和“復雜問題意圖拆分”上跟云端大模型差距還是明顯的。如果你的場景是內(nèi)部報表問答本地小模型可以接受如果要面向不太懂數(shù)據(jù)的業(yè)務用戶那就得在調(diào)度層多寫一些規(guī)則兜底或者用云端模型做前期意圖理解。還有一個折中方案把embedding和意圖識別放云端但實際的數(shù)據(jù)文件始終留在本地代碼執(zhí)行也在本地完成。這樣幾乎沒有數(shù)據(jù)外泄風險因為原始數(shù)據(jù)根本不會離開你的環(huán)境。具體能不能實現(xiàn)取決于你的合規(guī)要求但這個思路值得考慮。5.4 多sheet協(xié)同別讓模型在迷宮里迷路一份Excel經(jīng)常幾十個sheet有些sheet之間還有聯(lián)動關(guān)系。最典型的場景是“把銷售匯總和庫存明細關(guān)聯(lián)起來算庫存周轉(zhuǎn)天數(shù)”。這時候你不能讓模型自己大海撈針去找sheet名必須在調(diào)度層就給出一個明確的sheet索引地圖。我的做法是每個sheet有一個帶描述的角色標簽比如“sheet_01: 銷售明細_訂單級數(shù)據(jù)_包含退換貨標記”。調(diào)度模型在拿到問題后第一步先決定需要哪些sheet參與計算。如果這個問題涉及兩張表計算引擎會先分別取出需要的數(shù)據(jù)再交給Pandas做連接。加了這個環(huán)節(jié)以后之前那些“它自己找錯sheet導致數(shù)字完全對不上”的案子少了很多。本質(zhì)上是因為大模型在沒有導航的情況下處理多表關(guān)系時跟人類面對幾十個標簽不清的Excel文件一樣容易懵。寫在最后前陣子一個做數(shù)據(jù)運營的朋友跑來問我說他有個同事每次統(tǒng)計數(shù)據(jù)都要手動翻Excel能不能用AI自動替代。我說你直接給AI一張表讓它干活它大概率給你提供情緒價值和一堆錯誤數(shù)字但如果你給它一套“會看表、會查數(shù)、會寫代碼驗算”的管道它反而能頂一個初級的分析師。這話糙理不糙也是我做完這套表格RAG系統(tǒng)后最大的感觸。最后再分享一個只有踩過坑才會懂的小技巧不要一上來就追求全自動化。哪怕你的檢索和計算環(huán)節(jié)都已經(jīng)穩(wěn)定在上線初期也保留一個“人工復核”按鈕把每個回答對應的計算代碼和取數(shù)范圍展示給用戶。等信任建立起來以后再逐步放開自動化這樣既不會讓業(yè)務部門天天來投訴也能讓你有充分時間調(diào)優(yōu)底層參數(shù)。我就是這么一步步把準確率從“見不得人”磨到能交付的。