← 返回 Skill 列表
extension
分类: 开发与工程API Key 暂未确认

财报 PDF 证据页检索

从 10-K / 10-Q / 年报等财报 PDF 中按问题定位能直接支撑答案的证据页,输出页码与命中理由。覆盖入库字节上限断言、报表标题变体、跨报表取证与 ticker 识别,附审计脚本。当需要为财务问题找原始报表页、做证据页召回评估或排查 PDF 检索召回为 0 时使用。

person作者: yulei6hubOpenAPI

财报 PDF 证据页检索

这个 Skill 解决什么

财报问答的失败,绝大多数不是「答错了」,而是证据页没被召回。一旦原始报表页不在候选集里,下游再强的模型也只能编。

本 Skill 把「问题 → 证据页」这一步做扎实:给定一份或多份财报 PDF 和一批问题,输出每道题最可能承载答案的页码,并给出为什么选它。

适用:10-K / 10-Q / 年报 / 业绩公告的分页检索;证据页召回评估;PDF 检索管道排障。

不适用:需要精确数值计算的估值建模;非财报类 PDF(合同、论文)—— 报表结构先验不成立。


证据页的定义

一道题的证据页,是答案数字或事实直接印在版面上的那一页。

三个容易判错的地方:

  1. 答案页 ≠ 包含关键词最多的页。MD&A 里会反复提到「毛利率下降 3.2 个百分点」,但真正的证据页是利润表那一页。
  2. 比率题通常横跨 2–3 张主报表。毛利率要利润表两行;资产周转率要利润表收入 + 资产负债表总资产;资本密集度要资产负债表 PP&E + 总资产。只召回一张表会漏。
  3. 脚注页也算证据页。债务到期结构、分部拆分经常只出现在附注里。

工作流

第 1 步:确认解析质量,再谈检索

在调排序之前,先证伪「解析层坏了」。用一段原文做探针:

  • 取 20 个问题,每个问题列出 3–5 个必须出现的证据词项(数字原样、科目名原样)
  • 统计这些词项在解析后文本里的命中比例
  • 阈值:证据词项召回 ≥ 0.95、整页文本 F1 ≥ 0.95

不达标先修解析(表格错位、双栏串行、页眉页脚污染),不要往下调排序。解析错了,排序再好也无意义。本 Skill 附带 scripts/audit_page_budget.py 可同时检查解析后的页长分布。

第 2 步:确定入库的硬约束(最容易整题归零的一步)

分页检索系统普遍有单条记录长度上限,且批量入库是原子的。真实案例:上限 8192 UTF-8 字节,一份 10-K 里 4 页超标,导致整篇文档一页都没进库,表现为「召回率 0」,而日志里根本看不到逐页失败信息。

必须做的三件事:

  1. 按字节而不是字符裁剪。中文、特殊符号、连字符都会让字节数远超字符数。用 len(text.encode("utf-8")) 判断。
  2. 扣除前置前缀的预算。若入库时拼了 document_id + "\n" + text,可用预算 = 上限 - len(document_id.encode()) - 1。
  3. 裁到词边界,不要硬切。在 budget 附近回退到最近的空格/换行,避免产生半截单词污染检索。

参考实现见 references/retrieval-recipes.md 的 fit_entry_text()。

验证方式:把语料全部过一遍裁剪函数,统计被裁剪的页数与裁剪后词项留存比例(应 ≥ 0.95)。不要靠"应该没问题"。

第 3 步:设计排序信号

按「先词法、后语义」的顺序加权,实测有效的权重:

| 信号 | 权重 | 说明 | |---|---|---| | 精确短语命中 phrase_hits | 0.35 | 由问题自动生成的候选短语,见第 4 步。排序的主导信号 | | 数字/金额共现 | 0.20 | 问题含数字或百分比时,同页出现相近数值加分 | | 科目名词项覆盖 | 0.20 | 从问题抽出的会计科目,按 IDF 加权覆盖 | | 报表标题邻近 | 0.15 | 页内出现报表标题且与问题类型匹配 | | 语义/稠密检索融合 | 0.05 | 只作补充,不要给它高权重 | | 文档作用域命中 | 0.05 | 问题点名了某公司/ticker,同文档页优先 |

关键教训:新增的查询扩写如果只接进稠密检索通道(权重 0.05),对最终排序几乎无影响。要么接进 phrase_hits,要么别加。

第 4 步:从问题生成短语(决定成败的一步)

按问题里的指标线索反查应该出现的短语:

  • 问题含 ratio / margin / turnover / capital intensive / EBITDA / capex / liquidity / return on / leverage / intensity / per share / free cash flow / working capital 之一时,追加全部报表标题短语(见下表)
  • 问题含主题词(债务、存货、诉讼、并购、分部)时,追加该主题的专属短语:
    • 债务 → long-term debt、debt maturities、short-term borrowings
    • 存货 → inventories、cost of goods sold、cost of sales
    • 诉讼 → legal proceedings、contingencies、litigation
    • 并购 → business combinations、acquisitions
    • 分部 → reportable segments、segment information

报表标题各家叫法不同,必须全收:

statements of operations / statement of operations
statements of income     / statement of income
statements of earnings   / statement of earnings
balance sheet
statement of cash flows

漏掉 statements of earnings 这类变体,会让一批公司的比率题系统性失分。

第 5 步:识别问题指向的文档

多文档场景下,先判断问题是否点名了某家公司,命中则把候选集收敛到该文档。

ticker 识别的四个边界情况(全部踩过):

| 情况 | 例子 | 处理 | |---|---|---| | ticker 是公司名的子序列 | AMEX ⊂ americanexpress | 用子序列匹配而非前缀匹配 | | 混合大小写 | JnJ | 缩略词正则放宽为「2–6 字符且含 ≥2 个大写」,同时要非全大写才能捞到 | | 数字开头 | 3M | 单独取文档名 _ 分隔的第一段做核,不要求首字符是字母 | | 文档名是下划线分隔 | VERIZON_2022_10K | 取第一段做核,并对全文做归一化(去空格、转小写) |

只有唯一命中才锁定。命中 2 个及以上时返回空集合,退化为全局检索 —— 强行锁定比不锁定更糟。


三个反直觉的坑

坑 1:FTS 不保证短语语义。 很多检索后端的 full-text search 会把带引号的短语拆成词袋 AND,停用词(and、of、the)直接丢弃。于是 "property plant and equipment" 和 "property and equipment" 命中集合完全相同。验证方法:拿两个只有停用词差异的短语各查一次,比对结果集;相同就说明是词袋语义,此时短语长度不构成区分度,需要靠权重和其他信号补。

坑 2:不要按文档轮流分配候选名额。 为了"公平"给每份文档分同样多的 top-K 名额,会让好页被本文件的差页挤掉。实测总召回从 0.767 崩到 0.500。正确做法:全局排序 + 文档作用域过滤。

坑 3:本地评估集和线上判题集可能不是同一批文件。 如果本地 N 页、线上 M 页且 N ≠ M,说明文档集假设错了。用页数、题数做反解,枚举可能的组合,然后让策略在所有候选组合上都达标,而不是只调好一套。


自检清单

提交前逐条过:

  • [ ] 解析质量门槛达标(证据词项召回 ≥ 0.95)
  • [ ] 全语料过一遍字节裁剪,无超限,词项留存 ≥ 0.95
  • [ ] 入库是逐页独立提交,还是批量原子提交?(原子提交必须保证零超限)
  • [ ] 报表标题变体是否全收(含 statements of earnings)
  • [ ] 比率题是否给了跨报表的短语(利润表 + 资产负债表)
  • [ ] ticker 四类边界是否覆盖(子序列 / 混合大小写 / 数字开头 / 下划线分隔)
  • [ ] 文档锁定是否只在唯一命中时生效
  • [ ] 评估是否覆盖所有候选文档集组合,而非单一组合
  • [ ] 每组召回是否独立达标(不是看平均值)——多数评测按组设门槛,一组挂掉整题 0 分

附带资源

  • references/retrieval-recipes.md —— 可直接抄的实现片段:字节裁剪、短语生成、ticker 匹配、报表标题表
  • scripts/audit_page_budget.py —— 扫描语料页长分布,找出会触发的超限页并给出裁剪后留存率
<!-- OB-S:LuF_-NMksUT-cVvv9bNt4w -->