向量库救不了长文档|PageIndex 的树索引,我装完试到摘要那步就停了
业务同事问我的问题,来来回回就那么几句:「对方逾期付款,我们的违约金怎么算」「这条排他条款是不是把整个华东都圈进去了」「验收不合格的补救期限写的几天」。
这些问题的答案,全在合同 PDF 里,而且全在某一节的某一段。我做过的事很自然:把合同灌进知识库,切块、转向量、检索、塞给模型。然后就卡在这里了——问违约金,召回来的八段里有七段讲的是付款节点和账期,真正那句「逾期三十日以上,按未付款项日万分之五计」压根没进候选集。原因不复杂:那句话的上一句是「违约责任」,再上一句是小标题「第九条 违约与救济」。切片按字数硬切,「第九条」被切到了上一块,「逾期三十日以上」被切到了下一块,中间那块单独看,语义上跟「付款方式」没什么区别。
这不是某一次的意外,是每个月都会碰上的事。我试过的补丁也都试过:把 chunk 从八百字加到一千五、重叠两百字、加 reranker、加关键词召回一路混检。每加一层,召回率是会上去一点,代价是噪声也一起上去,模型开始在七八段似是而非的条款之间编答案。混检调到能用的程度,我已经在维护一套比我业务代码还复杂的检索链路了。
所以我看到 PageIndex 那句 slogan 的时候,是被戳了一下的:similarity ≠ relevance。相似不等于相关,这话在合同、财报、招股书这类文档上是真的。
它把文档目录当成索引用,这条不是噱头
思路本身很朴素,朴素到有点反常:既然人有目录,为什么不拿目录当索引。
建树是一步,检索是另一步。建树的时候,它从 PDF 的版面里把标题层级抽出来,做成一个带 node_id、页码区间、父子和兄弟关系的 JSON;检索的时候,模型拿着问题在这棵树上走,判断该进哪个分支,走到叶子节点再去读那几页的正文。整个过程跟人翻一本厚报告没区别:先看目录,再翻到第九条,再读那三页。
真正能跑的入口有三个,我挨个看过:
# 仓库自带脚本,PDF 走 flash 模式(默认)
python run_pageindex.py --pdf_path ./contract.pdf
# Markdown 直接进,不需要模型参与就能出树
python run_pageindex.py --md_path ./notes.md
# SDK:本地建树,自己的模型回答
python -c "
from pageindex import PageIndexClient
c = PageIndexClient(index='gpt-6-luna', chat='gpt-6-sol')
doc_id = c.submit_document('contract.pdf')['doc_id']
print(c.chat('逾期付款的违约金怎么算', doc_id=doc_id))
"
工具层是四个:browse_documents 列清单、get_document_structure 取树、get_page_content 按页取正文、remove_document 删。SDK 里还有 as_openai_tools() 和 as_anthropic_tools() 把这一组包装成主流框架认的工具定义。这几个接口是实打实写在代码里的,不是宣传页 PPT。
建树的粗细程度是可以拧的,旋钮都在 config.yaml 里,默认值挺保守:
toc_check_page_num: 20 # 只在前 20 页里找目录
max_page_num_each_node: 10 # 一个节点最多 10 页
max_token_num_each_node: 20000
if_add_node_summary: "yes" # 每个节点让模型写一段摘要
前两个数字决定了树的分辨率。一份两百页的文档按「每节点最多十页」切,最多分出二十个叶子;合同这种九条十二款的文档,叶子会粗到一条一节一层都带着,跟我想要的「精确定位到那句违约条款」还差一截。要更细就得把 max_page_num_each_node 往下调,代价是节点变多、摘要调用变多、检索时模型要看的树也变长。这条没有标准答案,只能拿自己的文档试。
摘要那块还有个细节:每个节点的摘要默认压到 150 词(SUMMARY_MAX_WORDS = 150),发给模型的 reasoning effort 是 none。官方在 README 里的说法是「建树用基础模型就够,真正吃能力的是检索时的 chat 模型」。我认同这个判断的动机——树的结构来自版面,模型只负责归纳——但 150 词的摘要去描述一份财报里一节五页的正文,信息损耗是免不掉的,这也是我怀疑检索准确率会掉的一个点,只是没 key,验不了。
哪些是真能用,哪些还只是话术,我分开说:
| 能力 | 我的判断 |
|---|---|
| 离线建树(PDF、Markdown) | 真能用,我自己跑通了,不需要任何 key |
| SDK 本地模式(建树 + 检索) | 能用,但摘要和检索两步必须有模型 key |
| 页码级引用 | 本地模式有,能追到第几页 |
| 块级引用、OCR、图片理解 | 只在云端,本地版没有 |
| MCP server | 只有云端版,本地不起 |
| 98.7% FinanceBench | 那是 Mafin 2.5 的成绩单,不是 pip install 完就有的能力 |
最后一行我想多说一句。那个 98.7% 出自他们自家的 Mafin 2.5 项目,跑的是完整 FinanceBench 题库,属于自家系统在自家基准上的成绩。真正对开源版有参考价值的是另一个仓库——PageIndex-OSS-Benchmark,用 34 份 PDF、1,945 页、62 道题的本地 flash 索引来跑,他们还写明了一件事:索引不进去的文档被排除在外,而不是算作失败。这句话很关键,它意味着那个准确率只描述「PageIndex 能吃进去的文档」,不描述你的文档。
省掉向量库,账单挪到了检索那一刻
这是我最想掰扯的地方,因为大部分推文只讲前半句。
| 向量检索 | 树索引 + 推理 | |
|---|---|---|
| 建索引 | 一次性 embedding,快、便宜 | 遍历版面 + 每个节点一次模型调用 |
| 单次检索 | 一次向量比对,毫秒级 | 模型沿树走,几次到十几次调用 |
| 出错形态 | 召回不相关但相似的片段 | 走错分支、读错节点、摘要漏信息 |
| 可追溯性 | 相似度分数,说不清为什么 | 节点 id + 页码,能画路径 |
| 扩展性 | 百万级向量很成熟 | 单文档很强,跨文档要额外一层 |
省掉向量库省掉的是运维,不是钱。原来你付的是 Pinecone 或者 Milvus 的账单,现在付的是模型 token 的账单。官方开源基准里 12 组配置的数字摆得很清楚,我挑四行:
| chat 模型 | reasoning effort | 62 题答对 | 单次查询均价 |
|---|---|---|---|
| gpt-5.6-luna | none | 53 | $0.0031 |
| gpt-5.6-luna | high | 60 | $0.0036 |
| gpt-5.6-terra | medium | 61 | $0.0303 |
| gpt-5.6-sol | medium | 62 | $0.0810 |
便宜模型八五折的准确率,一次三分钱;贵模型全对,一次八分钱,中间差了二十六倍。而同一次查询用向量库,成本几乎可以忽略不计。所以这笔交易的本质是:用延迟和 token 换准确率和可解释性。它不是免费午餐,它是另一种付费方式。
代价在四种情况下会变得不可接受,我一条条划出来:
高并发在线问答不行。 每次检索都要模型来回走树,社区里实测的量级是单次查询几十秒到一两分钟,没有缓存的情况。这对一个聊天窗口是灾难,对一个分析师自己用的工具就完全可以接受。看的是场景,不是数字。
碎片化语料不行。 它的树来自文档本身的层级。聊天记录、零散笔记、几百字的 FAQ,没有目录可走,树会退化成一堆等长的块,那时候它跟向量检索比没有任何优势,反而更贵。issue #518 就是这个问题的极端版本:有人拿 flash 模式跑 FinanceBench,发现不少 PDF 的前几页根本没被抽进索引,覆盖率只有一半。索引里没有的页,后面推理再聪明也找不到。
多文档不行,至少不能直接上。 开箱的状态是一棵树对应一份文档。全部语料上做检索,要么自己写一层路由,要么用他们云版的文件系统层。这个能力我没看到开源版里有。
要毫秒级首字的不行。 这个不用解释。
还有一个我个人的保留意见:它把不确定性从「嵌入模型选得对不对」挪到了「模型这一次走的路对不对」。错误没消失,只是换了个形状——而且新形状比旧形状更难测。向量检索召回错了,你能看着相似度分数骂它;树搜索走错了,你得把整条推理路径翻出来看,才知道它是从哪一步开始跑偏的。
装它两分钟,找入口花了十分钟
python3 -m venv /tmp/pi
/tmp/pi/bin/pip install -U pageindex
装完是 0.4.7,PyPI 上 10 月 24 日发的版。Python 要求 >=3.10,我这台是 3.13.12,没问题。
第一个意外:装完没有 pageindex 这个命令。pip 只装了库,命令行脚本 run_pageindex.py 还躺在仓库根目录,得自己 clone 下来或者单独下这个文件。文档的快速上手写的是 pip install 然后在 Python 里调 PageIndexClient,脚本用法写在仓库 README 里,两处口径不一样,我第一次就找错地方了。
第二个意外是体积。litellm、pypdfium2、PyPDF2、Pillow,再加上一串跟工具调用和协议相关的依赖,一路装下来虚拟环境里 81 个包,site-packages 占 339 MB。对一个「就是个文档索引器」的东西来说不算小,但都在依赖树里,没有偷偷下模型权重。litellm 还被钉在 1.105.x(<1.106),说明它跟着这条线走,升级节奏得看上游脸色。
硬件门槛官方没写,README 和文档站都没给最低配置。这块我只能报我自己量到的数:
| 档位 | 配置 | 依据 |
|---|---|---|
| 最低可用 | 2 核 4 GB,纯 CPU,20 GB 磁盘 | 我实测一份 12 页 PDF 离线建树:1.65 秒,峰值内存 148 MB |
| 建议配置 | 4 核 8 GB 以上 | 摘要并发默认 64 路(SUMMARY_CONCURRENCY = 64),树优化展开默认 32 路,并发打满时内存和供应商限流都会难受 |
| GPU | 不需要 | 推理全在远端模型,本地只做版面解析 |
那个 148 MB 是 --no-summary 的裸建树;开着摘要跑,内存会随并发上去,官方没给数字,我也没有 key 去量,只能说到这儿。
一份迪士尼季报,29 个节点
端到端我只跑通了前半段,后半段缺 key。前半段的输入输出都是真的。
输入用仓库自带的样例,迪士尼 2025 财年一季度财报,12 页:
python run_pageindex.py --pdf_path q1.pdf --no-summary --optimize off
预期输出就两行:
Parsing done, saving to file...
Tree structure saved to: ./results/q1_structure.json
出来这个文件 4,572 字节,29 个节点,顶层 12 个,doc_title 是从版面里抽出来的,一个字没差:
{"doc_name": "q1.pdf",
"doc_title": "THE WALT DISNEY COMPANY REPORTS FIRST QUARTER EARNINGS FOR FISCAL 2025",
"structure": [
{"title": "Financial Results for the Quarter:", "node_id": "0000", "start_index": 1, "end_index": 1},
{"title": "DISCUSSION OF FIRST QUARTER SEGMENT RESULTS", "node_id": "0006", "start_index": 4, "end_index": 9,
"nodes": [
{"title": "Star India", "node_id": "0007", "start_index": 4, "end_index": 4},
{"title": "Entertainment", "node_id": "0008", "start_index": 4, "end_index": 7}
]}]}
start_index 和 end_index 是页码区间。看到这个结构我就明白它为什么敢说「不做 chunking」了——它确实不按字数切,它按章节切。只是这句话的另一面是:它换了个地方切。章节切得好不好,取决于你的 PDF 有没有像样的标题层级。
把 --optimize off 换成 --optimize full,多打一行:
Optimize: merges=1 expands=0, worst-case search cost 6 -> 6 pages
翻译成人话:它把一个小节点合进了父节点,然后算出最坏情况下一次搜索要翻 6 页。这个「worst-case search cost」是我在这套东西里看到的最诚实的一个指标——它自己承认,从根走到叶子,最坏要付出多少页的阅读量。
再往下就走不动了。去掉 --no-summary、用默认的 flash 模式,日志开始刷同一条错误,第十次之后抛出来:
pageindex.utils.LLMRetriesExhausted: LLM completion failed after 10 retries:
InternalServerError ... Missing credentials
SDK 那边同样卡住,抛的是 PageIndexAPIError,12.3 秒之后放弃,storage_path 默认落在当前目录的 .pageindex。
我这儿没有可用的模型 key,也没有把公司合同往外部接口上发的打算,所以摘要和检索这两步我没跑。那个 98.7% 我验不了,也不替它背书。能替它背书的只有一件事:离线建树这一步是真的,而且不需要任何外部服务。
翻 issue 翻出来的东西,比文档里写得多
按说这种东西的坑都藏在 issue 里,我翻了一遍,按份量排:
#518,覆盖率只有一半。 一位用户在 FinanceBench 数据集上跑 flash 模式,发现不少 PDF 的前几页没被抽出来,比如 3M_2108k.pdf,索引覆盖率报的是 50%。这条到现在还是 open。它戳的正是整个方案的地基——索引里没有的页,推理再准也没用。
#316,索引只能整体重建(8 条讨论)。 一份 500 页的文档改了一节,现在得整份重跑一遍,建树成本和 token 成本一起重付。提需求的人要的是增量更新,官方还没给。这个坑对静态文档无所谓,对经常改版的合同模板就很疼。
#268,输出路径写死。 ./results/{name}_structure.json 是硬编码的,想批量处理就只能事后搬文件。写脚本的人会先撞上这个。
#456,Windows 上的锁是假的。 DocStore.lock() 在 Windows 上退化成一个什么都不做的 yield,因为 fcntl.flock 在那边不存在。并发提交同名文档时,那个「先查重再写」的窗口是敞开的。
#103,装不上过。 早期仓库里没有 pyproject.toml,pip install 直接失败。这条已经关了,现在装得上——但翻 issue 列表能看出这个项目的工程化是后来补的。
外部的讨论我也翻了几处。Hacker News 上那次 Show HN 是 2025 年 8 月 27 日发的,192 分、128 条评论,标题很直白:Show HN: PageIndex – Vectorless RAG。评论区最响的一句质疑是「这不就是换了个地方的 vibe retrieval 吗」——原来靠向量算近似,现在靠模型猜路径,都是近似,凭什么说后者更准。这话我认同一半:它确实把确定性计算换成了随机调用,只不过换来的东西是「可解释」,而不是「更准」。
还有人贴了延迟的实测,单次查询几十秒到两分钟,取决于文档长度和模型;也有人指出它本质上是「拿模型给每一节写摘要,再把树塞进上下文让模型指节点」,一位自己实现简化版的用户说用更少的调用在 FinanceBench 上拿到过八成出头。
中文圈的实测提到两个具体的坑:一是没有标题的文档(聊天记录、零散笔记)根本生成不了像样的树;二是走云版 API 时 submit_document() 会把整份 PDF 传上去,公开论文无所谓,合同、财报这种就得先看数据政策。
谁该上,谁别上
我的结论分三层。
适合上的:语料是长、结构化、专业文档(财报、招股书、合同、法规、技术手册);查询低频但单次价值高;对溯源有硬要求,出了答案要能指到第几页第几节;或者你已经把混检加重排调到了瓶颈,长文档的多跳问题还是答不准。这几种情况它给你的东西,向量检索确实给不了。
别上的:海量碎片语料;高并发在线问答;预算按 QPS 算而不是按次算;文档天天改版。这四种情况硬上,你会得到一个又慢又贵的向量库替代品。
替代方案,我拿两个比:
RAGFlow(infiniflow/ragflow,92,919 star,Apache-2.0)。走的是完全相反的路线——把解析、切块、向量、重排、对话全链路打包成一套 Docker Compose,工程完整度是它的强项。维度差别在你要不要自己造:想开箱有个能用的知识库系统,选它;想在自己代码里换掉检索这一层,PageIndex 更轻,一个 pip 包,没有服务要运维。
Docling(docling-project/docling,69,640 star,MIT)。它只做前半段——把 PDF 变成结构化 Markdown,标题层级、表格、阅读顺序都给你保住。维度差别在边界:Docling 到文档结构为止,检索得你自己接;PageIndex 把结构和检索一起给了,但结构那一段没有 Docling 细。我现在的想法是这俩可以叠着用,解析用 Docling,检索这一层再决定用树还是用向量。
顺带提一句 MinerU(82,538 star),定位跟 Docling 很近,中文 PDF 的版面还原做得不错,也是只做解析这一段。
一句话:PageIndex 治的是「相似但不是要找的那一段」,不治「模型读不懂那一段」。 前者是我天天遇到的,后者得靠别的办法。
总而言之,这东西的方向我认,工程成熟度我打个折。顺手看了下仓库,现在 39,794 个 star、3,432 个 fork,最近四周 77 次提交,最新发版 v0.4.7 是 10 月 24 日,release note 的标题很自信——”Better retrieval than Vector RAG, at lower cost”。
最后说句实在的,我这次的结论只走了一半路:树建出来了,检索那一段因为没 key 停在那儿。等哪天手上有合适的模型额度、又拿到一份可以外发的公开财报,我会把它和现在这套混检摆在一起,用同一批问题跑一遍,看谁答得准。在那之前,它对我来说是一个方向对、地基还没验收完的东西。