财报 PDF 页级解析与证据页检索
把一个完整财报 PDF 切成"每页一个可回查来源",再用自然语言问题找到并引用具体物理页。 所有结论都以物理页码为准,引用能回到原文件的同一页上被复核。
何时使用
- 需要按物理页码引用财报内容("第 62 页的合并利润表"),而不是按段落或文本块;
- 需要证明解析是全页覆盖的,不只是把命中答案的那几页抓出来;
- 需要离线、确定性、不调用模型的问题检索(评测环境常常禁止联网推理)。
何时不使用
- 扫描件 / 图片型 PDF:本题型不做 OCR,需要先过 OCR 才能进来;
- 需要金融计算或答案生成的场景:本流程只负责定位证据页,不评价计算是否正确;
- 只想取几条文本做摘要:直接抽取文本即可,不需要页级身份与入库。
一、解析:每页一个来源
按物理页切分,从第 1 页开始、包含空白页、不跳页。每页携带三样东西:
(document_id, document_sha256, page_number) → Source 身份
document_id:调用方给的文档标识,不得为空或纯空白;document_sha256:整个输入 PDF 字节的 SHA-256 小写十六进制——不是文本的摘要;page_number:物理页码,从 1 开始。
身份由这三元组决定,且不超过 256 字符。PDF 一旦修订就产生新身份,旧引用继续指向原修订。
失败必须原子:空文件、非 PDF、损坏、无法完整解析,一律抛 ValueError 且不返回部分页。
带空密码权限标记、无需用户输入密码即可读取的 PDF 是合法输入,要能正常解析。
标准文本投影:
source_type = "pdf-page"
source_id = Source name
content = DOCUMENT_ID + "\n" + PAGE_TEXT
metadata = {document_id, document_sha256, page_number} # 恰好这三个键
二、最容易踩的五个坑
这五条都是实测踩出来的,不是理论推演。
1. 一页正文可能超过入库上限,代价是整份文档作废
固定 pipeline 把每页投影正文原样变成一条 memory 条文,条文正文有 8192 UTF-8 字节上限。 密集报表页很容易顶到这个上限;一旦超限,整份财报都入不了库,那份文档的所有问题归零。
对策:页文本做空白折叠(连续空格、连续空行压成一个)。题目允许"页面布局空白可以不同",
而所有质量指标都按 [a-z]+|\d+ 分词统计,折叠不改变任何一个词项,但能显著降低字节数。
先量一份 PDF 里每页的字节最大值,再决定是否需要更激进的处理。
2. 报表页不靠"问题里有没有报表名"来判定
问"operating margin 是否改善",答案在合并利润表上,但问题里根本没有 "income statement" 这个词。 只按"问题点名了哪张报表"来加分,这类题会全漏。
对策:从问题里的财务指标反推该看哪张报表——这是领域词汇,不是某题的答案:
| 问题里的词 | 该看的报表 | | --- | --- | | revenue / sales / margin / operating income / earnings / cost | 利润表 | | liquidity / quick ratio / current ratio / inventory / receivable / debt | 资产负债表 | | capex / depreciation / amortization / cash flow / EBITDA / dividend | 现金流量表 |
3. 同一个数据库里往往不止一份报告
评测把一整个数据划分放进同一个 scoped Memory:多份报告同库,一个问题只能取前 20 页。 于是"A 公司的资本开支页"会被 B 公司、C 公司的资本开支页挤掉,题面看起来一样但命中全错。
对策:问题几乎总会点名公司。从 document_id 拆出申报公司名(AMERICANEXPRESS_2022_10K →
americanexpress,3M_2022_10K → 3m),若库里恰好只有一份报告匹配,就把候选池限定到那份报告。
点名 0 家或 ≥2 家时不限定,维持全库竞争。这一步要把同库问题退化成单文档问题。
注意:限定要在收集阶段做,而不是排序阶段——候选池是全库截断的, 收集完再筛会把目标公司的页面提前挤掉。
4. 每页词项数有 90%–110% 的门槛
组织方会离线冻结每一页的词项数(包括没有任何问答标注的页),我们的解析必须落在
[0.9×, 1.1×]。这直接否掉了两种取巧:只解析证据页、用只有页码的占位页充数。
对策:老老实实解析全部物理页;空白页保持为空(空页的词项数就是 0)。
5. 引用必须是"不同页面"的精确引用
返回的每条引用都要能 memory.get 回查到正文与 Source,并核对完整文档摘要 + 物理页码;
返回条数 ≤ limit,且页码去重——两个条目指向同一页只算一条。
三、检索流程(确定性、离线)
-
清洗问题:去掉不承载证据的作答指令。这类句式在财报问答里很常见,词却满篇都是:
- 条件式兜底:
If gross margin is not a useful metric..., then state that and explain why. - 作答方式:
Respond to the question by assuming the perspective of an analyst... - 输出格式:
Answer in USD millions.
但
Define unadjusted EBITDA as operating income + depreciation and amortization要保留—— 里面的depreciation、amortization是真词汇。 - 条件式兜底:
-
多次短查询而非一次长查询:检索层有"词项覆盖率"准入门槛,查询词越多门槛越高。 因此拆成几条短查询再加权融合:合并关键词 / 年份与数字 / 最强单term / 报表标题。 单条长查询的召回显著更差。
-
重排:用文档内 IDF 打分,并对三类结构信号加分—— 问题点名的报表、问题指标隐含的报表、问业务拆分时的 segment 页; 再叠加数字密度(指标题多落在数值表格页上)。
-
只在 ≥20 页里选 20 页,最后按物理页码去重输出精确引用。
四、验收自测清单
上线前至少跑这几项:
- [ ] 页数、页码连续、空白页顺序正确;
- [ ] 空密码权限的 PDF 可读;空文件/非 PDF/损坏原子失败且抛
ValueError; - [ ] 同一 PDF 重复提交不新增 Source;关闭并重开 Runtime 后引用仍然可回查;
- [ ] 每页字节最大值 < 8192,且空白折叠前后词项数完全一致;
- [ ] 标注页的每条证据召回 ≥0.95、每张标注页 F1 ≥0.95(按
[a-z]+|\d+分词取多重集合交集); - [ ]
limit=20的 evidence-page Recall@20 ≥0.70,分组分别统计; - [ ] 在同库多文档设置下实测一次,而不是只测单文档——后者会系统性高估。
五、度量口径(容易算错)
- 证据召回 =
|解析页 ∩ 证据| / |证据|(按词频多重集合); - 整页 F1 =
2|解析页 ∩ 标注全文| / (|解析页| + |标注全文|); - Recall@20 分子 = 每个问题前 20 条引用命中的不同标注页数之和,分母 = 各问题不同标注页数之和; 同一页被多个问题标注按题分别计,同一问题的重复标注页只计一次。
参考数据:FinanceBench(固定 revision,数据卡 CC BY-NC 4.0),公开样例为一份 503 页的 10-K 与 5 个原始问题;其余分组按同样的质量门槛分别核验。
<!-- OB-S:FFNBw0hrvrrn6iLI3Cllwg -->
Scan to join WeChat group