尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

金融财报问答大模型LLM:RAG检索增强生成实战与避坑指南

金融财报问答大模型LLM:RAG检索增强生成实战与避坑指南 简介这份资源面向金融科技开发者、量化分析师与AI学习者聚焦财报解析与智能问答场景提供一套可本地部署的大模型工程实践。包内共42个文件以22个Python脚本为核心覆盖意图识别、信息抽取、相关性打分、实体识别、开放问答与答案生成等模块另有6个Shell脚本负责数据与模型下载、训练及服务启停配合3份README、3个JSON配置、2个YAML与2个TXT说明整体压缩包约54KB结构紧凑、便于按模块研读。资源围绕ChatGLM2-6B展开包含LoRA、AdaLora、QLora三种微调脚本并集成Weaviate与Elasticsearch检索服务形成从数据准备、模型微调到检索增强问答的完整链路。目前已有390人学习下载适合希望理解金融领域大模型落地流程、借鉴多模态与检索增强思路的读者参考。1. 金融财报问答大模型LLM一份压缩包背后要跑通的三件事一份名为「金融财报问答大模型LLM.zip」的压缩包真正值钱的地方不在模型权重而在它把「财报 PDF → 结构化问答」这条链路压缩成了一个可复现的最小闭环。财报问答和通用闲聊完全是两码事一份 200 页的年报里营收、毛利率、经营性现金流分散在正文、附注和表格里数字还带单位、带同比、带口径切换。通用大模型直接读原文十有八九会把「归母净利润」和「净利润」混着答或者把「万元」当成「元」。所以这个方向要解决的核心问题有三个怎么把非结构化财报变成可检索的知识、怎么让 LLM 只依据检索到的片段作答、怎么验证它没在编。适合谁做做金融数据产品的后端、想给投研团队搭内部问答工具的工程师以及手里有一堆年报 PDF 却只能靠 CtrlF 的人。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。2. 财报问答为什么不能直接上通用 LLMRAG 的选型理由2.1 财报文本的三个特殊性决定了架构先想清楚财报和普通文档差在哪。第一是表格密度极高资产负债表、利润表、现金流量表三张主表加上几十个附注表格纯文本抽取会把行列关系打散一个「2023 年」可能对应十几行数字。第二是口径敏感同一份报告里「营业收入」和「营业总收入」是两个数「扣非净利润」和「归母净利润」也不是一回事答错一个词整段结论就废了。第三是时效与版本一家公司一年出年报、半年报、季报还有更正公告检索时必须能区分「2023 年报」和「2024 一季报」。这三点直接否掉了「把整份 PDF 塞进长上下文让模型自己读」的做法。上下文再长模型对表格里第 37 行第 4 列的数字定位能力也会衰减而且每次问答都重读 200 页成本和延迟都不可接受。常见做法是RAG检索增强生成先把财报切块、向量化存进向量库提问时先检索出最相关的若干片段再让 LLM 基于这些片段作答。这样模型看到的是「营收 2023 年为 1234.5 亿元同比增长 8.2%」这种已经定位好的句子而不是让它自己在表格里数格子。2.2 检索层和生成层各自要做什么RAG 拆开就是两件事。检索层负责「找得准」把财报按语义单元切块一张表、一段附注、一节管理层讨论每块生成 embedding 存库提问时用问题向量去召回 Top-K。生成层负责「答得对」把召回片段拼进 prompt明确要求「只依据以下材料回答材料中没有的信息回答不知道」。这两层任何一层拉胯最终答案都会翻车——检索召回的是隔壁公司的数据或者生成时模型自由发挥补了个不存在的数字。选型上embedding 模型优先选对中文金融语料友好的别直接用纯英文模型向量库用 FAISS 或 Milvus 都行数据量在几万块以内 FAISS 本地跑足够。LLM 这一层如果只是内部工具用开源模型本地部署能省掉数据外传的顾虑如果追求答案质量商用 API 的指令遵循能力通常更稳。这里不展开具体品牌按团队的数据合规要求和预算定。2.3 一个最小可跑的 RAG 骨架下面这段是检索层的最小实现用 FAISS 做向量索引把财报切块后存进去。先跑通它再谈优化。import faiss import numpy as np from sentence_transformers import SentenceTransformer # 1. 加载中文 embedding 模型换成你验证过的中文金融语料模型 model SentenceTransformer(BAAI/bge-base-zh-v1.5) # 2. 财报切块这里用最简单的按段落切实际要按表格/章节切 chunks [ 2023年公司实现营业收入1234.5亿元同比增长8.2%。, 2023年归属于上市公司股东的净利润为98.7亿元同比下降3.1%。, 经营活动产生的现金流量净额为156.2亿元同比增长12.4%。, ] # 3. 向量化并归一化内积等价于余弦相似度 emb model.encode(chunks, normalize_embeddingsTrue) dim emb.shape[1] index faiss.IndexFlatIP(dim) # IP 内积配合归一化即余弦 index.add(np.array(emb, dtypefloat32)) # 4. 检索 query 2023年净利润是多少 q_vec model.encode([query], normalize_embeddingsTrue) scores, ids index.search(np.array(q_vec, dtypefloat32), k2) for i, s in zip(ids[0], scores[0]): print(fscore{s:.3f} {chunks[i]})逻辑说明normalize_embeddingsTrue把向量归一化后IndexFlatIP的内积就等于余弦相似度省去额外计算。k2是召回条数财报问答里建议 35太少容易漏太多会稀释 prompt。参数上bge-base-zh-v1.5的向量维度是 768如果换模型要同步改dim否则index.add会直接报维度不匹配。跑通后你会看到「净利润」那条得分最高说明检索层基本可用。提示切块粒度是财报 RAG 最容易被低估的参数。按固定字数切会把一张表切成两半建议按「章节标题 表格整体」切单块控制在 300600 字。3. 把财报 PDF 变成可检索知识解析、切块与入库3.1 PDF 解析表格是最大的拦路虎财报 PDF 分两种一种是原生电子版文字可选中另一种是扫描件得先 OCR。原生版用pdfplumber或PyMuPDF抽文本和表格扫描件走 OCR 后同样处理。关键在表格——pdfplumber的extract_tables()能把表格还原成二维列表但财报表格常有合并单元格、跨页续表直接抽会错位。import pdfplumber def parse_financial_pdf(path): pages [] with pdfplumber.open(path) as pdf: for page_no, page in enumerate(pdf.pages): text page.extract_text() or tables page.extract_tables() # 表格转成 Markdown保留行列关系 table_md [] for tb in tables: rows [| | .join(str(c or ) for c in row) | for row in tb] table_md.append(\n.join(rows)) pages.append({ page: page_no 1, text: text, tables: table_md, }) return pages逻辑说明把表格转成 Markdown 是为了保留行列结构后续 embedding 时模型能感知「这是同一行的数据」。参数上extract_tables()对有线框的表格效果好无线框表格要调table_settings里的vertical_strategy和horizontal_strategy。这一步的产出是「页 → 文本 表格列表」后面切块时表格单独成块不要和正文混在一起。3.2 切块策略按语义单元而不是字数切块的目标是让每一块「自包含」。一张利润表是一块一段「主营业务分析」是一块一条附注是一块。实操里我会给每块加上元数据公司名、报告期、章节标题、页码。这样检索时可以先按元数据过滤比如只搜「2023 年报」再做向量召回准确率能明显提升。def build_chunks(pages, company, period): chunks [] for p in pages: # 正文按段落切过滤空行 for para in p[text].split(\n): para para.strip() if len(para) 20: # 太短的段落丢弃多为页眉页脚 continue chunks.append({ text: para, meta: {company: company, period: period, page: p[page], type: text}, }) # 表格单独成块 for tb in p[tables]: chunks.append({ text: tb, meta: {company: company, period: period, page: p[page], type: table}, }) return chunks逻辑说明len(para) 20这个阈值是过滤页眉页脚和页码的财报里这类噪声很多不滤掉会污染检索。元数据里的type字段让后续可以针对表格和正文用不同的 prompt 模板。参数上段落长度阈值可以按报告排版调2050 之间试。3.3 入库与元数据过滤检索把切块和向量一起写进向量库检索时先按元数据过滤再算相似度。FAISS 本身不支持元数据过滤简单做法是召回后按 meta 筛或者用 Milvus 这类支持标量过滤的库。def search_with_filter(index, chunks, model, query, periodNone, k5): q_vec model.encode([query], normalize_embeddingsTrue) # 多召回一些再按元数据过滤 scores, ids index.search(np.array(q_vec, dtypefloat32), kk * 3) results [] for i, s in zip(ids[0], scores[0]): if i -1: continue c chunks[i] if period and c[meta][period] ! period: continue results.append((s, c)) if len(results) k: break return results逻辑说明k * 3是过采样因为过滤会淘汰一部分先多召回再筛能保证最终条数。period参数就是解决「2023 年报」和「2024 一季报」混淆的关键提问时把报告期作为过滤条件传进去。这一步跑通检索层就算完整了。4. 让 LLM 只依据财报作答Prompt 设计与答案校验4.1 Prompt 模板把「不知道」写进规则生成层的核心是约束模型别自由发挥。财报问答的 prompt 要明确三件事只依据材料、数字要带单位、材料没有就说不知道。PROMPT_TEMPLATE 你是一名财报分析助手。请严格依据以下材料回答问题。 规则 1. 只使用材料中出现的信息不要引入外部知识。 2. 涉及数字必须保留原文单位和口径如归母净利润营业收入。 3. 材料中没有相关信息时回答根据提供的材料无法回答。 4. 回答后附上依据的材料片段编号。 材料 {context} 问题{question} 回答 def build_prompt(chunks, question): context \n.join( f[{i1}] {c[text]} for i, (_, c) in enumerate(chunks) ) return PROMPT_TEMPLATE.format(contextcontext, questionquestion)逻辑说明规则 2 是财报场景的命门不写清楚模型会把「万元」省略掉。规则 4 要求附片段编号是为了后面做人工校验时有据可查。参数上context拼接顺序按相似度从高到低模型对靠前的内容注意力更集中。4.2 用 LLM as judge 做答案校验生成完不能直接信得校验。常见做法是用另一个 LLM 当裁判判断答案是否完全由材料支撑。这就是热词里的LLM as judge思路在财报场景特别有用因为数字错一位就是事故。JUDGE_TEMPLATE 判断以下回答是否完全依据材料且数字、口径无误。 材料 {context} 回答 {answer} 请输出 JSON{{supported: true/false, reason: ...}} def judge_answer(context, answer, judge_llm): prompt JUDGE_TEMPLATE.format(contextcontext, answeranswer) resp judge_llm(prompt) return resp # 解析 JSON 后看 supported 字段逻辑说明裁判模型最好和生成模型不同避免同源偏差。supportedfalse的样本要人工复核积累成测试集。参数上裁判 prompt 要求输出 JSON 是为了程序化处理别让它自由文本输出。4.3 数字一致性校验正则兜底LLM 裁判也有漏判的时候数字层面再加一道正则校验把答案里的数字和材料里的数字做比对答案里出现但材料里没有的数字直接标红。import re def extract_numbers(text): # 匹配带小数和百分号的数字 return set(re.findall(r\d\.?\d*%?, text)) def check_numbers(answer, context): ans_nums extract_numbers(answer) ctx_nums extract_numbers(context) # 答案里有、材料里没有的数字 可疑 suspicious ans_nums - ctx_nums return suspicious逻辑说明这个校验很粗暴但有效能抓住大部分「模型自己编数字」的情况。suspicious非空就转人工。参数上正则可以根据财报数字格式调整比如加上「亿元」「万元」等单位词。5. 财报问答落地避坑五条血泪经验5.1 表格跨页断裂导致数字错位现象问「2023 年资产负债率」模型答了一个明显偏小的数。原因资产负债表跨了两页extract_tables()把续表当成新表第二页的表头丢失数字和科目对不上。解决解析后做表格合并检测相邻页表格列数一致且首页末行无合计时判定为续表并拼接或者干脆按页存表检索时把相邻页一起召回。5.2 口径混淆归母净利润 vs 净利润现象用户问「净利润」模型答的是归母净利润两者差了几个亿。原因财报里两个词都出现embedding 检索时没区分开prompt 也没强调口径。解决切块时把科目名作为元数据检索时对科目名做关键词加权prompt 里明确要求「若材料同时出现多个口径分别列出」。5.3 报告期串台现象问 2023 年报的数据模型混进了 2022 年的对比数。原因财报里同比数据大量存在检索召回了含两年数据的片段模型没分清哪个是本期。解决元数据加period字段做硬过滤prompt 里要求「明确标注数据所属报告期」。5.4 扫描件 OCR 把小数点吃掉现象营收 1234.5 亿被识别成 12345 亿。原因扫描件 OCR 对小数点、千分位逗号识别不稳。解决OCR 后加数字格式校验对异常大的数字比如超过行业量级标红复核关键财报优先找原生电子版。5.5 检索召回为空时模型硬答现象问了一个财报里没有的数据模型编了一个。原因prompt 里「不知道」的规则不够强或者检索返回了低分片段被当成有效材料。解决设相似度阈值低于阈值直接返回「未找到相关信息」不进生成层prompt 里把「不知道」放在规则第一条。6. 进阶把问答准确率从能用推到可信跑通上面这套你已经有一个能答财报问题的原型了。但要推到投研团队敢用还得做两件事建评测集和做检索重排。评测集不用大50100 条就够覆盖三类问题单数字查询「2023 年营收多少」、对比查询「2023 比 2022 营收增长多少」、口径陷阱「扣非净利润是多少」。每条标注标准答案和依据页码。每次改切块策略或换模型跑一遍评测集看准确率变化别凭感觉调参。检索重排是提升准确率性价比最高的一步。向量召回 Top-20 后用一个交叉编码器cross-encoder对这 20 条重新打分取 Top-5 进 prompt。交叉编码器把问题和片段拼在一起算相关性比单纯向量相似度准得多代价是慢一些但财报问答对延迟不敏感。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) def rerank(query, candidates, top_k5): pairs [(query, c[text]) for _, c in candidates] scores reranker.predict(pairs) ranked sorted(zip(scores, candidates), keylambda x: -x[0]) return [c for _, c in ranked[:top_k]]逻辑说明candidates是向量召回的结果reranker.predict返回相关性分数按分数重排后取前 5。参数上bge-reranker-base对中文支持不错如果显存够可以换 large。这一步做完口径混淆和报告期串台的问题会明显减少。最后说个我自己的习惯每次上线新版本前我会挑 10 条「模型答得特别自信但可能是错的」样本人工过一遍这类样本往往暴露的是检索层的系统性问题比随机抽样有效得多。财报问答这行宁可模型说「不知道」也不能让它编一个看起来很像的数字。希望帮到你。本文还有配套的精品资源点击获取
返回列表