企业文档智能(Enterprise Document Intelligence)[Vol.1 #14B] —— 没有共享字段,就意味着没有可建的索引。每个文件一行摘要,加上各文件自己的目录,检索时向下路由两级。
配图 by Beyza Kaplan,via Pexels。
关于如何把 RAG 系统从「单篇文档」推进到「多文档」,标准建议是建立索引:每篇文档一行,每个可能要过滤的字段一列,再在检索之前加一道过滤。
这个建议的前提是文档之间存在共有字段。但很多文件夹并非如此。
以某个研究文件夹为例:一份 492 页的安全控制目录、一份零信任架构规范、一份 AI 风险框架、三十一篇机器学习论文,外加七份大宗商品市场报告。试问它们共有哪一列?没有客户,没有金额,没有在它们之间含义一致的生效日期,其他业务用户能说出的字段也同样没有。
当不存在共享字段时,就无表可建。这听起来像缺了一环,其实恰恰相反。它意味着准备步骤塌缩成两件产出物,而其中一件解析器本来就会免费返回。
本文把这种情况推演到底:
哪两个问题能把你和「确实需要索引」的情况区分开 准备工作——每个文件一行摘要,加上各文件自己的目录,仅此而已 为什么摘要行必须写给「路由器」而非写给「读者」 回答问题的两级路由,以及为什么整个层级大纲从不整体进入提示词 它会崩溃的四种方式,包括扁平文件列表开始无法扩展的那个临界点
本文是企业文档智能(Enterprise Document Intelligence)系列第四部分的一篇文章。该系列用四块基石搭建一套企业级 RAG 系统。第四部分探讨当输入是「文件夹」而非「文件」时会发生什么变化,而答案取决于你手上是哪一类文件夹。本文属于其中需要新增机制最少的那一类。
🧭 刚接触这个系列?从地图开始:Prompt、Context、Loop(提示、上下文、循环)讲清了每个 RAG 系统所构建的三层工程结构——prompt(调用本身)、context(填充模型窗口的内容)、loop(下一次调用何时触发、何时停止),并以此为镜头逐篇走过整个系列。这是看清覆盖了什么、本文处在什么位置的最短路径。
本文所处的位置:第四部分,那个不需要索引的文件夹 – 图片由作者绘制
📓 在配套笔记本里,对你的文件夹构建两级索引:每文件写一行摘要,打印 level-0 列表,然后运行路由调用,看它在读取任何一页之前,如何从 63 个文件中只保留 1 个。代码仓库 → doc-intel/notebooks-vol1。
doc-intel/notebooks-vol1 的公开配套代码仓库 – 图片由作者绘制
本文全程使用的一个文件夹包含 63 份公开 PDF,合计 4,211 页:25 份 NIST 文件(其中 24 份出版物外加其中一份的三页扫描件,属美国政府作品,在美国属公共领域,详见 NIST 版权声明)、31 篇 arXiv 论文(各自遵循其摘要页上声明的 arXiv 非独占分发许可),以及 7 期世界银行《商品市场展望》(CC BY 3.0 IGO,见 2026 年 4 月 OKR 出版页声明)。
1. 你的文件夹属于哪一类
第 14A 篇文章(三类语料,以及为错的类型构建要付出的代价)列出了集合可能呈现的三种形态,并指出每种形态需要不同的架构。本文选取其中「文档之间毫无共通之处」的那一种。在动手构建之前,值得先确认你确实处于这种情况,因为猜错方向的代价在两端都很大。
1.1. 决定类别的两个问题
第一个问题:文件夹里是否任意两份文档会互相引用? 一份修订案引用它的主协议;一份续约引用去年的保单;一份证书属于某份合同。如果存在这种情况,这个文件夹就有一个你必须建模的结构,本文就不是你要找的那篇。
第二个问题:业务用户能否说出一个「每份文档都有、且在每份中含义相同」的字段? 客户、生效日期、保费、保单号。如果回答又快又笃定,那这个文件夹就是一张还没人录入的数据库,而录入它就是你要做的活儿。
如果两个答案都是「否」,那就没什么可放进列里的。你当然仍可抽取元数据(文件名、页数、创建日期),但这些都不会是任何人会去过滤的字段。一列对大多数行都为空、对其余行又含义各异,那它根本不是一列。
同样四个问题,两个文件夹,两种架构 – 图片由作者绘制
混合文件夹是存在的,而且很常见。一个共享盘里,可能既有 5,000 份长得像的合同,又紧挨着 300 份谁也不属于的杂项文件。这不是矛盾,而是两个语料共用一块磁盘。把它们拆开,各自跑对的架构即可。
1.2. 另一种情况能得到、而本情况得不到的东西
同质化的情况(第 14C 篇,业务已经知道有哪些列)能得到本情况无法拥有的三样东西。
它得到一个过滤器。一句「零售商 X,当前有效」会变成一条 SQL 子句,在读取任何内容之前,18,000 行先被压成 3 行。
它得到聚合。」本季度有多少保单到期」由索引本身作答,完全无需打开任何文档。
它得到一套共享词表。每行都有客户和日期,于是把用户用词映射到列值的本体(ontology)能在整个语料上产生回报。
这些无一能迁移到此处。无子句可写,无内容可数,而一份被控制目录和大宗商品报告共享的词表,只会是一套关于「无物」的词表。第 15 篇(准备语料)、第 16 篇(列背后的本体)、第 17 篇(先用 SQL 查询语料)构建了全部三样,当上面第二个问题答「是」时,它们才是你要找的文章。
把这些都拿掉之后,剩下的东西比看上去小,而它正是本情况的完整架构:一个文档之间毫无共通之处的文件夹,就是一篇长文档。 那些文件就是它的章节,它们的小节就是它的子节。你早就会读长篇文档了——你读它的目录。
2. 准备:每文件一行,加上解析器返回的大纲
两件产出物,在摄入(ingestion)时一次性构建。
Level 0 是文件列表,每行一个,说明该文件讲什么。Level 1 是每个文件自己的目录。二者合起来就是一份层级大纲:文件夹的章节,再往下是各章节的小节。
整个准备就这么多。没有分类环节,没有字段抽取,没有 schema,没有本体。
2.1. 摘要行就是索引,所以要写给路由器
质量绝大部分就藏在这里,而它容易被低估,因为这件产出物只是一句话。
摘要行不是文档说明,没人会读它。它唯一的消费者是那个必须从 63 行中判断「哪些文件可能藏着答案」的路由调用。它该被写得像「列定义」,而不是像「简介文案」。
同一个文件,两种写法,以及每种写法让路由器能做什么 – 图片由作者绘制
三个差别让两个版本分出高下。
用两种词表写清主体。 用户会输入的词,以及文档自身使用的词。一个关于 account management(账户管理)的问题,必须命中一行写着 account management 与 AC-2 的文件,因为路由器是按文本匹配,而非按意图匹配。
文件所回答内容的形态。 一节一个带编号的控制项,和一篇叙述性报告,是两种不同的承诺。它告诉路由器答案是「一节」还是「一整章」,而这正是第二级随后要依此行动的东西。
文件「不」讲什么。 这是人们会跳过、却恰恰是让路由变精确的部分。不是实施指南 这一句,就能把所有「怎么做」类问题一次性地从该文件中排除。负向信息,正是让路由器能不打开任何文件就丢掉另外 62 个的原因。
产出这些行很便宜。摄入时对每个文档调用一次,输入其前几页加目录,就得到可用草稿;对这么大的文件夹,你也能用一个下午手写完成。无论哪种方式,成本按每个文档只付一次,且仅在文件变更时再付。关键在:这些行要像 schema 那样被审阅,而不是像文案那样。
2.2. 第二级不增加任何新成本
解析这块基石本来就会返回一个 toc_df,每个标题一行,带其页码范围。第 5B 篇(解析器返回的关系表)从 PDF 的原生大纲(native outline)构建它。本文没有任何扩展。Level 1 就是那张表,按文件各自保留。
就这里的文件夹而言,63 份 PDF 中有 47 份自带原生大纲,这 47 份每份只需一次解析。其余 16 份什么都没带:一份扫描备忘录、一份幻灯片导出、一份两页便签。
对这 16 份有两招可用,而由篇幅长短决定选哪一招。短文件变成一个叶子(leaf):路由器一旦保留它,你就整篇读掉,level 1 永远不会运行。长文件则先重建它的目录。第 5septies 篇(从印有目录的那一页重建目录)覆盖的是「PDF 展示了目录页却从未把它编码为大纲」的情况。Building Document Structure with Loop Engineering(用循环工程构建文档结构)覆盖更难的情况:连目录页都没有,标题只能从正文排版中恢复。
63 个文件、4,211 页,而一份索引只有 63 句话那么长 – 图片由作者绘制
整个文件夹上,两级合计 63 行与 2,422 行大纲。那 63 行是每次提问模型都会读到的东西;那 2,422 行是它能触达的,一次一个文件,且几乎从不碰。
3. 查询时:向下路由两级
拿一个问题穿过这个文件夹:「账户管理控制项要求什么?」 答案在 NIST SP 800-53 Rev. 5(信息系统与组织的安全与隐私控制,美国政府作品,在美国属公共领域)的控制项 AC-2,第 46 至 50 页。其余 62 个文件都不可能装着它,也都不会被打开。
从 63 行里挑文件,再在下钻进存活下来的那些内部 – 图片由作者绘制
3.1. Level 0,挑文件
一次调用。模型读 63 行,每行是一个文件名、它的路由摘要,以及一个关键词计数(keyword tally,即命中统计)。它返回「可能藏着答案」的文件 id,并为每个给出理由。通常返回一到三个文件。
关键词计数是并行运行的,而非取而代之。统计问题里的术语出现在某文件摘要行、及其大纲标题中的次数,成本为零,且它能抓住摘要句被「四舍五入」掉的东西:像 AC-2 或某个确切编码,摘要句从未把它拼出来。两个信号进同一个提示词,由模型拍板——这正是第 7B 篇(并行探测器,最后做一次调用)在单篇文档内部确立的模式。这里只是把它抬高了一层,原样未改。
这个调用有两个值得一提的属性。它有界:无论文件夹在磁盘上有多重,都只是 63 行短文本。它可审计:理由字符串说明了为何保留某文件,于是错误答案能追溯到路由决策,而不是某个相似度分数。
3.2. Level 1,在存活者内部下钻
从这里起就是单篇文档的情况,毫无新增。该文件自己的大纲成为当前层级,模型挑一个分支;若那个分支还有更细的子节点,循环就打开它们并重复。它在叶子处停下,或停在「短到足以整段读掉」的小节处。第 7quater 篇(层级检索,按目录读一篇长文档)在这同一份目录上把这套循环完整展开:十一章,再二十个控制族,再二十五个控制项,再到 AC-2。
两级是同一个调用。
# The folder's file list is the top level of ONE table of contents:# level 0 rows are files, deeper rows are those files' own sections.level = corpus_toc[corpus_toc.level == 0] # 63 rows, one per filefiles = reason_on_toc(question, level, # the SAME router call section_signals=keyword_hits(question, level))sections = []for file_id in files.section_ids: # typically 1 to 3 level = corpus_toc[(corpus_toc.file_id == file_id) & (corpus_toc.level == 1)]whileTrue: # the one-document loop pick = reason_on_toc(question, level) section = level[level.section_id.isin(pick.section_ids)] kids = immediate_children(corpus_toc, section)if kids.empty or section.n_pages.max() <= SMALL:break# a leaf, or short enough level = kids # open it, descend sections.append(section)# 63 files -> 1 file -> AC-2 ACCOUNT MANAGEMENT -> 5 pages out of 4,211每一行输出都保留着自己的文件 id,于是引用能指名它来自哪个文件。在一个文档之间不共享词表的文件夹里,这一点比平时更重要:一份用控制目录和大宗商品报告缝出来的答案,读起来通顺,却一文不值。
3.3. 为什么整份层级大纲从不放进同一个提示词
层级大纲是一个对象,63 行加 2,422 行。没有什么能阻止你把全部序列化进一次调用。不这么做的三个理由:
它会放不下。 把这个文件夹乘以二十,光大纲就已是长提示词,再没空间用于推理。而需要这套架构的文件夹,恰恰是那些会持续增长的。
精度在窗口耗尽之前先掉。 在 level 0,模型比较 63 个整文档描述,这是个它做得好的决策。若塞给它 2,485 行混合内容,它需要的那一节,要和一份毫不相干文档的 357 个同级项竞争,而从文档中单独抽出的一个节标题,本就是个弱信号。
它每次提问都要花钱。 Level 0 是 63 行短文本,每次必付。Level 1 仅为存活文件的那些大纲,于是它按「一个文件」而非「六十三个文件」付费。
下钻也是可选的。在一个由八个短文件、大纲很浅的文件夹里,level 0 挑出一个文件后便无可下钻:循环跑一次,表现得像扁平路由。模型在每一层自行判断「再深一层是否划算」,这与单篇文档循环所用的终止规则相同。
4. 它在哪里崩溃
两级文件夹索引会出四种问题,且严重程度不等。第一种是下午就能修好的写作问题。接下来两种是表现为检索问题的解析问题。最后一种是真正的天花板。
文件夹索引出错的四种方式,以及各自需要什么 – 图片由作者绘制
4.1. 那行什么都没说的摘要
「一份关于网络安全的技术报告。」 十二个文件都符合这描述,于是 level 0 要么把十二个全返回,要么毫无可复原理由地挑其中三个。症候很具体:level-0 调用保留了过多文件,而 level 1 在它拿到的每个文件上都看起来十分健康。
修法是重写那行,而测试很便宜。留出十个真实问题,对每个跑一次 level-0 调用,检查你手动会挑的那个是否回来。十个问题能抓出大部分问题,因为一行坏摘要通常不是对「一个问题」坏,而是对「一类问题」整体都坏。
把那些行当作一件待审的产出物。它们就是索引;一行潦草的行,就是一条潦草的列定义,而且它不会自己报信。
4.2. 那份没有结构的文档
Level 1 需要能下钻的东西。一份没有标题的文档,让循环无从挑选,于是路由器要么整篇读它,要么什么也不读。
整篇读掉,多数时候没问题。一份三页备忘录是个叶子,两级方案天然能处理叶子。失败情形窄而具体:长 且 无结构。一份 200 页、没有标题的扫描合集,在任何层级都无法被路由,因为根本没有可路由的东西。
这一份不是检索问题,而是解析问题;它在本文这套机制运行之前,就由「从文档携带的任何信号重建大纲」先行解决了。
4.3. 扁平文件列表停止扩展
Level 0 是一份扁平列表。只要列表在一次调用里还读得完,它就工作。
几百个文件很从容。几千个就不行了,而失败方式不是人们预期的那种。先撑不住的不是上下文窗口,而是路由精度。到 3,000 行时,level-0 调用已变成「整个架构本要规避的 top-k 问题」,只是被抬高了一层。
修法不是换架构,而是再加一层。按文件夹、按来源、按年份、按书架上已有的任何分组把文件聚起,再给每组自己的摘要行。Level 0 变成 40 行「组」摘要,level 1 是路由器保留的那些组内部的文件,level 2 是它们的目录。同一个路由调用,多转一圈同样的循环。
这也正是本情况开始觊觎「同质化情况从第一天起就有」的东西的临界点。如果那个自然分组到头来成了「每个文件都有的一个字段」,那么 1.1 节里的第二个问题就已悄悄改了答案,而这个文件夹终究想要一份索引。
5. 结论
一个文档之间毫无共通之处的文件夹,就是一篇长文档,它想要的是你已有的检索,而非周围那些文章里的语料机制。
它不需要的东西值得列出来,因为每一项都是别人会另行提议的项目。不需要关系型索引:没有可填的列。不需要本体:一份被控制目录和大宗商品报告共享的词表,描述的什么也不是。不需要 SQL 智能体:无物可查。不需要对语料跑一遍实体抽取:实体不在文件间复现,于是无物可连。
它需要的,是每文件一句话——写给路由器,以及解析器本就会返回的目录。然后路由跑两次:一次在文件列表上,一次在存活下来的文件内部。63 个文件、4,211 页,模型在落到那 5 页之前,只读了 63 行加一份目录。
本组中两篇后续文章,取另外两种形态。第 14C 篇(同质化语料,业务已知道有哪些列)覆盖第二个问题答「是」的文件夹。第 14D 篇(案卷,关于单一实体的一个合集)覆盖「内部异构、跨案卷重复」的文件夹,它需要两者兼有一点。
6. 延伸阅读与来源
本系列更早处,关于本文复用的那些构件:
Building Document Structure with Loop Engineering:从正文排版恢复 PDF 大纲用于 RAG。关于本文件夹里那 16 份不带原生大纲的文件该怎么办。 Most RAG Hallucinations Are Retrieval Failures:Fix Retrieval, Not the Prompt. 为何 level 0 的一次路由失误,后来会表现为生成问题。 Context engineering for RAG question parsing:从原始问题到能引导检索与生成的 typed 字段。level-0 计数所统计的那些关键词从何而来。 Prompt Engineering Isn’t Enough:四块上下文工程基石如何止住 RAG 幻觉。填充路由所喂调用的那四个 typed 构件。 Most RAG Hallucinations Are Extraction Errors:面向 Typed 生成契约的七种模式。生成用下钻返回的那些行做什么,以及为何每行都携带自己的文件 id。
上文另有引用、此处不附链接列出:第 5B 篇(解析器返回的关系表)、第 5septies 篇(从印有目录的那一页重建目录)、第 7B 篇(并行探测器,最后做一次调用)、第 7quater 篇(单篇长文档内部的层级检索)、第 14A 篇(三类语料,以及为错的类型构建的代价)、第 14C 篇(同质化语料)、第 14D 篇(案卷),以及第 15 至 17 篇(索引、本体,与另一类文件夹的 SQL 优先查询路径)。
外部来源:
Document Summary Index,LlamaIndex 文档(docs.llamaindex.ai)。与本文 level-0 相同的思路,作为已发布开源组件:每文档一份摘要,检索先按摘要挑文档、再挑段落。值得一读的是它用来生成摘要的提示词。 Parth Sarthi 等,RAPTOR:面向树状组织检索的递归抽象式处理,2024(arXiv 2401.18059)。当不存在结构时,通过聚类与递归摘要构建树。对比点正在于此:一文件夹文档本就自带一棵树,于是无物可推断。 Anthropic,Introducing Contextual Retrieval,2024(anthropic.com)。给每个分块前置一小段文档级上下文,使一段文字不再与其来源脱节。本文的摘要行,正是「在文件粒度上」做的同一招。 Yunfan Gao 等,Retrieval-Augmented Generation for Large Language Models:A Survey,2023(arXiv 2312.10997)。本文基线所对照的朴素、进阶、模块化那套词汇。 Scott Barnett 等,Seven Failure Points When Engineering a Retrieval Augmented Generation System,2024(arXiv 2401.05856)。经实地测量的失效模式;其中「缺文档」与「错文档」两点,正是 level-0 路由旨在消除的。
夜雨聆风