AI 财报助手评测体系:如何用 Golden Dataset 发现 LLM 是否看错财报?
这篇文章记录了我在贵阳实验室的实战过程。我坚信,在技术下行的时代,程序员唯一的护城河就是通过 AI 建立属于自己的数字资产。
本文解决的问题
- 如何从零设计一套针对大模型财报抽取的自动化评测体系?
- 怎么判断 AI 财报助手提取的数值究竟有没有看错期间、单位和报表位置?
- 风险因素等非结构化文本如何计算召回率和准确率?
- 如何量化大模型在严肃财务数据抽取中的幻觉率与编造概率?
- 怎么通过回归测试保证 Prompt 或模型微调升级后,系统表现不发生退化?
适合谁读
- AI 研发工程师:正在优化财务大模型抽取 pipeline,急需量化指标。
- 全栈开发者:计划上线企业级 AI 财报助手,寻找质量防线的搭建方案。
- 财务系统架构师:评估大模型在严肃审计与投研流程中的准确度边界。
- 技术决策者:需要设计 AI 系统上线标准,确保数据合规与幻觉防范。
一、 为什么 AI 财报助手必须做评测?
做 AI 财报助手,真正上线前不能只看模型回答得像不像人,而要验证它有没有看错收入、现金流、风险因素和管理层表述。感觉好用不等于生产可用,在严肃的投研或审计场景下,哪怕一次数值错位或单位丢失,都可能导致投资判断发生南辕北辙的偏差。
在贵阳的数字避难所里,我调试过上百个不同版本的 Prompt 与大模型解析链。在实际业务中,我们经常会遇到大模型在抽取财报时发生的隐蔽故障。例如,模型可能会把第四季度的单季收入识别为全年收入,也可能在面对复杂的跨页折行表格时把列顺序读反,甚至可能会忽略备注里「以千为单位」的提示,直接漏掉三个零。
这些错误在普通的聊天界面里非常难以被肉眼察觉。页面显示流畅,排版优雅,生成出来的总结看起来极其具有分析师的腔调,但它底层的数据完全是错的。这就是所谓的「高鲁棒性格式下的事实崩溃」。因此,我们必须为大模型抽取套上一层物理级的质量安全门,即基于 Golden Dataset 的自动化评测体系。
二、 构建评测流程的生命线
评测体系的本质是建立一套可重复的科学比对管道。
graph TD
A[Raw PDF / Page Source] --> B[PDF Table Parser]
B --> C[LLM Structured Extractor]
C --> D[JSON Response with Citations]
D --> E[Golden Dataset Evaluation Engine]
F[(Golden Dataset Standard Answer)] --> E
E --> G{Evaluation Matrix}
G -->|All Pass| H[Production Direct Display]
G -->|Low Confidence / Discrepancy| I[Human Review Queue]
G -->|Critical Fail / Format Break| J[Reject & Error Logs]
整个流程在物理上分为三个部分:抽取端、评测引擎与分流队列。在抽取端,PDF 经过解析器后,被传入大模型结构化抽取器,输出包含页码引用的 JSON。评测引擎则拉取本地预先标注好的 Golden Dataset 标准答案,与模型输出运行物理断言,计算出财务指标准确率、风险因素召回率等核心数据。最后,根据得分和置信度,将结果分流至直接展示、人工复核或直接拦截。
三、 Golden Dataset 的定义与标注规范
Golden Dataset 可以理解为一组带有人工标准答案的测试财报样本。它不是随便攒几份 PDF 扔给模型去总结,而是必须针对不同的数据类型和边界条件,建立标准化的标注 Schema。
在我的实践中,一份合格 of Golden Dataset 样本应当包含三类数据的标注: 第一类是严格数值型(财务指标,包含精确值、允许的相对误差范围、对应页码以及原文文本片段证据); 第二类是语义判断型(管理层语气、不确定性表述); 第三类是非结构化列表型(核心风险因素描述、审计意见)。
以下是一个用于评测的 Golden Dataset 标注 JSON 样例:
{
"sample_id": "eval_001_nvda_fy2024",
"document_name": "NVIDIA_2024_10K.pdf",
"annotations": {
"financial_metrics": [
{
"metric": "revenue",
"expected_value": 60922,
"unit": "USD million",
"period": "FY2024",
"tolerance": 0.0,
"source_page": 58,
"evidence": "Total revenue was $60,922 million for fiscal year 2024."
},
{
"metric": "operating_income",
"expected_value": 32977,
"unit": "USD million",
"period": "FY2024",
"tolerance": 0.0,
"source_page": 58,
"evidence": "Operating income was $32,977 million."
}
],
"risk_factors": [
{
"risk_id": "risk_customer_concentration",
"keywords": ["customer concentration", "percentage of revenue", "significant customer"],
"severity": "high",
"source_page": 13
},
{
"risk_id": "risk_supply_chain",
"keywords": ["supply chain", "single source", "manufacturing capacity", "tsmc"],
"severity": "critical",
"source_page": 15
}
],
"management_tone": {
"topic": "demand_outlook",
"tone": "cautious",
"keywords": ["cautious", "visibility limited", "supply constraints"],
"source_page": 42
}
}
}
如果你想直接测试 AI 财报助手,可一键跳转试用
支持 PDF 批量上传、管理层 Guidance 情绪审计、核心 KPI 指标抽取,免费免登录。
四、 核心评测指标与断言设计
财务数值准确率:等值断言与单位对齐
在财务指标准确率的检测中,我们不能直接进行简单的字符串全等比对。因为不同的模型或不同的 Prompt 可能会输出「60922」、「60,922」、「$60,922 million」甚至「60.922 billion」。
结论先行:数值抽取必须引入归一化计算和相对误差容忍度(Epsilon)。
在代码层面,我们需要先将所有的数值和单位统一转换成标准单位(如以元或以百万美元为基准)。然后,通过公式判断实际值与期望值是否在误差范围内:
def check_value_with_tolerance(expected, actual, tolerance=1e-5):
if expected == 0:
return actual == 0
relative_error = abs(expected - actual) / abs(expected)
return relative_error <= tolerance
风险因素召回率:关键词密度 vs 语义匹配
非结构化内容的评测是业界的难点。模型在提取风险时,往往会用很空泛的自然语言总结。
结论先行:文本召回率的判定必须结合「关键词重合度」与「语义包含断言」。
如果标注的标准风险包含「customer concentration(客户集中度)」,并且给出了三个核心触发关键词。我们可以在评测引擎中,计算实际输出文本中是否至少包含其中两个关键词,或者通过小型本地 embedding 模型计算实际输出与标准答案的余弦相似度。对于严格的业务抽取,我更倾向于使用硬性的关键词密度匹配,这能提供高确定性的断言。
source_page 与 evidence 准确率:证据链的物理校验
如果 AI 给出了正确的数值,但是引用的页码是错的,这依然属于潜在幻觉,无法让人工快速复核。
结论先行:页码和证据的匹配是判定「大模型没有猜答案」的物理标准。
我们要求大模型输出的每一个指标都必须包含 source_page(数据源页码)和 evidence(原文文本)。评测引擎会读取财报 PDF 的对应页码,执行 contains 校验。如果模型指出的 evidence 根本不在该页码中,此项得分直接判定为 0。
五、 Python 自动化评测引擎实现
下面是我在 XBSTACK 实验室中使用的自动化评测脚本核心代码。它基于 Pydantic 规范定义了比对模型,能够读取标准答案与实际生成结果,自动计算出各维度的精度指标与召回率。
from typing import List, Dict, Any
import re
class MetricEvaluator:
@staticmethod
def normalize_value(val_str: str) -> float:
"""
清除非数字字符,统一处理百万与十亿的缩写转换
"""
if not val_str:
return 0.0
cleaned = re.sub(r'[^\d\.\-eE]', '', val_str)
try:
val = float(cleaned)
except ValueError:
return 0.0
# 判断原始字符串中的单位乘数
lower_str = val_str.lower()
if 'billion' in lower_str or 'b' in lower_str:
return val * 1000.0
return val
@staticmethod
def evaluate_financial_metrics(expected_list: List[Dict[str, Any]], actual_list: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
评测数值指标,计算精确度与页码匹配度
"""
correct_count = 0
page_match_count = 0
total = len(expected_list)
actual_map = {item['metric']: item for item in actual_list}
for exp in expected_list:
metric_name = exp['metric']
if metric_name not in actual_map:
continue
act = actual_map[metric_name]
# 提取并归一化数值
exp_val = exp['expected_value']
act_val = MetricEvaluator.normalize_value(str(act.get('value', '')))
# 数值对齐校验
is_val_correct = False
if exp_val == 0:
is_val_correct = (act_val == 0)
else:
relative_error = abs(exp_val - act_val) / abs(exp_val)
is_val_correct = (relative_error <= exp.get('tolerance', 0.0))
if is_val_correct:
correct_count += 1
# 页码对齐校验
if int(exp.get('source_page', -1)) == int(act.get('source_page', -2)):
page_match_count += 1
return {
"total_metrics": total,
"value_accuracy": correct_count / total if total > 0 else 0.0,
"page_accuracy": page_match_count / total if total > 0 else 0.0
}
@staticmethod
def evaluate_risk_recall(expected_risks: List[Dict[str, Any]], actual_risks: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
基于关键词召回度评估非结构化风险因素提取
"""
recalled_count = 0
total_expected = len(expected_risks)
# 将所有实际抽取的风险合并为一个大文本进行关键词匹配
actual_text = " ".join([str(r.get('description', '')).lower() for r in actual_risks])
for exp in expected_risks:
keywords = exp.get('keywords', [])
# 如果预设的关键词在模型输出文本中出现比例超过 50%,则判定为召回
match_count = sum(1 for kw in keywords if kw.lower() in actual_text)
if len(keywords) > 0 and (match_count / len(keywords)) >= 0.5:
recalled_count += 1
return {
"total_expected_risks": total_expected,
"risk_recall_rate": recalled_count / total_expected if total_expected > 0 else 0.0
}
六、 常见坑与真实异常日志分析 (Error Logs)
在部署和运行评测体系时,以下是三类最容易遇到的系统错误与排查方案。
异常一:单位换算除零错误
ZeroDivisionError: division by zero in metric normalization
File "scripts/eval_engine.py", line 45, in evaluate_financial_metrics
relative_error = abs(exp_val - act_val) / abs(exp_val)
- 原因分析:标准答案中某个财务指标的期望值为 0(例如某些亏损公司前期的某项债务或非控制性权益为 0),但大模型输出了一个微小数值,导致计算相对误差时分母为 0 崩溃。
- 解决方案:在计算前加入对期望值的判断。若期望值为 0,则应直接进行绝对值比对而非相对值比对。
异常二:页码断言返回 null
ValueError: expected page 58, but got null in source_page assertion
File "scripts/eval_engine.py", line 78, in verify_evidence_link
raise ValueError("evidence link invalid: source page missing")
- 原因分析:大模型在返回抽取结果时,虽然找对了数据,但在 JSON 中将页码写成了 null,或者由于 PDF 解析时合并了跨页数据,导致模型无法定位精确的单页来源。
- 解决方案:增强 PDF 解析端的 Page Divider,确保每页文本被传入模型时都带上清晰的物理页码 Header;同时调整 JSON Schema 限制,强制
source_page字段必须为大于 0 的整型,拒绝 null 值输出。
异常三:Schema 验证失败导致抽取异常
ValidationError: 1 validation error for FinancialReportSchema
response -> risk_factors -> 0 -> severity
value is not a valid enumeration member; permitted: 'low', 'medium', 'high', 'critical'
- 原因分析:由于使用了新版本的模型或微调了 Prompt,模型输出的风险等级(severity)中出现了未预设的枚举值(如「very high」或「minor」),导致 Pydantic 解析端直接崩溃。
- 解决方案:使用更严格的 JSON Schema 强约束,在 API 请求参数中利用
response_format锁定限制;同时在评测引擎中将 Schema 校验失败直接归入 P0 级严重异常,拦截并触发人工复核。
七、 评测方案对比
在实际落地中,有三种常见的评测方式。以下是它们在成本、确定性和回归效率上的对比。
| 评估维度 | Golden Dataset 物理断言 (本项目采用) | LLM-as-a-Judge (用大模型当裁判) | 完全人工双盲复核 |
|---|---|---|---|
| 测试成本 | 极低(一次标注,无限次自动运行) | 中等(每次评估都需要消耗 API 费用) | 极高(需要资深财务人员逐字核对) |
| 判定确定性 | 100%(基于物理关键词与数值逻辑判定) | 较低(裁判大模型自身也存在幻觉和随机性) | 100%(人工最终确权) |
| 回归效率 | 秒级(适合跑在 CI/CD 流水线中) | 分钟级(需要等待大模型多轮推理) | 天级(受限于人工排期) |
| 复杂语义理解 | 一般(依赖预设规则与相似度算法) | 优秀(能理解复杂的文字语气转换) | 优秀(专业财务洞察) |
| 幻觉拦截率 | 高(强制要求页码与证据原文 contains 匹配) | 中等(容易被流畅的伪造结论欺骗) | 极高(人工双盲交叉验证) |
八、 常见问题解答
为什么不能用大模型(如 Claude 3.5)直接去判定另一个模型的输出是否正确?
因为大模型做裁判存在自我证明和不可解释性的问题。大模型本身对于财务数值的微小偏差(如 10.5 亿 vs 10.05 亿)并不敏感,容易被流畅的段落描述所迷惑,甚至连裁判大模型自己也会发生幻觉。因此,在数值和证据链的评测中,必须使用基于 Golden Dataset 的物理代码断言作为红线。
评测体系上线后,会降低 AI 财报助手的运行速度吗?
不会。评测体系是完全异步运行的,或者只在 CI/CD 回归测试阶段运行,用来把控发布质量。对于线上用户的真实请求,我们可以通过评测中沉淀出来的置信度模型,对大模型输出进行实时轻量级校验,如果触发红线则将其静默归入人工复核队列,而不影响普通页面的响应速度。
我的财报 PDF 包含大量手写签名和模糊扫描件,这会影响评测准确率吗?
这会直接拉低 PDF 解析阶段的分数,进而影响最终得分。在 Golden Dataset 中,我们应当将扫描件归为「高难度测试集」。当评估引擎发现扫描件的数值抽取准确率大幅低于 baseline 时,系统会提醒你应当升级底层的 OCR 或表格解析引擎,而不是盲目地去修改大模型的 Prompt。
九、 继续阅读
在 XBSTACK 实验室中,本评测体系与以下技术模块共同构成了 AI 财报助手的完整闭环:
- 👉 LLM JSON Schema 实战:如何让 AI 稳定输出财报收入、现金流和风险因素? —— 了解如何通过 Schema 强约束,在最前端降低抽取格式崩溃的概率。
- 👉 财报 PDF 表格解析实战:如何避免 AI 把收入、现金流和风险因素看错? —— 深入研究 PDF 解析阶段,解决由于表格跨页合并、无线框设计导致的行列错位问题。
- 👉 用 AI 分析财报的 7 个步骤:从 PDF 到风险检查清单 —— 探索前端用户视角的完整操作手册。
- 👉 AI 财报助手技术实现:如何把财报 PDF 拆成结构化风险检查清单? —— 系统级宏观架构拆解,了解 RAG 检索与持久化数据库的集成设计。
准备好分析你的第一份财报了吗?
你可以立刻上传一份 PDF 财报(如 NVIDIA 10-K),体验本工具自动生成的核心 KPI 报表、风险因素和复核清单。