金融财报问答系统实践:RAG+Agent+Spring AI全链路解析

📅 2026/8/27 13:01:19
金融财报问答系统实践:RAG+Agent+Spring AI全链路解析
简介大模型应用正从通用对话走向垂直领域深度落地而金融财报问答是其中最具挑战的场景之一。核心难点在于如何让模型基于真实财报数据作答并确保每个数字可追溯、可验真。检索增强生成RAG技术通过将知识存储于向量库、推理交给大模型有效缓解了事实性错误Agent编排则赋予系统拆解复杂问题、调用计算工具的能力。同时Spring AI作为Java生态的集成框架为企业级系统接入大模型提供了稳定通道。本文从数据管道、多级召回、重排策略到Agent工具调用系统解析构建金融领域问答系统的完整技术路径并分享应对模型幻觉、跨期数据混淆、PDF解析等实际问题的工程经验。该方案适合投研分析、风控合规及企业管理等需要精准数据查询与证据链支撑的业务场景为搭建私有数据智能助手提供可落地的参考框架。 做金融财报问答这个项目是我这半年多踩坑最多、也收获最大的一段经历。标题里的“LLM.zip”看似是个压缩包实际上是一整套围绕大模型构建垂直领域问答系统的完整方案。这套东西从数据管道、向量化策略、Agent编排到模型推理全链路都有涉及不是跑通一个demo那么简单。如果你正准备用大模型做金融文档问答、或者想在企业内部落地一个基于私有数据的智能助手这篇内容应该能帮你少走很多弯路。先说清楚这套系统到底解决什么问题。财务报表、招股书、审计报告这类文档动辄几百页里面的数据、口径、备注信息环环相扣。传统做法是找个分析师人工翻阅效率低不说口径不一致的问题经常出现。而直接拿开源大模型去问它要么根本不知道你这家公司的情况要么一本正经地编数据——这在金融场景是绝对不可接受的。所以金融财报问答系统的核心不是“有个LLM就行”而是要把LLM的推理能力、检索系统的精准召回能力、以及金融领域的数据结构化能力三者捏合在一起。我在这套系统里最终采用的是RAG检索增强生成作为主架构配合Agent工具调用底层用Spring AI作为编排框架。选这个组合有几个现实考量RAG保证回答内容来自真实财报数据可追溯到原文Agent负责拆解复杂问题、按需调用工具Spring AI则解决了Java生态对接大模型的痒点问题让整个系统能嵌入现有的企业级技术栈。下面我从设计思路到落地细节把整个项目的关键环节都拆开讲一遍。1. 项目概述与核心需求拆解1.1 财报问答到底难在哪先说一个容易忽略的事实金融财报问答本质上不是一个“问答问题”而是一个“数据可信度问题”。你问“公司2023年营收是多少”如果模型答错了数字哪怕只是错一个小数点轻则误导投资决策重则引发合规问题。而通用大模型在训练时根本没见过你这家公司的内部分析报告它只能靠泛化知识猜测。这就带来了财报问答的第一个难点必须让模型基于真实的给定文档作答而不是凭借训练时的记忆。第二个难点是财报语言的高度结构化。财报里大量使用表格、附注、会计政策、分部报告等格式纯文本解析很容易丢失上下文关系。比如某个营收数据在“合并利润表”和“附注42”里都有提及但口径一个含税一个不含税简单做文本片段召回的话模型看到哪个片段就引用哪个答非所问的概率非常高。第三个难点是多跳推理。用户不会只问“营收多少”更常问“毛利率变化的原因是什么”“现金流为什么改善”这类需要连续抽取多张表、多个附注才能回答的问题。单轮检索往往拿不全上下文需要Agent设计多步检索方案。我在做需求调研时还发现一个很实际的痛点投研人员真正想要的不是“一个答案”而是“带引证的答案”。也就是说模型给的每个数字最好都能定位到原文哪一页、哪个表格。这要求问答系统不仅要回答还要在回答中附上可验证的证据链。1.2 系统边界与用户画像这套系统的目标用户我划成三类投研分析师需要快速定位数据与口径、风控合规人员需要核对指标是否异常、是否有披露瑕疵、以及企业管理者需要自然语言查询历史财务数据辅助决策。不同用户对精度和速度的容忍度不一样。分析师接受30秒以内的查询延迟但要求答案必须可溯源管理者希望10秒内返回结论性摘要合规人员则要求所有回答都能输出完整的证据路径。考虑到这些差异我在系统里没有做“一刀切”而是把问答链路拆成“快问快答”和“深度分析”两个模式。快问快答走单轮RAG召回回答聚焦具体数值深度分析走Agent多步检索结合多源数据交叉验证。明确边界很重要。一开始我们想做一个“全知全能”的系统什么都能问结果模型召回范围失控精度直线下降。后来把范围收敛为只回答与财报、财务指标、管理层讨论、审计意见相关的问题超出边界直接拒答。这个拒答设计在金融场景不是功能缺失而是安全合规的必要保障。2. 技术选型与整体架构设计2.1 为什么选RAG而不是微调做过大模型应用的人应该都纠结过垂直领域到底微调还是RAG我的答案很明确涉及高频更新的私有数据必须走RAG微调只适合沉淀稳定的业务逻辑和表达风格。原因很简单。财务报表每个季度要更新如果靠微调来学习新数据意味着每个季度都要重新训练一次模型成本高、周期长而且训练数据一混入历史旧数据模型很容易遗忘新口径。RAG则把“知识”和“推理”解耦——知识放在外部向量库里模型只负责根据检索结果做推理。数据更新了直接换索引就行模型本身不用动。当然RAG也分两种形态。一种是朴素RAG用户问题直接embedding向量检索Top-K片段拼进Prompt让模型回答。另一种是Agent化RAG让模型先意图识别再决定检索什么、用什么工具、要不要二次检索。财报场景必须用后者这一点我在后面的“多级召回”部分会详细展开。我选择RAG的另一个原因是金融场景对幻觉零容忍。微调模型本质上还是在生成token你没法保证它生成的内容100%忠于训练数据。而RAG只要检索到的上下文是真实的再配合Prompt强约束“只能基于上下文作答”幻觉概率就大幅度下降。虽然不能完全消除但至少能把错误控制在“检索不到正确答案”和“上下文不完整”这两个可识别的范围内。2.2 Agent编排与MCP的角色如果只是“检索-生成”两步那根本不用上Agent。但财报问答的场景天然需要工具调用数据计算要调脚本、跨财报期对比要查历史索引、看指标口径要查会计政策库、甚至用户上传一份PDF要转表格分析。这些都依赖Agent来做规划。我在系统里用了Spring AI去接MCPModel Context Protocol客户端。MCP相当于给LLM配了一套通用的“USB接口”让模型能统一调用外部工具和数据源。Spring AI这个框架的好处是它吸收了大量Agent编程的最佳实践把模型对话、工具注册、状态管理都抽象得很干净。我在原来的LangChain和Spring AI之间做过对比LangChain在Python生态里确实灵活但一旦涉及企业内部系统对接Java的Spring AI能和已有的服务治理、监控体系无缝衔接这个优势是Python生态给不了的。Agent的职责我做了三层编排。第一层是意图分类判断问题属于“数值查询”“趋势分析”“异常核验”还是“报告解读”第二层是子任务拆解比如“毛利率为什么下降”会被拆成“找出本期毛利率”“找出上期毛利率”“定位导致成本变化的附注”“对比解释”第三层是工具调度根据子任务决定走向量检索、查SQL还是调用计算脚本。这套三层设计让复杂问题能系统性拆解而不是一把梭地全丢给模型。2.3 向量模型与重排模型的选择说到检索embedding模型是重头戏。我先后试过openai的text-embedding-3-large、bge-m3、以及若干国产的金融领域模型。实测下来通用领域性能其实差距不大但在财报这种中英混排、表格密集、术语繁多的文档上bge-m3的表现反而更稳尤其是对中英文混合语义的对齐做得比较好。如果你的环境允许调海外APIOpenAI那款也不错但考虑到金融数据的合规敏感性我们最终选型是本地化部署的bge-m3。只靠embedding和向量相似度还不够。财报问答的检索精度很大程度上依赖重排rerank策略。初始向量检索Top50再用cross-encoder重排出Top5或Top8进Prompt。这一层排序效果非常明显我测试过召回率直接提升15%左右。原因在于向量相似度只能衡量语义相关性而重排模型能更精细地捕捉上下文匹配度尤其是在判断“这段文本里到底有没有用户要的数据”上效果要好得多。注意向量库我选的Milvus单机版跑起来足够没必要一开始就上分布式。数据量小的时候用什么库都差不多真正区别在于后续扩展性和可维护性。3. 核心模块实现与实操细节3.1 财报文档的数据管道建设文档解析这一步是全网教程最少、实际坑最多的地方。财报PDF有很多是扫描件图片形式存在普通的PDF解析库根本拿不到文字。我的处理链路是这样的第一步PDF解析。对文本型PDF用pdfplumber和PyMuPDF双解析把页面上的文本、表格按坐标提取出来。对扫描型PDF先调用OCR服务识别。金融扫描件通常版式复杂表格线密集我用的是PaddleOCR的表格识别模型它能把表格结构还原成HTML比纯文本框切割再拼接的方式准确率高很多。第二步版面结构还原。这是容易被忽略但至关重要的点。直接平铺文本会把“合并资产负债表”和“母公司资产负债表”混淆还会把附注里的数字错接到正文。我是先把PDF按页还原成版面流识别出标题层级、表格区域、正文区域再根据标题把内容组织成一系列带元数据的chunk。每个chunk不仅包含文本还包含所属章节、页码、表格ID、期间如2023年、2024年上半年等结构化字段。第三步chunk语义化切分。财报文档的特殊之处在于一个完整的表格可能跨好几页按固定长度切分会把表头和数据断开。我的做法是先按照章节和表格边界硬切再对过长段落做滑动窗口补充。切分之后的chunk信息密度明显更高每个chunk都是一个相对完整、能被独立理解的知识单元。第四步metadata打标。每个chunk写入向量库时同时写入期间、报表类型、页码、来源文件等字段。这一步在后续做时间过滤和来源追溯时是救命稻草。这套管道跑起来之后一个500页的年报大概能生成1600到2200个chunk平均每个chunk约400到600字。效果好的关键在于解析的准确性和chunk的信息完整性而不是追求chunk数量多。3.2 多级召回与时间过滤财报问答有个很独特的检索难点同一个公司不同年份的数据在语义上高度相似。“2023年营收”和“2022年营收”这两段文本在向量空间里距离非常近如果你不做时间过滤检索出来的Top-K片段可能全在讲2022年。我是怎么解决的用两层过滤第一层是硬性元数据过滤。用户在提问时如果带上了“2023年”这样的时间限定前端意图识别模块会抽出时间实体直接过滤向量库中metadata不符合条件的chunk。这一步不是靠模型理解而是走规则引擎精准且快。第二层是语义检索后的重排过滤。如果用户没有明确说哪一年那就要靠重排模型判断到底哪个片段和问题最匹配。但这里有个技巧检索时我会把“问题本身”和“问题加所有候选年份”的变体一起送入检索让向量相似度能捕捉到潜在的时间关联。实测表明这种方式对于比较型问题特别有效。多级召回还有一个我在踩坑中总结出的经验不能只检索内容相关的chunk还要检索“结构相关的chunk”。比如用户问“非经常性损益”多数时候结构层面有一个专门的附注章节我先从章节索引里定位到“非经常性损益明细表”这个章节再在这个章节内部做细粒度检索比全局检索再找答案要快得多也准得多。3.3 提示词工程与输出约束很多做LLM应用的同学认为Prompt就是写几句话其实金融问答的Prompt设计需要系统和严谨。我的Prompt结构固定为五段式角色设定、任务说明、检索上下文、回答约束、输出格式。角色设定不写“你是AI助手”而是写“你是持牌分析机构的财报分析师仅依据提供的上下文回答禁止外部知识”。角色越具体模型的推理风格越收敛。检索上下文这是关键战场。我把多个召回来源按优先级排列报表数据块、附注文本、管理层讨论与分析、历史同期数据、审计意见。上下文多的时候模型容易被淹没所以我会在上下文前面加一个简短的“数据来源索引”标明每一段是什么含义让模型自己在回答时选择合适的信息。回答约束必须包含三块内容——结论、推导依据、数据来源标记。且结论只能用当前上下文中的数据凡是没在上下文中出现的数字一律不允许编造。这条约束配合输出格式能极大缓解幻觉。输出格式我强制模型用JSON返回结构化字段包括answer、evidence、confidence、disclaimer。其中evidence字段是数组每个元素包含引用的chunkId和页码。这样后端可以直接把答案渲染成带脚注的卡片非常用户友好。提示千万别让模型生成[citation:1]这种格式的引用标记然后靠正则去匹配——模型经常把编号写错位导致引用错乱。最好让模型直接返回chunkId由后端代码查metadata去还原页码。3.4 Agent工具调用与Spring AI接入工具调用是Agent化改造里最有意思的部分。我设计了三类工具第一类是向量查询工具封装了Milvus的检索接口参数包括query文本、topK、时间过滤条件。模型觉得需要检索的时候会自动构造好参数。第二类是数值计算工具比如计算同比增长率、毛利率、资产负债率。这类工具接收JSON参数输出计算结果。设计这类工具的初衷是让模型直接算数值容易出错比如除以零、取错数位但我把计算逻辑固化成Python脚本后模型只需要负责传参准确率一下就上去了。第三类是表格解析工具当用户上传自己的Excel或PDF时这个工具负责把表格转换成结构化数据再进检索管道。Spring AI接入这块我踩了不少版本的坑。市面上相关的文章很多但版本更新快。我的建议是直接锁定一个版本不要追新。在写这篇内容时我用的是Spring AI 1.0.0-M6版本用OpenAI兼容接口对接本地部署的Qwen模型整体很顺畅。如果用别的版本接口签名变化会非常折磨人。Agent的对话管理也很关键。我用的不是简单的“每次请求无状态”而是保留了一个上下文池存储最近10轮的用户问题与Agent执行轨迹。为什么保留轨迹因为在多步工具调用中模型需要知道自己上一步检索到了什么才能决定下一步怎么走。如果没有轨迹Agent会反复做类似的检索既浪费时间又浪费token。4. 实操过程与核心环节实现4.1 环境搭建与模型推理部署我推荐一套可以直接抄的配置清单以下均为实测可用的版本推理框架vLLM版本0.6.1模型Qwen2.5-14B-Instruct如果没有特殊合规要求也可以选更大的72B但推理速度会下降明显向量模型bge-m3部署在独立容器重排模型bge-reranker-v2-m3向量数据库Milvus 2.4编排框架Spring Boot 3.2 Spring AI 1.0.0-M6文档解析pdfplumber PyMuPDF PaddleOCR部署的时候有个经验GPU显存要优先保证主模型。我的主模型占用大概30GB显存开启8-bit量化向量模型和重排模型是CPU推理跑得动性能也能接受。如果你只有单卡24GB显存建议把主模型降到Qwen2.5-7B同时用bge-m3的轻量版本。vLLM启动命令参考python -m vllm.entrypoints.openai.api_server \ --model /models/Qwen2.5-14B-Instruct \ --served-model-name qwen-finance \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --port 8000参数说明--max-model-len我设了8192这是Prompt和回答的联合最大长度。财报问答的上下文经常很长建议不要低于4096否则长上下文会被截断导致回答不完整。4.2 财报数据入库与索引构建数据入库是整个项目里最需要耐心的环节。一个财报季度要处理几百份PDF我写了一个调度任务跑以下流程读取源文件解析PDF。调用OCR接口处理扫描页。结构化提取表格与文本。执行chunk切分与metadata打标。把chunk写入Milvus。入库时需要注意一个性能问题不要逐条调用向量模型的接口要批量embedding一次能跑32条或者64条速度能提升10倍以上。Milvus也建议批量插入我这里用的是100个chunk一批。索引构建的核心参数是向量维度。bge-m3输出1024维所以collection必须要设好维度。字段设计我建议包含id主键、textchunk内容、embedding向量字段、metadataJSON字符串或独立字段、create_time。往Milvus插入数据的伪代码from pymilvus import Collection, utility collection Collection(financial_reports) data [ [chunk_ids], [texts], [embeddings], [metadatas], ] collection.insert(data) collection.flush()这里有个新手容易踩的坑必须调用flush否则数据只存在缓存里搜索时可能查不到。另外如果是首次建索引记得给向量字段创建IVF_FLAT或者HNSW索引。我用的HNSW参数M16efConstruction200精度和性能比较平衡。4.3 问答接口的完整实现链路问答接口是整个系统的门面我把它的完整调用链路整理成了一段伪代码方便你理解各模块的协作关系def financial_qa(question, user_id, top_k8): intent agent.plan(question) # 意图识别 子任务拆解 if intent.type numeric_query: candidates vector_search(question, top_k50) filtered time_filter(candidates, intent.year) reranked rerank(question, filtered) evidences build_evidence(reranked[:top_k]) answer llm_generate(question, evidences) return format_response(answer, evidences) elif intent.type trend_analysis: required_years extract_years(question) results [] for year in required_years: candidates vector_search(question, top_k50, yearyear) reranked rerank(question, candidates) results.append(reranked[:3]) merged_context merge_context(results) answer llm_generate(question, merged_context) return format_response(answer, results) elif intent.type out_of_scope: return reject(question)这段逻辑的核心在于意图不同检索策略就不同。数值查询走“时间过滤单轮检索”趋势分析走“多时间点多轮检索上下文合并”超纲问题直接拒答。这样才能兼顾精度与性能。4.4 完整测试案例演示我拿一份真实的年报做了一次完整测试。问题是“2023年公司营业收入是多少同比增长了多少主要增长驱动力是什么”第一个子问题“营业收入是多少”系统检索后命中了“合并利润表”相关chunk模型返回数字“31,202,175,284.25元”evidence里包含了对应的chunkId和页码。第二个子问题“同比增长”Agent自动调用了数值计算工具对比了2022年同期数据计算出增长率约7.15%。第三个子问题“增长驱动力”检索定位到“管理层讨论与分析”章节模型摘录了关于市场拓展、产品结构优化的描述并给出了原文引用。整个链路用时8.7秒其中检索占1.2秒重排0.8秒模型生成4.9秒剩下的时间在工具调用和数据传递上。对于交互式问答来说这个响应时间是可以接受的。我还测了一个刁钻的问题“2023年非经常性损益对公司净利润影响多大”这需要综合查找非经常性损益明细表、当期净利润、扣非净利润。Agent拆解成了三个步骤先找明细表再找利润表最后把两个数传给计算工具做差值。最终给到的回答是“非经常性损益为4.2亿元对当期净利润的影响比例约为5.7%”同时附上了两张表的引用来源。这种多步拆解能力是朴素RAG根本做不到的。5. 常见问题与排查技巧实录5.1 模型幻觉问题金融问答最怕的就是幻觉我遇到的幻觉分三类凭空捏造数据、混淆年份数据、张冠李戴公司数据。每种的处理方式不一样。凭空捏造的根治措施是严格限制上下文。我在Prompt里明确写了“如果上下文中没有答案直接回答无法获取并给出原因建议”。同时开启vLLM的--repetition-penalty参数可避免模型在不确定时反复生成同一个错误答案。混淆年份数据的问题是时间过滤不严导致的。我加强了两点一是意图层抽时间实体二是检索层强制限定年份。一旦过滤做对这类问题基本消失。张冠李戴公司数据通常出现在一次检索里同时命中多家公司财报的场景。我的解决办法是对向量库做partition每家公司一个partition检索时先限定公司实体。这个设计在to B场景尤为重要因为用户经常会在一个会话里问多家公司。5.2 检索召不回正确答案召回率为零是另一个高频问题。我排查过几次原因一般是chunk被分得太碎关键信息被截断。比如一个财务指标在表格里但表格被切分成了两半而检索命中的恰巧是后半段里面只有数字没有指标名。解决方案是切分策略里加大表格完整性判断如果某个chunk的表格内容占比较高就把它和前一页的表格合并或者把表头相关信息重复写入切分后的chunk里。还有一类问题是专业术语导致的。财报里的“扣非净利润”是“归属于上市公司股东的扣除非经常性损益的净利润”的简称用户用简称问向量检索用全称的embedding匹配经常对不上。我建了一个同义词扩展表把常见术语的简称、全称、别名都映射起来检索前先做实体归一下类再进行匹配。5.3 PDF解析乱码与扫描件问题财报PDF质量参差不齐有的内嵌字体有问题直接提取出来的文本全是乱码有的表格线提取后错乱。我的经验是先用pdfplumber提取文本如果提取结果里中文占比低于40%就认定为扫描件或者字体异常直接转PaddleOCR识别。注意PaddleOCR对大表格的还原效率不高因此我会先用图像处理把表格区域检测出来再对每个表格区域单独识别这样表格结构能保留得更完整。注意识别完一定要人工抽检。哪怕OCR准确率是99%几百页财报也意味着每一页都可能有几处错误如果这些错误恰好出现在关键数字上影响是被放大的。5.4 Agent超时与任务失控Agent在复杂问题上容易陷入死循环。最典型的表现是模型反复调用同一个工具每次都被拒绝但它就是不停重试。我在这块设了三道防火墙一是工具调用次数上限默认每个问题最多调用8次工具二是单次工具调用超时以指标计算为例超过10秒直接杀掉三是子任务去重如果Agent重复发起相同参数的检索第二次直接返回上一次的结果不再重复执行。这三道防火墙守住了系统稳定性也让线上问题的排障简单了很多。6. 后续扩展方向与优化经验项目做到这个阶段基本能在一个可控范围内稳定运行了。但我自己也清楚现有系统距离“智能投研助手”这个目标还有不小的距离。我目前已经在尝试的两个扩展方向一是接入实时行情数据让系统不但能问财报还能问股价、估值、资金流向等时序数据。这部分需要改造Agent的数据源层把MCP客户端嫁接到行情API上让模型具备按需拉取实时数据的能力。二是增加图表生成能力分析结果出来后自动生成趋势图和对比图让使用者不用再去Excel里手工做可视化。实现路径是让Agent调用一个基于ECharts的渲染工具将结构化表格转成图表配置。另外我还在测试用更小参数的模型做“快问快答”场景的实时推理把响应时间压缩到2秒以内。现在跑了一个Qwen2.5-3B的量化版本效果还不如14B理想但已经把延迟降到了1.8秒左右。后续如果能在准确性上再优化一版整个系统的体验会再上一个台阶。在做这个项目的过程中我最大的感悟是金融问答系统考验的从来不是模型有多强而是工程系统有多严密。数据管道是否可靠、检索策略是否精准、Agent编排是否可控、证据链是否完整这些才是一个真正能用的金融LLM应用的底色。希望这篇内容能给你提供一个可落地的参考框架少踩几个我踩过的坑。本文还有配套的精品资源点击获取