:從財報解析到可信問答的完整鏈路搭建)
把一份五十頁的年度財報丟給大模型讓它準確說出“去年第四季度毛利率是多少比第三季度變化了多少”你會得到什么如果直接問通用大模型它大概率會給你一個看似合理、其實是編出來的數(shù)字。這正是我想通過 Financial RAG Demo 解決的核心問題用一條完整的RAG服務鏈路把金融文檔變成可問答、可溯源、可驗證的“本地知識庫”。這篇內容會完整拆解這個Demo從文檔解析、切片、向量檢索到生成回答的整個設計與實操過程適合正在做知識庫問答、金融NLP場景或者剛接觸RAG應用開發(fā)的同學參考。金融場景對RAG的要求和通用文檔問答不太一樣數(shù)據(jù)密度高、術語密集、表格多、對數(shù)字準確性要求極高。整個Demo做完我最大的感受是RAG服務的難點不在“接入大模型”而在前面的數(shù)據(jù)準備和檢索質量。鏈路中任何一個環(huán)節(jié)偷懶最后都會在回答質量上暴露出來。下面直接進入正題按我實際開發(fā)的順序把所有關鍵技術決策和踩坑經驗逐一講清楚。1. 為什么做Financial RAG Demo金融場景對RAG服務的特殊要求1.1 金融文檔問答難在哪里金融領域的文檔管理是個典型的海量數(shù)據(jù)場景。上市公司的年報、季報、臨時公告、研報、監(jiān)管政策文件動輒幾十上百頁PDF格式居多內部還塞滿了財務報表、管理層討論、腳注注釋。過去這些文檔的利用方式基本就是“存起來、搜出來、人肉看”想要跨文檔得到一個綜合性的答案效率非常低。我把這個項目定位在“金融文檔問答”而不是泛泛的“聊天機器人”就是因為金融問答有幾個很難繞開的痛點。第一個痛點是術語密集。像“毛利率”“凈資產收益率”“經營活動產生的現(xiàn)金流量凈額”這種表述一個概念有多種叫法縮寫和全稱混用。通用問答系統(tǒng)很難理解這些術語之間的關聯(lián)檢索時容易出現(xiàn)“關鍵詞命中了但拿到的段落根本不是用戶想問的那個指標”。第二個痛點是數(shù)字敏感。金融問答最怕胡說一個人問“去年研發(fā)投入多少錢”如果系統(tǒng)回答里數(shù)字跟真實財報對不上這個系統(tǒng)基本就失去信任了。數(shù)字對錯的根源往往不在大模型而在檢索階段有沒有把正確的表格和上下文段落召回來。RAG服務如果不針對表格做專門處理檢索到的內容經常是殘缺的。第三個痛點是來源必須可溯?!案鶕?jù)哪份報告、哪一頁得出這個結論”在金融場景幾乎是剛需?;鸾浝?、風控人員、研究員拿到一個AI回答第一反應就是去看原始依據(jù)。沒有引用來源的答案在金融場景等于無效答案。第四個痛點是時效性。財報是定期發(fā)布的公告是實時增加的模型固有的知識永遠滯后。RAG天然適合這種“知識庫持續(xù)變化、需要即時更新”的場景換數(shù)據(jù)源就能換知識不需要重新訓練。1.2 為什么是RAG而不是微調大模型在做這個Demo之前一個繞不開的問題是為什么不用微調的方式把財報內容直接喂給大模型訓練我當時把兩種方案放在一起對比過結論非常明確。對比維度RAG方案全量微調方案知識更新成本換索引、加文檔即可分鐘級生效需要重新訓練小時到天級成本高輸出可信度回答基于檢索到的原文片段可溯源知識固化在參數(shù)里無法定位證據(jù)幻覺控制能力可在提示詞層強約束“只能依據(jù)上下文”模型容易把訓練知識與上下文混雜實施成本無需訓練GPU開發(fā)成本集中在檢索鏈路需要訓練環(huán)境和高質量標注數(shù)據(jù)適合場景動態(tài)知識庫、專有文檔、持續(xù)更新的資料固定領域語料的指令遵循能力提升微調解決的是“讓模型更懂這個領域的說話方式”而不是“讓模型記住這份文檔里寫了什么”。你把一份三千行的財報PDF塞進微調數(shù)據(jù)集訓練完之后你問它其中某個具體數(shù)字它依然可能編造。因為大模型的學習本質是壓縮和泛化不是精確存儲。RAG的思路完全不同我不要求模型記住文檔內容我只要求它學會“基于給定的材料回答問題”。知識存儲在檢索系統(tǒng)里模型負責組織和表達。這一層解耦是金融場景格外看重的因為文檔是流動的、權限是分級的、答案是必須能翻到原始頁面的。1.3 Demo項目的目標與邊界一個Demo不能什么都做否則會變成“什么都很糙的半成品”。我在動手之前先劃定了邊界明確這個Financial RAG Demo要證明三件事一是能對結構化程度低的金融PDF完成高質量解析與切片二是能準確檢索到包含關鍵數(shù)字和對比信息的片段三是能基于檢索內容生成帶來源引用的可讀回答。演示數(shù)據(jù)源我選的是公開的模擬財報PDF、模擬公司公告和模擬行業(yè)研報一共準備了十幾份。為什么不直接用真實財務數(shù)據(jù)一是合規(guī)風險二是不想在Demo階段花大量時間處理數(shù)據(jù)授權問題。模擬數(shù)據(jù)已經足夠把鏈路跑通、把問題暴露出來。這個項目的定位是“最小可用的金融問答參考實現(xiàn)”不是生產級系統(tǒng)。在這個定位下我要的是快速驗證方案、沉淀可復用的工程代碼而不是一開始就去追求過高的準確率。2. RAG鏈路逐個拆從金融PDF到可信答案2.1 文檔解析金融PDF結構化處理不能只做文本抽取RAG服務的上游是文檔解析。很多人一開始覺得解析很簡單不就是把PDF里的文字提取出來嘛。金融PDF分分鐘教你做人。我遇到的第一個坑是表格。財報的利潤表、資產負債表、現(xiàn)金流量表全是表格。如果用最簡單的文本抽取工具直接提取表格會變成一行行沒有任何結構邏輯的文字堆疊數(shù)字和項目名稱的對應關系全部丟失。比如“營業(yè)收入 1023456789”和“營業(yè)成本 987654321”在一張表里提取成純文本后你根本不知道哪個數(shù)字對應哪個指標。要解決這個問題必須使用能識別表格結構邊界、能按單元格關系輸出的解析流程。我實際用的方案是兩層處理先用布局分析方式識別頁面中的文本塊和表格區(qū)域再對表格區(qū)域單獨走表格結構識別邏輯嘗試還原出行列關系。解析完成后每條內容都帶上坐標信息、所在頁碼和所屬章節(jié)標題這一步為后面的切片和引用打基礎。頁眉頁腳也是必須去掉的不然每頁重復的“某某公司2023年年度報告”字樣會被切進多個切片檢索時嚴重干擾相關性排序。這里有一個容易被忽視的點解析結果不要直接扔給切片器而是要轉成統(tǒng)一的中間結構。我給每個文檔塊設計了一個簡單的元數(shù)據(jù)模型包括doc_id、page、section、content、type文本/表格/圖表標題字段。后面所有環(huán)節(jié)都依賴這套元數(shù)據(jù)作用類似給每塊內容貼上了身份證檢索到之后能回答“這段出自哪里”。2.2 切片策略保留語義、保住上下文、還要留得住表格切片是RAG里看似簡單、實際影響非常大的環(huán)節(jié)。切片太粗檢索粒度大相關性漂移嚴重一個大片段里只含一小段有效信息浪費向量空間切片太細語義不完整一個財務指標的定義被切斷或者表格的表頭和數(shù)據(jù)被拆到兩個切片里檢索到了也沒法用。我測試過三種切片方式結果差異非常明顯切片方式優(yōu)點缺點適合場景固定長度切片如每512字一刀實現(xiàn)簡單索引穩(wěn)定切斷段落語義表格極易拆散純文本資料按章節(jié)/標題切片語義完整邊界自然章節(jié)長度差異大長章節(jié)片段過大結構化良好的文檔自定義語義切片標題段落表格保護兼顧語義與檢索精度需要額外開發(fā)規(guī)則金融財報、公告我最終用的是第三種核心邏輯很簡單先把文檔按標題層級切成大的區(qū)塊再對超過長度閾值的區(qū)塊按段落邊界二次切分同時加了一個保護規(guī)則——表格區(qū)域凡是檢測到表頭和首行數(shù)據(jù)的就整個作為一個獨立切片不允許切散。切片之間加少量重疊窗口避免把一個概念的結尾和下一個概念的開頭隔開。重疊窗口大小我設了64個字符左右。這個數(shù)值不是拍腦袋定的是通過檢索召回率對比得出來的。太小起不到銜接作用太大會讓相鄰切片高度相似檢索結果里出現(xiàn)四五條高度重復的內容白白占用上下文窗口。2.3 Embedding與檢索混合檢索加排序為什么是標配這個Demo的檢索模塊沒有只依賴向量相似度而是用了“稀疏檢索稠密檢索”的混合方案。原因很現(xiàn)實向量檢索擅長處理語義相近但字面不同的表達比如“公司賺了多少錢”和“凈利潤”的語義關聯(lián)但在金融場景關鍵詞精確匹配依然非常重要尤其是年份、股票代碼、專有指標名這種字符特征強烈的查詢稀疏檢索的精確命中率往往高于向量召回。我使用的Embedding特征不是自己訓練的而是選擇了在通用語料上預訓練好的模型來生成向量。選擇模型時主要看兩方面一是對中文長文檔的支持二是金融術語的理解能力。實測下來通用Embedding模型對“ROE”“經營性現(xiàn)金流”這類術語的字面匹配還行但有時會抓不住“股東回報能力”這種同義表達。當時時間有限沒有自訓練模型但我在檢索層做了補償加入BM25稀疏召回再用重排序模型把兩路結果合并打分這樣即使向量語義召回沒抓住精確關鍵詞BM25也能補位反之亦然。混合檢索的合并我用的是RRF算法對兩路候選按排名融合而不是簡單加權平均分數(shù)。因為不同檢索算法產出的分數(shù)區(qū)間差異很大直接加權容易被某一方帶偏。排序階段再通過一個輕量級的重排序模型對召回的Top 20段重新打分取Top 5作為上下文。這一步的效果立竿見影Top 5命中率明顯提升冗余內容也被濾掉不少。2.4 生成增強提示詞約束、來源引用與拒答機制檢索做得再好生成環(huán)節(jié)設計不到位回答質量同樣拉跨。我在提示詞層面做了三個重要設計。第一個是上下文約束。原原本本在系統(tǒng)提示詞里寫清楚“只根據(jù)下面提供的資料回答問題。如果資料未提及直接回答‘未在現(xiàn)有資料中找到相關信息’嚴禁編造。”這是對抗幻覺的第一道防線雖然不能完全消除幻覺但能大幅降低模型自由發(fā)揮的概率。第二個是來源標注。我要求模型在回答的每個關鍵結論后用中括號標注資料編號例如“[資料1][資料3]”。這些編號對應檢索結果的元數(shù)據(jù)最終在展示層映射回“報告名稱頁碼”。用戶看到數(shù)字想核實只要照著頁碼翻原文就行。這是金融場景必須有的能力。第三個是拒答機制。對于檢索相關度不夠高的提問我設置了一個門檻邏輯如果重排序后的最高分數(shù)低于設定閾值或者上下文與問題的關鍵詞重疊度太低服務端就直接返回“當前資料庫中沒有覆蓋到您的問題”。寧可說不知道也不硬答。這個思路在可靠性要求高的場景非常值得推廣。3. 實操記錄我如何搭起這個RAG服務3.1 技術選型與環(huán)境準備技術選型只有一個原則能用最簡方案搞定的就不引入重型組件。整套RAG服務驗證下來我的核心組成是一個PDF解析模塊負責布局分析和文本抽取、一個向量化模塊負責將切片轉為向量、一個向量數(shù)據(jù)庫負責存儲和召回、一個搜索引擎做BM25稀疏檢索、一個重排序模型負責精排以及一個大模型服務負責生成最終答案。為什么向量數(shù)據(jù)庫我選了本地部署而不是云服務因為這個Demo涉及金融資料處理數(shù)據(jù)不出本地是更穩(wěn)妥的姿態(tài)。實際選型時看重的點也不復雜是否支持過濾條件、是否有穩(wěn)定易用的客戶端、查詢延遲能不能控制在幾百毫秒以內。開源方案完全夠用。大模型這一層我同時測試了本地部署的開源輕量模型和商業(yè)API兩種方案。Demo階段為了效果展示更穩(wěn)定最終選擇了API方式因為顯卡資源有限。但代碼層面我把模型服務封裝成了可替換的接口后期如果要在純內網(wǎng)環(huán)境部署只需要把接口實現(xiàn)換成本地模型其他邏輯完全不用動。這一點對金融項目尤其重要因為生產環(huán)境很可能不允許調用外部服務。3.2 離線索引構建的完整流程索引構建是RAG服務的離線環(huán)節(jié)我剛才梳理了一下完整流程大概是這樣的讀取PDF文件逐頁做布局分析識別正文、表格區(qū)域和頁眉頁腳對表格區(qū)域走表格結構還原邏輯整理成帶有行列信息的Markdown格式文本將每頁內容按“章節(jié)切片區(qū)塊切分表格保護”的規(guī)則切分同時記錄元數(shù)據(jù)對每個切片做清洗去掉空白字符修正編碼問題調用Embedding接口生成向量將切片文本、元數(shù)據(jù)、向量全部寫入向量數(shù)據(jù)庫同時將文本寫入稀疏檢索引擎構建完成后做一個抽樣自檢把幾個典型問題對應的切片撈出來確認內容是否完整。清洗這一步在代碼里必須做好否則后面檢索會出現(xiàn)各種奇怪問題金融文本里常見的全角半角混用、制表符殘留、編號列表的縮進符號都需要在這一步抹平。索引構建完成后我建議加個自動統(tǒng)計腳本輸出切片總數(shù)、平均長度、表格切片數(shù)量等指標。這一步能幫你迅速發(fā)現(xiàn)解析環(huán)節(jié)有沒有出問題。我第一次跑完發(fā)現(xiàn)表格切片數(shù)量異常少回去排查才發(fā)現(xiàn)表格識別邏輯對某些樣式的表格失效了這個腳本直接幫我定位了問題非常實用。3.3 在線問答服務的關鍵代碼實現(xiàn)在線問答服務我拆成了三個模塊Query理解、檢索召回、生成回答。以下是我核心鏈路簡化后的實現(xiàn)邏輯# 檢索模塊混合召回 RRF融合 重排序 def retrieve_documents(query: str, top_k: int 20, final_k: int 5): # 1. 稀疏召回BM25 sparse_results sparse_searcher.search(query, top_k) # 2. 稠密召回向量 query_vector embedding_service.embed(query) dense_results vector_db.search(query_vector, top_k) # 3. RRF融合兩路結果 fused rrf_fusion(sparse_results, dense_results) # 4. 用重排序模型做精排取Top K reranked rerank_service.rerank(query, fused) return reranked[:final_k]# RRF融合按排名倒數(shù)累加分數(shù) def rrf_fusion(sparse_results, dense_results, k: int 60): score_dict {} for rank, doc in enumerate(sparse_results): doc_id doc[id] score_dict[doc_id] score_dict.get(doc_id, 0) 1 / (k rank) for rank, doc in enumerate(dense_results): doc_id doc[id] score_dict[doc_id] score_dict.get(doc_id, 0) 1 / (k rank) return sorted(score_dict.items(), keylambda x: x[1], reverseTrue)生成模塊的提示詞我構建成了一段結構化的文本把檢索到的片段按編號排列并要求大模型嚴格按照編號引用請回答用戶問題。只允許參考下面列出的資料嚴禁使用資料以外的知識作答。 資料 [1] 來源模擬公司2023年度報告 第12頁 內容報告期內公司實現(xiàn)營業(yè)收入142.8億元同比增長18.5%... [2] 來源模擬公司2023年三季度報告 第5頁 ... 要求 - 回答每一點關鍵信息后標注來源編號例如[1] - 若資料中沒有相關信息直接回復“現(xiàn)有資料中未找到相關信息” - 語言簡潔使用金融行業(yè)常用表述。有個接口細節(jié)必須注意問答接口的響應時間幾乎全花在大模型生成上檢索階段一般耗時不大。用戶側體驗上建議加流式輸出一邊生成一邊推送而不是等全部生成完再一次性返回。金融場景里用戶看到的是逐段出現(xiàn)的答案加引用標注體驗會好很多。3.4 演示界面與效果評估Demo要給別人看一個丑陋的命令行界面肯定不行。我花了一下午用輕量級Web框架搭了個極簡的聊天界面左側是文檔目錄和索引狀態(tài)右側是對話窗口回答內容下面的“查看來源”按鈕可以直接定位到原始PDF的頁碼。界面本身只做展示重點是把檢索鏈路的所有中間信息暴露出來。展示中間信息是我特別堅持的一點。每次問答界面右側會顯示命中的Top 5片段、各片段的重排序分數(shù)、用了哪些資料。別小看這個設計它能極大增強使用者的信任感也讓這個Demo更適合用于內部技術評審。別人看到的不只是一個“漂亮的回答”而是一條可解釋的處理路徑。效果評估我建了一個小規(guī)模的人工評測集三十個問題覆蓋三類事實查詢型“去年營收是多少”、對比分析型“四季度毛利率環(huán)比變化”、跨文檔綜合型“兩份報告中關于研發(fā)投入的描述有什么差異”。我按四個維度打分檢索命中率、回答準確率、來源引用準確率、拒答合理率。初版全量跑完回答準確率大概在七成左右后來經過切片策略和重排序調優(yōu)后有明顯提升。這里說一句構建評測集不需要很多條但要保證問題的典型性和多樣性比盲目堆數(shù)量有用得多。4. 踩坑實錄金融RAG的常見問題與排查4.1 表格數(shù)據(jù)解析后“散架”了我踩過無數(shù)次的坑表格數(shù)據(jù)解析成Markdown后行錯位、列錯位。原因很隱蔽PDF里某些單元格的文字過長會自動換行如果解析器沒有正確處理“單元格內換行”就會把下一列的內容頂?shù)较乱恍姓麄€表的結構直接崩掉。排查方式是把解析后的表格與原文并排對比逐列檢查。修復方式有兩種一是選用對單元格邊界更敏感的解析配置二是在解析后增加一條校驗邏輯對項目名與數(shù)字的對應關系做簡單規(guī)則檢查發(fā)現(xiàn)明顯異常就觸發(fā)重新解析。這個教訓讓我明白一件事金融PDF解析永遠需要人工抽檢不能只看一兩個頁面的效果就默認全部沒問題。4.2 財報數(shù)字檢索錯位指標和數(shù)值對不上有一次測試問“經營活動現(xiàn)金流凈額”系統(tǒng)返回的是“投資活動現(xiàn)金流”那個表的片段。乍一看兩個表結構非常像但指標名差了幾個字數(shù)字完全不是用戶想要的。原因在于Embedding模型對“經營活動”和“投資活動”這兩個高度相似短語的區(qū)分能力不足向量距離太近導致錯誤召回。解決辦法是引入字段級元數(shù)據(jù)的硬約束檢索時如果問題中識別到“經營”“投資”“籌資”這類關鍵詞就在向量檢索的過濾條件中限定對應的表頭區(qū)域不讓其他表的數(shù)據(jù)參與候選?;旌蠙z索加規(guī)則化是金融RAG必須有的組合純粹靠語義理解來處理財務術語的精確對應目前還不可靠。4.3 金融縮寫與專業(yè)術語讓向量檢索失效“ROE”這個詞通用Embedding模型大概率能關聯(lián)到“凈資產收益率”但“EBITDA”這種更專業(yè)的縮寫向量召回效果就明顯差了。測試問“公司EBITDA情況”召回到的片段經常是正文里順帶提到這個縮寫的那句話而不是真正披露指標的表格頁。我之前一直強調混合檢索和重排序就是在這里發(fā)揮作用的。BM25對“EBITDA”這種精確詞命中很好重排序模型也能根據(jù)上下文重新識別真正包含指標的表格段落。此外我還手動維護了一小份金融術語同義詞詞典查詢進來先做一遍術語擴展把“ROE”映射成“凈資產收益率”這樣一來向量檢索的命中率有明顯提高。4.4 答案仍然幻覺提示詞約束并不萬能就算提示詞寫了“嚴禁編造”模型在上下文中找不到明確答案時依然可能把相鄰年份的數(shù)據(jù)張冠李戴或者把上下文沒有的因果關系編出來。我見過最典型的情況是上下文中只有2022年和2023年的營收數(shù)據(jù)問“2021年營收”的時候模型硬是從兩句數(shù)據(jù)的表述里推測了一個數(shù)字還說得很篤定。應對幻覺靠兩道防線。第一道是檢索質量只要正確的切片被召回到Top位置幻覺概率就大降第二道是“拒答門檻”我在生成前置邏輯里加了相關性閘門如果檢索結果綜合分數(shù)過低就不再調用生成接口而是直接返回提示讓用戶換種問法。前面提到過寧可答不上來也不要給一個貌似嚴謹?shù)腻e誤答案這在金融領域是一條底線。4.5 檢索性能與索引更新問題文本量放大之后檢索延遲會明顯上升。我曾在索引里塞了幾千份文檔發(fā)現(xiàn)一次查詢到了秒級趕緊查了原因——向量索引的索引參數(shù)沒有調優(yōu)導致暴力檢索。解決方式是把索引類型切換成更高效的近鄰檢索并調整構建參數(shù)延遲立刻降到了百毫秒以內。建議在壓測階段就關注這個參數(shù)不要等線上慢了再來排查。索引更新是另一個容易被忽略的環(huán)節(jié)。業(yè)務上新增了一份財報需要讓新知識立刻被檢索到。我采用的是全量重建與增量寫入結合的方式常規(guī)更新走增量接口文檔結構大改或數(shù)據(jù)清洗規(guī)則變化時觸發(fā)重建。增量更新時務必維護好文檔ID與切片ID的映射表否則舊切片無法定位更新后容易出現(xiàn)新老數(shù)據(jù)混答的問題。最后再分享一個小技巧整套Financial RAG Demo跑下來如果要我說最值得關注的優(yōu)化點不是換更大的模型而是把數(shù)據(jù)準備和檢索調優(yōu)做扎實。數(shù)據(jù)準備階段的每個決策都會在最終的回答質量上加分或減分。我建議每個做類似項目的人在第一次跑通端到端之后不要急著加強大模型能力而是建立一個“檢索失敗案例臺賬”把問答中檢索不到正確片段的案例一條條記錄下來分析是切片問題、解析問題還是Embedding問題。我自己就是這樣連續(xù)記錄和修復了三周數(shù)據(jù)準確率才真正穩(wěn)定下來。這個臺賬方法免費、實用、任何場景都能用強烈推薦。