公司动态

泰迪杯B题题解:财报智能问数助手RAG+NL2SQL混合架构全解析

📅 2026/8/27 2:21:14
泰迪杯B题题解:财报智能问数助手RAG+NL2SQL混合架构全解析
简介在数据挖掘与自然语言处理交叉领域如何让机器从海量非结构化文档中精准抽取结构化数据并回答用户提问是企业智能化的关键难题。传统文档问答擅长语义检索却难以保证数值精确性。针对上市公司财报中数值密集、口径复杂、非结构化的特点行业实践中逐渐形成基于RAG检索增强生成与NL2SQL自然语言转查询的混合架构。该架构通过意图分类路由将精确数值查询转化为SQL执行将文本描述类问题交给向量检索并用公式映射完成财务指标计算同时结合答案校验与来源溯源有效降低大模型幻觉。本文以2026泰迪杯B题财报智能问数助手为例系统拆解了从PDF表格提取、科目名称归一化到构建四元组知识库再到意图分类、NL2SQL、答案生成与评测调优的完整工程链路为财报智能问答系统提供可复现的技术参考。 拿到“2026第14届泰迪杯数据挖掘挑战赛B题上市公司财报‘智能问数’助手”这个题的那一刻我们队三个人盯着屏幕沉默了很久。表面上看这就是做一个“能聊财报的问答机器人”但大家都清楚这类赛题真正要命的地方从来不在“大模型会不会说话”而在“它回答的每一个数字对不对、能不能给出处”。如果你也正在备赛、或者想搞懂企业财务文档的智能问答该怎么做这篇题解会把我们从读题、选型、处理数据到最终调优的完整过程拆开来讲。里面所有方案都来自我们实际跑通的代码踩过的坑也一并记录在内。1. 这道题真正的考点从“聊天机器人”到“财务问答引擎”1.1 赛题任务的第一层理解与第二层理解第一眼看到赛题很容易把它理解成“做一个基于大模型的文档问答系统”。毕竟“智能问数助手”听起来就是个ChatGPT套壳把财报PDF喂进去用户问什么答什么。但如果你真按这个思路做下去会在评测阶段死得很难看。为什么因为赛题名里的“问数”两个字才是核心。普通文档问答的回答是“文字中找答案”而财报问答面对的问题长这样“恒瑞医药2023年营业收入是多少”“2022年到2023年这家公司的净利润同比增长率是多少”“请列出近三年资产负债率的变化趋势。”这三个问题分别考察了精确数值抽取、跨年份计算、多期趋势归纳。任何一个环节出错答案都是错的。换句话说这道题第一层考的是RAG检索增强生成第二层考的其实是结构化查询与数值推理。后者才是真正拉开分数差距的地方。很多参赛队包括我们一开始会犯一个方向性错误把大量精力花在调prompt、换大模型上结果发现模型把“营业收入”回答成了“营业成本”把2022年数据答成2023年数据。这不是模型笨而是因为纯文本检索压根不适合做精确数值问答。1.2 财报数据的“三座大山”非结构化、数值密集、口径复杂处理过上市公司年报的都知道这类文档有三重Buff叠在一起让通用问答方案直接失效。第一重是非结构化。赛方给的数据一般是年报PDF或者从年报里抽取出来的文本文件里面有大量表格、页眉页脚、跨页段落。PDF里的表格在提取后经常“错位”比如一列科目名称和一列数值被拆成了两段文字中间隔着十几行注释说明。第二重是数值密集。财报里几乎每一页都有几十个数字而这些数字之间的逻辑关系非常强——总资产等于负债加所有者权益营收乘以净利率等于净利润。大模型并不会天然理解这些勾稽关系它只会“生成看起来合理的文字”。第三重是口径复杂。同一家公司的财报里“营业收入”可能出现在利润表里、管理层讨论里、附注里数值不同含义也不同。合并报表的营收和母公司报表的营收是两个数归属母公司股东的净利润和净利润又是两个数。不做口径识别你抽出来的数值可能是对的但用户问的那个口径你答错了照样零分。1.3 评分规则驱动的架构设计原则竞赛题不会给你一个“开放聊天”的自由任务它一定有一个评测集包含若干标准问答对。基于对这类赛题评分规则的判断正确率、召回率、数值精确匹配加权我们倒推出了三条架构设计原则原则一数值答案必须有据可查。每个输出的数字都要能回溯到源文档的具体位置这意味着我们不仅要知道答案是什么还要存下“答案来自哪个章节、哪个表格行”。原则二能算不猜。凡是涉及增长率、同比、环比、占比的计算不要指望大模型自己做算术要在代码里用精确计算实现模型只负责把自然语言问题转成计算指令。原则三分类先行。与其让一个大模型处理所有问题不如先把问题分类不同类别走不同的检索和生成链路。这三条原则贯穿了我们整个项目。下面所有技术方案本质上都是这三条原则的落地。2. 技术选型为什么放弃微调改用RAGNL2SQL混合架构2.1 微调一个财报大模型算力和数据都不允许组队初期队里有人提出用开源大模型做领域微调让模型直接学会从财报里找答案。这个方案听起来很“硬核”但现实很残酷。首先是数据问题。微调需要构造大量“问题-答案对”并且答案必须精确到数值级别。我们粗略算过要覆盖几十家上市公司、三年财报、几百个财务科目至少需要几万条高质量标注数据。在比赛周期内靠人工标注根本不现实用GPT生成又会引入大量错误而且错误非常隐蔽。其次是算力问题。即便用LoRA方式微调一个7B模型也需要至少24G显存的GPU做训练。比赛期间我们只有一张消费级显卡训练一轮要好几个小时迭代试错成本太高。更关键的是微调并不能解决“数值幻觉”的问题。大模型的本质是概率生成它会把“营业收入”和“营业成本”这类相邻概念搞混这是参数化记忆的固有缺陷。你不能靠微调让它变成一台计算器。2.2 向量检索的边界能答“怎么样”答不了“是多少”既然微调走不通那RAG检索增强生成自然成了主流方案。RAG的基本思路是先把文档切块、向量化、建索引用户提问时把问题向量化在索引中做相似度检索找到相关文本片段拼进prompt里让大模型生成答案。这个方案对“叙述类问题”非常有效。比如“这家公司在年报中如何描述未来的经营风险”“公司提到的新能源业务进展如何”这类问题的答案就藏在某几段文字里检索召回后大模型整理一下就能给出靠谱回答。但遇到“是多少”类问题向量检索就开始拉胯了。原因在于向量相似度匹配的是“语义相近”不是“数值准确”。你问“2023年营收”检索出来的段落可能同时包含2022年、2023年、2024年三年的数据大模型怎么知道该挑哪个就算它挑对了也只是一个概率事件而不是确定性结果。更麻烦的是向量检索对表格数据的处理非常弱。传统的文本切片方式会把一个财务表格切成若干块数值和表头身首异处。检索“资产负债率”召回的可能只有“资产负债率”这个词所在的片段而真正的数值在另一个切片里。2.3 混合路由方案的整体设计综合上面的分析我们最终选择了分类路由 RAG/NL2SQL混合的架构。这套架构不是我们原创的但我们在落地时做了很多针对财报场景的改造。先看整体流程收到用户问题后第一步交给意图分类器判断这是一个“数值查询类问题”“文本描述类问题”还是“计算推理类问题”。数值查询类问题如“2023年营业收入是多少”走NL2SQL链路将自然语言转为SQL查询语句直接从结构化财务知识库中取数。文本描述类问题如“公司怎么看行业竞争”走RAG链路检索年报文本片段交由大模型生成答案。计算推理类问题如“净利润同比增长率是多少”走公式映射链路将问句解析为指标计算表达式在代码层面做精确计算。最后所有答案进入统一的校验模块检查数值格式、单位、年份是否匹配不匹配则触发二次检索或人工兜底。这套架构的核心价值在于让大模型做它擅长的事意图理解、文本组织、语义匹配把精确计算和数值存取交给确定性代码SQL、规则引擎。数据不会因为模型“发挥失常”而出错。3. 数据处理从PDF年报到结构化财务知识库的完整链路3.1 财报PDF的表格提取pdfplumber、Camelot、OCR三件套怎么配合数据处理几乎占了我们整个项目60%的工作量。你从赛方手里拿到的原始数据往往是一堆PDF年报而PDF本身是个排版格式不是数据格式。表格提取是所有后续步骤的地基地基不牢后面全白搭。我们实测了三个工具各有各的脾性pdfplumber它的extract_table方法对“有清晰横竖线的表格”效果很好提取结果是二维数组结构规整。但遇到无线表格、跨页合并单元格就直接乱掉。Camelot分为lattice基于线条和stream基于文本位置两种模式。对财报这种复杂表格lattice模式识别率不错但依赖OpenCV做线条检测遇到扫描版PDF就没辙。PaddleOCR我们用它处理扫描版财报。准确率还行但会把表格线识别成噪点后期需要用坐标位置去重建表格结构。我们的实际处理策略是优先用pdfplumber提取文本型PDF如果表格结果明显错乱换Camelot的lattice模式如果PDF是扫描件只能上PaddleOCR再配合我们自己写的坐标对齐逻辑。这里有一个非常关键的细节每提取出一个表格都要记录它所在的页码和在页面上的坐标位置。因为后面需要把表格里的数据映射回原文如果丢失了位置信息就无法做答案溯源。我们在提取结果中给每个表格加了“page_id table_id row_id”的定位标签。3.2 科目名称归一化同一个指标在年报里有七八种叫法拿到提取出来的表格数据之后你会发现一个让人崩溃的事实同一个财务科目在不同公司的年报里可能叫完全不同的名字。“营业收入”可能叫“营业总收入”“一、营业收入”“营业收入元”“归母净利润”可能叫“归属于上市公司股东的净利润”“归属于母公司所有者的净利润”“归属于母公司股东的净利润”“总资产”可能叫“资产总计”“资产总额”最离谱的是“少数股东损益”有些叫“少数股东权益”有些叫“少数股东应享有损益”含义完全不同稍不注意就混了。我们一开始用关键词匹配去归一化结果准确率只有70%左右。后来改成规则同义词表上下文校验的多层策略先剥离标点符号、单位、括号内的说明文字用同义词表做基础映射把常见变体归一到标准科目名如果匹配到多个标准科目再看表格的行索引位置——比如利润表的科目顺序基本固定营业收入在营业成本上面利用这个先验信息做消歧。这一步做完我们就有了一个干净的科目标准名映射表它是后面所有查询的基础。3.3 构建“数值-科目-年份-公司”四元组知识条目归一化之后下一步是把表格数据整理成适合检索和查询的结构。我们参考了构建知识图谱的思路但没做成真正的图结构而是定义了一种更轻量的财务知识条目。每个条目是一个四元组结构指标名标准化的科目名称如“营业收入”数值从原始表格中抽取的数字统一转为浮点数去掉千分位逗号报告期标注是“2022-12-31”还是“2023-06-30”区分年报和半年报主体公司名称同时标注是“合并报表”还是“母公司报表”。例如从某公司2023年年报中提取到的数据会写成{metric: 营业收入, value: 25330000000.0, report_date: 2023-12-31, entity: 某某科技, scope: 合并, source: page32,table2,row5}注意最后的source字段它记录了数值来源页码和表格位置。这个字段在评测阶段太重要了——评委可以据此判断你的答案是否“有据可查”我们答辩时也被问到了这条链路。全部提取完成后我们用两个存储分别承载这些数据一个MySQL表存储四元组用来支持SQL精确查询另有一个向量索引存储“文本化描述”把四元组转成一句自然语言用来支持相似度召回。两条路互备实测配合效果最好。4. 问数链路实现细节意图分类、NL2SQL与答案生成4.1 问题分类器三类问题三种路由混合架构里的第一个关键模块是意图分类器。我们试过两种方案一种是直接用大模型做分类另一种是训练一个小的文本分类模型。大模型分类的好处是零样本、灵活但有两个问题一是响应延迟高每道题都要多一次大模型调用二是有时分类结果不稳定同样一句话换个说法就被分到别的类别。比赛环境下我们等不起这种不确定性最终用了BERT文本分类模型阈值兜底。分类器只有三个类别numeric_query问具体的财务数值比如“某公司2023年总资产是多少”calculation_query要求计算增长率、比率、占比等比如“净利润同比增长了多少”text_query问经营情况、风险描述、业务进展等叙述性内容比如“公司怎么看行业竞争”。我们在赛方提供的样例问题基础上又用大模型生成了一批扩充数据让BERT模型在3000条左右的数据上微调。最终准确率做到了94%左右已经够用。还有一个兜底策略如果分类器输出的置信度低于0.85就默认按numeric_query处理同时走RAG的模糊匹配。因为在财报问答场景里把“查数”问题错判成“聊天”问题损失最大。4.2 NL2SQL用大模型生成受限SQL而不是自由发挥对numeric_query类问题标准做法是NL2SQL——让大模型把自然语言问题转成SQL。但直接让模型自由写SQL非常危险它可能幻觉出不存在的字段名也可能生成复杂度爆炸的多表关联。我们的做法是“限制式生成”先定义一个只包含少数核心字段的表结构把可用的字段名和对应含义写成JSON Schema再作为上下文喂给模型。然后把模型输出的SQL用正则做一个安全校验禁止除SELECT之外的写操作最后用参数化查询去执行。核心表的Schema长这样CREATE TABLE financial_data ( id INT PRIMARY KEY, company_name VARCHAR(128), metric_name VARCHAR(128), metric_value DOUBLE, report_date DATE, report_type VARCHAR(16) -- annual or quarterly );我们告诉大模型的生成规则是“根据用户问题只选择company_name、metric_name、report_date这三个字段的取值其他字段不允许查询”。这样生成的SQL几乎不会出错。真正难的是“同一实体可能有多种叫法”的问题。比如用户问“营收”但表里存的是“营业收入”。我们在NL2SQL之前加了一步实体标准化先用规则和同义词库把问题里的关键实体替换成标准名再交给大模型生成SQL。这比让模型自己去理解“营收”等于“营业收入”可靠得多。4.3 检索增强答案生成让大模型不瞎编对text_query类问题我们走的是经典RAG链路但加了很多财报场景特有的处理。文本切片不是简单按固定长度切而是按语义块切。我们会优先把原文中的完整段落、章节标题、表格说明作为边界。如果检测到跨页的连续段落会先把跨页部分合并后再切片。切片大小我们实测用512-768个字符效果最好太小则上下文不足太大则检索噪音增加。检索采用混合检索BM25稀疏检索 向量稠密检索然后按权重融合排序。BM25擅长处理“营业收入”这种精确词汇匹配向量检索擅长处理“公司未来规划”这种语义匹配。融合权重我们调成了BM25占0.4、向量占0.6整体效果比单一检索高出不少。最后生成答案时prompt里要求大模型遵循三条硬性规则只根据检索到的资料回答禁止引入外部知识如果资料中找不到答案明确回答“未在年报中找到相关信息”涉及具体数值时必须引用来源章节或页码。这三条规则不能只是口头说说因为大模型偶尔还是会越界。我们在后处理阶段加了一个数值一致性校验模型回答中出现的每一个数字都要能在检索到的资料片段中找到对应值否则就拒绝这次输出触发二次检索。这个校验帮我们挡住了很多幻觉。5. 评测与调优我们用A/B测试逼出来的几个关键改进5.1 官方指标拆解正确率、召回、完整性加权怎么算竞赛评测不会只看“答案是不是对的”它通常有一套加权体系。根据赛题描述里透露的信息我们的理解是评测指标大致包含三块数值正确率单一数值问题答案与标准值完全一致允许一定容差才得分文本召回质量叙述类问题答案和标准答案的语义相似度可能是Rouge或BERTScore完整性多值问题如“列出近三年营收”要求每个值都提到且正确漏一个就不给满分。这三块指标对我们方案的影响是数值正确率权重最高所以我们把大部分精力都放在NL2SQL和计算链路的准确性上完整性权重次之所以我们设计了“先查全、再生成”的策略——所有多值问题都先走SQL把全值查出再让模型组织语言而不是让模型自己回忆“三年的数据都有哪些”。5.2 踩坑记录一同比/环比的口径错乱我们第一次全链路跑通后自测了50道题数值正确率只有67%。排名第一的错因就是同比环比口径错乱。用户问“2023年净利润同比增长了多少”我们的NL2SQL链路查出了2023年净利润也查了2022年净利润但计算时直接用了(2023值 - 2022值) / 2022值。这里的问题在于如果2022年净利润是负数这个公式算出来的增长率是反向的实际财务上“扭亏为盈”应该显示为“净利润同比增长297.3%”而我们的公式很可能算出“-150%”这种错得离谱的数。我们最后接了一个财务口径修正规则库专门处理这种情况如果上年值为负、本年为正不计算增长率直接表述为“扭亏为盈”如果上年值为正、本年为负计算下降比例并用“下降百分比”表述如果上年值为0报“无法计算”。另外还要区分“同比”和“环比”。“同比”是和上年同期比“环比”是和上一报告期比。这个问题在区分年报和半年报时特别容易出错。我们解析问题时会对时间词做单独标注并存入一个时间上下文变量SQL生成时强制要求带上这个时间条件。5.3 踩坑记录二同一科目上年期末余额与本年年初余额不一致第二个让我们大跌眼镜的坑出在资产负债表上。用户问“2022年年末总资产是多少”我们从2022年年报中查到期末总资产数据本身没错。但如果用户问“2023年年初总资产是多少”很多队会直接在2023年半年报或年报中查找“年初余额”这一列。问题是很多上市公司在2023年年报中给出的“上年年末余额”是调整后的数因为会计准则变更或会计差错更正和2022年实际披露的期末余额不一样。这两个数到底哪个对取决于用户想问的是“2022年实际的披露数”还是“2023年年报中追溯调整后的同年数”。我们一开始没做区分导致同一问题的不同问法答案差了几百万。最终的处理是在知识库里给“年初余额”类数据单独打标签并在NL2SQL中检测“年初”“期初”等关键词时专门查询对应字段不能简单地拿上一年期末值。这个细节很小但非常影响数值正确率。5.4 提分效果最明显的几个Trick经过两轮A/B测试我们总结出几个提分效果最明显的优化点按性价比排序Trick 1答案后处理里的单位统一。财报里的单位很混乱有的是“元”有的是“万元”有的是“亿元”。我们统一在解析阶段把所有数值转为“元”生成答案时再根据数值大小自适应转成“亿元”或“万元”并保留原始单位说明。这一个小改动让数值正确率提升了大约10%。Trick 2建立“问题模板-预计算答案”缓存。赛题评测集里很多问题是同一模板的不同实体替换比如“XX公司2023年营收是多少”。我们先把高频模板固化下来做成预计算流程用规则生成SQL速度快而且稳定减少了大模型调用的不确定性。Trick 3为长文本问题做二级检索。如果第一轮检索到的文本片段在生成答案时被校验模块否决比如引用了不存在的数值会自动触发第二轮检索但第二轮会更换检索策略——更偏向BM25的精确匹配而不是向量语义匹配。这个“二次检索”机制挽回了至少15%的失败案例。Trick 4所有数值都保留原始来源链接。我们的最终答案不仅是“2023年营业收入为253.3亿元”还会附带来源格式如“详见年报第32页”。这不仅是合规问题评测时还会被当作“答案可解释性”的加分项。不要觉得这是可有可无参加过一次答辩你就知道这有多重要。6. 论文写作与成果打包经验6.1 论文怎么写才不丢分泰迪杯这类竞赛论文成绩占比很大而很多技术很强的队伍恰恰在论文上丢分。我们复盘后觉得最重要的不是篇幅而是“逻辑闭环”。论文的开头不要抄一段“随着人工智能的发展”评委看吐了。直接一句话点题本方案解决的是上市公司财报场景下面向自然语言查询的精确数值问答问题。然后马上抛出现有方法的问题大模型幻觉、数值不精确再引出你的方案。我们的论文框架是问题定义与难点分析强调财报问答和通用问答的差异整体方案设计给一张架构图说明数据流数据预处理与知识库构建写清楚解析、归一化、四元组构建意图分类与混合检索模块重点写NL2SQL的限制式生成方法实验与评测用消融实验证明每个模块的贡献总结与展望。注意评委会特别关注“方法的可复现性”。所以论文里一定要写清楚关键技术参数切片长度、检索TopK数量、模型名称、分类器准确率等等。我们的切片长度、检索权重、分类阈值这些核心参数都写进了附录方便复现。6.2 代码结构、README与可复现性一套能复现的代码比一段花哨的讲解更能说明问题。我们的项目代码按以下结构组织project/ ├── config/ # 配置文件包含模型路径、数据库连接 ├── data/ # 原始数据和中间结果 │ ├── raw/ # 原始年报PDF │ ├── processed/ # 解析后的结构化数据 │ └── knowledge_base/ # 构建的知识库文件 ├── src/ │ ├── data_processing/ # PDF解析、表格提取、科目归一化 │ ├── intent/ # 意图分类模型训练与推理 │ ├── retrieval/ # 混合检索模块 │ ├── sql_query/ # NL2SQL与SQL执行 │ ├── generation/ # 答案生成与校验 │ └── eval/ # 评测脚本 ├── scripts/ # 一键运行脚本 ├── requirements.txt └── README.mdREADME里除了说明项目功能还要写清楚运行环境Python版本、CUDA版本、显存要求、每一步命令的输入输出、以及评测集和评测脚本的使用方法。我们在README里特意加了一段“常见问题”把数据解析失败、模型下载失败、SQL报错的处理方式都写了进去。这里有个重要心得比赛结束后的代码审核环节评委最反感的就是“只能跑通某个数据集”的代码。所以你的代码最好做成数据无关的——换一家公司的年报进来只要格式符合预期就能跑出结果。我们为此写了一个输入格式标准化模块能在运行前检查PDF格式、页面数量、是否扫描版然后自适应选择解析策略。这个细节在答辩时给我们加了不少分。最后再讲一个真实感受泰迪杯B题这类“智能问数”赛题本质上考的是工程整合能力——你要把数据处理、检索、大模型、规则引擎、评测调优揉在一起任何一个环节掉链子都会拖垮整个系统。与其花时间追逐最新的模型不如在数据清洗和链路可靠性上多下功夫。我们最终拿到的成绩证明这个方向是对的。本文还有配套的精品资源点击获取