Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Metadata 设计:比模型大小更重要的东西

两个翻车现场:数字对了,答案照样错

用户问“腾讯 2025 年上半年收入“,系统返回 184,504。数字没错——184,504 百万元,LLM 却读成“184,504 元“,差六个数量级。chunk 里有数字,没有 unit 字段——错不在模型,错在 metadata。

模型拿到裸数字只能猜:换算一下告诉你“约 1.84 亿“,听着合理,错得隐蔽。中文财报里“百萬元““億元”“千元“常混着出现;正文写“亿元”、表格写“百万元“时,模型两头都见过,会把 184,504(百万元)和 184.5(billion)混成一类,造出“184.5 亿“这种哪儿都不存在的伪冲突数字。

第二个现场更隐蔽。用户问“腾讯经营利润率的变化趋势“,系统从 2020、2022、2025 三份年报各召回一段,LLM 拼出一条看似合理的趋势。问题在于:2020 年那段是“非國際財務報告準則“口径,2022 和 2025 是“國際財務報告準則“口径。口径不同,数字不可比,趋势不成立——但生成端不知道,它只负责把话讲圆。

两个现场是同一个结构:语义相似度回答“像不像“,用户问的是“哪一年、哪个期间、什么单位、什么口径“——前者 embedding 管,后者只有 metadata 管。第 4 篇讲分片时说过,chunk 切好了还是“黑户“,这两个现场就是黑户的下场。

先分家:文档级和 chunk 级是两件事

设计 metadata 的第一步不是列字段,是分家。文档级一份 PDF 只标一次、所有 chunk 共享;chunk 级每个 chunk 单独一份,从它的 TextBlock 继承。

为什么不能混?查询模式不同。文档级用于入库打标签和查询预过滤——“先把腾讯 2025 中报这一组挑出来”;chunk 级用于召回排序和展示——“这段在哪页、什么口径”。混进同一份 schema,query 解析得先猜用户说哪一层,schema 演进也容易出错。

实际跑出来长这样(腾讯中报的 ChunkRecord,字段有删减):

{
    "chunk_id": "...",
    "source_file": "腾讯 -2025-中期报告.pdf",
    "company": "腾讯",
    "report_year": "2025",
    "report_type": "interim_report",
    "page_start": 4,
    "page_end": 5,
    "section": "財務表現摘要",
    "period_type": "quarterly",
    "unit": "人民幣百萬元",
    "text": "..."
}

前四个字段是文档级,随每个 chunk 冗余存一份,过滤时不用回查父文档;page_start 往下是 chunk 级,各管各的。

两层各放什么,一张表:

字段层级示例用途
stock_code文档级0700.HK多股票路由
report_type文档级中报 / 年报数据粒度
fiscal_year文档级2025时间过滤
market文档级HK / A 股 / US口径来源
currency文档级HKD / CNY数值归一
accounting_standard文档级HKFRS / IFRS / CAS / US GAAP口径差异根源
period_typechunk 级H1 / Q4 / FY时间精度
sectionchunk 级管理层讨论叙述 / 数据区分
table_typechunk 级利润表跨文件对齐
unitchunk 级million单位标定
is_auditedchunk 级true / false审计状态(有法律含义)

准则的枚举值按市场分,别图省事用一个词盖过去:A 股是 CAS(中国企业会计准则;业内常随口叫“CN GAAP“,入库枚举用正式缩写);港股是 HKFRS,和 IFRS 高度趋同但法律上是两套准则,腾讯这类公司则直接按 IFRS 编制;美股本土公司是 US GAAP,外国私人发行人可以直接用 IFRS 申报;Non-GAAP 是各市场都可能附加的调整口径。

period_type 是分错层的现成反例。同一份中报里,“截至 6 月 30 日止六個月“是 H1,“截至 12 月 31 日止年度“是 FY——不同章节对应不同期间。错标成文档级,整份 PDF 的 chunk 都继承同一个时间精度,错的那个。

字段清单:值得标的和可以缓的

标注有成本。每加一个字段,我只问一句:检索阶段会不会用到它?用不到的,标得再准也是库存。

按这个标准,我把字段排成三档:

  • T0,不做没法用:fiscal_year、report_type——没有它们,跨年混检无解;market、stock_code——多股票场景的路由前提。
  • T1,做了明显提升质量:unit(防住开头的数量级事故)、period_type(问“上半年“时不返回全年数据)、section。
  • T2,有余力再做:table_type(跨文件同表对比才用得上)、is_audited(合规场景有用,一般问答可不管)、accounting_standard(跨市场多准则才需要)。

section 比看起来有用。问题类型和章节有稳定对应:问风险,偏风险因素和附注;问管理层看法,偏主席报告和 MD&A;问精确数字,偏财务摘要和主表。章节名本身携带检索意图,是个免费的 query 分类器。

时间信息也要结构化,不能只靠文件名——“2025 中期报告“只是文件级信息,正文里“截至 6 月 30 日止三个月“和“六个月“就是两个不同期间。

边界先交代:这三档是财报场景的经验排序,不是通用标准。单市场单公司的库,T0 可以直接缩水——market 和 stock_code 退化成常量,不标也罢。

标注手段:规则优先,LLM 兜底

谁来标?规则优先,LLM 兜底,按字段逐个看。

fiscal_year、report_type、market,文件名和 PDF 元数据里直接拿,不需要 AI。

unit 用正则:在 chunk 附近搜“百萬元““億元”“千元”。要小心货币前缀——“人民幣百萬元“和“港幣百萬元“是两个 unit,正则得把币种一起捕获。

table_type 和 section 是规则开始吃力的地方。在我这份库上,规则大概 80%,LLM 能到 95%+——这是个人经验估计,不是行业数值【待补:80% / 95%+ 无实测记录,发布前需补一次小样本实测】。差距在边界情况:“流動資產“出现在正文叙述而非表格里时,规则会误判。这种边角案例写规则不划算,LLM 一眼能看出来。成本也交代清楚:几百个 chunk,中档模型打一轮,几毛到几块钱。

最麻烦的是 period_type。中文财报没有固定的 Q1–Q4 模板,期间靠自然语言描述:“截至 6 月 30 日止六個月“是 H1,“截至 12 月 31 日止年度“是 FY。规则要解析月份范围再算跨度月数,纯正则能做,但中文日期表达花样太多,鲁棒性撑不住,我最后让 LLM 复核了一遍。

分工就这么定:规则吃掉确定性的部分,LLM 只花在规则够不着的边界上。全量丢给 LLM,钱花得不疼,但那是把确定性问题交给概率性工具,不值。

标了不用,等于白标

前面都在讲入库,但 metadata 的账,要到检索层才算清。

第一条:metadata 只有在检索层被使用时才有意义。标了 unit 却不在召回和生成时读它,等于白标。自查很简单:查询代码里,你标的字段有几个真被读到了?

第二条:filter 是硬约束,weight 是软约束。年份、stock_code 是硬的:不同年份的财报不该同现,除非用户明确要求比较。section 是软的:问风险因素时对应 section 排前面,但不排除其他 section——风险也可能藏在管理层讨论里。

第三条:标注规则在同市场、同报告类型内可复用,跨市场别强行复用。港股和 A 股的财报结构差异显著,一套规则吃两边,大概率两边都不准。

为什么先 filter?财报问题天然带强约束:问“腾讯 2025 中期报告里广告业务收入的变化“,候选集本就不该包含其他公司、年份、报告类型。先 filter 再向量召回,比“全库召回再筛“效率高一个数量级;候选集干净了,串公司串年份的事故从源头就没了。

这套软硬划法也是财报场景的经验,但“硬走 filter、软走 weight“的结构通用。至于“时间字段标了为什么还召回错年份“——那是下一个问题,这里只把“硬约束“立住。

收尾:模型是公用的,metadata 是私有的

RAG 社区讨论最多的是 embedding 选型、chunk 大小、reranker 精度。都有用,但模型层大家就那两三个选择,拉不开差距。库和库真正的差异,在 metadata 设计和 ingestion 质量——这才是私有资产。

我的工程结论:财报 RAG 的上限不由模型大小决定,而由 ingestion 质量、metadata 设计、chunk 边界和 retrieval 策略决定。这来自单公司财报场景,通用文档场景各项权重未必相同,别直接搬。

metadata 标好了,filter 也加了,检索就万事大吉吗?不是。最典型的一类失败,恰恰发生在“标了时间字段、却没拿它当硬约束“的时候:问 2025 Q4,召回 2020 Q4,相似度分数还很高。这是 metadata 的第一个用武之地,也是第一个错觉——第 6 篇《当向量相似度把“2020 Q4“当成“2025 Q4“》就讲这个。