建:多模態(tài)文檔解析與DeepSeek實體抽取實戰(zhàn))
簡介面向證券投研、NLP與知識圖譜方向的從業(yè)者與研究者這份192頁方案系統(tǒng)講解如何基于DeepSeek-VL2多模態(tài)文檔理解技術(shù)從券商研報中自動抽取實體關(guān)系并完成知識圖譜構(gòu)建適用于投研數(shù)據(jù)中臺、智能研報解析與金融知識服務(wù)等場景。文檔共51個大章節(jié)覆蓋本體設(shè)計、多模態(tài)數(shù)據(jù)預(yù)處理、表格與圖片信息抽取、數(shù)據(jù)標(biāo)注與質(zhì)量控制、Prompt語義擴(kuò)充、增量預(yù)訓(xùn)練、分布式環(huán)境搭建、長尾實體損失優(yōu)化、關(guān)系抽取聯(lián)合訓(xùn)練、微調(diào)方案對比及跨券商泛化測試等內(nèi)容。方案不僅給出技術(shù)選型依據(jù)還包含訓(xùn)練監(jiān)控、梯度優(yōu)化、Prompt模板設(shè)計與蒸餾分析等工程細(xì)節(jié)便于讀者直接復(fù)用到同類證券文本處理任務(wù)中。資源共1個PDF文件壓縮包大小約11.15MB目前已有98人學(xué)習(xí)下載。1. 為什么證券投研需要可計算的知識圖譜一份券商深度研報通常在30到100頁之間少數(shù)行業(yè)框架報告會沖到192頁以上。真正決定信息價值的部分往往不在摘要和正文里而在跨頁的財務(wù)表格、產(chǎn)業(yè)鏈?zhǔn)疽鈭D、產(chǎn)品對比圖和腳注的風(fēng)險提示中。過去做研報知識圖譜卡在兩道墻PDF版面結(jié)構(gòu)無法批量還原實體和關(guān)系只能靠規(guī)則模板硬抽。多模態(tài)文檔理解把第一道墻拆掉DeepSeek這類生成式模型則把實體關(guān)系抽取從特征工程變成指令工程。這套證券投研知識圖譜構(gòu)建方案主線是研報從PDF變成結(jié)構(gòu)化JSON再變成帶置信度的三元組經(jīng)過實體鏈接寫入Neo4j最終服務(wù)于投研問答和產(chǎn)業(yè)鏈傳導(dǎo)分析。適合正在做投研系統(tǒng)、量化因子庫或企業(yè)知識中臺的工程師。2. 多模態(tài)研報解析把PDF版面中的表格、圖表和正文變成統(tǒng)一JSON2.1 研報PDF為什么難解析先分清三類內(nèi)容研報PDF不是干凈的文本文檔問題集中體現(xiàn)在三個層面雙欄或三欄排版導(dǎo)致文本閱讀順序錯亂帶框線表格跨頁后表頭重復(fù)且列對齊失敗產(chǎn)業(yè)鏈圖和景氣度曲線以圖片形式嵌入導(dǎo)致數(shù)字完全不可見。若直接對整頁做純文本抽取碳酸鋰價格走勢這類關(guān)鍵圖表里的數(shù)據(jù)會全部丟失。多模態(tài)文檔理解在工程上的落地方式并不是只靠某個視覺大模型把整頁讀一遍而是把版面拆成文本流、表格結(jié)構(gòu)、圖表圖像三軌分別解析后再融合成統(tǒng)一JSON。這樣做的理由是每一條解析路徑都可獨(dú)立調(diào)試、獨(dú)立緩存并且失敗時可以降級。文本層用pdfplumber提取表格層需要指定抽取策略圖表層則先把頁面渲染成圖片再交給OCR或視覺模型接管。| 內(nèi)容形態(tài) | 解析工具 | 輸出結(jié)構(gòu) | | 正文文本 | pdfplumber extract_text | 純文本加坐標(biāo) | | 表格 | pdfplumber extract_tables | 二維數(shù)組轉(zhuǎn)markdown | | 圖表圖片 | PyMuPDF渲染加OCR | 圖片路徑加文字識別結(jié)果 | | 頁眉頁腳 | 版面過濾規(guī)則 | 丟棄或標(biāo)記為噪聲 |券商研報的版權(quán)頁、評級說明和免責(zé)聲明集中在首尾頁通常與投研實體無關(guān)。解析階段直接按頁號范圍丟棄這兩部分既能減少噪聲也能降低后續(xù)LLM抽取的token消耗。2.2 一套可復(fù)現(xiàn)的PDF解析流水線我一般會把解析分成三個函數(shù)extract_text、extract_tables、render_pages最后按頁合并。下面這個實現(xiàn)基于PyMuPDF和pdfplumber用pip安裝后即可運(yùn)行。import fitz import pdfplumber from pathlib import Path def parse_research_report(pdf_path: str, out_dir: str) - list: pages [] render_dir Path(out_dir) / pages render_dir.mkdir(parentsTrue, exist_okTrue) doc fitz.open(pdf_path) with pdfplumber.open(pdf_path) as pdf: for idx, page in enumerate(pdf.pages): # 文本層保留相對坐標(biāo)用于后續(xù)按欄切分 text page.extract_text( x_tolerance1.5, # 字符間超過1.5pt視為列邊界 y_tolerance3.0, # 垂直方向的字符合并容差 layoutFalse # False得到帶換行的純文本 ) # 表格層優(yōu)先按線條切分研報表格通常帶完整框線 tables page.extract_tables( { vertical_strategy: lines, horizontal_strategy: lines, snap_tolerance: 3, } ) # 圖像層150DPI渲染整頁供視覺模型或OCR兜底 pix doc[idx].get_pixmap(dpi150) img_path str(render_dir / f{idx:04d}.png) pix.save(img_path) pages.append({ page_no: idx 1, text: text, tables: [t for t in tables if t], image_path: img_path, source: pdf_path }) doc.close() return pages代碼邏輯說明x_tolerance1.5控制字符間距達(dá)到多大時允許換行。研報雙欄場景下同欄字符間距通常遠(yuǎn)小于欄間空隙設(shè)1.5能在保留欄內(nèi)完整句子的同時避免把兩欄文本混在一起。y_tolerance3.0控制垂直方向多近的字符會被合并為同一行用來容忍行內(nèi)輕微偏移。表格抽取采用lines策略因為券商研報表格幾乎都帶框線命中率高如果遇到無框線表格可以退回text策略按文本間隙識別。渲染150DPI是為了給視覺模型清晰輸入同時控制顯存占用。有一個容易忽略的點(diǎn)extract_tables返回的是二維數(shù)組跨頁表格需要自己檢測表頭重復(fù)。常見做法是記錄每頁第一行并和上一頁表頭行比較完全相同就丟棄本頁表頭避免實體抽取階段把單位億元當(dāng)作數(shù)據(jù)行。2.3 表格與圖表內(nèi)容如何進(jìn)入知識抽取pdfplumber拿到的表格是二維數(shù)組不能直接塞給實體關(guān)系抽取。我習(xí)慣把表格轉(zhuǎn)成markdown再拼進(jìn)文本流因為LLM對markdown表格的理解穩(wěn)定性遠(yuǎn)高于裸數(shù)組且能保留表頭與列的對應(yīng)關(guān)系。def table_to_markdown(table: list) - str: if not table: return lines [] for row in table: cells [str(c).replace(\n, ).strip() if c else for c in row] lines.append(| | .join(cells) |) return \n.join(lines)圖表部分則是另一條路徑。DeepSeek開放平臺目前的對話模型以文本輸入為主所以生產(chǎn)環(huán)境里常見做法是OCR識別圖內(nèi)文字再結(jié)合圖注文本做一次摘要。把產(chǎn)業(yè)鏈圖中的公司名、箭頭關(guān)系、區(qū)域標(biāo)簽全部OCR出來后按節(jié)點(diǎn)名、連線、邊方向重新組織就能補(bǔ)齊純文本缺失的關(guān)系信息。3. 實體關(guān)系自動抽取DeepSeek從提示詞到JSON的工程化配置3.1 先定義Schema再寫Prompt投研圖譜要抽什么實體抽取之前必須先畫清楚邊界。投研知識圖譜里的實體不是通用NER里的人名地名機(jī)構(gòu)名而是業(yè)務(wù)對象上市公司、集團(tuán)母公司、行業(yè)、產(chǎn)品、原材料、區(qū)域產(chǎn)能、財務(wù)指標(biāo)、風(fēng)險事件。關(guān)系也不是泛化的相關(guān)而是要支撐后續(xù)推理的類型。我一般用一張表固定schema并把表里的定義寫進(jìn)system prompt讓模型每次輸出都落在預(yù)設(shè)軌道上。| 實體類型 | 別名覆蓋 | 示例 | | 上市公司 | 股票簡稱、曾用名 | 寧德時代、CATL | | 集團(tuán) | 母公司、控股平臺 | 華友鈷業(yè)、浙江華友 | | 行業(yè) | 申萬或中信分類 | 動力電池、正極材料 | | 產(chǎn)品與原材料 | 規(guī)格、俗稱 | 磷酸鐵鋰、六氟磷酸鋰 | | 財務(wù)指標(biāo) | 報告期、口徑 | 營收、歸母凈利潤 |關(guān)系類型固定為實控、主營、采購、銷售、競爭、產(chǎn)能布局、業(yè)績驅(qū)動、風(fēng)險傳導(dǎo)。關(guān)系必須有方向prompt里寫死主語在前賓語在后。沒有方向的三元組在后續(xù)圖查詢里會變成全表掃描。3.2 DeepSeek API調(diào)用參數(shù)與JSON提取模板研報文本按1000到1500字切塊塊與塊之間重疊150字避免跨塊的三元組被拆散。切塊前把OCR錯誤、表格噪聲統(tǒng)一清洗保留原文所在頁碼。抽取prompt里最關(guān)鍵的三個約束只抽原文明確提到的信息不推斷不補(bǔ)全輸出嚴(yán)格JSON每個實體帶page字段。以下代碼兼容DeepSeek開放平臺的OpenAI協(xié)議設(shè)置好環(huán)境變量即可運(yùn)行。import json import os from openai import OpenAI client OpenAI( api_keyos.environ[DEEPSEEK_API_KEY], base_urlhttps://api.deepseek.com ) SYSTEM_PROMPT 你是證券研報信息抽取引擎。 只抽取原文明確出現(xiàn)的事實禁止推測和補(bǔ)全。 實體類型限定為: 上市公司、集團(tuán)、行業(yè)、產(chǎn)品、原材料、區(qū)域、財務(wù)指標(biāo)、風(fēng)險事件。 關(guān)系類型限定為: 實控、主營、采購、銷售、競爭、產(chǎn)能布局、業(yè)績驅(qū)動、風(fēng)險傳導(dǎo)。 輸出JSON格式: {triples: [{head: {text: , type: , page: 1}, relation: , tail: {text: , type: , page: 1}}]} def extract_triples(chunk: str, page_offset: int) - dict: resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f從以下研報片段抽取知識三元組\n{chunk}} ], temperature0, top_p0.1, response_format{type: json_object}, max_tokens2048 ) return json.loads(resp.choices[0].message.content)參數(shù)說明temperature0讓解碼近似貪心減少同一段文本反復(fù)抽取時的結(jié)果漂移top_p0.1進(jìn)一步收窄采樣空間對抽取類任務(wù)比單獨(dú)調(diào)temperature更有效。response_format強(qiáng)制JSON輸出避免模型把三元組寫進(jìn)markdown代碼塊。max_tokens2048是因為一塊研報文本最多可能抽出幾十組三元組太小會截斷成非法JSON。3.3 抽取結(jié)果的后處理去重、合法性校驗與置信度模型返回的三元組不能直接入庫。第一關(guān)是字符串校驗head和tail的text長度必須在2到60之間relation必須在schema枚舉內(nèi)。第二關(guān)是重復(fù)合并同一公司全稱和簡稱在抽取結(jié)果里會出現(xiàn)多次用上市公司簡稱表先做一次歸一化。第三關(guān)是置信度近似三元組在原文中能找到連續(xù)原文片段的記為高置信只在表格中出現(xiàn)的記為中置信兩者都不滿足的直接丟棄。還有一個高頻坑研報正文里公司指代上一段的主語模型經(jīng)常把三元組主語抽成公司兩個字。對策是在分塊前做簡單指代消解把公司替換為離它最近的實體全稱或者把段落標(biāo)題拼進(jìn)句子開頭再抽取。4. 實體鏈接與Neo4j圖譜構(gòu)建把抽取結(jié)果對齊到統(tǒng)一實體庫4.1 為什么要做實體鏈接同名不同實體與異名同實體的邊界實體關(guān)系抽取得到的是文本片段還不是知識圖譜里的節(jié)點(diǎn)。同一家公司在不同研報里可能被寫成寧德時代CATL寧德而中信證券既指證券公司又指在上交所上市的標(biāo)的在不同上下文里指代的對象完全不同。實體鏈接的任務(wù)就是把每個抽取到的mention映射到統(tǒng)一實體庫的唯一ID上。簡單做字符串匹配只能解決第一層問題。研報里大量簡稱變體會把精確匹配打穿湖南裕能被寫成裕能股份時僅靠名稱判等會全部落空。我采用的混合方案分兩步走候選生成階段用歸一化名稱加編輯距離候選排序階段用embedding相似度加行業(yè)上下文特征。| 問題類型 | 典型例子 | 處理階段 | | 異名同實體 | CATL、寧德時代 | 別名表加embedding排序 | | 同名不同實體 | 中信證券作為券商與上市公司 | 行業(yè)上下文過濾 | | 簡稱截斷 | 湖南裕能、裕能股份 | 編輯距離候選生成 |4.2 向量加規(guī)則的混合實體鏈接實現(xiàn)候選生成階段要快。先做字符串歸一化把全角括號、空格、后綴統(tǒng)一再查別名表。別名精確命中直接進(jìn)入排序環(huán)節(jié)。沒有別名命中的mention用編輯距離找候選這一步控制在Top 20以內(nèi)。候選排序階段用embedding把mention和候選實體的名稱、行業(yè)、主營產(chǎn)品拼成同一段描述文本分別向量化后算余弦相似度。def link_mention(mention: str, candidates: list, embed_fn, top_k5): mention: 抽取到的實體文本 candidates: 候選實體列表元素格式為 {id: , name: , seq: } embed_fn: 文本向量化函數(shù)接收字符串返回向量 m_vec embed_fn(mention) scored [] for cand in candidates: c_vec embed_fn(cand[seq]) sim cosine_similarity(m_vec, c_vec) scored.append((cand[id], sim)) scored.sort(keylambda x: x[1], reverseTrue) return scored[:top_k]注意這里的candidates必須來自別名表和模糊匹配不是全體實體庫。全庫檢索在實體規(guī)模超過十萬時會拖慢整條鏈路而且相似度噪聲大。行業(yè)上下文可以放在embedding拼接階段云鋁股份和云南鋁業(yè)在描述文本里的語義距離會因行業(yè)前綴趨近比單純比較名稱更穩(wěn)定。鏈接準(zhǔn)確率至少要達(dá)到95%否則錯誤三元組會污染圖譜下游推理。4.3 Neo4j寫入約束、MERGE與批量提交知識圖譜存儲選Neo4j是投研場景的常見選擇Cypher在多跳關(guān)系查詢上的表達(dá)能力最適合產(chǎn)業(yè)鏈分析。落庫前先建唯一約束這是冪等寫入的前提。CREATE CONSTRAINT company_name IF NOT EXISTS FOR (n:Company) REQUIRE n.uid IS UNIQUE;寫入用MERGE而不是CREATE。MERGE會先按uid查重存在則跳過或更新屬性不存在才創(chuàng)建。關(guān)系同樣需要一個去重鍵否則同一份研報重跑會重復(fù)建邊。常見做法是把關(guān)系的source_report、page、triple序號拼接成字符串作為關(guān)系上的sig屬性。UNWIND $batch AS row MERGE (c:Company {uid: row.company_uid}) ON CREATE SET c.name row.company_name, c.first_seen row.process_time MERGE (p:Product {uid: row.product_uid}) ON CREATE SET p.name row.product_name MERGE (c)-[r:MAIN_PRODUCT {sig: row.edge_sig}]-(p) ON CREATE SET r.source_report row.report_id, r.confidence row.confidence, r.page row.page;MERGE關(guān)系時節(jié)點(diǎn)屬性只寫uid其余屬性放入ON CREATE SET這樣節(jié)點(diǎn)已存在時不會被反復(fù)覆蓋。后端批量提交時每個batch控制在500行以內(nèi)事務(wù)提交更穩(wěn)失敗重試的成本也更低。5. 圖譜構(gòu)建的評測與增量更新Precision之外還要盯住三種錯誤5.1 評測集與指標(biāo)誰算對誰算錯沒有評測集就談不上優(yōu)化。從研報庫里按行業(yè)和年份分層抽樣取100段文本每段標(biāo)注實體邊界、實體類型、關(guān)系方向、鏈接目標(biāo)。標(biāo)注規(guī)范要寫清楚兩個最容易扯皮的點(diǎn)跨句關(guān)系算不算隱含實體算不算。我的默認(rèn)口徑是實體必須在原文出現(xiàn)關(guān)系允許跨兩個相鄰句子不允許跨段推斷。指標(biāo)按三元組級別計算正確性要求比純文本抽取更嚴(yán)格。| 指標(biāo) | 計算方式 | 合格線 | | 實體F1 | 邊界和類型同時正確才算正例 | 0.85 | | 關(guān)系F1 | head、relation、tail全部正確 | 0.80 | | 鏈接準(zhǔn)確率 | mention正確映射到標(biāo)準(zhǔn)實體ID的比例 | 0.95 |5.2 三種高頻錯誤與修正回路第一種是邊界錯誤。動力電池龍頭被抽成實體時模型常把龍頭帶上或者只給電池。修法是在prompt里強(qiáng)調(diào)實體字段必須是最小可獨(dú)立指稱的名詞短語不要修飾語。第二種是關(guān)系方向顛倒。公司采購碳酸鋰被抽成碳酸鋰采購公司。修法是把關(guān)系定義寫成主語是行為發(fā)出方賓語是行為承受方并在校驗階段檢查三元組的頭尾實體類型是否匹配關(guān)系定義。比如采購關(guān)系的head必須是公司或集團(tuán)tail必須是產(chǎn)品或原材料。第三種是幻覺補(bǔ)充。模型依據(jù)研報標(biāo)題補(bǔ)充了原文沒有的結(jié)論。修法有兩個層面prompt里明確沒有找到就輸出空數(shù)組后處理里用原文回溯若三元組在原文chunk里找不到任意兩個詞的共現(xiàn)直接丟棄。5.3 增量更新與回滾研報庫是流式的不是一次性導(dǎo)入投研場景中研報持續(xù)發(fā)布同一個公司會被多家券商覆蓋舊報告也可能被撤回。圖譜更新要有三個機(jī)制按報告冪等寫入、按時間戳回滾、按事件重算子圖。def rollback_report(graph, report_id: str): # 刪除某份研報寫入的全部關(guān)系保留節(jié)點(diǎn) query MATCH (a)-[r]-(b) WHERE r.source_report $report_id DELETE r graph.execute(query, {report_id: report_id})回滾時只刪關(guān)系不刪節(jié)點(diǎn)因為同一節(jié)點(diǎn)可能被多份報告關(guān)聯(lián)。節(jié)點(diǎn)孤島清理放到夜間批任務(wù)中單獨(dú)執(zhí)行避免正常業(yè)務(wù)被誤刪。如果研報數(shù)據(jù)有合規(guī)要求可以把DeepSeek開放平臺接口替換成內(nèi)網(wǎng)部署的推理服務(wù)模型權(quán)重不變抽取鏈路只改base_url指向評測和回滾邏輯不用動。增量更新調(diào)度建議按report_date做水位線每天只處理新導(dǎo)入的報告并記錄每個報告的處理版本號。6. 圖譜用起來把Neo4j子圖變上下文喂給DeepSeek的投研問答技巧圖譜構(gòu)建完成后價值體現(xiàn)在問答和傳導(dǎo)分析。常見做法是圖檢索加文本檢索混合先用Cypher找到答案所在的子圖再把子圖序列化成證據(jù)文本喂給DeepSeek。這個子圖轉(zhuǎn)上下文的環(huán)節(jié)決定回答質(zhì)量。我會把每個三元組轉(zhuǎn)成一句帶出處的證據(jù)鏈保留source_report、page和原始句子。LLM回答時必須引用證據(jù)不允許脫離上下文編造。MATCH (c:Company {uid: $uid})-[r:SUPPLY]-(m:Material) RETURN c.name, type(r), m.name, r.source_report, r.page LIMIT 20;查詢結(jié)果在應(yīng)用層拼成根據(jù)某研報第X頁某公司采購某原材料再把多條證據(jù)拼接成上下文塊放進(jìn)prompt。這個技巧能明顯減少DeepSeek在不相關(guān)實體上繞圈子的概率每個事實都能回溯出處。傳導(dǎo)分析走多跳查詢比如上游碳酸鋰漲價影響哪些正極材料公司。把圖查詢結(jié)果按最短路徑展開逐段解釋傳導(dǎo)機(jī)制本質(zhì)是圖譜推理加語言模型生成。如果回答延遲偏高就把常用路徑預(yù)計算成物化視圖查詢直接走視圖讓響應(yīng)時間穩(wěn)定在百毫秒級。本文還有配套的精品資源點(diǎn)擊獲取