
简介面向证券投研、NLP与知识图谱方向的从业者与研究者这份192页方案系统讲解如何基于DeepSeek-VL2多模态文档理解技术从券商研报中自动抽取实体关系并完成知识图谱构建适用于投研数据中台、智能研报解析与金融知识服务等场景。文档共51个大章节覆盖本体设计、多模态数据预处理、表格与图片信息抽取、数据标注与质量控制、Prompt语义扩充、增量预训练、分布式环境搭建、长尾实体损失优化、关系抽取联合训练、微调方案对比及跨券商泛化测试等内容。方案不仅给出技术选型依据还包含训练监控、梯度优化、Prompt模板设计与蒸馏分析等工程细节便于读者直接复用到同类证券文本处理任务中。资源共1个PDF文件压缩包大小约11.15MB目前已有98人学习下载。1. 为什么证券投研需要可计算的知识图谱一份券商深度研报通常在30到100页之间少数行业框架报告会冲到192页以上。真正决定信息价值的部分往往不在摘要和正文里而在跨页的财务表格、产业链示意图、产品对比图和脚注的风险提示中。过去做研报知识图谱卡在两道墙PDF版面结构无法批量还原实体和关系只能靠规则模板硬抽。多模态文档理解把第一道墙拆掉DeepSeek这类生成式模型则把实体关系抽取从特征工程变成指令工程。这套证券投研知识图谱构建方案主线是研报从PDF变成结构化JSON再变成带置信度的三元组经过实体链接写入Neo4j最终服务于投研问答和产业链传导分析。适合正在做投研系统、量化因子库或企业知识中台的工程师。2. 多模态研报解析把PDF版面中的表格、图表和正文变成统一JSON2.1 研报PDF为什么难解析先分清三类内容研报PDF不是干净的文本文档问题集中体现在三个层面双栏或三栏排版导致文本阅读顺序错乱带框线表格跨页后表头重复且列对齐失败产业链图和景气度曲线以图片形式嵌入导致数字完全不可见。若直接对整页做纯文本抽取碳酸锂价格走势这类关键图表里的数据会全部丢失。多模态文档理解在工程上的落地方式并不是只靠某个视觉大模型把整页读一遍而是把版面拆成文本流、表格结构、图表图像三轨分别解析后再融合成统一JSON。这样做的理由是每一条解析路径都可独立调试、独立缓存并且失败时可以降级。文本层用pdfplumber提取表格层需要指定抽取策略图表层则先把页面渲染成图片再交给OCR或视觉模型接管。| 内容形态 | 解析工具 | 输出结构 | | 正文文本 | pdfplumber extract_text | 纯文本加坐标 | | 表格 | pdfplumber extract_tables | 二维数组转markdown | | 图表图片 | PyMuPDF渲染加OCR | 图片路径加文字识别结果 | | 页眉页脚 | 版面过滤规则 | 丢弃或标记为噪声 |券商研报的版权页、评级说明和免责声明集中在首尾页通常与投研实体无关。解析阶段直接按页号范围丢弃这两部分既能减少噪声也能降低后续LLM抽取的token消耗。2.2 一套可复现的PDF解析流水线我一般会把解析分成三个函数extract_text、extract_tables、render_pages最后按页合并。下面这个实现基于PyMuPDF和pdfplumber用pip安装后即可运行。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): # 文本层保留相对坐标用于后续按栏切分 text page.extract_text( x_tolerance1.5, # 字符间超过1.5pt视为列边界 y_tolerance3.0, # 垂直方向的字符合并容差 layoutFalse # False得到带换行的纯文本 ) # 表格层优先按线条切分研报表格通常带完整框线 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控制字符间距达到多大时允许换行。研报双栏场景下同栏字符间距通常远小于栏间空隙设1.5能在保留栏内完整句子的同时避免把两栏文本混在一起。y_tolerance3.0控制垂直方向多近的字符会被合并为同一行用来容忍行内轻微偏移。表格抽取采用lines策略因为券商研报表格几乎都带框线命中率高如果遇到无框线表格可以退回text策略按文本间隙识别。渲染150DPI是为了给视觉模型清晰输入同时控制显存占用。有一个容易忽略的点extract_tables返回的是二维数组跨页表格需要自己检测表头重复。常见做法是记录每页第一行并和上一页表头行比较完全相同就丢弃本页表头避免实体抽取阶段把单位亿元当作数据行。2.3 表格与图表内容如何进入知识抽取pdfplumber拿到的表格是二维数组不能直接塞给实体关系抽取。我习惯把表格转成markdown再拼进文本流因为LLM对markdown表格的理解稳定性远高于裸数组且能保留表头与列的对应关系。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开放平台目前的对话模型以文本输入为主所以生产环境里常见做法是OCR识别图内文字再结合图注文本做一次摘要。把产业链图中的公司名、箭头关系、区域标签全部OCR出来后按节点名、连线、边方向重新组织就能补齐纯文本缺失的关系信息。3. 实体关系自动抽取DeepSeek从提示词到JSON的工程化配置3.1 先定义Schema再写Prompt投研图谱要抽什么实体抽取之前必须先画清楚边界。投研知识图谱里的实体不是通用NER里的人名地名机构名而是业务对象上市公司、集团母公司、行业、产品、原材料、区域产能、财务指标、风险事件。关系也不是泛化的相关而是要支撑后续推理的类型。我一般用一张表固定schema并把表里的定义写进system prompt让模型每次输出都落在预设轨道上。| 实体类型 | 别名覆盖 | 示例 | | 上市公司 | 股票简称、曾用名 | 宁德时代、CATL | | 集团 | 母公司、控股平台 | 华友钴业、浙江华友 | | 行业 | 申万或中信分类 | 动力电池、正极材料 | | 产品与原材料 | 规格、俗称 | 磷酸铁锂、六氟磷酸锂 | | 财务指标 | 报告期、口径 | 营收、归母净利润 |关系类型固定为实控、主营、采购、销售、竞争、产能布局、业绩驱动、风险传导。关系必须有方向prompt里写死主语在前宾语在后。没有方向的三元组在后续图查询里会变成全表扫描。3.2 DeepSeek API调用参数与JSON提取模板研报文本按1000到1500字切块块与块之间重叠150字避免跨块的三元组被拆散。切块前把OCR错误、表格噪声统一清洗保留原文所在页码。抽取prompt里最关键的三个约束只抽原文明确提到的信息不推断不补全输出严格JSON每个实体带page字段。以下代码兼容DeepSeek开放平台的OpenAI协议设置好环境变量即可运行。import json import os from openai import OpenAI client OpenAI( api_keyos.environ[DEEPSEEK_API_KEY], base_urlhttps://api.deepseek.com ) SYSTEM_PROMPT 你是证券研报信息抽取引擎。 只抽取原文明确出现的事实禁止推测和补全。 实体类型限定为: 上市公司、集团、行业、产品、原材料、区域、财务指标、风险事件。 关系类型限定为: 实控、主营、采购、销售、竞争、产能布局、业绩驱动、风险传导。 输出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)参数说明temperature0让解码近似贪心减少同一段文本反复抽取时的结果漂移top_p0.1进一步收窄采样空间对抽取类任务比单独调temperature更有效。response_format强制JSON输出避免模型把三元组写进markdown代码块。max_tokens2048是因为一块研报文本最多可能抽出几十组三元组太小会截断成非法JSON。3.3 抽取结果的后处理去重、合法性校验与置信度模型返回的三元组不能直接入库。第一关是字符串校验head和tail的text长度必须在2到60之间relation必须在schema枚举内。第二关是重复合并同一公司全称和简称在抽取结果里会出现多次用上市公司简称表先做一次归一化。第三关是置信度近似三元组在原文中能找到连续原文片段的记为高置信只在表格中出现的记为中置信两者都不满足的直接丢弃。还有一个高频坑研报正文里公司指代上一段的主语模型经常把三元组主语抽成公司两个字。对策是在分块前做简单指代消解把公司替换为离它最近的实体全称或者把段落标题拼进句子开头再抽取。4. 实体链接与Neo4j图谱构建把抽取结果对齐到统一实体库4.1 为什么要做实体链接同名不同实体与异名同实体的边界实体关系抽取得到的是文本片段还不是知识图谱里的节点。同一家公司在不同研报里可能被写成宁德时代CATL宁德而中信证券既指证券公司又指在上交所上市的标的在不同上下文里指代的对象完全不同。实体链接的任务就是把每个抽取到的mention映射到统一实体库的唯一ID上。简单做字符串匹配只能解决第一层问题。研报里大量简称变体会把精确匹配打穿湖南裕能被写成裕能股份时仅靠名称判等会全部落空。我采用的混合方案分两步走候选生成阶段用归一化名称加编辑距离候选排序阶段用embedding相似度加行业上下文特征。| 问题类型 | 典型例子 | 处理阶段 | | 异名同实体 | CATL、宁德时代 | 别名表加embedding排序 | | 同名不同实体 | 中信证券作为券商与上市公司 | 行业上下文过滤 | | 简称截断 | 湖南裕能、裕能股份 | 编辑距离候选生成 |4.2 向量加规则的混合实体链接实现候选生成阶段要快。先做字符串归一化把全角括号、空格、后缀统一再查别名表。别名精确命中直接进入排序环节。没有别名命中的mention用编辑距离找候选这一步控制在Top 20以内。候选排序阶段用embedding把mention和候选实体的名称、行业、主营产品拼成同一段描述文本分别向量化后算余弦相似度。def link_mention(mention: str, candidates: list, embed_fn, top_k5): mention: 抽取到的实体文本 candidates: 候选实体列表元素格式为 {id: , name: , seq: } embed_fn: 文本向量化函数接收字符串返回向量 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必须来自别名表和模糊匹配不是全体实体库。全库检索在实体规模超过十万时会拖慢整条链路而且相似度噪声大。行业上下文可以放在embedding拼接阶段云铝股份和云南铝业在描述文本里的语义距离会因行业前缀趋近比单纯比较名称更稳定。链接准确率至少要达到95%否则错误三元组会污染图谱下游推理。4.3 Neo4j写入约束、MERGE与批量提交知识图谱存储选Neo4j是投研场景的常见选择Cypher在多跳关系查询上的表达能力最适合产业链分析。落库前先建唯一约束这是幂等写入的前提。CREATE CONSTRAINT company_name IF NOT EXISTS FOR (n:Company) REQUIRE n.uid IS UNIQUE;写入用MERGE而不是CREATE。MERGE会先按uid查重存在则跳过或更新属性不存在才创建。关系同样需要一个去重键否则同一份研报重跑会重复建边。常见做法是把关系的source_report、page、triple序号拼接成字符串作为关系上的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关系时节点属性只写uid其余属性放入ON CREATE SET这样节点已存在时不会被反复覆盖。后端批量提交时每个batch控制在500行以内事务提交更稳失败重试的成本也更低。5. 图谱构建的评测与增量更新Precision之外还要盯住三种错误5.1 评测集与指标谁算对谁算错没有评测集就谈不上优化。从研报库里按行业和年份分层抽样取100段文本每段标注实体边界、实体类型、关系方向、链接目标。标注规范要写清楚两个最容易扯皮的点跨句关系算不算隐含实体算不算。我的默认口径是实体必须在原文出现关系允许跨两个相邻句子不允许跨段推断。指标按三元组级别计算正确性要求比纯文本抽取更严格。| 指标 | 计算方式 | 合格线 | | 实体F1 | 边界和类型同时正确才算正例 | 0.85 | | 关系F1 | head、relation、tail全部正确 | 0.80 | | 链接准确率 | mention正确映射到标准实体ID的比例 | 0.95 |5.2 三种高频错误与修正回路第一种是边界错误。动力电池龙头被抽成实体时模型常把龙头带上或者只给电池。修法是在prompt里强调实体字段必须是最小可独立指称的名词短语不要修饰语。第二种是关系方向颠倒。公司采购碳酸锂被抽成碳酸锂采购公司。修法是把关系定义写成主语是行为发出方宾语是行为承受方并在校验阶段检查三元组的头尾实体类型是否匹配关系定义。比如采购关系的head必须是公司或集团tail必须是产品或原材料。第三种是幻觉补充。模型依据研报标题补充了原文没有的结论。修法有两个层面prompt里明确没有找到就输出空数组后处理里用原文回溯若三元组在原文chunk里找不到任意两个词的共现直接丢弃。5.3 增量更新与回滚研报库是流式的不是一次性导入投研场景中研报持续发布同一个公司会被多家券商覆盖旧报告也可能被撤回。图谱更新要有三个机制按报告幂等写入、按时间戳回滚、按事件重算子图。def rollback_report(graph, report_id: str): # 删除某份研报写入的全部关系保留节点 query MATCH (a)-[r]-(b) WHERE r.source_report $report_id DELETE r graph.execute(query, {report_id: report_id})回滚时只删关系不删节点因为同一节点可能被多份报告关联。节点孤岛清理放到夜间批任务中单独执行避免正常业务被误删。如果研报数据有合规要求可以把DeepSeek开放平台接口替换成内网部署的推理服务模型权重不变抽取链路只改base_url指向评测和回滚逻辑不用动。增量更新调度建议按report_date做水位线每天只处理新导入的报告并记录每个报告的处理版本号。6. 图谱用起来把Neo4j子图变上下文喂给DeepSeek的投研问答技巧图谱构建完成后价值体现在问答和传导分析。常见做法是图检索加文本检索混合先用Cypher找到答案所在的子图再把子图序列化成证据文本喂给DeepSeek。这个子图转上下文的环节决定回答质量。我会把每个三元组转成一句带出处的证据链保留source_report、page和原始句子。LLM回答时必须引用证据不允许脱离上下文编造。MATCH (c:Company {uid: $uid})-[r:SUPPLY]-(m:Material) RETURN c.name, type(r), m.name, r.source_report, r.page LIMIT 20;查询结果在应用层拼成根据某研报第X页某公司采购某原材料再把多条证据拼接成上下文块放进prompt。这个技巧能明显减少DeepSeek在不相关实体上绕圈子的概率每个事实都能回溯出处。传导分析走多跳查询比如上游碳酸锂涨价影响哪些正极材料公司。把图查询结果按最短路径展开逐段解释传导机制本质是图谱推理加语言模型生成。如果回答延迟偏高就把常用路径预计算成物化视图查询直接走视图让响应时间稳定在百毫秒级。本文还有配套的精品资源点击获取