简介:金融财报问答大模型LLM是一套面向金融分析师、AI研究者及企业架构师的财报智能问答源码包,依托大模型与多模态技术,解决财务文本、图表混合场景下的信息提取与精准答题难题,整体设计兼顾开源易部署与二次开发灵活性。压缩包共42个文件、约54KB,以22个Python脚本和6个Shell脚本为骨架,辅以JSON/YAML配置、Readme与License说明;Python脚本覆盖意图识别、信息抽取、实体识别、相关性评分、开放问答、答案生成及ChatGLM2微调(含LoRA/QLoRA/AdaLoRA)等关键链路,Shell脚本提供数据下载、模型下载与一键启停,配置文件则预设训练、推理及向量/全文检索服务的接入参数。目前已有390人学习浏览,适合具备一定大模型基础的中高级开发者快速落地验证。透过这份源码包,使用者可完整搭建财报问答Pipeline,按需替换语料与模型权重,显著降低金融知识库问答系统的搭建门槛。
1. 金融财报问答大模型LLM.zip:先别急着解压,你得知道交付物里装的是什么
第一次拿到金融财报问答大模型LLM.zip这个压缩包时,大多数人的第一反应是解压、找模型文件、直接跑起来,然后在三小时后发现哪里都不对。这个东西本质上不是"一个模型",而是一份围绕财报问答场景打包的落地交付物:里面通常会有模型权重或微调脚本、tokenizer、推理代码、少量财报样例数据,以及一份你必须先读的 README。它解决的是"把财报里的数字和事实变成可追问的答案"这件事,适合做投研辅助、风控稽查、企业财报系统对接的 NLP 工程师。在动任何代码之前,先把 ZIP 里的结构、格式和约束盘清楚,比急着加载权重重要得多。
2. 解压后先盘目录:训练包还是推理包,决定你接下来三小时干什么
2.1 一个典型的财报问答 LLM 压缩包里都有什么
拿到 zip 后第一件事不是解压,而是先列目录。常见做法是unzip -l只读不解压。因为一个交付包可能几百 MB 到几十 GB,直接解压到默认目录很容易把磁盘占满。一个标准的财报问答大模型交付包,内部目录结构通常长这样:
| 路径 | 类型 | 作用 |
|---|---|---|
pytorch_model.bin/model.safetensors | 权重 | 模型参数,问答能力的核心 |
config.json | 配置 | 层数、头数、模型类型,决定加载方式 |
tokenizer.json/vocab.json/merges.txt | 词表 | 必须与权重配套,否则输出乱码 |
infer.py/predict.py | 脚本 | 交付方给出的最小推理入口 |
finetune.sh/train.py | 脚本 | 微调入口,只有训练包才会带 |
samples/或data/ | 数据 | 财报问题样例,用来验收效果 |
README.md | 文档 | 版本、底座、显存要求、免责声明,最重要 |
注意:这里的 README 决定你把包当训练包还是推理包用。带train.py和finetune.sh说明交付方希望你继续微调;只有infer.py那就是让你先用起来。还有一种是压缩包里根本没有 .py 文件,只有一个量化过的model.gguf,那是给本地私有化部署用的,需要用 llama.cpp 之类的运行时加载,不能直接塞进 transformers。这个区分在开头花五分钟界定,能省掉后面调一整天环境的时间。
2.2 先看三份关键文件:README、requirements、config
我的习惯是解压后先看三份文件:README、requirements.txt、config.json。命中关键词的顺序比解压顺手双击一个模型文件重要得多。下面这一串命令够用:
# 先看包内清单,避免覆盖现有目录 unzip -l 金融财报问答大模型LLM.zip | head -50 unzip -q 金融财报问答大模型LLM.zip -d ./finance_llm cd finance_llm # 用 file 判断权重文件到底是什么格式 file config.json tokenizer.json pytorch_model.bin 2>/dev/null # 看哪些子目录占了大部分体积 du -sh */ head -c 800 README.mdunzip -l先看包内文件清单,避免覆盖现有目录;du -sh */快速看哪个子目录占体积,帮助判断权重到底有多大。file命令很关键:safetensors、bin、gguf在输出里会直接标明data或Zip archive,一眼能识别出是什么格式。requirements.txt 决定依赖范围,比如是否包含bitsandbytes、peft、accelerate这些微调相关库,也能反过来佐证它是训练包还是推理包。
config.json 是一定要打开看的。里面有model_type和architectures字段。看到LlamaForCausalLM、Qwen2ForCausalLM这类才敢用AutoModelForCausalLM加载;如果看到的是BloomForSequenceClassification,那是分类模型,不是生成模型,跑问答脚本会直接报错。另外max_position_embeddings这一项告诉你模型原生支持多长的上下文,压缩包里配的推理代码如果没做长文本处理,超过这个长度就会被保底截断。这些配置确定之后,再决定是跑 micro 脚本、还是自己起一个 FastAPI 服务。
2.3 权重格式和底座识别:bin、safetensors、gguf 各是什么
权重文件格式是踩坑重灾区。.bin是 PyTorch 原始的torch.save序列化结果,加载快,但是黑匣子,历史上出现过反序列化执行恶意代码的争议,所以现在新交付的包基本都带.safetensors。safetensors 不会执行任意 Python 对象,只存张量,安全性好很多。.gguf是量化部署格式,常见于 llama.cpp 这类私有化部署方案,适合没有 GPU 的环境或者企业内网部署,CPU 也能跑,但精度一般比 FP16 低。
# 用 python 看 safetensors 的 key 数量和参数规模 python - <<'PY' from safetensors import safe_open f = safe_open("model.safetensors", framework="pt", device="cpu") keys = list(f.keys()) print("tensor count:", len(keys)) print(keys[:5]) PY这段代码只做了一件事:不把整个权重载入内存,先打开 safetensors 的元数据,输出张量名。tensor count如果只有几十个,说明是 LoRA 之类的低秩适配器权重,不是完整底座;如果输出里带有model.layers.0.self_attn.q_proj.weight这类名字,才是完整大模型。这个判断直接关系到你想用它做什么:只有 LoRA 权重,加载时需要先加载底座再挂适配器;有完整权重,直接AutoModelForCausalLM.from_pretrained就行。别拿到什么就加载什么,先花两分钟读张量名,是这个领域最常见的省钱动作。
提示:
unzip -l和file都是在动文件之前了解交付包的低成本手段,别跳过。
2.4 识别微调痕迹:LoRA、全参、QLoRA 决定你能否直接推理
如果一个 zip 里带了finetune.sh、train.py、lora_config.json,那它多半是训练交付包。这时候要注意三点:第一,看lora_config.json里的r、lora_alpha、target_modules,如果 target_modules 里有q_proj、k_proj、v_proj这类名字,说明只微调了 attention 投影层,最终权重必须和底座合并;第二,看bf16和gradient_checkpointing的设置,全参微调包的显存门槛很高,个人机器基本不用想;第三,有些包的权重是 QLoRA 的 4bit 量化状态,加载时还得依赖bitsandbytes,且必须是安装过相应 CUDA 版本的库,否则会报load_not_ready。
这三个判断做完,你对这个包是"直接推理"还是"还要继续训练"已经有结论。如果是训练包,建议先用它默认的 eval 脚本跑一遍评测集,看效果再考虑自己微调;不要直接拿业务数据从头微调,这是很多团队翻车的起手式,后面再补。
3. 最小推理链路:在本地把财报问答模型跑通的第一条命令
3.1 环境准备:CUDA、conda、requirements
跑通一个 LLM 不需要先了解全部 AI 大模型基础理论,但环境要稳。我的做法是新建一个专用环境,避免把系统 Python 弄脏:
conda create -n finance_llm python=3.10 -y conda activate finance_llm pip install -r requirements.txt pip install torch --index-url https://download.pytorch.org/whl/cu121这里先装 requirements.txt 再单独指定 torch 版本,是因为很多交付包的 requirements 里只写torch不写版本,最低的 torch 版本可能自动装上 CPU 版,后面加载模型会慢到怀疑人生。cu121表示 CUDA 12.1,如果你的机器是 CUDA 11.8,就把 URL 换成cu118。查看本机 CUDA 版本用nvidia-smi右上角,不需要nvcc -V,前者看的是驱动支持的版本,后者看的是编译工具链版本,经常不一致。
如果机器没有独立显卡,也不是不能跑。可以装llama-cpp-python加载 GGUF 格式的量化权重,用 CPU 跑小一点(7B 以下量化为 Q4)的模型,回答速度慢但能演示。如果交付包只有原始 FP16 权重且机器没 GPU,就别硬闯,去找同事要一个量化过的 GGUF 版本,或者把包导到有 GPU 的机器上。这一步的常见误区是上来就pip install transformers最新版,结果和交付方用的旧版 API 不兼容,导致model.generate的参数名变了,后面再排查半天。
3.2 用 transformers 加载模型并跑一次财报问答
环境就绪后,先用最小代码跑通一次推理,不要先调参数:
import torch from transformers import AutoTokenizer, AutoModelForCausalLM model_dir = "./finance_llm" tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_dir, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True, ) question = "根据资产负债表,2024年末这家公司的资产负债率是多少?" inputs = tokenizer(question, return_tensors="pt").to(model.device) outputs = model.generate( inputs["input_ids"], max_new_tokens=512, do_sample=False, temperature=0.1, ) answer = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) print(answer)这段代码的要点有三个。第一,trust_remote_code=True只有在交付包自带自定义模型类时才需要加,比如某些基于开源底座做微调的权重会引用自定义 module,不加会报错;如果有安全顾虑,先打开压缩包里的 modeling 文件看一眼,确认没有可疑代码再加。第二,torch_dtype=torch.float16能把显存占用砍一半,一个 7B 权重 FP16 大约占 14GB,没有device_map="auto"的情况下容易直接 OOM。第三,do_sample=False对财报这种事实型任务很重要,采样开启会引入随机性,数值型问题不需要随机;输出解码时用inputs["input_ids"].shape[1]:跳过输入部分,只打印生成的 token,否则会把整个问题再打印一遍。
如果加载中途报错,先看报错里有没有safetensors_metadata、key not found、unexpected key这些词。有这类词基本就是权重和 config 不匹配,具体细节在第5章讲。
3.3 prompt 模板:财报问答和通用对话完全是两套写法
跑通之后,最影响回答质量的是 prompt 模板。财报问答和通用闲聊的 prompt 写法完全是两件事。通用对话可能只需要"请回答下面的问题",财报问答必须把来源片段、问题、输出格式显式地放在一起,否则模型会自由发挥。推荐一个我在财报场景反复调过的模板:
def build_prompt(question: str, report_text: str) -> str: return f"""你是一名财报分析师。 请严格依据下面提供的财报原文回答,不要编造数字。 财报原文: {report_text[:2000]} 问题:{question} 回答要求: 1. 给出结论并引用原文中的行号; 2. 如果原文没有足够信息,回答"原文未提及",不要自行推测。 回答:"""这里有两个参数值得注意。report_text[:2000]是上下文截断长度,不是越大越好。财报表格转成的文本会有大量数字 token,模型上下文窗口是有限的,塞进去 8000 字很可能把关键行挤出 attention 范围,反而答得更差。建议先 2000 字起步,做几组对比再放大。回答要求里的约束是给模型划边界,尤其"原文未提及"这条能明显减少幻觉。你如果观察过不约束的生成结果,会发现模型常常自信地补出一个看似合理的资产负债率,这在金融系统里是不能接受的。
除了模板,还要调整生成参数。max_new_tokens设 256 到 512 就够,财报问答的回答通常是一段结论加一个数字,不需要长文;temperature建议固定在 0 或 0.1 以下,数值事实型问题一旦加温,同一个问题会出现漂移。如果你在做的是要交付给业务方的系统,把温度设为 0,把不确定性转移给代码层去处理,而不是让模型随机发挥。温度调参是最容易陷入玄学的地方,我的底线是:凡是答案要被业务使用的,一律do_sample=False。
4. 让财报答案从「像样」到「能用」:RAG 与表格注入怎么做
4.1 纯 LLM 答财报为什么必翻车
很多新手跑通上面的推理链路后,马上把整份年报丢进去问,结果被一串看似准确实则编造的数字打击到。这不是模型不行,是任务本身就不该让纯 LLM 直接干。财报是结构化表格加数字的组合:资产负债表、利润表、现金流量表。模型在预训练阶段见到的是互联网文本为主,对"数字的精确值"天生不敏感,它擅长的是语义理解,不是算术。
三个具体的翻车场景。第一,上下文超窗。一份年报动辄几万字,模型窗口装不下,向量检索顶多帮你找回一小段,纯靠模型去"记住"全局只会把长尾数字遗忘。第二,数字幻觉。模型倾向于补全真实感强的数字,比如问"货币资金是多少"它能答出"人民币 10.8 亿元"这种貌似合理的数字,但原文根本没有。第三,单位换算和口径。同一家公司财报里"万元"和"千元"混用,模型不会主动统一口径。要解决这些问题,正确方向不是加大模型,而是把"答案从哪里来"这件事从模型手里接管过来。
4.2 表格注入:把资产负债表转成模型能读的文本
把 Excel / CSV 里的财报数据转成带行号的线性文本,让模型在原文基础上回答。这一步不依赖向量库,也不依赖你真有 GPU 做 embedding,一个 pandas 脚本就够:
import pandas as pd def table_to_context(path: str, max_rows: int = 80) -> str: # 读取资产负债表 sheet,按行转成带编号的文本 df = pd.read_excel(path, sheet_name="资产负债表", header=0) lines = [] for idx, row in df.iterrows(): if idx >= max_rows: break item = str(row.iloc[0]).strip() period = str(row.iloc[1]).strip() value = row.iloc[2] lines.append(f"第{idx+1}行: {item} | {period} | {value}") return "\n".join(lines) context = table_to_context("balance_sheet.xlsx") print(context[:500])这段代码的核心是给每一行编号。把"第3行: 货币资金 | 2024-12-31 | 1.08e9"放进去,模型回答时引用"第3行"而不是自己造出处。max_rows=80是控制 token 量的边界,80 行结构化文本大约消耗 1500 token,加上问题和回答要求,刚好留出生成空间。如果你手里的财报有多个会计周期,建议把同一科目不同列拼接成一行,比如"应收账款 | 2023: 9.2亿 | 2024: 10.8亿",这样问同比变化时模型能看到两个年份的数字,不需要跨行查找。
而1.08e9这样的科学计数法会让模型懵掉。最好在清洗阶段就把单位统一成"亿"或"万元"并保留原值:
def fmt_value(v): # 统一金额单位,避免模型在“元/万元/亿元”之间来回切换 if v is None: return "N/A" try: v = float(v) if abs(v) >= 1e8: return f"{v/1e8:.2f}亿" return f"{v/1e4:.2f}万" except ValueError: return str(v)这段函数看似简单,却是后面少踩坑的关键。把"亿元/万元/元"统一成同一个单位,模型在一次回答里不用切换口径,吞吐量和准确率都更稳。我一般会把这个函数作为清洗管线的一部分,在做 RAG 之前就把它处理干净,不放进 prompt 里让模型自己判断。
4.3 数值计算类问题:让模型写代码而不是心算
问"毛利润同比增长了多少"这类问题时,如果让模型直接算,它大概率会在百分之五点几和百分之六点几之间来回飘。因此我采用"能动手算就动手算"策略:让模型输出可执行的 Python 表达式,再用系统执行计算,最后把计算结果放回答案。代码:
import math, re prompt = """你是财报计算器。根据提供的表格行,写一段Python表达式计算「毛利同比增长率」。 只输出表达式,不要解释。 表格: 第1行: 营业收入 | 2023 | 100.0亿 第2行: 营业收入 | 2024 | 120.5亿 """ # 模型返回类似: (120.5 - 100.0) / 100.0 * 100 expr = run_llm(prompt) # 只保留表达式字符,防止注入额外代码 expr = re.sub(r"[^0-9+\-*/().% ]", "", expr) result = eval(expr, {"__builtins__": {}}, {"math": math}) print(f"同比增速: {result:.2f}%")这里eval带了只读环境,{"__builtins__": {}}防止表达式里执行系统命令。虽然生产环境更建议用ast.literal_eval或者沙箱容器执行,但小规模落地时这个内置空环境加正则清洗已经能挡住大多数攻击。核心思路是:模型负责"理解问题并组织表达式",计算交给eval,两边都做自己擅长的事。这样输出结果由表达式和原始值决定,不管模型自己的"心算"能力有多不稳定。
注意:
eval只适合小规模内部工具,生产环境请用沙箱或独立计算服务。
4.4 召回优先用关键词,不急着上向量检索
提起 RAG 时有的团队会条件反射式地接 embedding 服务。但财报问答的文档结构非常规律,问题里通常带着"货币资金""应收账款""资产负债率"这些标准科目名,关键词命中比语义召回更可靠,也更便宜。我一般第一版做一个简单的倒排索引:把财报文件按章节/表格行切成 chunk,记录每个 chunk 里出现的科目词,用户提问时先按科目名做检索,命中的 chunk 直接拼进上下文。
如果问题问得比较口语化,比如"这家公司手头紧不紧",关键词召回的效果就会变差。此时再考虑向量召回:把每个 chunk 和用户问题都用 embedding 模型编码,取 topK,再把结果和关键词结果做融合。不要一上来就上双路召回,因为成本和复杂度都高,而经典财报问题里七成以上是标准指标问答。先用关键词 + 表格注入跑通,把效果评估数据摆出来,再判断要不要加向量。
5. 避坑与排查:金融财报问答大模型落地的 5 个常见翻车点
5.1 现象:加载报错 KeyError / 张量形状不匹配
现象是from_pretrained时抛KeyError: 'model.layers.0...'或size mismatch for ...。原因一般是压缩包里的权重和config.json不是同一套模型,或者交付方用 LoRA 微调后只打包了 adapter 权重,而加载代码没用 PEFT。解决办法分两步:先打开adapter_config.json看看有没有base_model_name_or_path字段,有的话就是 LoRA,要用peft.PeftModel.from_pretrained挂载;没有 adapter 文件,再看张量名是否是完整模型的 key。排查命令:
# 数一下权重里的张量数量,几十个是LoRA,几百个是完整底座 python -c "from safetensors import safe_open; f=safe_open('model.safetensors', framework='pt', device='cpu'); print(len(f.keys()))"如果打印出来的张量数是几十个而不是数百个,基本可以确定是 LoRA,按 adapter 方式加载。这个坑的本质是压缩包交付的人没写清楚权重类型,README 里也经常漏写。所以我盘目录时第一件事就是数张量数。
5.2 现象:回答里的数字一本正经地错
现象是模型给出"资产负债率 62.3%"这种看起来非常可信的答案,但原文计算出来是 58.9%。原因是生成模型本质是在做 token 概率预测,对精确数字的"记忆"不可靠,尤其遇到上下文里同时出现多个年份的数字时,模型很容易串。解决办法是把数值计算从模型里剥离,凡是能通过表达式计算的指标,都让模型输出表达式、系统执行计算,原始值必须来自表格注入后的结构化文本。如果必须靠模型直接抽取,那就要求它严格引用"第几行"并提供原文片段,把人类复核的路径留出来。
5.3 现象:长上下文导致 OOM 或推理速度骤降
现象是输入整份年报后,显卡显存直接占满,或者生成一个字要好几秒。原因是注意力机制的显存占用随序列长度平方级上涨,max_position_embeddings允许的长度和显卡能承载的长度完全是两回事。解决办法是不要整篇喂年报,按第4章的方式把表格转成结构化行,截断到max_rows=80左右;同时在generate时设置use_cache=True避免重复计算。如果你的场景确实需要超长上下文,再考虑引入 RAG 按需召回章节,或者换用支持更长上下文的底座。别在长上下文这个点上硬扛,最后钱花在显存上效果还不好。
5.4 现象:输出乱码、英文混排或出现特殊 token
现象是回答里混着<s>、</s>或者中英文夹杂。原因多数是 tokenizer 和模型权重不配套,比如压缩包里的tokenizer.json来自一个旧底座,而模型是另一个底座的微调版;也可能是decode时没加skip_special_tokens=True。排查先从tokenizer_config.json看tokenizer_class和model_max_length,如果是Qwen2Tokenizer却加载底座是 Llama,那必须先对齐。解决:把压缩包里的 tokenizer 文件全部替换成底座配套文件,然后重新验证一个样例问题,看特殊 token 是否还漏出来。
5.5 现象:同一问题换一种问法,答案差别很大
现象是"这家公司 2024 年毛利率是多少"和"请问该公司在报告期末的销售毛利率水平"得到两个数值。原因是 prompt 对上下文位置很敏感:截断位置不同、命中的表格行不同、模型对同义词的组织方式不同,都会导致答案漂移。解决办法:把 prompt 模板固定成第3.3节的结构化模板,temperature=0保证输出确定;抽取表格行时别用 top 截断,优先按科目关键词命中;最后对每个问题跑三次取一致的答案作为最终结果,必要时用另一个模型做 LLM as judge 校验两个答案的标准是否对齐。这个"多跑几次取稳定输出"的习惯,比调任何超参数都有效。
6. 进阶技巧:用系统计算校验 LLM 的数字,把置信度当成兜底护栏
到了这一步,模型已经能稳定生成财报问答结果。但金融场景的底线不是"大部分时候对",而是"错的时候能被发现"。我一般会加一层校验:模型回答里凡是出现数值,先让它输出对应的原始行号,系统再用 pandas 从原表重新计算一遍。两边不一致就直接拒绝输出,而不是把错误数字交出去。
# 解析模型答案里的行号,然后用原始表格重算,不一致就拦截 answer = "资产负债率为58.9%(第23行)" row_no = extract_line(answer) # 23 actual = df.loc[row_no - 1, "rate"] if abs(float(answer_numeric) - actual) > 0.001: raise ValueError("模型答案与原始财报不一致,拦截")这层护栏的代价不高,却能把模型幻觉从"交出去的业务风险"变成"系统内可拦截的异常"。另外我习惯在返回接口里同时附上model_answer、calc_answer、source_line、confidence四个字段,业务方拿到结果后能自己判断要不要信。如果团队已经有二次校验的流程,可以再让一个轻量模型做 LLM as judge,对同一问题的多轮生成结果评分,过滤掉低置信度答案。
做这一类金融财报问答项目,我的一个教训是:永远不要假设模型会给你精确数字,而是把计算、来源、置信度三件事全部落到系统层。模型负责理解语言,系统负责验证数字。早期我一度把准确率压到 95% 后就交付了,结果业务方反馈的问题全是那 5% 的错误数字造成的,后来加上这层护栏,人力复核的负担才真正降下来。把这个习惯带到你后面的每个 LLM 落地项目里,财报问答才能从"像样"变成"敢上线"。希望帮到你。
本文还有配套的精品资源,点击获取