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_type | chunk 级 | H1 / Q4 / FY | 时间精度 |
section | chunk 级 | 管理层讨论 | 叙述 / 数据区分 |
table_type | chunk 级 | 利润表 | 跨文件对齐 |
unit | chunk 级 | million | 单位标定 |
is_audited | chunk 级 | 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“》就讲这个。